custom modules install after any non custom modules selected and after the core, manifest tracks custom modules separately to ensure always installed from the custom cache

This commit is contained in:
Brian Madison 2025-12-14 10:03:25 +08:00
parent 9fe79882b2
commit 7f742d4af6
12 changed files with 340 additions and 51 deletions

View File

@ -73,7 +73,7 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.json-language-features"

View File

@ -4,16 +4,16 @@
# #
# The installer will: # The installer will:
# 1. Ask users if they want to install subagents (all/selective/none) # 1. Ask users if they want to install subagents (all/selective/none)
# 2. Ask where to install (project-level .claude/agents/{bmad_folder}/ or user-level ~/.claude/agents/{bmad_folder}/) # 2. Ask where to install (project-level .claude/agents/_bmad/ or user-level ~/.claude/agents/_bmad/)
# 3. Only inject content related to selected subagents # 3. Only inject content related to selected subagents
# 4. Templates stay in {bmad_folder}/ directory and are referenced from there # 4. Templates stay in _bmad/ directory and are referenced from there
# 5. Injections are placed at specific sections where each subagent is most valuable # 5. Injections are placed at specific sections where each subagent is most valuable
injections: injections:
# ===== PRD WORKFLOW INJECTIONS ===== # ===== PRD WORKFLOW INJECTIONS =====
# PRD Subagent Instructions # PRD Subagent Instructions
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md" - file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-subagent-instructions" point: "prd-subagent-instructions"
requires: "all-prd-subagents" requires: "all-prd-subagents"
content: | content: |
@ -25,7 +25,7 @@ injections:
- <CRITICAL>Use `bmm-technical-decisions-curator` to capture all technical mentions</CRITICAL> - <CRITICAL>Use `bmm-technical-decisions-curator` to capture all technical mentions</CRITICAL>
# PRD Requirements Analysis # PRD Requirements Analysis
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md" - file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-requirements-analysis" point: "prd-requirements-analysis"
requires: "requirements-analyst" requires: "requirements-analyst"
content: | content: |
@ -33,7 +33,7 @@ injections:
**Subagent Hint**: Use `bmm-requirements-analyst` to validate requirements are testable and complete. **Subagent Hint**: Use `bmm-requirements-analyst` to validate requirements are testable and complete.
# PRD User Journey Mapping # PRD User Journey Mapping
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md" - file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-user-journey" point: "prd-user-journey"
requires: "user-journey-mapper" requires: "user-journey-mapper"
content: | content: |
@ -41,7 +41,7 @@ injections:
**Subagent Hint**: Use `bmm-user-journey-mapper` to map all user types and their value paths. **Subagent Hint**: Use `bmm-user-journey-mapper` to map all user types and their value paths.
# PRD Epic Optimization # PRD Epic Optimization
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md" - file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-epic-optimization" point: "prd-epic-optimization"
requires: "epic-optimizer" requires: "epic-optimizer"
content: | content: |
@ -49,7 +49,7 @@ injections:
**Subagent Hint**: Use `bmm-epic-optimizer` to validate epic boundaries deliver coherent value. **Subagent Hint**: Use `bmm-epic-optimizer` to validate epic boundaries deliver coherent value.
# PRD Document Review # PRD Document Review
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md" - file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-checklist-review" point: "prd-checklist-review"
requires: "document-reviewer" requires: "document-reviewer"
content: | content: |
@ -57,7 +57,7 @@ injections:
**Subagent Hint**: Use `bmm-document-reviewer` to validate PRD completeness before finalizing. **Subagent Hint**: Use `bmm-document-reviewer` to validate PRD completeness before finalizing.
# Technical Decisions Curator # Technical Decisions Curator
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md" - file: "_bmad/bmm/workflows/prd/instructions.md"
point: "technical-decisions-curator" point: "technical-decisions-curator"
requires: "technical-decisions-curator" requires: "technical-decisions-curator"
content: | content: |
@ -71,7 +71,7 @@ injections:
# ===== MARKET RESEARCH TEMPLATE INJECTIONS ===== # ===== MARKET RESEARCH TEMPLATE INJECTIONS =====
# Market TAM/SAM/SOM Calculations # Market TAM/SAM/SOM Calculations
- file: "{bmad_folder}/bmm/templates/market.md" - file: "_bmad/bmm/templates/market.md"
point: "market-tam-calculations" point: "market-tam-calculations"
requires: "data-analyst" requires: "data-analyst"
content: | content: |
@ -82,7 +82,7 @@ injections:
</llm> </llm>
# Market Trends Analysis # Market Trends Analysis
- file: "{bmad_folder}/bmm/templates/market.md" - file: "_bmad/bmm/templates/market.md"
point: "market-trends-analysis" point: "market-trends-analysis"
requires: "trend-spotter" requires: "trend-spotter"
content: | content: |
@ -93,7 +93,7 @@ injections:
</llm> </llm>
# Market Customer Personas # Market Customer Personas
- file: "{bmad_folder}/bmm/templates/market.md" - file: "_bmad/bmm/templates/market.md"
point: "market-customer-segments" point: "market-customer-segments"
requires: "user-researcher" requires: "user-researcher"
content: | content: |
@ -104,7 +104,7 @@ injections:
</llm> </llm>
# Market Research Review # Market Research Review
- file: "{bmad_folder}/bmm/templates/market.md" - file: "_bmad/bmm/templates/market.md"
point: "market-executive-summary" point: "market-executive-summary"
requires: "document-reviewer" requires: "document-reviewer"
content: | content: |
@ -116,7 +116,7 @@ injections:
# ===== COMPETITOR ANALYSIS TEMPLATE INJECTIONS ===== # ===== COMPETITOR ANALYSIS TEMPLATE INJECTIONS =====
# Competitor Intelligence Gathering # Competitor Intelligence Gathering
- file: "{bmad_folder}/bmm/templates/competitor.md" - file: "_bmad/bmm/templates/competitor.md"
point: "competitor-intelligence" point: "competitor-intelligence"
requires: "market-researcher" requires: "market-researcher"
content: | content: |
@ -127,7 +127,7 @@ injections:
</llm> </llm>
# Competitor Technical Analysis # Competitor Technical Analysis
- file: "{bmad_folder}/bmm/templates/competitor.md" - file: "_bmad/bmm/templates/competitor.md"
point: "competitor-tech-stack" point: "competitor-tech-stack"
requires: "technical-evaluator" requires: "technical-evaluator"
content: | content: |
@ -138,7 +138,7 @@ injections:
</llm> </llm>
# Competitor Metrics Analysis # Competitor Metrics Analysis
- file: "{bmad_folder}/bmm/templates/competitor.md" - file: "_bmad/bmm/templates/competitor.md"
point: "competitor-metrics" point: "competitor-metrics"
requires: "data-analyst" requires: "data-analyst"
content: | content: |
@ -148,7 +148,7 @@ injections:
</llm> </llm>
# Competitor Analysis Review # Competitor Analysis Review
- file: "{bmad_folder}/bmm/templates/competitor.md" - file: "_bmad/bmm/templates/competitor.md"
point: "competitor-executive-summary" point: "competitor-executive-summary"
requires: "document-reviewer" requires: "document-reviewer"
content: | content: |
@ -160,7 +160,7 @@ injections:
# ===== PROJECT BRIEF TEMPLATE INJECTIONS ===== # ===== PROJECT BRIEF TEMPLATE INJECTIONS =====
# Brief Problem Validation # Brief Problem Validation
- file: "{bmad_folder}/bmm/templates/brief.md" - file: "_bmad/bmm/templates/brief.md"
point: "brief-problem-validation" point: "brief-problem-validation"
requires: "market-researcher" requires: "market-researcher"
content: | content: |
@ -170,7 +170,7 @@ injections:
</llm> </llm>
# Brief Target User Analysis # Brief Target User Analysis
- file: "{bmad_folder}/bmm/templates/brief.md" - file: "_bmad/bmm/templates/brief.md"
point: "brief-user-analysis" point: "brief-user-analysis"
requires: "user-researcher" requires: "user-researcher"
content: | content: |
@ -180,7 +180,7 @@ injections:
</llm> </llm>
# Brief Success Metrics # Brief Success Metrics
- file: "{bmad_folder}/bmm/templates/brief.md" - file: "_bmad/bmm/templates/brief.md"
point: "brief-success-metrics" point: "brief-success-metrics"
requires: "data-analyst" requires: "data-analyst"
content: | content: |
@ -190,7 +190,7 @@ injections:
</llm> </llm>
# Brief Technical Feasibility # Brief Technical Feasibility
- file: "{bmad_folder}/bmm/templates/brief.md" - file: "_bmad/bmm/templates/brief.md"
point: "brief-technical-feasibility" point: "brief-technical-feasibility"
requires: "technical-evaluator" requires: "technical-evaluator"
content: | content: |
@ -200,7 +200,7 @@ injections:
</llm> </llm>
# Brief Requirements Extraction # Brief Requirements Extraction
- file: "{bmad_folder}/bmm/templates/brief.md" - file: "_bmad/bmm/templates/brief.md"
point: "brief-requirements" point: "brief-requirements"
requires: "requirements-analyst" requires: "requirements-analyst"
content: | content: |
@ -210,7 +210,7 @@ injections:
</llm> </llm>
# Brief Document Review # Brief Document Review
- file: "{bmad_folder}/bmm/templates/brief.md" - file: "_bmad/bmm/templates/brief.md"
point: "brief-final-review" point: "brief-final-review"
requires: "document-reviewer" requires: "document-reviewer"
content: | content: |

