From 7f742d4af668bef0dd184777373e90e756edc29c Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sun, 14 Dec 2025 10:03:25 +0800 Subject: [PATCH] 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 --- .vscode/settings.json | 2 +- .../sub-modules/claude-code/injections.yaml | 44 ++-- .../bmm/sub-modules/claude-code/readme.md | 2 +- .../storyteller-sidecar/stories-told.md | 7 + .../storyteller-sidecar/story-preferences.md | 7 + .../{ => storyteller}/storyteller.agent.yaml | 4 + .../lib/core/custom-module-cache.js | 10 +- tools/cli/installers/lib/core/installer.js | 65 +++++- .../installers/lib/core/manifest-generator.js | 12 +- tools/cli/installers/lib/modules/manager.js | 14 ++ tools/cli/lib/agent/compiler.js | 6 + tools/cli/lib/ui.js | 218 +++++++++++++++++- 12 files changed, 340 insertions(+), 51 deletions(-) create mode 100644 src/modules/cis/agents/storyteller/storyteller-sidecar/stories-told.md create mode 100644 src/modules/cis/agents/storyteller/storyteller-sidecar/story-preferences.md rename src/modules/cis/agents/{ => storyteller}/storyteller.agent.yaml (80%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 05795716..1f500542 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -73,7 +73,7 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" diff --git a/src/modules/bmm/sub-modules/claude-code/injections.yaml b/src/modules/bmm/sub-modules/claude-code/injections.yaml index f1dda0cd..e8fabbe2 100644 --- a/src/modules/bmm/sub-modules/claude-code/injections.yaml +++ b/src/modules/bmm/sub-modules/claude-code/injections.yaml @@ -4,16 +4,16 @@ # # The installer will: # 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 -# 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 injections: # ===== PRD WORKFLOW INJECTIONS ===== # PRD Subagent Instructions - - file: "{bmad_folder}/bmm/workflows/prd/instructions.md" + - file: "_bmad/bmm/workflows/prd/instructions.md" point: "prd-subagent-instructions" requires: "all-prd-subagents" content: | @@ -25,7 +25,7 @@ injections: - Use `bmm-technical-decisions-curator` to capture all technical mentions # PRD Requirements Analysis - - file: "{bmad_folder}/bmm/workflows/prd/instructions.md" + - file: "_bmad/bmm/workflows/prd/instructions.md" point: "prd-requirements-analysis" requires: "requirements-analyst" content: | @@ -33,7 +33,7 @@ injections: **Subagent Hint**: Use `bmm-requirements-analyst` to validate requirements are testable and complete. # PRD User Journey Mapping - - file: "{bmad_folder}/bmm/workflows/prd/instructions.md" + - file: "_bmad/bmm/workflows/prd/instructions.md" point: "prd-user-journey" requires: "user-journey-mapper" content: | @@ -41,7 +41,7 @@ injections: **Subagent Hint**: Use `bmm-user-journey-mapper` to map all user types and their value paths. # PRD Epic Optimization - - file: "{bmad_folder}/bmm/workflows/prd/instructions.md" + - file: "_bmad/bmm/workflows/prd/instructions.md" point: "prd-epic-optimization" requires: "epic-optimizer" content: | @@ -49,7 +49,7 @@ injections: **Subagent Hint**: Use `bmm-epic-optimizer` to validate epic boundaries deliver coherent value. # PRD Document Review - - file: "{bmad_folder}/bmm/workflows/prd/instructions.md" + - file: "_bmad/bmm/workflows/prd/instructions.md" point: "prd-checklist-review" requires: "document-reviewer" content: | @@ -57,7 +57,7 @@ injections: **Subagent Hint**: Use `bmm-document-reviewer` to validate PRD completeness before finalizing. # Technical Decisions Curator - - file: "{bmad_folder}/bmm/workflows/prd/instructions.md" + - file: "_bmad/bmm/workflows/prd/instructions.md" point: "technical-decisions-curator" requires: "technical-decisions-curator" content: | @@ -71,7 +71,7 @@ injections: # ===== MARKET RESEARCH TEMPLATE INJECTIONS ===== # Market TAM/SAM/SOM Calculations - - file: "{bmad_folder}/bmm/templates/market.md" + - file: "_bmad/bmm/templates/market.md" point: "market-tam-calculations" requires: "data-analyst" content: | @@ -82,7 +82,7 @@ injections: # Market Trends Analysis - - file: "{bmad_folder}/bmm/templates/market.md" + - file: "_bmad/bmm/templates/market.md" point: "market-trends-analysis" requires: "trend-spotter" content: | @@ -93,7 +93,7 @@ injections: # Market Customer Personas - - file: "{bmad_folder}/bmm/templates/market.md" + - file: "_bmad/bmm/templates/market.md" point: "market-customer-segments" requires: "user-researcher" content: | @@ -104,7 +104,7 @@ injections: # Market Research Review - - file: "{bmad_folder}/bmm/templates/market.md" + - file: "_bmad/bmm/templates/market.md" point: "market-executive-summary" requires: "document-reviewer" content: | @@ -116,7 +116,7 @@ injections: # ===== COMPETITOR ANALYSIS TEMPLATE INJECTIONS ===== # Competitor Intelligence Gathering - - file: "{bmad_folder}/bmm/templates/competitor.md" + - file: "_bmad/bmm/templates/competitor.md" point: "competitor-intelligence" requires: "market-researcher" content: | @@ -127,7 +127,7 @@ injections: # Competitor Technical Analysis - - file: "{bmad_folder}/bmm/templates/competitor.md" + - file: "_bmad/bmm/templates/competitor.md" point: "competitor-tech-stack" requires: "technical-evaluator" content: | @@ -138,7 +138,7 @@ injections: # Competitor Metrics Analysis - - file: "{bmad_folder}/bmm/templates/competitor.md" + - file: "_bmad/bmm/templates/competitor.md" point: "competitor-metrics" requires: "data-analyst" content: | @@ -148,7 +148,7 @@ injections: # Competitor Analysis Review - - file: "{bmad_folder}/bmm/templates/competitor.md" + - file: "_bmad/bmm/templates/competitor.md" point: "competitor-executive-summary" requires: "document-reviewer" content: | @@ -160,7 +160,7 @@ injections: # ===== PROJECT BRIEF TEMPLATE INJECTIONS ===== # Brief Problem Validation - - file: "{bmad_folder}/bmm/templates/brief.md" + - file: "_bmad/bmm/templates/brief.md" point: "brief-problem-validation" requires: "market-researcher" content: | @@ -170,7 +170,7 @@ injections: # Brief Target User Analysis - - file: "{bmad_folder}/bmm/templates/brief.md" + - file: "_bmad/bmm/templates/brief.md" point: "brief-user-analysis" requires: "user-researcher" content: | @@ -180,7 +180,7 @@ injections: # Brief Success Metrics - - file: "{bmad_folder}/bmm/templates/brief.md" + - file: "_bmad/bmm/templates/brief.md" point: "brief-success-metrics" requires: "data-analyst" content: | @@ -190,7 +190,7 @@ injections: # Brief Technical Feasibility - - file: "{bmad_folder}/bmm/templates/brief.md" + - file: "_bmad/bmm/templates/brief.md" point: "brief-technical-feasibility" requires: "technical-evaluator" content: | @@ -200,7 +200,7 @@ injections: # Brief Requirements Extraction - - file: "{bmad_folder}/bmm/templates/brief.md" + - file: "_bmad/bmm/templates/brief.md" point: "brief-requirements" requires: "requirements-analyst" content: | @@ -210,7 +210,7 @@ injections: # Brief Document Review - - file: "{bmad_folder}/bmm/templates/brief.md" + - file: "_bmad/bmm/templates/brief.md" point: "brief-final-review" requires: "document-reviewer" content: | diff --git a/src/modules/bmm/sub-modules/claude-code/readme.md b/src/modules/bmm/sub-modules/claude-code/readme.md index a477ac5a..c0ef2694 100644 --- a/src/modules/bmm/sub-modules/claude-code/readme.md +++ b/src/modules/bmm/sub-modules/claude-code/readme.md @@ -84,4 +84,4 @@ To test subagent installation: 2. Select BMM module and Claude Code 3. Verify prompts appear for subagent selection 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/...` diff --git a/src/modules/cis/agents/storyteller/storyteller-sidecar/stories-told.md b/src/modules/cis/agents/storyteller/storyteller-sidecar/stories-told.md new file mode 100644 index 00000000..782f0f64 --- /dev/null +++ b/src/modules/cis/agents/storyteller/storyteller-sidecar/stories-told.md @@ -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 + + diff --git a/src/modules/cis/agents/storyteller/storyteller-sidecar/story-preferences.md b/src/modules/cis/agents/storyteller/storyteller-sidecar/story-preferences.md new file mode 100644 index 00000000..6d8cc272 --- /dev/null +++ b/src/modules/cis/agents/storyteller/storyteller-sidecar/story-preferences.md @@ -0,0 +1,7 @@ +# Story Record Template + +Purpose: Record a log of learned users story telling or story building preferences. + +## User Preferences + + diff --git a/src/modules/cis/agents/storyteller.agent.yaml b/src/modules/cis/agents/storyteller/storyteller.agent.yaml similarity index 80% rename from src/modules/cis/agents/storyteller.agent.yaml rename to src/modules/cis/agents/storyteller/storyteller.agent.yaml index dee40280..a2e031d4 100644 --- a/src/modules/cis/agents/storyteller.agent.yaml +++ b/src/modules/cis/agents/storyteller/storyteller.agent.yaml @@ -14,6 +14,10 @@ agent: 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. + 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: - trigger: story exec: "{project-root}/_bmad/cis/workflows/storytelling/workflow.yaml" diff --git a/tools/cli/installers/lib/core/custom-module-cache.js b/tools/cli/installers/lib/core/custom-module-cache.js index e138f774..378f94ca 100644 --- a/tools/cli/installers/lib/core/custom-module-cache.js +++ b/tools/cli/installers/lib/core/custom-module-cache.js @@ -144,12 +144,18 @@ class CustomModuleCache { const sourceHash = await this.calculateHash(sourcePath); 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] = { originalHash: sourceHash, cacheHash: cacheHash, cachedAt: new Date().toISOString(), - ...metadata, + ...cleanMetadata, }; await this.updateCacheManifest(cacheManifest); diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 9063f5d9..b132c824 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -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) let moduleConfigs; + let customModulePaths = new Map(); + if (config._quickUpdate) { // Quick update already collected all configs, use them directly moduleConfigs = this.configCollector.collectedConfig; } else { // Build custom module paths map from customContent - const customModulePaths = new Map(); // Handle selectedFiles (from existing install path or manual directory input) 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) // Only include modules that were actually selected for installation 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 this.moduleManager.setBmadFolderName(bmadFolderName); this.moduleManager.setCoreConfig(moduleConfigs.core || {}); + this.moduleManager.setCustomModulePaths(customModulePaths); this.ideManager.setBmadFolderName(bmadFolderName); // 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...'; 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 const projectRoot = getProjectRoot(); @@ -790,6 +819,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: 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 // Create a temporary module manager that knows about custom content locations 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 }); - // Make sure custom modules are discoverable - 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, { + const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, { verbose: config.verbose, moduleManager: tempModuleManager, }); @@ -974,7 +1011,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: config._customModulesToTrack.push({ id: customInfo.id, name: customInfo.name, - sourcePath: sourcePath, + sourcePath: useCache ? `_config/custom/${customInfo.id}` : sourcePath, installDate: new Date().toISOString(), }); } 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, { ides: config.ides || [], 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) @@ -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 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, }); + // 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 - 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 const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n'; diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 54349ad8..e36194bd 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -34,13 +34,19 @@ class ManifestGenerator { // Store modules list (all modules including preserved ones) const preservedModules = options.preservedModules || []; + const customModules = options.customModules || []; // Scan the bmad directory to find all actually installed modules const installedModules = await this.scanInstalledModules(bmadDir); - // Deduplicate modules list to prevent duplicates - this.modules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])]; - this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])]; // All installed modules get rescanned + // Filter out custom modules from the regular modules list + const customModuleIds = new Set(customModules.map((cm) => cm.id)); + 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 // preservedModules controls which modules stay as-is in the CSV (don't get rescanned) diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 3b152a99..e96fcd2e 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -29,6 +29,7 @@ class ModuleManager { 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 } /** @@ -47,6 +48,14 @@ class ModuleManager { this.coreConfig = coreConfig; } + /** + * Set custom module paths for priority lookup + * @param {Map} customModulePaths - Map of module ID to source path + */ + setCustomModulePaths(customModulePaths) { + this.customModulePaths = customModulePaths; + } + /** * Copy a file and replace _bmad placeholder with actual folder name * @param {string} sourcePath - Source file path @@ -340,6 +349,11 @@ class ModuleManager { async findModuleSource(moduleName) { 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 const srcModulePath = path.join(this.modulesSourcePath, moduleName); if (await fs.pathExists(srcModulePath)) { diff --git a/tools/cli/lib/agent/compiler.js b/tools/cli/lib/agent/compiler.js index 5df3f1e8..4ab01b7c 100644 --- a/tools/cli/lib/agent/compiler.js +++ b/tools/cli/lib/agent/compiler.js @@ -346,6 +346,12 @@ async function compileAgent(yamlContent, answers = {}, agentName = '', targetPat // Replace {bmad_memory} in XML content 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) { xml = xml.replaceAll('{bmad_memory}', finalAnswers.bmad_memory); } diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 0465feb1..2bb397e8 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -40,19 +40,22 @@ class UI { let legacyBmadPath = null; // First check for legacy .bmad folder (instead of _bmad) - const entries = await fs.readdir(confirmedDirectory, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory() && entry.name === '.bmad') { - hasLegacyBmadFolder = true; - legacyBmadPath = path.join(confirmedDirectory, '.bmad'); - bmadDir = legacyBmadPath; + // Only check if directory exists + if (await fs.pathExists(confirmedDirectory)) { + const entries = await fs.readdir(confirmedDirectory, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name === '.bmad') { + hasLegacyBmadFolder = true; + legacyBmadPath = path.join(confirmedDirectory, '.bmad'); + bmadDir = legacyBmadPath; - // Check if it has _cfg folder - const cfgPath = path.join(legacyBmadPath, '_cfg'); - if (await fs.pathExists(cfgPath)) { - hasLegacyCfg = true; + // Check if it has _cfg folder + const cfgPath = path.join(legacyBmadPath, '_cfg'); + if (await fs.pathExists(cfgPath)) { + hasLegacyCfg = true; + } + break; } - break; } } @@ -154,6 +157,7 @@ class UI { choices: [ { name: 'Quick Update (Settings Preserved)', value: 'quick-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: 'Compile Agents (Quick rebuild of all agent .md files)', value: 'compile' }, { 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 if (actionType === 'compile') { return { @@ -198,6 +252,24 @@ class UI { // 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 coreConfig = await this.collectCoreConfig(confirmedDirectory); @@ -237,6 +309,9 @@ class UI { } // Filter out custom content markers and add module IDs 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) { // User provided custom content but didn't select any customContentConfig.selected = false; @@ -1184,6 +1259,127 @@ class UI { 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 };