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 };