View File

@ -84,4 +84,4 @@ To test subagent installation:
2. Select BMM module and Claude Code 2. Select BMM module and Claude Code
3. Verify prompts appear for subagent selection 3. Verify prompts appear for subagent selection
4. Check `.claude/agents/` for installed subagents 4. Check `.claude/agents/` for installed subagents
5. Verify injection points are replaced in `.claude/commands/{bmad_folder}/` and the various tasks and templates under `{bmad_folder}/...` 5. Verify injection points are replaced in `.claude/commands/_bmad/` and the various tasks and templates under `_bmad/...`

View File

@ -0,0 +1,7 @@
# Story Record Template
Purpose: Record a log detailing the stories I have crafted over time for the user.
## Narratives Told Record
<!-- track stories created metadata with the user over time -->

View File

@ -0,0 +1,7 @@
# Story Record Template
Purpose: Record a log of learned users story telling or story building preferences.
## User Preferences
<!-- record any user preferences about story crafting the user prefers -->

View File

@ -14,6 +14,10 @@ agent:
communication_style: Speaks like a bard weaving an epic tale - flowery, whimsical, every sentence enraptures and draws you deeper communication_style: Speaks like a bard weaving an epic tale - flowery, whimsical, every sentence enraptures and draws you deeper
principles: Powerful narratives leverage timeless human truths. Find the authentic story. Make the abstract concrete through vivid details. principles: Powerful narratives leverage timeless human truths. Find the authentic story. Make the abstract concrete through vivid details.
critical_actions:
- "Load COMPLETE file {agent_sidecar_folder}/storyteller-sidecar/story-preferences.md and review remember the User Preferences"
- "Load COMPLETE file {agent_sidecar_folder}/storyteller-sidecar/stories-told.md and review the history of stories created for this user"
menu: menu:
- trigger: story - trigger: story
exec: "{project-root}/_bmad/cis/workflows/storytelling/workflow.yaml" exec: "{project-root}/_bmad/cis/workflows/storytelling/workflow.yaml"

