Compare commits
6 Commits
782f08b956
...
ea94745758
| Author | SHA1 | Date |
|---|---|---|
|
|
ea94745758 | |
|
|
c24821b6ed | |
|
|
2c4c2d9717 | |
|
|
d5e5796ba3 | |
|
|
43c6b6e5bd | |
|
|
65c93c529c |
|
|
@ -104,52 +104,104 @@
|
|||
</action>
|
||||
<action>Find at least 3 more specific, actionable issues</action>
|
||||
</check>
|
||||
|
||||
<!-- Store context-aware findings for later consolidation -->
|
||||
<action>Set {{context_aware_findings}} = all issues found in this step (numbered list with file:line locations)</action>
|
||||
</step>
|
||||
|
||||
<step n="4" goal="Present findings and fix them">
|
||||
<action>Categorize findings: HIGH (must fix), MEDIUM (should fix), LOW (nice to fix)</action>
|
||||
<step n="4" goal="Run information-asymmetric adversarial review">
|
||||
<critical>Reviewer has FULL repo access but NO knowledge of WHY changes were made</critical>
|
||||
<critical>DO NOT include story file in prompt - asymmetry is about intent, not visibility</critical>
|
||||
<critical>Reviewer can explore codebase to understand impact, but judges changes on merit alone</critical>
|
||||
|
||||
<!-- Construct diff of story-related changes -->
|
||||
<action>Construct the diff of story-related changes:
|
||||
- Uncommitted changes: `git diff` + `git diff --cached`
|
||||
- Committed changes (if story spans commits): `git log --oneline` to find relevant commits, then `git diff base..HEAD`
|
||||
- Exclude story file from diff: `git diff -- . ':!{{story_path}}'`
|
||||
</action>
|
||||
<action>Set {{asymmetric_target}} = the diff output (reviewer can explore repo but is prompted to review this diff)</action>
|
||||
|
||||
<!-- Execution hierarchy: cleanest context first -->
|
||||
<check if="Task tool available (can spawn subagent)">
|
||||
<action>Launch general-purpose subagent with adversarial prompt:
|
||||
"You are a cynical, jaded code reviewer with zero patience for sloppy work.
|
||||
A clueless weasel submitted the following changes and you expect to find problems.
|
||||
Find at least ten findings to fix or improve. Look for what's missing, not just what's wrong.
|
||||
Number each finding (1., 2., 3., ...). Be skeptical of everything.
|
||||
|
||||
Changes to review:
|
||||
{{asymmetric_target}}"
|
||||
</action>
|
||||
<action>Collect numbered findings into {{asymmetric_findings}}</action>
|
||||
</check>
|
||||
|
||||
<check if="no Task tool BUT can use Bash to invoke CLI for fresh context">
|
||||
<action>Execute adversarial review via CLI (e.g., claude --print) in fresh context with same prompt</action>
|
||||
<action>Collect numbered findings into {{asymmetric_findings}}</action>
|
||||
</check>
|
||||
|
||||
<check if="cannot create clean slate agent by any means (fallback)">
|
||||
<action>Execute adversarial prompt inline in main context</action>
|
||||
<action>Note: Has context pollution but cynical reviewer persona still adds significant value</action>
|
||||
<action>Collect numbered findings into {{asymmetric_findings}}</action>
|
||||
</check>
|
||||
</step>
|
||||
|
||||
<step n="5" goal="Consolidate findings and present to user">
|
||||
<critical>Merge findings from BOTH context-aware review (step 3) AND asymmetric review (step 4)</critical>
|
||||
|
||||
<action>Combine {{context_aware_findings}} from step 3 with {{asymmetric_findings}} from step 4</action>
|
||||
|
||||
<action>Deduplicate findings:
|
||||
- Identify findings that describe the same underlying issue
|
||||
- Keep the more detailed/actionable version
|
||||
- Note when both reviews caught the same issue (validates severity)
|
||||
</action>
|
||||
|
||||
<action>Assess each finding:
|
||||
- Is this a real issue or noise/false positive?
|
||||
- Assign severity: 🔴 CRITICAL, 🟠 HIGH, 🟡 MEDIUM, 🟢 LOW
|
||||
</action>
|
||||
|
||||
<action>Filter out non-issues:
|
||||
- Remove false positives
|
||||
- Remove nitpicks that do not warrant action
|
||||
- Keep anything that could cause problems in production
|
||||
</action>
|
||||
|
||||
<action>Sort by severity (CRITICAL → HIGH → MEDIUM → LOW)</action>
|
||||
|
||||
<action>Set {{fixed_count}} = 0</action>
|
||||
<action>Set {{action_count}} = 0</action>
|
||||
|
||||
<output>**🔥 CODE REVIEW FINDINGS, {user_name}!**
|
||||
|
||||
**Story:** {{story_file}}
|
||||
**Story:** {{story_path}}
|
||||
**Git vs Story Discrepancies:** {{git_discrepancy_count}} found
|
||||
**Issues Found:** {{high_count}} High, {{medium_count}} Medium, {{low_count}} Low
|
||||
**Issues Found:** {{critical_count}} Critical, {{high_count}} High, {{medium_count}} Medium, {{low_count}} Low
|
||||
|
||||
## 🔴 CRITICAL ISSUES
|
||||
- Tasks marked [x] but not actually implemented
|
||||
- Acceptance Criteria not implemented
|
||||
- Story claims files changed but no git evidence
|
||||
- Security vulnerabilities
|
||||
| # | Severity | Summary | Location |
|
||||
|---|----------|---------|----------|
|
||||
{{findings_table}}
|
||||
|
||||
## 🟡 MEDIUM ISSUES
|
||||
- Files changed but not documented in story File List
|
||||
- Uncommitted changes not tracked
|
||||
- Performance problems
|
||||
- Poor test coverage/quality
|
||||
- Code maintainability issues
|
||||
|
||||
## 🟢 LOW ISSUES
|
||||
- Code style improvements
|
||||
- Documentation gaps
|
||||
- Git commit message quality
|
||||
**{{total_count}} issues found** ({{critical_count}} critical, {{high_count}} high, {{medium_count}} medium, {{low_count}} low)
|
||||
</output>
|
||||
|
||||
<ask>What should I do with these issues?
|
||||
|
||||
1. **Fix them automatically** - I'll update the code and tests
|
||||
1. **Fix them automatically** - I'll fix all HIGH and CRITICAL, you approve each
|
||||
2. **Create action items** - Add to story Tasks/Subtasks for later
|
||||
3. **Show me details** - Deep dive into specific issues
|
||||
3. **Details on #N** - Explain specific issue
|
||||
|
||||
Choose [1], [2], or specify which issue to examine:</ask>
|
||||
|
||||
<check if="user chooses 1">
|
||||
<action>Fix all HIGH and MEDIUM issues in the code</action>
|
||||
<action>Fix all CRITICAL and HIGH issues in the code</action>
|
||||
<action>Add/update tests as needed</action>
|
||||
<action>Update File List in story if files changed</action>
|
||||
<action>Update story Dev Agent Record with fixes applied</action>
|
||||
<action>Set {{fixed_count}} = number of HIGH and MEDIUM issues fixed</action>
|
||||
<action>Set {{fixed_count}} = number of CRITICAL and HIGH issues fixed</action>
|
||||
<action>Set {{action_count}} = 0</action>
|
||||
</check>
|
||||
|
||||
|
|
@ -166,13 +218,13 @@
|
|||
</check>
|
||||
</step>
|
||||
|
||||
<step n="5" goal="Update story status and sync sprint tracking">
|
||||
<step n="6" goal="Update story status and sync sprint tracking">
|
||||
<!-- Determine new status based on review outcome -->
|
||||
<check if="all HIGH and MEDIUM issues fixed AND all ACs implemented">
|
||||
<check if="all CRITICAL and HIGH issues fixed AND all ACs implemented">
|
||||
<action>Set {{new_status}} = "done"</action>
|
||||
<action>Update story Status field to "done"</action>
|
||||
</check>
|
||||
<check if="HIGH or MEDIUM issues remain OR ACs not fully implemented">
|
||||
<check if="CRITICAL or HIGH issues remain OR ACs not fully implemented">
|
||||
<action>Set {{new_status}} = "in-progress"</action>
|
||||
<action>Update story Status field to "in-progress"</action>
|
||||
</check>
|
||||
|
|
|
|||
|
|
@ -23,3 +23,11 @@
|
|||
- [ ] Acceptance criteria satisfied
|
||||
- [ ] Tech-spec updated (if applicable)
|
||||
- [ ] Summary provided to user
|
||||
|
||||
## Adversarial Review
|
||||
|
||||
- [ ] Diff constructed (tracked changes from {baseline_commit} + new untracked files)
|
||||
- [ ] Adversarial review executed (subagent preferred)
|
||||
- [ ] Findings presented with severity and classification
|
||||
- [ ] User chose handling approach (walk through / auto-fix / skip)
|
||||
- [ ] Findings resolved or acknowledged
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
<step n="1" goal="Load project context and determine execution mode">
|
||||
|
||||
<action>Record current HEAD as baseline for later review. Run `git rev-parse HEAD` and store the result as {baseline_commit}.</action>
|
||||
|
||||
<action>Check if {project_context} exists. If yes, load it - this is your foundational reference for ALL implementation decisions (patterns, conventions, architecture).</action>
|
||||
|
||||
<action>Parse user input:
|
||||
|
|
@ -170,7 +172,7 @@ Use holistic judgment, not mechanical keyword matching.</action>
|
|||
|
||||
</step>
|
||||
|
||||
<step n="4" goal="Verify and complete">
|
||||
<step n="4" goal="Verify and transition to review">
|
||||
|
||||
<action>Verify: all tasks [x], tests passing, AC satisfied, patterns followed</action>
|
||||
|
||||
|
|
@ -185,17 +187,89 @@ Use holistic judgment, not mechanical keyword matching.</action>
|
|||
**Tests:** {{test_summary}}
|
||||
**AC Status:** {{ac_status}}
|
||||
|
||||
---
|
||||
Running adversarial code review...
|
||||
</output>
|
||||
|
||||
**Before committing (Recommended): Copy this code review prompt to a different LLM**
|
||||
<action>Proceed immediately to step 5</action>
|
||||
|
||||
</step>
|
||||
|
||||
<step n="5" goal="Adversarial code review (automatic)">
|
||||
|
||||
<action>Construct diff of all changes since workflow started and capture as {diff_output}:
|
||||
|
||||
**Tracked file changes:**
|
||||
|
||||
```bash
|
||||
git diff {baseline_commit}
|
||||
```
|
||||
You are a cynical, jaded code reviewer with zero patience for sloppy work. These uncommitted changes were submitted by a clueless weasel and you expect to find problems. Find at least five issues to fix or improve in it. Number them. Be skeptical of everything.
|
||||
```
|
||||
|
||||
**New files created by this workflow:**
|
||||
Only include untracked files that YOU actually created during steps 2-4. Do not include pre-existing untracked files. For each new file you created, include its full content as a "new file" addition.
|
||||
|
||||
Combine both into {diff_output} for review. Do NOT `git add` anything - this is read-only inspection.</action>
|
||||
|
||||
<action>Execute adversarial review using this hierarchy (try in order until one succeeds):
|
||||
|
||||
1. **Spawn subagent** (preferred) - pass the diff output along with this prompt:
|
||||
|
||||
```
|
||||
You are a cynical, jaded code reviewer with zero patience for sloppy work. This diff was submitted by a clueless weasel and you expect to find problems. Find at least five issues to fix or improve. Number them. Be skeptical of everything.
|
||||
|
||||
<diff>
|
||||
{diff_output}
|
||||
</diff>
|
||||
```
|
||||
|
||||
2. **CLI fallback** - pipe diff to `claude --print` with same prompt
|
||||
|
||||
3. **Inline self-review** - Review the diff output yourself using the cynical reviewer persona above
|
||||
</action>
|
||||
|
||||
<check if="zero findings returned">
|
||||
<action>HALT - Zero findings is suspicious. Adversarial review should always find something. Request user guidance.</action>
|
||||
</check>
|
||||
|
||||
<action>Process findings:
|
||||
|
||||
- Assign IDs: F1, F2, F3...
|
||||
- Assign severity: 🔴 Critical | 🟠 High | 🟡 Medium | 🟢 Low
|
||||
- Classify each: **real** (confirmed issue) | **noise** (false positive) | **uncertain** (needs discussion)
|
||||
</action>
|
||||
|
||||
<output>**Adversarial Review Findings**
|
||||
|
||||
| ID | Severity | Classification | Finding |
|
||||
| --- | -------- | -------------- | ------- |
|
||||
| F1 | 🟠 | real | ... |
|
||||
| F2 | 🟡 | noise | ... |
|
||||
| ... |
|
||||
|
||||
</output>
|
||||
|
||||
<action>You must explain what was implemented based on {user_skill_level}</action>
|
||||
<ask>How would you like to handle these findings?
|
||||
|
||||
**[1] Walk through** - Discuss each finding individually
|
||||
**[2] Auto-fix** - Automatically fix issues classified as "real"
|
||||
**[3] Skip** - Acknowledge and proceed to commit</ask>
|
||||
|
||||
<check if="1">
|
||||
<action>Present each finding one by one. For each, ask: fix now / skip / discuss</action>
|
||||
<action>Apply fixes as approved</action>
|
||||
</check>
|
||||
|
||||
<check if="2">
|
||||
<action>Automatically fix all findings classified as "real"</action>
|
||||
<action>Report what was fixed</action>
|
||||
</check>
|
||||
|
||||
<check if="3">
|
||||
<action>Acknowledge findings were reviewed and user chose to skip</action>
|
||||
</check>
|
||||
|
||||
<output>**Review complete. Ready to commit.**</output>
|
||||
|
||||
<action>Explain what was implemented based on {user_skill_level}</action>
|
||||
|
||||
</step>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,15 +21,6 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle agent compilation separately
|
||||
if (config.actionType === 'compile') {
|
||||
const result = await installer.compileAgents(config);
|
||||
console.log(chalk.green('\n✨ Agent compilation complete!'));
|
||||
console.log(chalk.cyan(`Rebuilt ${result.agentCount} agents and ${result.taskCount} tasks`));
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle quick update separately
|
||||
if (config.actionType === 'quick-update') {
|
||||
const result = await installer.quickUpdate(config);
|
||||
|
|
|
|||
|
|
@ -439,6 +439,33 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// For regular updates (modify flow), check manifest for custom module sources
|
||||
if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
|
||||
for (const customModule of config._existingInstall.customModules) {
|
||||
// Ensure we have an absolute sourcePath
|
||||
let absoluteSourcePath = customModule.sourcePath;
|
||||
|
||||
// Check if sourcePath is a cache-relative path (starts with _config)
|
||||
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
|
||||
// Convert cache-relative path to absolute path
|
||||
absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
|
||||
}
|
||||
// If no sourcePath but we have relativePath, convert it
|
||||
else if (!absoluteSourcePath && customModule.relativePath) {
|
||||
// relativePath is relative to the project root (parent of bmad dir)
|
||||
absoluteSourcePath = path.resolve(projectDir, customModule.relativePath);
|
||||
}
|
||||
// Ensure sourcePath is absolute for anything else
|
||||
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
|
||||
absoluteSourcePath = path.resolve(absoluteSourcePath);
|
||||
}
|
||||
|
||||
if (absoluteSourcePath) {
|
||||
customModulePaths.set(customModule.id, absoluteSourcePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build custom module paths map from customContent
|
||||
|
||||
// Handle selectedFiles (from existing install path or manual directory input)
|
||||
|
|
@ -589,20 +616,39 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
|
||||
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
|
||||
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
||||
console.log(chalk.dim(`DEBUG: Read ${existingFilesManifest.length} files from manifest`));
|
||||
console.log(chalk.dim(`DEBUG: Manifest has hashes: ${existingFilesManifest.some((f) => f.hash)}`));
|
||||
|
||||
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
|
||||
|
||||
console.log(chalk.dim(`DEBUG: Found ${customFiles.length} custom files, ${modifiedFiles.length} modified files`));
|
||||
if (modifiedFiles.length > 0) {
|
||||
console.log(chalk.yellow('DEBUG: Modified files:'));
|
||||
for (const f of modifiedFiles) console.log(chalk.dim(` - ${f.path}`));
|
||||
}
|
||||
|
||||
config._customFiles = customFiles;
|
||||
config._modifiedFiles = modifiedFiles;
|
||||
|
||||
// Also check cache directory for custom modules (like quick update does)
|
||||
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||
if (await fs.pathExists(cacheDir)) {
|
||||
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
|
||||
for (const cachedModule of cachedModules) {
|
||||
if (cachedModule.isDirectory()) {
|
||||
const moduleId = cachedModule.name;
|
||||
|
||||
// Skip if we already have this module from manifest
|
||||
if (customModulePaths.has(moduleId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cachedPath = path.join(cacheDir, moduleId);
|
||||
|
||||
// Check if this is actually a custom module (has module.yaml)
|
||||
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYamlPath)) {
|
||||
customModulePaths.set(moduleId, cachedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update module manager with the new custom module paths from cache
|
||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||
}
|
||||
|
||||
// If there are custom files, back them up temporarily
|
||||
if (customFiles.length > 0) {
|
||||
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
||||
|
|
@ -625,20 +671,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
|
||||
await fs.ensureDir(tempModifiedBackupDir);
|
||||
|
||||
console.log(chalk.yellow(`\nDEBUG: Backing up ${modifiedFiles.length} modified files to temp location`));
|
||||
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
|
||||
for (const modifiedFile of modifiedFiles) {
|
||||
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
||||
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
|
||||
console.log(chalk.dim(`DEBUG: Backing up ${relativePath} to temp`));
|
||||
await fs.ensureDir(path.dirname(tempBackupPath));
|
||||
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
||||
}
|
||||
spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
|
||||
|
||||
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
||||
} else {
|
||||
console.log(chalk.dim('DEBUG: No modified files detected'));
|
||||
}
|
||||
}
|
||||
} else if (existingInstall.installed && config._quickUpdate) {
|
||||
|
|
@ -654,6 +696,34 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
config._customFiles = customFiles;
|
||||
config._modifiedFiles = modifiedFiles;
|
||||
|
||||
// Also check cache directory for custom modules (like quick update does)
|
||||
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||
if (await fs.pathExists(cacheDir)) {
|
||||
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
|
||||
for (const cachedModule of cachedModules) {
|
||||
if (cachedModule.isDirectory()) {
|
||||
const moduleId = cachedModule.name;
|
||||
|
||||
// Skip if we already have this module from manifest
|
||||
if (customModulePaths.has(moduleId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cachedPath = path.join(cacheDir, moduleId);
|
||||
|
||||
// Check if this is actually a custom module (has module.yaml)
|
||||
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYamlPath)) {
|
||||
customModulePaths.set(moduleId, cachedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update module manager with the new custom module paths from cache
|
||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||
}
|
||||
|
||||
// Back up custom files
|
||||
if (customFiles.length > 0) {
|
||||
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
||||
|
|
@ -832,7 +902,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
// For dependency resolution, we need to pass the project root
|
||||
// Create a temporary module manager that knows about custom content locations
|
||||
const tempModuleManager = new ModuleManager({
|
||||
scanProjectForModules: true,
|
||||
bmadDir: bmadDir, // Pass bmadDir so we can check cache
|
||||
});
|
||||
|
||||
|
|
@ -1057,7 +1126,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
|
||||
// Pass pre-collected configuration to avoid re-prompting
|
||||
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
||||
selectedModules: config.modules || [],
|
||||
selectedModules: allModules || [],
|
||||
preCollectedConfig: ideConfigurations[ide] || null,
|
||||
verbose: config.verbose,
|
||||
});
|
||||
|
|
@ -1184,11 +1253,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
// Report custom and modified files if any were found
|
||||
if (customFiles.length > 0) {
|
||||
console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`));
|
||||
console.log(chalk.dim('The following custom files were found and restored:\n'));
|
||||
for (const customFile of customFiles) {
|
||||
const relativePath = path.relative(projectDir, customFile);
|
||||
console.log(chalk.dim(` • ${relativePath}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiedFiles.length > 0) {
|
||||
|
|
@ -2184,41 +2248,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
const configuredIdes = existingInstall.ides || [];
|
||||
const projectRoot = path.dirname(bmadDir);
|
||||
|
||||
// Get custom module sources from manifest and cache
|
||||
// Get custom module sources from cache
|
||||
const customModuleSources = new Map();
|
||||
|
||||
// First check manifest for backward compatibility
|
||||
if (existingInstall.customModules) {
|
||||
for (const customModule of existingInstall.customModules) {
|
||||
// Ensure we have an absolute sourcePath
|
||||
let absoluteSourcePath = customModule.sourcePath;
|
||||
|
||||
// Check if sourcePath is a cache-relative path (starts with _config/)
|
||||
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
|
||||
// Convert cache-relative path to absolute path
|
||||
absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
|
||||
}
|
||||
// If no sourcePath but we have relativePath, convert it
|
||||
else if (!absoluteSourcePath && customModule.relativePath) {
|
||||
// relativePath is relative to the project root (parent of bmad dir)
|
||||
absoluteSourcePath = path.resolve(projectRoot, customModule.relativePath);
|
||||
}
|
||||
// Ensure sourcePath is absolute for anything else
|
||||
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
|
||||
absoluteSourcePath = path.resolve(absoluteSourcePath);
|
||||
}
|
||||
|
||||
// Update the custom module object with the absolute path
|
||||
const updatedModule = {
|
||||
...customModule,
|
||||
sourcePath: absoluteSourcePath,
|
||||
};
|
||||
|
||||
customModuleSources.set(customModule.id, updatedModule);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check cache directory for any modules not in manifest
|
||||
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||
if (await fs.pathExists(cacheDir)) {
|
||||
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
|
|
@ -2277,126 +2308,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
}
|
||||
}
|
||||
|
||||
// Check for untracked custom modules (installed but not in manifest)
|
||||
const untrackedCustomModules = [];
|
||||
for (const installedModule of installedModules) {
|
||||
// Skip standard modules and core
|
||||
const standardModuleIds = ['bmb', 'bmgd', 'bmm', 'cis', 'core'];
|
||||
if (standardModuleIds.includes(installedModule)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this installed module is not tracked in customModules
|
||||
if (!customModuleSources.has(installedModule)) {
|
||||
const modulePath = path.join(bmadDir, installedModule);
|
||||
if (await fs.pathExists(modulePath)) {
|
||||
untrackedCustomModules.push({
|
||||
id: installedModule,
|
||||
name: installedModule, // We don't have the original name
|
||||
path: modulePath,
|
||||
untracked: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found untracked custom modules, offer to track them
|
||||
if (untrackedCustomModules.length > 0) {
|
||||
spinner.stop();
|
||||
console.log(chalk.yellow(`\n⚠️ Found ${untrackedCustomModules.length} custom module(s) not tracked in manifest:`));
|
||||
|
||||
for (const untracked of untrackedCustomModules) {
|
||||
console.log(chalk.dim(` • ${untracked.id} (installed at ${path.relative(projectRoot, untracked.path)})`));
|
||||
}
|
||||
|
||||
const { trackModules } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'trackModules',
|
||||
message: chalk.cyan('Would you like to scan for their source locations?'),
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (trackModules) {
|
||||
const { scanDirectory } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'scanDirectory',
|
||||
message: 'Enter directory to scan for custom module sources (or leave blank to skip):',
|
||||
default: projectRoot,
|
||||
validate: async (input) => {
|
||||
if (input && input.trim() !== '') {
|
||||
const expandedPath = path.resolve(input.trim());
|
||||
if (!(await fs.pathExists(expandedPath))) {
|
||||
return 'Directory does not exist';
|
||||
}
|
||||
const stats = await fs.stat(expandedPath);
|
||||
if (!stats.isDirectory()) {
|
||||
return 'Path must be a directory';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (scanDirectory && scanDirectory.trim() !== '') {
|
||||
console.log(chalk.dim('\nScanning for custom module sources...'));
|
||||
|
||||
// Scan for all module.yaml files
|
||||
const allModulePaths = await this.moduleManager.findModulesInProject(scanDirectory);
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const mm = new ModuleManager({ scanProjectForModules: true });
|
||||
|
||||
for (const untracked of untrackedCustomModules) {
|
||||
let foundSource = null;
|
||||
|
||||
// Try to find by module ID
|
||||
for (const modulePath of allModulePaths) {
|
||||
try {
|
||||
const moduleInfo = await mm.getModuleInfo(modulePath);
|
||||
if (moduleInfo && moduleInfo.id === untracked.id) {
|
||||
foundSource = {
|
||||
path: modulePath,
|
||||
info: moduleInfo,
|
||||
};
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Continue searching
|
||||
}
|
||||
}
|
||||
|
||||
if (foundSource) {
|
||||
console.log(chalk.green(` ✓ Found source for ${untracked.id}: ${path.relative(projectRoot, foundSource.path)}`));
|
||||
|
||||
// Add to manifest
|
||||
await this.manifest.addCustomModule(bmadDir, {
|
||||
id: untracked.id,
|
||||
name: foundSource.info.name || untracked.name,
|
||||
sourcePath: path.resolve(foundSource.path),
|
||||
installDate: new Date().toISOString(),
|
||||
tracked: true,
|
||||
});
|
||||
|
||||
// Add to customModuleSources for processing
|
||||
customModuleSources.set(untracked.id, {
|
||||
id: untracked.id,
|
||||
name: foundSource.info.name || untracked.name,
|
||||
sourcePath: path.resolve(foundSource.path),
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.yellow(` ⚠ Could not find source for ${untracked.id}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.dim('\nUntracked custom modules will remain installed but cannot be updated without their source.'));
|
||||
spinner.start('Preparing update...');
|
||||
}
|
||||
|
||||
// Handle missing custom module sources using shared method
|
||||
const customModuleResult = await this.handleMissingCustomSources(
|
||||
customModuleSources,
|
||||
|
|
@ -2414,18 +2325,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
|||
hasUpdate: true,
|
||||
}));
|
||||
|
||||
// Add untracked modules to the update list but mark them as untrackable
|
||||
for (const untracked of untrackedCustomModules) {
|
||||
if (!customModuleSources.has(untracked.id)) {
|
||||
customModulesFromManifest.push({
|
||||
...untracked,
|
||||
isCustom: true,
|
||||
hasUpdate: false, // Can't update without source
|
||||
untracked: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
||||
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ class ModuleManager {
|
|||
this.modulesSourcePath = getSourcePath('modules');
|
||||
this.xmlHandler = new XmlHandler();
|
||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||
this.scanProjectForModules = options.scanProjectForModules !== false; // Default to true for backward compatibility
|
||||
this.customModulePaths = new Map(); // Initialize custom module paths
|
||||
}
|
||||
|
||||
|
|
@ -116,76 +115,6 @@ class ModuleManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all modules in the project by searching for module.yaml files
|
||||
* @returns {Array} List of module paths
|
||||
*/
|
||||
async findModulesInProject() {
|
||||
const projectRoot = getProjectRoot();
|
||||
const modulePaths = new Set();
|
||||
|
||||
// Helper function to recursively scan directories
|
||||
async function scanDirectory(dir, excludePaths = []) {
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
// Skip hidden directories, node_modules, and literal placeholder directories
|
||||
if (
|
||||
entry.name.startsWith('.') ||
|
||||
entry.name === 'node_modules' ||
|
||||
entry.name === 'dist' ||
|
||||
entry.name === 'build' ||
|
||||
entry.name === '{project-root}'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip excluded paths
|
||||
if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip core module - it's always installed first and not selectable
|
||||
if (entry.name === 'core') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this directory contains a module (module.yaml OR custom.yaml)
|
||||
const moduleConfigPath = path.join(fullPath, 'module.yaml');
|
||||
const installerConfigPath = path.join(fullPath, '_module-installer', 'module.yaml');
|
||||
const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml');
|
||||
const rootCustomConfigPath = path.join(fullPath, 'custom.yaml');
|
||||
|
||||
if (
|
||||
(await fs.pathExists(moduleConfigPath)) ||
|
||||
(await fs.pathExists(installerConfigPath)) ||
|
||||
(await fs.pathExists(customConfigPath)) ||
|
||||
(await fs.pathExists(rootCustomConfigPath))
|
||||
) {
|
||||
modulePaths.add(fullPath);
|
||||
// Don't scan inside modules - they might have their own nested structures
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recursively scan subdirectories
|
||||
await scanDirectory(fullPath, excludePaths);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors (e.g., permission denied)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan the entire project, but exclude src/modules since we handle it separately
|
||||
await scanDirectory(projectRoot, [this.modulesSourcePath]);
|
||||
|
||||
return [...modulePaths];
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available modules (excluding core which is always installed)
|
||||
* @returns {Object} Object with modules array and customModules array
|
||||
|
|
@ -228,43 +157,19 @@ class ModuleManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Then, find all other modules in the project (only if scanning is enabled)
|
||||
if (this.scanProjectForModules) {
|
||||
const otherModulePaths = await this.findModulesInProject();
|
||||
for (const modulePath of otherModulePaths) {
|
||||
const moduleName = path.basename(modulePath);
|
||||
const relativePath = path.relative(getProjectRoot(), modulePath);
|
||||
|
||||
// Skip core module - it's always installed first and not selectable
|
||||
if (moduleName === 'core') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleInfo = await this.getModuleInfo(modulePath, moduleName, relativePath);
|
||||
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
||||
// Avoid duplicates - skip if we already have this module ID
|
||||
if (moduleInfo.isCustom) {
|
||||
customModules.push(moduleInfo);
|
||||
} else {
|
||||
modules.push(moduleInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for cached custom modules in _config/custom/
|
||||
if (this.bmadDir) {
|
||||
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
|
||||
if (await fs.pathExists(customCacheDir)) {
|
||||
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
|
||||
for (const entry of cacheEntries) {
|
||||
if (entry.isDirectory()) {
|
||||
const cachePath = path.join(customCacheDir, entry.name);
|
||||
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_config/custom');
|
||||
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
||||
moduleInfo.isCustom = true;
|
||||
moduleInfo.fromCache = true;
|
||||
customModules.push(moduleInfo);
|
||||
}
|
||||
// Check for cached custom modules in _config/custom/
|
||||
if (this.bmadDir) {
|
||||
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
|
||||
if (await fs.pathExists(customCacheDir)) {
|
||||
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
|
||||
for (const entry of cacheEntries) {
|
||||
if (entry.isDirectory()) {
|
||||
const cachePath = path.join(customCacheDir, entry.name);
|
||||
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_config/custom');
|
||||
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
||||
moduleInfo.isCustom = true;
|
||||
moduleInfo.fromCache = true;
|
||||
customModules.push(moduleInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,11 +195,7 @@ class UI {
|
|||
}
|
||||
|
||||
// Common actions
|
||||
choices.push(
|
||||
{ name: 'Modify BMAD Installation', value: 'update' },
|
||||
{ name: 'Add / Update Custom Content', value: 'add-custom' },
|
||||
{ name: 'Rebuild Agents', value: 'compile' },
|
||||
);
|
||||
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
|
||||
|
||||
const promptResult = await inquirer.prompt([
|
||||
{
|
||||
|
|
@ -224,64 +220,6 @@ class UI {
|
|||
};
|
||||
}
|
||||
|
||||
// Handle add custom content separately
|
||||
if (actionType === 'add-custom') {
|
||||
customContentConfig = await this.promptCustomContentSource();
|
||||
// After adding custom content, continue to select additional modules
|
||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||
|
||||
// Ask if user wants to add additional modules
|
||||
const { wantsMoreModules } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'wantsMoreModules',
|
||||
message: 'Do you want to add any additional modules?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
let selectedModules = [];
|
||||
if (wantsMoreModules) {
|
||||
const moduleChoices = await this.getModuleChoices(installedModuleIds, customContentConfig);
|
||||
selectedModules = await this.selectModules(moduleChoices);
|
||||
|
||||
// Process custom content selection
|
||||
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
|
||||
|
||||
if (selectedCustomContent.length > 0) {
|
||||
customContentConfig.selected = true;
|
||||
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
|
||||
|
||||
// Convert to module IDs
|
||||
const customContentModuleIds = [];
|
||||
const customHandler = new CustomHandler();
|
||||
for (const customFile of customContentConfig.selectedFiles) {
|
||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
||||
if (customInfo) {
|
||||
customContentModuleIds.push(customInfo.id);
|
||||
}
|
||||
}
|
||||
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
actionType: 'update',
|
||||
directory: confirmedDirectory,
|
||||
installCore: false, // Don't reinstall core
|
||||
modules: selectedModules,
|
||||
customContent: customContentConfig,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle agent compilation separately
|
||||
if (actionType === 'compile') {
|
||||
return {
|
||||
actionType: 'compile',
|
||||
directory: confirmedDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
// If actionType === 'update', handle it with the new flow
|
||||
// Return early with modify configuration
|
||||
if (actionType === 'update') {
|
||||
|
|
@ -293,7 +231,7 @@ class UI {
|
|||
{
|
||||
type: 'confirm',
|
||||
name: 'changeModuleSelection',
|
||||
message: 'Change which modules are installed?',
|
||||
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
|
@ -307,6 +245,14 @@ class UI {
|
|||
selectedModules = [...installedModuleIds];
|
||||
}
|
||||
|
||||
// After module selection, ask about custom modules
|
||||
const customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
|
||||
|
||||
// Merge any selected custom modules
|
||||
if (customModuleResult.selectedCustomModules.length > 0) {
|
||||
selectedModules.push(...customModuleResult.selectedCustomModules);
|
||||
}
|
||||
|
||||
// Get tool selection
|
||||
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
|
||||
|
||||
|
|
@ -337,7 +283,7 @@ class UI {
|
|||
ides: toolSelection.ides,
|
||||
skipIde: toolSelection.skipIde,
|
||||
coreConfig: coreConfig,
|
||||
customContent: { hasCustomContent: false },
|
||||
customContent: customModuleResult.customContentConfig,
|
||||
enableAgentVibes: enableTts,
|
||||
agentVibesInstalled: false,
|
||||
};
|
||||
|
|
@ -352,7 +298,7 @@ class UI {
|
|||
{
|
||||
type: 'confirm',
|
||||
name: 'wantsOfficialModules',
|
||||
message: 'Will you be installing any official modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
|
@ -368,7 +314,7 @@ class UI {
|
|||
{
|
||||
type: 'confirm',
|
||||
name: 'wantsCustomContent',
|
||||
message: 'Will you be installing any locally stored custom content?',
|
||||
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
|
@ -734,14 +680,8 @@ class UI {
|
|||
|
||||
// Add official modules
|
||||
const { ModuleManager } = require('../installers/lib/modules/manager');
|
||||
// For new installations, don't scan project yet (will do after custom content is discovered)
|
||||
// For existing installations, scan if user selected custom content
|
||||
const shouldScanProject =
|
||||
!isNewInstallation && customContentConfig && customContentConfig.hasCustomContent && customContentConfig.selected;
|
||||
const moduleManager = new ModuleManager({
|
||||
scanProjectForModules: shouldScanProject,
|
||||
});
|
||||
const { modules: availableModules, customModules: customModulesFromProject } = await moduleManager.listAvailable();
|
||||
const moduleManager = new ModuleManager();
|
||||
const { modules: availableModules, customModules: customModulesFromCache } = await moduleManager.listAvailable();
|
||||
|
||||
// First, add all items to appropriate sections
|
||||
const allCustomModules = [];
|
||||
|
|
@ -749,14 +689,14 @@ class UI {
|
|||
// Add custom content items from directory
|
||||
allCustomModules.push(...customContentItems);
|
||||
|
||||
// Add custom modules from project scan (if scanning is enabled)
|
||||
for (const mod of customModulesFromProject) {
|
||||
// Add custom modules from cache
|
||||
for (const mod of customModulesFromCache) {
|
||||
// Skip if this module is already in customContentItems (by path)
|
||||
const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
|
||||
|
||||
if (!isDuplicate) {
|
||||
allCustomModules.push({
|
||||
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(${mod.source})`)}`,
|
||||
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`,
|
||||
value: mod.id,
|
||||
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
||||
});
|
||||
|
|
@ -803,7 +743,9 @@ class UI {
|
|||
},
|
||||
]);
|
||||
|
||||
return moduleAnswer.modules || [];
|
||||
const selected = moduleAnswer.modules || [];
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1472,6 +1414,136 @@ class UI {
|
|||
|
||||
return customContentConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle custom modules in the modify flow
|
||||
* @param {string} directory - Installation directory
|
||||
* @param {Array} selectedModules - Currently selected modules
|
||||
* @returns {Object} Result with selected custom modules and custom content config
|
||||
*/
|
||||
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
||||
// Get existing installation to find custom modules
|
||||
const { existingInstall } = await this.getExistingInstallation(directory);
|
||||
|
||||
// Check if there are any custom modules in cache
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
const installer = new Installer();
|
||||
const { bmadDir } = await installer.findBmadDir(directory);
|
||||
|
||||
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||
const cachedCustomModules = [];
|
||||
|
||||
if (await fs.pathExists(cacheDir)) {
|
||||
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYamlPath)) {
|
||||
const yaml = require('yaml');
|
||||
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
||||
const moduleData = yaml.parse(content);
|
||||
|
||||
cachedCustomModules.push({
|
||||
id: entry.name,
|
||||
name: moduleData.name || entry.name,
|
||||
description: moduleData.description || 'Custom module from cache',
|
||||
checked: selectedModules.includes(entry.name),
|
||||
fromCache: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
selectedCustomModules: [],
|
||||
customContentConfig: { hasCustomContent: false },
|
||||
};
|
||||
|
||||
if (cachedCustomModules.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ask user about custom modules
|
||||
console.log(chalk.cyan('\n⚙️ Custom Modules'));
|
||||
console.log(chalk.dim('Found custom modules in your installation:'));
|
||||
|
||||
const { customAction } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'customAction',
|
||||
message: 'What would you like to do with custom modules?',
|
||||
choices: [
|
||||
{ name: 'Keep all existing custom modules', value: 'keep' },
|
||||
{ name: 'Select which custom modules to keep', value: 'select' },
|
||||
{ name: 'Add new custom modules', value: 'add' },
|
||||
{ name: 'Remove all custom modules', value: 'remove' },
|
||||
],
|
||||
default: 'keep',
|
||||
},
|
||||
]);
|
||||
|
||||
switch (customAction) {
|
||||
case 'keep': {
|
||||
// Keep all existing custom modules
|
||||
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
||||
console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'select': {
|
||||
// Let user choose which to keep
|
||||
const choices = cachedCustomModules.map((m) => ({
|
||||
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
|
||||
value: m.id,
|
||||
}));
|
||||
|
||||
const { keepModules } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'keepModules',
|
||||
message: 'Select custom modules to keep:',
|
||||
choices: choices,
|
||||
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
|
||||
},
|
||||
]);
|
||||
result.selectedCustomModules = keepModules;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'add': {
|
||||
// First ask to keep existing ones
|
||||
const { keepExisting } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'keepExisting',
|
||||
message: 'Keep existing custom modules?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (keepExisting) {
|
||||
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
||||
}
|
||||
|
||||
// Then prompt for new ones (reuse existing method)
|
||||
const newCustomContent = await this.promptCustomContentSource();
|
||||
if (newCustomContent.hasCustomContent && newCustomContent.selected) {
|
||||
result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
|
||||
result.customContentConfig = newCustomContent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
// Remove all custom modules
|
||||
console.log(chalk.yellow('All custom modules will be removed from the installation'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UI };
|
||||
|
|
|
|||
Loading…
Reference in New Issue