View File

@ -144,12 +144,18 @@ class CustomModuleCache {
const sourceHash = await this.calculateHash(sourcePath); const sourceHash = await this.calculateHash(sourcePath);
const cacheHash = await this.calculateHash(cacheDir); const cacheHash = await this.calculateHash(cacheDir);
// Update manifest - don't store originalPath for source control friendliness // Update manifest - don't store absolute paths for portability
// Clean metadata to remove absolute paths
const cleanMetadata = { ...metadata };
if (cleanMetadata.sourcePath) {
delete cleanMetadata.sourcePath;
}
cacheManifest[moduleId] = { cacheManifest[moduleId] = {
originalHash: sourceHash, originalHash: sourceHash,
cacheHash: cacheHash, cacheHash: cacheHash,
cachedAt: new Date().toISOString(), cachedAt: new Date().toISOString(),
...metadata, ...cleanMetadata,
}; };
await this.updateCacheManifest(cacheManifest); await this.updateCacheManifest(cacheManifest);

View File

@ -417,12 +417,13 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Collect configurations for modules (skip if quick update already collected them) // Collect configurations for modules (skip if quick update already collected them)
let moduleConfigs; let moduleConfigs;
let customModulePaths = new Map();
if (config._quickUpdate) { if (config._quickUpdate) {
// Quick update already collected all configs, use them directly // Quick update already collected all configs, use them directly
moduleConfigs = this.configCollector.collectedConfig; moduleConfigs = this.configCollector.collectedConfig;
} else { } else {
// Build custom module paths map from customContent // Build custom module paths map from customContent
const customModulePaths = new Map();
// Handle selectedFiles (from existing install path or manual directory input) // Handle selectedFiles (from existing install path or manual directory input)
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) { if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
@ -435,6 +436,13 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
} }
} }
// Handle new custom content sources from UI
if (config.customContent && config.customContent.sources) {
for (const source of config.customContent.sources) {
customModulePaths.set(source.id, source.path);
}
}
// Handle cachedModules (from new install path where modules are cached) // Handle cachedModules (from new install path where modules are cached)
// Only include modules that were actually selected for installation // Only include modules that were actually selected for installation
if (config.customContent && config.customContent.cachedModules) { if (config.customContent && config.customContent.cachedModules) {
@ -479,6 +487,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Set bmad folder name on module manager and IDE manager for placeholder replacement // Set bmad folder name on module manager and IDE manager for placeholder replacement
this.moduleManager.setBmadFolderName(bmadFolderName); this.moduleManager.setBmadFolderName(bmadFolderName);
this.moduleManager.setCoreConfig(moduleConfigs.core || {}); this.moduleManager.setCoreConfig(moduleConfigs.core || {});
this.moduleManager.setCustomModulePaths(customModulePaths);
this.ideManager.setBmadFolderName(bmadFolderName); this.ideManager.setBmadFolderName(bmadFolderName);
// Tool selection will be collected after we determine if it's a reinstall/update/new install // Tool selection will be collected after we determine if it's a reinstall/update/new install
@ -733,6 +742,26 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.text = 'Creating directory structure...'; spinner.text = 'Creating directory structure...';
await this.createDirectoryStructure(bmadDir); await this.createDirectoryStructure(bmadDir);
// Cache custom modules if any
if (customModulePaths && customModulePaths.size > 0) {
spinner.text = 'Caching custom modules...';
const { CustomModuleCache } = require('./custom-module-cache');
const customCache = new CustomModuleCache(bmadDir);
for (const [moduleId, sourcePath] of customModulePaths) {
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
sourcePath: sourcePath, // Store original path for updates
});
// Update the customModulePaths to use the cached location
customModulePaths.set(moduleId, cachedInfo.cachePath);
}
// Update module manager with the cached paths
this.moduleManager.setCustomModulePaths(customModulePaths);
spinner.succeed('Custom modules cached');
}
// Get project root // Get project root
const projectRoot = getProjectRoot(); const projectRoot = getProjectRoot();
@ -790,6 +819,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const modulesToInstall = allModules; const modulesToInstall = allModules;
// For dependency resolution, we only need regular modules (not custom modules)
// Custom modules are already installed in _bmad and don't need dependency resolution from source
const regularModulesForResolution = allModules.filter((module) => {
// Check if this is a custom module
const isCustom =
customModulePaths.has(module) ||
(finalCustomContent && finalCustomContent.cachedModules && finalCustomContent.cachedModules.some((cm) => cm.id === module)) ||
(finalCustomContent &&
finalCustomContent.selected &&
finalCustomContent.selectedFiles &&
finalCustomContent.selectedFiles.some((f) => f.includes(module)));
return !isCustom;
});
// For dependency resolution, we need to pass the project root // For dependency resolution, we need to pass the project root
// Create a temporary module manager that knows about custom content locations // Create a temporary module manager that knows about custom content locations
const tempModuleManager = new ModuleManager({ const tempModuleManager = new ModuleManager({
@ -797,13 +840,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
bmadDir: bmadDir, // Pass bmadDir so we can check cache bmadDir: bmadDir, // Pass bmadDir so we can check cache
}); });
// Make sure custom modules are discoverable const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
// The dependency resolver needs to know about these modules
// We'll handle custom modules separately in the installation loop
}
const resolution = await this.dependencyResolver.resolve(projectRoot, allModules, {
verbose: config.verbose, verbose: config.verbose,
moduleManager: tempModuleManager, moduleManager: tempModuleManager,
}); });
@ -974,7 +1011,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
config._customModulesToTrack.push({ config._customModulesToTrack.push({
id: customInfo.id, id: customInfo.id,
name: customInfo.name, name: customInfo.name,
sourcePath: sourcePath, sourcePath: useCache ? `_config/custom/${customInfo.id}` : sourcePath,
installDate: new Date().toISOString(), installDate: new Date().toISOString(),
}); });
} else { } else {
@ -1116,6 +1153,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, { const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, {
ides: config.ides || [], ides: config.ides || [],
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
customModules: config._customModulesToTrack || [], // Custom modules to exclude from regular modules list
}); });
// Add custom modules to manifest (now that it exists) // Add custom modules to manifest (now that it exists)
@ -2086,12 +2124,17 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Compile using the same compiler as initial installation // Compile using the same compiler as initial installation
const { compileAgent } = require('../../../lib/agent/compiler'); const { compileAgent } = require('../../../lib/agent/compiler');
const { xml } = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), { const result = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
config: coreConfig, config: coreConfig,
}); });
// Check if compilation succeeded
if (!result || !result.xml) {
throw new Error(`Failed to compile agent ${agentName}: No XML returned from compiler`);
}
// Replace _bmad with actual folder name if needed // Replace _bmad with actual folder name if needed
const finalXml = xml.replaceAll('_bmad', path.basename(bmadDir)); const finalXml = result.xml.replaceAll('_bmad', path.basename(bmadDir));
// Write the rebuilt .md file with POSIX-compliant final newline // Write the rebuilt .md file with POSIX-compliant final newline
const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n'; const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';

View File

@ -34,13 +34,19 @@ class ManifestGenerator {
// Store modules list (all modules including preserved ones) // Store modules list (all modules including preserved ones)
const preservedModules = options.preservedModules || []; const preservedModules = options.preservedModules || [];
const customModules = options.customModules || [];
// Scan the bmad directory to find all actually installed modules // Scan the bmad directory to find all actually installed modules
const installedModules = await this.scanInstalledModules(bmadDir); const installedModules = await this.scanInstalledModules(bmadDir);
// Deduplicate modules list to prevent duplicates // Filter out custom modules from the regular modules list
this.modules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])]; const customModuleIds = new Set(customModules.map((cm) => cm.id));
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])]; // All installed modules get rescanned const regularModules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])].filter(
(module) => !customModuleIds.has(module),
);
this.modules = regularModules;
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])].filter((module) => !customModuleIds.has(module)); // Also exclude custom modules from rescanning
// For CSV manifests, we need to include ALL modules that are installed // For CSV manifests, we need to include ALL modules that are installed
// preservedModules controls which modules stay as-is in the CSV (don't get rescanned) // preservedModules controls which modules stay as-is in the CSV (don't get rescanned)

View File

@ -29,6 +29,7 @@ class ModuleManager {
this.xmlHandler = new XmlHandler(); this.xmlHandler = new XmlHandler();
this.bmadFolderName = 'bmad'; // Default, can be overridden this.bmadFolderName = 'bmad'; // Default, can be overridden
this.scanProjectForModules = options.scanProjectForModules !== false; // Default to true for backward compatibility this.scanProjectForModules = options.scanProjectForModules !== false; // Default to true for backward compatibility
this.customModulePaths = new Map(); // Initialize custom module paths
} }
/** /**
@ -47,6 +48,14 @@ class ModuleManager {
this.coreConfig = coreConfig; this.coreConfig = coreConfig;
} }
/**
* Set custom module paths for priority lookup
* @param {Map<string, string>} customModulePaths - Map of module ID to source path
*/
setCustomModulePaths(customModulePaths) {
this.customModulePaths = customModulePaths;
}
/** /**
* Copy a file and replace _bmad placeholder with actual folder name * Copy a file and replace _bmad placeholder with actual folder name
* @param {string} sourcePath - Source file path * @param {string} sourcePath - Source file path
@ -340,6 +349,11 @@ class ModuleManager {
async findModuleSource(moduleName) { async findModuleSource(moduleName) {
const projectRoot = getProjectRoot(); const projectRoot = getProjectRoot();
// First check custom module paths if they exist
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
return this.customModulePaths.get(moduleName);
}
// First, check src/modules // First, check src/modules
const srcModulePath = path.join(this.modulesSourcePath, moduleName); const srcModulePath = path.join(this.modulesSourcePath, moduleName);
if (await fs.pathExists(srcModulePath)) { if (await fs.pathExists(srcModulePath)) {

View File

@ -346,6 +346,12 @@ async function compileAgent(yamlContent, answers = {}, agentName = '', targetPat
// Replace {bmad_memory} in XML content // Replace {bmad_memory} in XML content
let xml = await compileToXml(cleanYaml, agentName, targetPath); let xml = await compileToXml(cleanYaml, agentName, targetPath);
// Ensure xml is a string before attempting replaceAll
if (typeof xml !== 'string') {
throw new TypeError('compileToXml did not return a string');
}
if (finalAnswers.bmad_memory) { if (finalAnswers.bmad_memory) {
xml = xml.replaceAll('{bmad_memory}', finalAnswers.bmad_memory); xml = xml.replaceAll('{bmad_memory}', finalAnswers.bmad_memory);
} }

View File

@ -40,19 +40,22 @@ class UI {
let legacyBmadPath = null; let legacyBmadPath = null;
// First check for legacy .bmad folder (instead of _bmad) // First check for legacy .bmad folder (instead of _bmad)
const entries = await fs.readdir(confirmedDirectory, { withFileTypes: true }); // Only check if directory exists
for (const entry of entries) { if (await fs.pathExists(confirmedDirectory)) {
if (entry.isDirectory() && entry.name === '.bmad') { const entries = await fs.readdir(confirmedDirectory, { withFileTypes: true });
hasLegacyBmadFolder = true; for (const entry of entries) {
legacyBmadPath = path.join(confirmedDirectory, '.bmad'); if (entry.isDirectory() && entry.name === '.bmad') {
bmadDir = legacyBmadPath; hasLegacyBmadFolder = true;
legacyBmadPath = path.join(confirmedDirectory, '.bmad');
bmadDir = legacyBmadPath;
// Check if it has _cfg folder // Check if it has _cfg folder
const cfgPath = path.join(legacyBmadPath, '_cfg'); const cfgPath = path.join(legacyBmadPath, '_cfg');
if (await fs.pathExists(cfgPath)) { if (await fs.pathExists(cfgPath)) {
hasLegacyCfg = true; hasLegacyCfg = true;
}
break;
} }
break;
} }
} }
@ -154,6 +157,7 @@ class UI {
choices: [ choices: [
{ name: 'Quick Update (Settings Preserved)', value: 'quick-update' }, { name: 'Quick Update (Settings Preserved)', value: 'quick-update' },
{ name: 'Modify BMAD Installation (Confirm or change each setting)', value: 'update' }, { name: 'Modify BMAD Installation (Confirm or change each setting)', value: 'update' },
{ name: 'Add Custom Content', value: 'add-custom' },
{ name: 'Remove BMad Folder and Reinstall (Full clean install - BMad Customization Will Be Lost)', value: 'reinstall' }, { name: 'Remove BMad Folder and Reinstall (Full clean install - BMad Customization Will Be Lost)', value: 'reinstall' },
{ name: 'Compile Agents (Quick rebuild of all agent .md files)', value: 'compile' }, { name: 'Compile Agents (Quick rebuild of all agent .md files)', value: 'compile' },
{ name: 'Cancel', value: 'cancel' }, { name: 'Cancel', value: 'cancel' },
@ -175,6 +179,56 @@ 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 // Handle agent compilation separately
if (actionType === 'compile') { if (actionType === 'compile') {
return { return {
@ -198,6 +252,24 @@ class UI {
// If actionType === 'update' or 'reinstall', continue with normal flow below // If actionType === 'update' or 'reinstall', continue with normal flow below
} }
// Handle custom content for new installations
if (!hasExistingInstall) {
const { wantsCustomContent } = await inquirer.prompt([
{
type: 'confirm',
name: 'wantsCustomContent',
message: 'Will you be installing any custom content?',
default: false,
},
]);
if (wantsCustomContent) {
customContentConfig = await this.promptCustomContentSource();
} else {
customContentConfig._shouldAsk = true; // Ask later after modules are selected
}
}
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory); const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
const coreConfig = await this.collectCoreConfig(confirmedDirectory); const coreConfig = await this.collectCoreConfig(confirmedDirectory);
@ -237,6 +309,9 @@ class UI {
} }
// Filter out custom content markers and add module IDs // Filter out custom content markers and add module IDs
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds]; selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
} else if (customContentConfig.selectedModuleIds && customContentConfig.selectedModuleIds.length > 0) {
// Custom modules were selected from sources
selectedModules = [...selectedModules, ...customContentConfig.selectedModuleIds];
} else if (customContentConfig.hasCustomContent) { } else if (customContentConfig.hasCustomContent) {
// User provided custom content but didn't select any // User provided custom content but didn't select any
customContentConfig.selected = false; customContentConfig.selected = false;
@ -1184,6 +1259,127 @@ class UI {
return { hasCustomContent: false }; return { hasCustomContent: false };
} }
} }
/**
* Prompt user for custom content source location
* @returns {Object} Custom content configuration
*/
async promptCustomContentSource() {
const customContentConfig = { hasCustomContent: true, sources: [] };
// Keep asking for more sources until user is done
while (true) {
console.log(chalk.cyan('\n📦 Adding Custom Content'));
let sourcePath;
let isValid = false;
while (!isValid) {
const { path: inputPath } = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter the path to your custom content folder:',
validate: async (input) => {
if (!input || input.trim() === '') {
return 'Path is required';
}
try {
// Expand the path
const expandedPath = this.expandUserPath(input.trim());
// Check if path exists
if (!(await fs.pathExists(expandedPath))) {
return 'Path does not exist';
}
// Check if it's a directory
const stat = await fs.stat(expandedPath);
if (!stat.isDirectory()) {
return 'Path must be a directory';
}
// Check for module.yaml in the root
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
if (!(await fs.pathExists(moduleYamlPath))) {
return 'Directory must contain a module.yaml file in the root';
}
// Try to parse the module.yaml to get the module ID
try {
const yaml = require('yaml');
const content = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(content);
if (!moduleData.code) {
return 'module.yaml must contain a "code" field for the module ID';
}
} catch (error) {
return 'Invalid module.yaml file: ' + error.message;
}
return true;
} catch (error) {
return 'Error validating path: ' + error.message;
}
},
},
]);
sourcePath = this.expandUserPath(inputPath);
isValid = true;
}
// Read module.yaml to get module info
const yaml = require('yaml');
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(moduleContent);
// Add to sources
customContentConfig.sources.push({
path: sourcePath,
id: moduleData.code,
name: moduleData.name || moduleData.code,
});
console.log(chalk.green(`✓ Added custom module: ${moduleData.name || moduleData.code}`));
// Ask if user wants to add more
const { addMore } = await inquirer.prompt([
{
type: 'confirm',
name: 'addMore',
message: 'Add another custom module?',
default: false,
},
]);
if (!addMore) {
break;
}
}
// Ask if user wants to add these to the installation
const { shouldInstall } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldInstall',
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
default: true,
},
]);
if (shouldInstall) {
customContentConfig.selected = true;
// Store paths to module.yaml files, not directories
customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
// Also include module IDs for installation
customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
}
return customContentConfig;
}
} }
module.exports = { UI }; module.exports = { UI };