diff --git a/.gitignore b/.gitignore index 045057da..8a9137a1 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,7 @@ src/modules/bmm/sub-modules/ src/modules/bmb/sub-modules/ src/modules/cis/sub-modules/ src/modules/bmgd/sub-modules/ - +shared-modules z*/ .bmad diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js index a2f0e755..d5742cf7 100644 --- a/tools/cli/commands/install.js +++ b/tools/cli/commands/install.js @@ -1,513 +1,11 @@ const chalk = require('chalk'); const path = require('node:path'); -const fs = require('fs-extra'); const { Installer } = require('../installers/lib/core/installer'); const { UI } = require('../lib/ui'); const installer = new Installer(); const ui = new UI(); -/** - * Install custom content (agents, workflows, modules) - * @param {Object} config - Installation configuration - * @param {Object} result - Installation result - * @param {string} projectDirectory - Project directory path - */ -async function installCustomContent(config, result, projectDirectory) { - const { customContent } = config; - const { selectedItems } = customContent; - const projectDir = projectDirectory; - const bmadDir = result.path; - - console.log(chalk.dim(`Project: ${projectDir}`)); - console.log(chalk.dim(`BMAD: ${bmadDir}`)); - - // Install custom agents - use agent-install logic - if (selectedItems.agents.length > 0) { - console.log(chalk.blue(`\nšŸ‘„ Installing ${selectedItems.agents.length} custom agent(s)...`)); - for (const agent of selectedItems.agents) { - await installCustomAgentWithPrompts(agent, projectDir, bmadDir, config); - } - } - - // Install custom workflows - copy and register with IDEs - if (selectedItems.workflows.length > 0) { - console.log(chalk.blue(`\nšŸ“‹ Installing ${selectedItems.workflows.length} custom workflow(s)...`)); - for (const workflow of selectedItems.workflows) { - await installCustomWorkflowWithIDE(workflow, projectDir, bmadDir, config); - } - } - - // Install custom modules - treat like regular modules - if (selectedItems.modules.length > 0) { - console.log(chalk.blue(`\nšŸ”§ Installing ${selectedItems.modules.length} custom module(s)...`)); - for (const module of selectedItems.modules) { - await installCustomModuleAsRegular(module, projectDir, bmadDir, config); - } - } - - console.log(chalk.green('\nāœ“ Custom content installation complete!')); -} - -/** - * Install a custom agent with proper prompts (mirrors agent-install.js) - */ -async function installCustomAgentWithPrompts(agent, projectDir, bmadDir, config) { - const { - discoverAgents, - loadAgentConfig, - addToManifest, - extractManifestData, - promptInstallQuestions, - createIdeSlashCommands, - updateManifestYaml, - saveAgentSource, - } = require('../lib/agent/installer'); - const { compileAgent } = require('../lib/agent/compiler'); - const inquirer = require('inquirer'); - const readline = require('node:readline'); - const yaml = require('js-yaml'); - - console.log(chalk.cyan(` Installing agent: ${agent.name}`)); - - // Load agent config - const agentConfig = loadAgentConfig(agent.yamlPath); - const agentType = agent.name; // e.g., "toolsmith" - - // Confirm/customize agent persona name (mirrors agent-install.js) - const rl1 = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - const defaultPersonaName = agentConfig.metadata.name || agentType; - console.log(chalk.cyan(`\n šŸ“› Agent Persona Name`)); - console.log(chalk.dim(` Agent type: ${agentType}`)); - console.log(chalk.dim(` Default persona: ${defaultPersonaName}`)); - console.log(chalk.dim(' Leave blank to use default, or provide a custom name.')); - console.log(chalk.dim(' Examples:')); - console.log(chalk.dim(` - (blank) → "${defaultPersonaName}" as ${agentType}.md`)); - console.log(chalk.dim(` - "Fred" → "Fred" as fred-${agentType}.md`)); - console.log(chalk.dim(` - "Captain Code" → "Captain Code" as captain-code-${agentType}.md`)); - - const customPersonaName = await new Promise((resolve) => { - rl1.question(`\n Custom name (or Enter for default): `, resolve); - }); - rl1.close(); - - // Determine final agent file name based on persona name - let finalAgentName; - let personaName; - if (customPersonaName.trim()) { - personaName = customPersonaName.trim(); - const namePrefix = personaName.toLowerCase().replaceAll(/\s+/g, '-'); - finalAgentName = `${namePrefix}-${agentType}`; - } else { - personaName = defaultPersonaName; - finalAgentName = agentType; - } - - console.log(chalk.dim(` Persona: ${personaName}`)); - console.log(chalk.dim(` File: ${finalAgentName}.md`)); - - // Get answers (prompt or use defaults) - let presetAnswers = {}; - - // If custom persona name provided, inject it as custom_name for template processing - if (customPersonaName.trim()) { - presetAnswers.custom_name = personaName; - } - - let answers; - if (agentConfig.installConfig) { - answers = await promptInstallQuestions(agentConfig.installConfig, agentConfig.defaults, presetAnswers); - } else { - answers = { ...agentConfig.defaults, ...presetAnswers }; - } - - // Create target directory - const targetDir = path.join(bmadDir, 'custom', 'agents', finalAgentName); - await fs.ensureDir(targetDir); - - // Compile agent with answers - const { xml, metadata } = compileAgent( - agentConfig.yamlContent, - answers, - finalAgentName, - `.bmad/custom/agents/${finalAgentName}/${finalAgentName}.md`, - ); - - // Write compiled agent - const compiledPath = path.join(targetDir, `${finalAgentName}.md`); - await fs.writeFile(compiledPath, xml, 'utf8'); - - // Copy sidecar files if exists - if (agent.hasSidecar) { - const entries = await fs.readdir(agent.path, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isFile() && !entry.name.endsWith('.agent.yaml')) { - await fs.copy(path.join(agent.path, entry.name), path.join(targetDir, entry.name)); - } - } - } - - // Save source YAML for reinstallation - const cfgAgentsBackupDir = path.join(bmadDir, '_cfg', 'custom', 'agents'); - await fs.ensureDir(cfgAgentsBackupDir); - const backupYamlPath = path.join(cfgAgentsBackupDir, `${finalAgentName}.agent.yaml`); - await fs.copy(agent.yamlPath, backupYamlPath); - - // Add to agent manifest - const manifestFile = path.join(bmadDir, '_cfg', 'agent-manifest.csv'); - const relativePath = `.bmad/custom/agents/${finalAgentName}/${finalAgentName}.md`; - const manifestData = extractManifestData(xml, { ...metadata, name: finalAgentName }, relativePath, 'custom'); - manifestData.name = finalAgentName; - manifestData.displayName = metadata.name || finalAgentName; - addToManifest(manifestFile, manifestData); - - // Update manifest.yaml - const manifestYamlPath = path.join(bmadDir, '_cfg', 'manifest.yaml'); - updateManifestYaml(manifestYamlPath, finalAgentName, finalAgentName); - - // Create IDE slash commands using existing IDEs from config - const ideResults = await createIdeSlashCommands(projectDir, finalAgentName, relativePath, metadata, config.ides || []); - const ideCount = Object.keys(ideResults).length; - - console.log(chalk.green(` āœ“ ${finalAgentName} (registered with ${ideCount} IDE${ideCount === 1 ? '' : 's'})`)); -} - -/** - * Install a custom workflow and register with all IDEs - */ -async function installCustomWorkflowWithIDE(workflow, projectDir, bmadDir, config) { - const targetDir = path.join(bmadDir, 'custom', 'workflows'); - - // Check if workflow is a directory or just a file - // workflow.path might be a file (workflow.md) or a directory - let sourcePath = workflow.path; - let isDirectory = false; - - try { - const stats = await fs.stat(workflow.path); - isDirectory = stats.isDirectory(); - } catch { - console.log(chalk.red(` ERROR: Cannot access workflow path: ${workflow.path}`)); - return; - } - - // If it's a file ending in workflow.md, use the parent directory - if (!isDirectory && workflow.path.endsWith('workflow.md')) { - sourcePath = path.dirname(workflow.path); - isDirectory = true; - } - - if (isDirectory) { - // Copy the entire workflow directory - const workflowName = path.basename(sourcePath); - const targetWorkflowDir = path.join(targetDir, workflowName); - await fs.copy(sourcePath, targetWorkflowDir); - - // Update manifest with the main workflow.md file - const relativePath = `.bmad/custom/workflows/${workflowName}/workflow.md`; - await addWorkflowToManifest(bmadDir, workflow.name, workflow.description, relativePath, 'custom'); - } else { - // Single file workflow - const targetFileName = path.basename(sourcePath); - const targetPath = path.join(targetDir, targetFileName); - await fs.copy(sourcePath, targetPath); - - // Update manifest - const relativePath = `.bmad/custom/workflows/${targetFileName}`; - await addWorkflowToManifest(bmadDir, workflow.name, workflow.description, relativePath, 'custom'); - } - - // Register workflow with all configured IDEs - const relativePath = `.bmad/custom/workflows/${path.basename(workflow.path)}`; - if (config.ides && config.ides.length > 0) { - const { IdeManager } = require('../installers/lib/ide/manager'); - const ideManager = new IdeManager(); - - for (const ide of config.ides) { - try { - // IdeManager uses a Map, not getHandler method - const ideHandler = ideManager.handlers.get(ide.toLowerCase()); - if (ideHandler && typeof ideHandler.registerWorkflow === 'function') { - await ideHandler.registerWorkflow(projectDir, bmadDir, workflow.name, relativePath); - console.log(chalk.dim(` āœ“ Registered with ${ide}`)); - } - } catch (error) { - console.log(chalk.yellow(` āš ļø Could not register with ${ide}: ${error.message}`)); - } - } - } - - console.log(chalk.green(` āœ“ ${workflow.name} (copied to custom workflows and registered with IDEs)`)); -} - -/** - * Helper to add workflow to manifest - */ -async function addWorkflowToManifest(bmadDir, name, description, relativePath, moduleType = 'custom') { - const workflowManifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv'); - - console.log(chalk.dim(`[DEBUG] Adding workflow to manifest: ${name} -> ${relativePath} (module: ${moduleType})`)); - - // Read existing manifest - let manifestContent = ''; - if (await fs.pathExists(workflowManifestPath)) { - manifestContent = await fs.readFile(workflowManifestPath, 'utf8'); - } - - // Ensure header exists - if (!manifestContent.includes('name,description,module,path')) { - manifestContent = 'name,description,module,path\n'; - } - - // Add workflow entry - const csvLine = `"${name}","${description}","${moduleType}","${relativePath}"\n`; - - // Check if workflow already exists in manifest - if (manifestContent.includes(`"${name}",`)) { - console.log(chalk.dim(`[DEBUG] Workflow already exists in manifest: ${name}`)); - } else { - try { - await fs.writeFile(workflowManifestPath, manifestContent + csvLine); - console.log(chalk.dim(`[DEBUG] Successfully added to manifest`)); - } catch (error) { - console.log(chalk.red(`[ERROR] Failed to write to manifest: ${error.message}`)); - } - } -} - -/** - * Install a custom module like a regular module - */ -async function installCustomModuleAsRegular(module, projectDir, bmadDir, config) { - const yaml = require('js-yaml'); - const path = require('node:path'); - - // The custom module path should be the source location - const customSrcPath = module.path; - - // Install the custom module by copying it to the custom modules directory - const targetDir = path.join(bmadDir, 'custom', 'modules', module.name); - await fs.copy(customSrcPath, targetDir); - - // Check if module has an installer and run it from the ORIGINAL source location - const installerPath = path.join(customSrcPath, '_module-installer', 'installer.js'); - if (await fs.pathExists(installerPath)) { - try { - // Clear require cache to ensure fresh import - delete require.cache[require.resolve(installerPath)]; - - // Load and run the module installer - const moduleInstaller = require(installerPath); - await moduleInstaller.install({ - projectRoot: projectDir, - config: config.coreConfig || {}, - installedIDEs: config.ides || [], - logger: { - log: (msg) => console.log(chalk.dim(` ${msg}`)), - error: (msg) => console.log(chalk.red(` ERROR: ${msg}`)), - }, - }); - console.log(chalk.green(` āœ“ ${module.name} (custom installer executed)`)); - } catch (error) { - console.log(chalk.yellow(` āš ļø ${module.name} installer failed: ${error.message}`)); - console.log(chalk.dim(` Module copied but not configured`)); - } - } else { - // No installer - check if module has agents/workflows to install - console.log(chalk.dim(` Processing module agents and workflows...`)); - - // Install agents from the module - const agentsPath = path.join(customSrcPath, 'agents'); - if (await fs.pathExists(agentsPath)) { - const agentFiles = await fs.readdir(agentsPath); - for (const agentFile of agentFiles) { - if (agentFile.endsWith('.yaml')) { - const agentPath = path.join(agentsPath, agentFile); - await installModuleAgent(agentPath, module.name, projectDir, bmadDir, config); - } - } - } - - // Install workflows from the module - const workflowsPath = path.join(customSrcPath, 'workflows'); - if (await fs.pathExists(workflowsPath)) { - const workflowDirs = await fs.readdir(workflowsPath, { withFileTypes: true }); - for (const workflowDir of workflowDirs) { - if (workflowDir.isDirectory()) { - const workflowPath = path.join(workflowsPath, workflowDir.name); - await installModuleWorkflow(workflowPath, module.name, projectDir, bmadDir, config); - } - } - } - - console.log(chalk.green(` āœ“ ${module.name}`)); - } - - // Update manifest.yaml to include custom module with proper prefix - const manifestYamlPath = path.join(bmadDir, '_cfg', 'manifest.yaml'); - - if (await fs.pathExists(manifestYamlPath)) { - const manifest = yaml.load(await fs.readFile(manifestYamlPath, 'utf8')); - - // Remove any old entries without custom- prefix for this module - const oldModuleName = module.name; - if (manifest.modules.includes(oldModuleName)) { - manifest.modules = manifest.modules.filter((m) => m !== oldModuleName); - console.log(chalk.dim(` Removed old entry: ${oldModuleName}`)); - } - - // Custom modules should be stored with custom- prefix - const moduleNameWithPrefix = `custom-${module.name}`; - if (!manifest.modules.includes(moduleNameWithPrefix)) { - manifest.modules.push(moduleNameWithPrefix); - console.log(chalk.dim(` Added to manifest.yaml as ${moduleNameWithPrefix}`)); - } - - // Write back the cleaned manifest - await fs.writeFile(manifestYamlPath, yaml.dump(manifest), 'utf8'); - } - - // Register module with IDEs (like regular modules do) - if (config.ides && config.ides.length > 0) { - const { IdeManager } = require('../installers/lib/ide/manager'); - const ideManager = new IdeManager(); - - for (const ide of config.ides) { - try { - // IdeManager uses a Map, not direct property access - const handler = ideManager.handlers.get(ide.toLowerCase()); - if (handler && handler.moduleInjector) { - // Check if module has IDE-specific customizations - const subModulePath = path.join(customSrcPath, 'sub-modules', ide); - if (await fs.pathExists(subModulePath)) { - console.log(chalk.dim(` āœ“ Found ${ide} customizations for ${module.name}`)); - } - } - } catch (error) { - console.log(chalk.yellow(` āš ļø Could not configure ${ide} for ${module.name}: ${error.message}`)); - } - } - } -} - -/** - * Install an agent from a module - */ -async function installModuleAgent(agentPath, moduleName, projectDir, bmadDir, config) { - const { - loadAgentConfig, - addToManifest, - extractManifestData, - createIdeSlashCommands, - updateManifestYaml, - } = require('../lib/agent/installer'); - const { compileAgent } = require('../lib/agent/compiler'); - - const agentName = path.basename(agentPath, '.yaml'); - console.log(chalk.dim(` Installing agent: ${agentName} (from ${moduleName})`)); - - // Load agent config - const agentConfig = loadAgentConfig(agentPath); - - // Compile agent with defaults (no prompts for module agents) - const { xml, metadata } = compileAgent( - agentConfig.yamlContent, - agentConfig.defaults || {}, - agentName, - `.bmad/custom/modules/${moduleName}/agents/${agentName}.md`, - ); - - // Create target directory - const targetDir = path.join(bmadDir, 'custom', 'modules', moduleName, 'agents'); - await fs.ensureDir(targetDir); - - // Write compiled agent - const compiledPath = path.join(targetDir, `${agentName}.md`); - await fs.writeFile(compiledPath, xml, 'utf8'); - - // Remove the raw YAML file after compilation - const yamlPath = path.join(targetDir, `${agentName}.yaml`); - if (await fs.pathExists(yamlPath)) { - await fs.remove(yamlPath); - } - - // Add to agent manifest - const manifestFile = path.join(bmadDir, '_cfg', 'agent-manifest.csv'); - const relativePath = `.bmad/custom/modules/${moduleName}/agents/${agentName}.md`; - const manifestData = extractManifestData(xml, { ...metadata, name: agentName }, relativePath, 'custom'); - manifestData.name = `${moduleName}-${agentName}`; - manifestData.displayName = metadata.name || agentName; - addToManifest(manifestFile, manifestData); - - // Update manifest.yaml - const manifestYamlPath = path.join(bmadDir, '_cfg', 'manifest.yaml'); - updateManifestYaml(manifestYamlPath, `${moduleName}-${agentName}`, agentName); - - // Create IDE slash commands - const ideResults = await createIdeSlashCommands(projectDir, `${moduleName}-${agentName}`, relativePath, metadata, config.ides || []); - const ideCount = Object.keys(ideResults).length; - - console.log(chalk.dim(` āœ“ ${agentName} (registered with ${ideCount} IDE${ideCount === 1 ? '' : 's'})`)); -} - -/** - * Install a workflow from a module - */ -async function installModuleWorkflow(workflowPath, moduleName, projectDir, bmadDir, config) { - const workflowName = path.basename(workflowPath); - - // Copy the workflow directory - const targetDir = path.join(bmadDir, 'custom', 'modules', moduleName, 'workflows', workflowName); - await fs.copy(workflowPath, targetDir); - - // Add to workflow manifest - const workflowManifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv'); - const relativePath = `.bmad/custom/modules/${moduleName}/workflows/${workflowName}/README.md`; - - // Read existing manifest - let manifestContent = ''; - if (await fs.pathExists(workflowManifestPath)) { - manifestContent = await fs.readFile(workflowManifestPath, 'utf8'); - } - - // Ensure header exists - if (!manifestContent.includes('name,description,module,path')) { - manifestContent = 'name,description,module,path\n'; - } - - // Add workflow entry - const csvLine = `"${moduleName}-${workflowName}","Workflow from ${moduleName} module","${moduleName}","${relativePath}"\n`; - - // Check if workflow already exists in manifest - if (!manifestContent.includes(`"${moduleName}-${workflowName}",`)) { - await fs.writeFile(workflowManifestPath, manifestContent + csvLine); - } - - // Register with IDEs - if (config.ides && config.ides.length > 0) { - const { IdeManager } = require('../installers/lib/ide/manager'); - const ideManager = new IdeManager(); - - for (const ide of config.ides) { - try { - const ideHandler = ideManager.handlers.get(ide.toLowerCase()); - if (ideHandler && typeof ideHandler.registerWorkflow === 'function') { - await ideHandler.registerWorkflow(projectDir, bmadDir, `${moduleName}-${workflowName}`, relativePath); - console.log(chalk.dim(` āœ“ Registered with ${ide}`)); - } - } catch (error) { - console.log(chalk.yellow(` āš ļø Could not register with ${ide}: ${error.message}`)); - } - } - } - - console.log(chalk.dim(` āœ“ ${workflowName} workflow added and registered`)); -} - module.exports = { command: 'install', description: 'Install BMAD Core agents and tools', @@ -520,6 +18,7 @@ module.exports = { if (config.actionType === 'cancel') { console.log(chalk.yellow('Installation cancelled.')); process.exit(0); + return; } // Handle agent compilation separately @@ -528,6 +27,7 @@ module.exports = { console.log(chalk.green('\n✨ Agent compilation complete!')); console.log(chalk.cyan(`Rebuilt ${result.agentCount} agents and ${result.taskCount} tasks`)); process.exit(0); + return; } // Handle quick update separately @@ -535,71 +35,8 @@ module.exports = { const result = await installer.quickUpdate(config); console.log(chalk.green('\n✨ Quick update complete!')); console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings`)); - - // After quick update, check for existing custom content and re-install to regenerate IDE commands - const { UI } = require('../lib/ui'); - const ui = new UI(); - const customPath = path.join(config.directory, 'bmad-custom-src'); - - // Check if custom content exists - if (await fs.pathExists(customPath)) { - console.log(chalk.cyan('\nšŸ“¦ Detecting custom content to update IDE commands...')); - - // Get existing custom content selections (default to all for updates) - const existingCustom = { - agents: (await fs.pathExists(path.join(customPath, 'agents'))) ? true : false, - workflows: (await fs.pathExists(path.join(customPath, 'workflows'))) ? true : false, - modules: (await fs.pathExists(path.join(customPath, 'modules'))) ? true : false, - }; - - // Auto-select all existing custom content for update - if (existingCustom.agents || existingCustom.workflows || existingCustom.modules) { - const customContent = await ui.discoverCustomContent(customPath); - - config.customContent = { - path: customPath, - selectedItems: { - agents: existingCustom.agents ? customContent.agents.map((a) => ({ ...a, selected: true })) : [], - workflows: existingCustom.workflows ? customContent.workflows.map((w) => ({ ...w, selected: true })) : [], - modules: existingCustom.modules ? customContent.modules.map((m) => ({ ...m, selected: true })) : [], - }, - }; - - await installCustomContent(config, result, config.directory); - - // Re-run IDE setup to register custom workflows with IDEs - if (config.ides && config.ides.length > 0) { - console.log(chalk.cyan('\nšŸ”§ Updating IDE configurations for custom content...')); - const { IdeManager } = require('../installers/lib/ide/manager'); - const ideManager = new IdeManager(); - - for (const ide of config.ides) { - try { - const ideResult = await ideManager.setup(ide, config.directory, result.path, { - selectedModules: [...(config.modules || []), 'custom'], // Include 'custom' for custom agents/workflows - skipModuleInstall: true, // Don't install modules again - verbose: false, - preCollectedConfig: { - ...config.coreConfig, - _alreadyConfigured: true, // Skip reconfiguration that might add duplicates - }, - }); - - if (ideResult.success) { - console.log(chalk.dim(` āœ“ Updated ${ide} with custom workflows`)); - } - } catch (error) { - console.log(chalk.yellow(` āš ļø Could not update ${ide}: ${error.message}`)); - } - } - } - } else { - console.log(chalk.dim(' No custom content found to update')); - } - } - - console.log(chalk.green('\n✨ Update complete with custom content!')); process.exit(0); + return; } // Handle reinstall by setting force flag @@ -618,43 +55,11 @@ module.exports = { // Check if installation was cancelled if (result && result.cancelled) { process.exit(0); + return; } // Check if installation succeeded if (result && result.success) { - // Install custom content if selected - if (config.customContent && config.customContent.selectedItems) { - console.log(chalk.cyan('\nšŸ“¦ Installing Custom Content...')); - await installCustomContent(config, result, config.directory); - - // Re-run IDE setup to register custom workflows with IDEs - if (config.ides && config.ides.length > 0) { - console.log(chalk.cyan('\nšŸ”§ Updating IDE configurations for custom content...')); - const { IdeManager } = require('../installers/lib/ide/manager'); - const ideManager = new IdeManager(); - - for (const ide of config.ides) { - try { - const ideResult = await ideManager.setup(ide, config.directory, result.path, { - selectedModules: [...(config.modules || []), 'custom'], // Include 'custom' for custom agents/workflows - skipModuleInstall: true, // Don't install modules again - verbose: false, - preCollectedConfig: { - ...config.coreConfig, - _alreadyConfigured: true, // Skip reconfiguration that might add duplicates - }, - }); - - if (ideResult.success) { - console.log(chalk.dim(` āœ“ Updated ${ide} with custom workflows`)); - } - } catch (error) { - console.log(chalk.yellow(` āš ļø Could not update ${ide}: ${error.message}`)); - } - } - } - } - console.log(chalk.green('\n✨ Installation complete!')); console.log(chalk.cyan('BMAD Core and Selected Modules have been installed to:'), chalk.bold(result.path)); console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!')); diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index d67b6256..99fca89d 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -182,14 +182,24 @@ class ConfigCollector { } // Load module's install config schema - const installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml'); - const legacyConfigPath = path.join(getModulePath(moduleName), 'config.yaml'); + // First, try the standard src/modules location + let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml'); + + // If not found in src/modules, we need to find it by searching the project + if (!(await fs.pathExists(installerConfigPath))) { + // Use the module manager to find the module source + const { ModuleManager } = require('../modules/manager'); + const moduleManager = new ModuleManager(); + const moduleSourcePath = await moduleManager.findModuleSource(moduleName); + + if (moduleSourcePath) { + installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml'); + } + } let configPath = null; if (await fs.pathExists(installerConfigPath)) { configPath = installerConfigPath; - } else if (await fs.pathExists(legacyConfigPath)) { - configPath = legacyConfigPath; } else { // No config schema for this module - use existing values if (this.existingConfig && this.existingConfig[moduleName]) { @@ -396,32 +406,25 @@ class ConfigCollector { if (!this.allAnswers) { this.allAnswers = {}; } - // Load module's config.yaml (check custom modules first, then regular modules) - let installerConfigPath; - let legacyConfigPath; + // Load module's config + // First, try the standard src/modules location + let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml'); - if (moduleName.startsWith('custom-')) { - // Handle custom modules - const actualModuleName = moduleName.replace('custom-', ''); + // If not found in src/modules, we need to find it by searching the project + if (!(await fs.pathExists(installerConfigPath))) { + // Use the module manager to find the module source + const { ModuleManager } = require('../modules/manager'); + const moduleManager = new ModuleManager(); + const moduleSourcePath = await moduleManager.findModuleSource(moduleName); - // Custom modules are in the BMAD-METHOD source directory, not the installation directory - const bmadMethodRoot = getProjectRoot(); // This gets the BMAD-METHOD root - const customSrcPath = path.join(bmadMethodRoot, 'bmad-custom-src', 'modules', actualModuleName); - installerConfigPath = path.join(customSrcPath, '_module-installer', 'install-config.yaml'); - legacyConfigPath = path.join(customSrcPath, 'config.yaml'); - - console.log(chalk.dim(`[DEBUG] Looking for custom module config in: ${installerConfigPath}`)); - } else { - // Regular modules - installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml'); - legacyConfigPath = path.join(getModulePath(moduleName), 'config.yaml'); + if (moduleSourcePath) { + installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml'); + } } let configPath = null; if (await fs.pathExists(installerConfigPath)) { configPath = installerConfigPath; - } else if (await fs.pathExists(legacyConfigPath)) { - configPath = legacyConfigPath; } else { // No config for this module return; diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 8332f816..f113c141 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -418,7 +418,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: const projectDir = path.resolve(config.directory); // If core config was pre-collected (from interactive mode), use it - if (config.coreConfig && !this.configCollector.collectedConfig.core) { + if (config.coreConfig) { this.configCollector.collectedConfig.core = config.coreConfig; // Also store in allAnswers for cross-referencing this.configCollector.allAnswers = {}; @@ -427,16 +427,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: } } - // Collect configurations for modules (skip if quick update already collected them or if pre-collected) + // Collect configurations for modules (skip if quick update already collected them) let moduleConfigs; if (config._quickUpdate) { // Quick update already collected all configs, use them directly moduleConfigs = this.configCollector.collectedConfig; - } else if (config.moduleConfig) { - // Use pre-collected configs from UI (includes custom modules) - moduleConfigs = config.moduleConfig; - // Also need to load them into configCollector for later use - this.configCollector.collectedConfig = moduleConfigs; } else { // Regular install - collect configurations (core was already collected in UI.promptInstall if interactive) moduleConfigs = await this.configCollector.collectAllConfigurations(config.modules || [], path.resolve(config.directory)); @@ -753,14 +748,13 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: spinner.text = 'Creating directory structure...'; await this.createDirectoryStructure(bmadDir); - // Resolve dependencies for selected modules (skip custom modules) + // Resolve dependencies for selected modules spinner.text = 'Resolving dependencies...'; const projectRoot = getProjectRoot(); - const regularModules = (config.modules || []).filter((m) => !m.startsWith('custom-')); - const modulesToInstall = config.installCore ? ['core', ...regularModules] : regularModules; + const modulesToInstall = config.installCore ? ['core', ...config.modules] : config.modules; // For dependency resolution, we need to pass the project root - const resolution = await this.dependencyResolver.resolve(projectRoot, regularModules, { verbose: config.verbose }); + const resolution = await this.dependencyResolver.resolve(projectRoot, config.modules || [], { verbose: config.verbose }); if (config.verbose) { spinner.succeed('Dependencies resolved'); @@ -775,17 +769,17 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: spinner.succeed('Core installed'); } - // Install modules with their dependencies (skip custom modules - they're handled by install.js) - if (regularModules.length > 0) { - for (const moduleName of regularModules) { + // Install modules with their dependencies + if (config.modules && config.modules.length > 0) { + for (const moduleName of config.modules) { spinner.start(`Installing module: ${moduleName}...`); await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]); spinner.succeed(`Module installed: ${moduleName}`); } - // Install partial modules (only dependencies) - skip custom modules + // Install partial modules (only dependencies) for (const [module, files] of Object.entries(resolution.byModule)) { - if (!regularModules.includes(module) && module !== 'core') { + if (!config.modules.includes(module) && module !== 'core') { const totalFiles = files.agents.length + files.tasks.length + diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index 542d8238..d05b985e 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -24,51 +24,6 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) { } } - // Get custom module agents (from bmad/custom/modules/*/agents/) - const customModulesDir = path.join(bmadDir, 'custom', 'modules'); - if (await fs.pathExists(customModulesDir)) { - const moduleDirs = await fs.readdir(customModulesDir, { withFileTypes: true }); - - for (const moduleDir of moduleDirs) { - if (!moduleDir.isDirectory()) continue; - - const moduleAgentsPath = path.join(customModulesDir, moduleDir.name, 'agents'); - if (await fs.pathExists(moduleAgentsPath)) { - const moduleAgents = await getAgentsFromDir(moduleAgentsPath, moduleDir.name); - agents.push(...moduleAgents); - } - } - } - - // Get custom agents from bmad/custom/agents/ directory - const customAgentsDir = path.join(bmadDir, 'custom', 'agents'); - if (await fs.pathExists(customAgentsDir)) { - const agentDirs = await fs.readdir(customAgentsDir, { withFileTypes: true }); - - for (const agentDir of agentDirs) { - if (!agentDir.isDirectory()) continue; - - const agentDirPath = path.join(customAgentsDir, agentDir.name); - const agentFiles = await fs.readdir(agentDirPath); - - for (const file of agentFiles) { - if (!file.endsWith('.md')) continue; - if (file.includes('.customize.')) continue; - - const filePath = path.join(agentDirPath, file); - const content = await fs.readFile(filePath, 'utf8'); - - if (content.includes('localskip="true"')) continue; - - agents.push({ - path: filePath, - name: file.replace('.md', ''), - module: 'custom', // Mark as custom agent - }); - } - } - } - // Get standalone agents from bmad/agents/ directory const standaloneAgentsDir = path.join(bmadDir, 'agents'); if (await fs.pathExists(standaloneAgentsDir)) { diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index f644991e..7a4cb9df 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -98,57 +98,110 @@ class ModuleManager { } /** - * List all available modules + * Find all modules in the project by searching for install-config.yaml files + * @returns {Array} List of module paths + */ + async findModulesInProject() { + const projectRoot = getProjectRoot(); + const modulePaths = new Set(); + + // Helper function to recursively scan directories + async function scanDirectory(dir, excludePaths = []) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + // Skip hidden directories and node_modules + if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'build') { + continue; + } + + // Skip excluded paths + if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) { + continue; + } + + if (entry.isDirectory()) { + // Skip core module - it's always installed first and not selectable + if (entry.name === 'core') { + continue; + } + + // Check if this directory contains a module (only install-config.yaml is valid now) + const installerConfigPath = path.join(fullPath, '_module-installer', 'install-config.yaml'); + + if (await fs.pathExists(installerConfigPath)) { + modulePaths.add(fullPath); + // Don't scan inside modules - they might have their own nested structures + continue; + } + + // Recursively scan subdirectories + await scanDirectory(fullPath, excludePaths); + } + } + } catch { + // Ignore errors (e.g., permission denied) + } + } + + // Scan the entire project, but exclude src/modules since we handle it separately + await scanDirectory(projectRoot, [this.modulesSourcePath]); + + return [...modulePaths]; + } + + /** + * List all available modules (excluding core which is always installed) * @returns {Array} List of available modules with metadata */ async listAvailable() { const modules = []; - if (!(await fs.pathExists(this.modulesSourcePath))) { - console.warn(chalk.yellow('Warning: src/modules directory not found')); - return modules; - } + // First, scan src/modules (the standard location) + if (await fs.pathExists(this.modulesSourcePath)) { + const entries = await fs.readdir(this.modulesSourcePath, { withFileTypes: true }); - const entries = await fs.readdir(this.modulesSourcePath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const modulePath = path.join(this.modulesSourcePath, entry.name); + // Check for module structure (only install-config.yaml is valid now) + const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml'); - for (const entry of entries) { - if (entry.isDirectory()) { - const modulePath = path.join(this.modulesSourcePath, entry.name); - // Check for new structure first - const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml'); - // Fallback to old structure - const configPath = path.join(modulePath, 'config.yaml'); + // Skip if this doesn't look like a module + if (!(await fs.pathExists(installerConfigPath))) { + continue; + } - const moduleInfo = { - id: entry.name, - path: modulePath, - name: entry.name.toUpperCase(), - description: 'BMAD Module', - version: '5.0.0', - }; + // Skip core module - it's always installed first and not selectable + if (entry.name === 'core') { + continue; + } - // Try to read module config for metadata (prefer new location) - const configToRead = (await fs.pathExists(installerConfigPath)) ? installerConfigPath : configPath; - if (await fs.pathExists(configToRead)) { - try { - const configContent = await fs.readFile(configToRead, 'utf8'); - const config = yaml.load(configContent); - - // Use the code property as the id if available - if (config.code) { - moduleInfo.id = config.code; - } - - moduleInfo.name = config.name || moduleInfo.name; - moduleInfo.description = config.description || moduleInfo.description; - moduleInfo.version = config.version || moduleInfo.version; - moduleInfo.dependencies = config.dependencies || []; - moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected; - } catch (error) { - console.warn(`Failed to read config for ${entry.name}:`, error.message); + const moduleInfo = await this.getModuleInfo(modulePath, entry.name, 'src/modules'); + if (moduleInfo) { + modules.push(moduleInfo); } } + } + } + // Then, find all other modules in the project + const otherModulePaths = await this.findModulesInProject(); + for (const modulePath of otherModulePaths) { + const moduleName = path.basename(modulePath); + const relativePath = path.relative(getProjectRoot(), modulePath); + + // Skip core module - it's always installed first and not selectable + if (moduleName === 'core') { + continue; + } + + const moduleInfo = await this.getModuleInfo(modulePath, moduleName, relativePath); + if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id)) { + // Avoid duplicates - skip if we already have this module ID modules.push(moduleInfo); } } @@ -156,6 +209,104 @@ class ModuleManager { return modules; } + /** + * Get module information from a module path + * @param {string} modulePath - Path to the module directory + * @param {string} defaultName - Default name for the module + * @param {string} sourceDescription - Description of where the module was found + * @returns {Object|null} Module info or null if not a valid module + */ + async getModuleInfo(modulePath, defaultName, sourceDescription) { + // Check for module structure (only install-config.yaml is valid now) + const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml'); + + // Skip if this doesn't look like a module + if (!(await fs.pathExists(installerConfigPath))) { + return null; + } + + const moduleInfo = { + id: defaultName, + path: modulePath, + name: defaultName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '), + description: 'BMAD Module', + version: '5.0.0', + source: sourceDescription, + }; + + // Read module config for metadata + try { + const configContent = await fs.readFile(installerConfigPath, 'utf8'); + const config = yaml.load(configContent); + + // Use the code property as the id if available + if (config.code) { + moduleInfo.id = config.code; + } + + moduleInfo.name = config.name || moduleInfo.name; + moduleInfo.description = config.description || moduleInfo.description; + moduleInfo.version = config.version || moduleInfo.version; + moduleInfo.dependencies = config.dependencies || []; + moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected; + } catch (error) { + console.warn(`Failed to read config for ${defaultName}:`, error.message); + } + + return moduleInfo; + } + + /** + * Find the source path for a module by searching all possible locations + * @param {string} moduleName - Name of the module to find + * @returns {string|null} Path to the module source or null if not found + */ + async findModuleSource(moduleName) { + const projectRoot = getProjectRoot(); + + // First, check src/modules + const srcModulePath = path.join(this.modulesSourcePath, moduleName); + if (await fs.pathExists(srcModulePath)) { + // Check if this looks like a module (has install-config.yaml) + const installerConfigPath = path.join(srcModulePath, '_module-installer', 'install-config.yaml'); + + if (await fs.pathExists(installerConfigPath)) { + return srcModulePath; + } + } + + // If not found in src/modules, search the entire project + const allModulePaths = await this.findModulesInProject(); + for (const modulePath of allModulePaths) { + if (path.basename(modulePath) === moduleName) { + return modulePath; + } + } + + // Also check by module ID (not just folder name) + // Need to read configs to match by ID + for (const modulePath of allModulePaths) { + const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml'); + + if (await fs.pathExists(installerConfigPath)) { + try { + const configContent = await fs.readFile(installerConfigPath, 'utf8'); + const config = yaml.load(configContent); + if (config.code === moduleName) { + return modulePath; + } + } catch { + // Skip if can't read config + } + } + } + + return null; + } + /** * Install a module * @param {string} moduleName - Name of the module to install @@ -167,12 +318,12 @@ class ModuleManager { * @param {Object} options.logger - Logger instance for output */ async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) { - const sourcePath = path.join(this.modulesSourcePath, moduleName); + const sourcePath = await this.findModuleSource(moduleName); const targetPath = path.join(bmadDir, moduleName); // Check if source module exists - if (!(await fs.pathExists(sourcePath))) { - throw new Error(`Module '${moduleName}' not found in ${this.modulesSourcePath}`); + if (!sourcePath) { + throw new Error(`Module '${moduleName}' not found in any source location`); } // Check if already installed @@ -210,12 +361,12 @@ class ModuleManager { * @param {boolean} force - Force update (overwrite modifications) */ async update(moduleName, bmadDir, force = false) { - const sourcePath = path.join(this.modulesSourcePath, moduleName); + const sourcePath = await this.findModuleSource(moduleName); const targetPath = path.join(bmadDir, moduleName); // Check if source module exists - if (!(await fs.pathExists(sourcePath))) { - throw new Error(`Module '${moduleName}' not found in source`); + if (!sourcePath) { + throw new Error(`Module '${moduleName}' not found in any source location`); } // Check if module is installed @@ -654,7 +805,11 @@ class ModuleManager { if (moduleName === 'core') { sourcePath = getSourcePath('core'); } else { - sourcePath = path.join(this.modulesSourcePath, moduleName); + sourcePath = await this.findModuleSource(moduleName); + if (!sourcePath) { + // No source found, skip module installer + return; + } } const installerPath = path.join(sourcePath, '_module-installer', 'installer.js'); diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 011f2d62..4c5b3379 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -23,7 +23,6 @@ const inquirer = require('inquirer'); const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); -const yaml = require('js-yaml'); const { CLIUtils } = require('./cli-utils'); /** @@ -120,27 +119,6 @@ class UI { const moduleChoices = await this.getModuleChoices(installedModuleIds); const selectedModules = await this.selectModules(moduleChoices); - // Check if custom module was selected - let customContent = null; - if (selectedModules.includes('custom')) { - // Remove 'custom' from selectedModules since it's not a real module - const customIndex = selectedModules.indexOf('custom'); - selectedModules.splice(customIndex, 1); - - // Handle custom content selection - customContent = await this.handleCustomContentSelection(confirmedDirectory); - - // Add custom modules to the selected modules list for proper installation - if (customContent && customContent.selectedItems && customContent.selectedItems.modules) { - for (const customModule of customContent.selectedItems.modules) { - selectedModules.push(`custom-${customModule.name}`); - } - } - } - - // NOW collect module configurations (including custom modules that were just added) - const moduleConfig = await this.collectModuleConfigs(confirmedDirectory, selectedModules, coreConfig); - // Prompt for AgentVibes TTS integration const agentVibesConfig = await this.promptAgentVibes(confirmedDirectory); @@ -159,488 +137,11 @@ class UI { ides: toolSelection.ides, skipIde: toolSelection.skipIde, coreConfig: coreConfig, // Pass collected core config to installer - moduleConfig: moduleConfig, // Pass collected module configs (including custom modules) enableAgentVibes: agentVibesConfig.enabled, // AgentVibes TTS integration agentVibesInstalled: agentVibesConfig.alreadyInstalled, - customContent: customContent, // Custom content to install }; } - /** - * Handle custom content selection in module phase - * @param {string} projectDir - Project directory - * @returns {Object} Custom content info with selected items - */ - async handleCustomContentSelection(projectDir) { - const defaultPath = path.join(projectDir, 'bmad-custom-src'); - const hasDefaultFolder = await fs.pathExists(defaultPath); - - let customPath; - - if (hasDefaultFolder) { - console.log(chalk.cyan('\nšŸ“ Custom Content Detected')); - console.log(chalk.dim(`Found custom folder at: ${defaultPath}`)); - - const { useDetected } = await inquirer.prompt([ - { - type: 'confirm', - name: 'useDetected', - message: 'Install from detected custom folder?', - default: true, - }, - ]); - - if (useDetected) { - customPath = defaultPath; - } - } - - if (!customPath) { - console.log(chalk.cyan('\nšŸ“ Custom Content Selection')); - - const { specifiedPath } = await inquirer.prompt([ - { - type: 'input', - name: 'specifiedPath', - message: 'Enter path to custom content folder:', - default: './bmad-custom-src', - validate: async (input) => { - if (!input.trim()) { - return 'Path is required'; - } - const resolvedPath = path.resolve(input.trim()); - if (!(await fs.pathExists(resolvedPath))) { - return `Path does not exist: ${resolvedPath}`; - } - return true; - }, - }, - ]); - - customPath = path.resolve(specifiedPath.trim()); - } - - // Discover and categorize custom content - const customContent = await this.discoverAndSelectCustomContent(customPath); - - return { - path: customPath, - selectedItems: customContent, - }; - } - - /** - * Discover and allow selection of custom content - * @param {string} customPath - Path to custom content - * @returns {Object} Selected items by type - */ - async discoverAndSelectCustomContent(customPath) { - CLIUtils.displaySection('Custom Content', 'Discovering agents, workflows, and modules'); - - // Discover each type - const agents = await this.discoverCustomAgents(path.join(customPath, 'agents')); - const workflows = await this.discoverCustomWorkflows(path.join(customPath, 'workflows')); - const modules = await this.discoverCustomModules(path.join(customPath, 'modules')); - - // Build choices for selection - const choices = []; - - if (agents.length > 0) { - choices.push({ name: '--- šŸ‘„ Custom Agents ---', value: 'sep-agents', disabled: true }); - for (const agent of agents) { - const shortDesc = agent.description.length > 50 ? agent.description.slice(0, 47) + '...' : agent.description; - choices.push({ - name: ` ${agent.name} - ${shortDesc}`, - value: { type: 'agent', ...agent }, - checked: true, - }); - } - } - - if (workflows.length > 0) { - choices.push({ name: '--- šŸ“‹ Custom Workflows ---', value: 'sep-workflows', disabled: true }); - for (const workflow of workflows) { - const shortDesc = workflow.description.length > 50 ? workflow.description.slice(0, 47) + '...' : workflow.description; - choices.push({ - name: ` ${workflow.name} - ${shortDesc}`, - value: { type: 'workflow', ...workflow }, - checked: true, - }); - } - } - - if (modules.length > 0) { - choices.push({ name: '--- šŸ”§ Custom Modules ---', value: 'sep-modules', disabled: true }); - for (const module of modules) { - const shortDesc = module.description.length > 50 ? module.description.slice(0, 47) + '...' : module.description; - choices.push({ - name: ` ${module.name} - ${shortDesc}`, - value: { type: 'module', ...module }, - checked: true, - }); - } - } - - if (choices.length === 0) { - console.log(chalk.yellow('āš ļø No custom content found')); - return { agents: [], workflows: [], modules: [] }; - } - - // Ask for selection - const { selectedItems } = await inquirer.prompt([ - { - type: 'checkbox', - name: 'selectedItems', - message: 'Select custom items to install:', - choices: choices, - pageSize: 15, - }, - ]); - - // Organize by type - const result = { agents: [], workflows: [], modules: [] }; - for (const item of selectedItems) { - switch (item.type) { - case 'agent': { - result.agents.push(item); - break; - } - case 'workflow': { - result.workflows.push(item); - break; - } - case 'module': { - result.modules.push(item); - break; - } - } - } - - console.log( - chalk.green(`\nāœ“ Selected: ${result.agents.length} agents, ${result.workflows.length} workflows, ${result.modules.length} modules`), - ); - - return result; - } - - /** - * Discover custom agents - */ - async discoverCustomAgents(agentsPath) { - const agents = []; - if (!(await fs.pathExists(agentsPath))) return agents; - - const entries = await fs.readdir(agentsPath, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isDirectory()) { - const agentPath = path.join(agentsPath, entry.name); - const yamlFiles = await fs.readdir(agentPath).then((files) => files.filter((f) => f.endsWith('.agent.yaml'))); - - if (yamlFiles.length > 0) { - const yamlPath = path.join(agentPath, yamlFiles[0]); - const yamlData = yaml.load(await fs.readFile(yamlPath, 'utf8')); - agents.push({ - name: entry.name, - path: agentPath, - yamlPath: yamlPath, - description: yamlData.metadata?.description || yamlData.description || 'Custom agent', - hasSidecar: true, - }); - } - } else if (entry.isFile() && entry.name.endsWith('.agent.yaml')) { - const yamlData = yaml.load(await fs.readFile(path.join(agentsPath, entry.name), 'utf8')); - agents.push({ - name: path.basename(entry.name, '.agent.yaml'), - path: agentsPath, - yamlPath: path.join(agentsPath, entry.name), - description: yamlData.metadata?.description || yamlData.description || 'Custom agent', - hasSidecar: false, - }); - } - } - - return agents; - } - - /** - * Discover custom workflows - */ - async discoverCustomWorkflows(workflowsPath) { - const workflows = []; - if (!(await fs.pathExists(workflowsPath))) return workflows; - - const entries = await fs.readdir(workflowsPath, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isFile() && entry.name.endsWith('.md')) { - const filePath = path.join(workflowsPath, entry.name); - const content = await fs.readFile(filePath, 'utf8'); - - // Extract YAML frontmatter - let title = path.basename(entry.name, '.md'); - let description = ''; - let yamlMetadata = {}; - - // Check for YAML frontmatter - if (content.startsWith('---\n')) { - const frontmatterEnd = content.indexOf('\n---\n', 4); - if (frontmatterEnd !== -1) { - const yamlContent = content.slice(4, frontmatterEnd); - try { - yamlMetadata = yaml.load(yamlContent); - title = yamlMetadata.name || yamlMetadata.title || title; - description = yamlMetadata.description || yamlMetadata.summary || ''; - } catch { - // If YAML parsing fails, fall back to markdown parsing - } - } - } - - // If no YAML frontmatter or no metadata, parse from markdown - if (!title || !description) { - const lines = content.split('\n'); - for (const line of lines) { - if (line.startsWith('# ')) { - title = line.slice(2).trim(); - } else if (line.startsWith('## Description:')) { - description = line.replace('## Description:', '').trim(); - } - if (title && description) break; - } - } - - workflows.push({ - name: title, - path: filePath, - description: description || 'Custom workflow', - metadata: yamlMetadata, - }); - } else if (entry.isDirectory()) { - // Check for workflow.md in subdirectories - const workflowMdPath = path.join(workflowsPath, entry.name, 'workflow.md'); - if (await fs.pathExists(workflowMdPath)) { - const content = await fs.readFile(workflowMdPath, 'utf8'); - - // Extract YAML frontmatter - let title = entry.name; - let description = ''; - let yamlMetadata = {}; - - // Check for YAML frontmatter - if (content.startsWith('---\n')) { - const frontmatterEnd = content.indexOf('\n---\n', 4); - if (frontmatterEnd !== -1) { - const yamlContent = content.slice(4, frontmatterEnd); - try { - yamlMetadata = yaml.load(yamlContent); - title = yamlMetadata.name || yamlMetadata.title || title; - description = yamlMetadata.description || yamlMetadata.summary || ''; - } catch { - // If YAML parsing fails, fall back to markdown parsing - } - } - } - - // If no YAML frontmatter or no metadata, parse from markdown - if (!title || !description) { - const lines = content.split('\n'); - for (const line of lines) { - if (line.startsWith('# ')) { - title = line.slice(2).trim(); - } else if (line.startsWith('## Description:')) { - description = line.replace('## Description:', '').trim(); - } - if (title && description) break; - } - } - - workflows.push({ - name: title, - path: path.join(workflowsPath, entry.name), // Store the DIRECTORY path, not the file - description: description || 'Custom workflow', - metadata: yamlMetadata, - }); - } - } - } - - return workflows; - } - - /** - * Discover custom modules - */ - async discoverCustomModules(modulesPath) { - const modules = []; - if (!(await fs.pathExists(modulesPath))) return modules; - - const entries = await fs.readdir(modulesPath, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isDirectory()) { - const modulePath = path.join(modulesPath, entry.name); - const installerPath = path.join(modulePath, '_module-installer'); - - if (await fs.pathExists(installerPath)) { - // Check for install-config.yaml - const configPath = path.join(installerPath, 'install-config.yaml'); - let description = 'Custom module'; - - if (await fs.pathExists(configPath)) { - const configData = yaml.load(await fs.readFile(configPath, 'utf8')); - description = configData.header || configData.description || description; - } - - modules.push({ - name: entry.name, - path: modulePath, - description: description, - }); - } - } - } - - return modules; - } - - /** - * Handle custom content installation - * @param {string} projectDir - Project directory - */ - async handleCustomContent(projectDir) { - const defaultPath = path.join(projectDir, 'bmad-custom-src'); - const hasDefaultFolder = await fs.pathExists(defaultPath); - - let customPath; - - if (hasDefaultFolder) { - console.log(chalk.cyan('\nšŸ“ Custom Content Detected')); - console.log(chalk.dim(`Found custom folder at: ${defaultPath}`)); - - const { useDetected } = await inquirer.prompt([ - { - type: 'confirm', - name: 'useDetected', - message: 'Install from detected custom folder?', - default: true, - }, - ]); - - if (useDetected) { - customPath = defaultPath; - } - } - - if (!customPath) { - console.log(chalk.cyan('\nšŸ“ Custom Content Installation')); - - const { specifiedPath } = await inquirer.prompt([ - { - type: 'input', - name: 'specifiedPath', - message: 'Enter path to custom content folder:', - default: './bmad-custom-src', - validate: async (input) => { - if (!input.trim()) { - return 'Path is required'; - } - const resolvedPath = path.resolve(input.trim()); - if (!(await fs.pathExists(resolvedPath))) { - return `Path does not exist: ${resolvedPath}`; - } - return true; - }, - }, - ]); - - customPath = path.resolve(specifiedPath.trim()); - } - - // Discover custom content - const customContent = { - agents: await this.discoverCustomAgents(path.join(customPath, 'agents')), - modules: await this.discoverCustomModules(path.join(customPath, 'modules')), - workflows: await this.discoverCustomWorkflows(path.join(customPath, 'workflows')), - }; - - // Show discovery results - console.log(chalk.cyan('\nšŸ” Custom Content Discovery')); - console.log(chalk.dim(`Scanning: ${customPath}`)); - - if (customContent.agents.length > 0) { - console.log(chalk.green(` āœ“ Found ${customContent.agents.length} custom agent(s)`)); - } - if (customContent.modules.length > 0) { - console.log(chalk.green(` āœ“ Found ${customContent.modules.length} custom module(s)`)); - } - if (customContent.workflows.length > 0) { - console.log(chalk.green(` āœ“ Found ${customContent.workflows.length} custom workflow(s)`)); - } - - if (customContent.agents.length === 0 && customContent.modules.length === 0 && customContent.workflows.length === 0) { - console.log(chalk.yellow(' āš ļø No custom content found in the specified folder')); - return; - } - - // Confirm installation - const { confirmInstall } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmInstall', - message: 'Install discovered custom content?', - default: true, - }, - ]); - - if (confirmInstall) { - console.log(chalk.green('\nšŸš€ Installing Custom Content...')); - // Store custom content for later installation - this._customContent = { - path: customPath, - items: customContent, - }; - console.log(chalk.dim(` Custom content queued for installation`)); - } - } - - /** - * Discover custom content in a directory - * @param {string} dirPath - Directory path to scan - * @returns {Promise} List of discovered items - */ - async discoverCustomContent(dirPath) { - const items = []; - - if (!(await fs.pathExists(dirPath))) { - return items; - } - - try { - const entries = await fs.readdir(dirPath, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isDirectory()) { - items.push({ - name: entry.name, - path: path.join(dirPath, entry.name), - type: 'directory', - }); - } else if (entry.isFile() && (entry.name.endsWith('.agent.yaml') || entry.name.endsWith('.md'))) { - items.push({ - name: entry.name, - path: path.join(dirPath, entry.name), - type: 'file', - }); - } - } - } catch { - // Silently ignore errors during discovery - } - - return items; - } - /** * Prompt for tool/IDE selection (called after module configuration) * @param {string} projectDir - Project directory to check for existing IDEs @@ -723,8 +224,6 @@ class UI { } } - // Custom option moved to module selection - CLIUtils.displaySection('Tool Integration', 'Select AI coding assistants and IDEs to configure'); let answers; @@ -742,8 +241,6 @@ class UI { }, ]); - // Custom selection moved to module phase - // If tools were selected, we're done if (answers.ides && answers.ides.length > 0) { break; @@ -778,7 +275,6 @@ class UI { return { ides: answers.ides || [], skipIde: !answers.ides || answers.ides.length === 0, - customContent: this._customContent || null, }; } @@ -974,35 +470,6 @@ class UI { return configCollector.collectedConfig.core; } - /** - * Collect module configurations - * @param {string} directory - Installation directory - * @param {Array} modules - Selected modules - * @param {Object} existingCoreConfig - Core config already collected - * @returns {Object} Module configurations - */ - async collectModuleConfigs(directory, modules, existingCoreConfig = null) { - const { ConfigCollector } = require('../installers/lib/core/config-collector'); - const configCollector = new ConfigCollector(); - - // Load existing configs first if they exist - await configCollector.loadExistingConfig(directory); - - // If core config was already collected, use it - if (existingCoreConfig) { - configCollector.collectedConfig.core = existingCoreConfig; - } - - // Collect configurations for all modules except core (already collected earlier) - // ConfigCollector now handles custom modules properly - const modulesWithoutCore = modules.filter((m) => m !== 'core'); - if (modulesWithoutCore.length > 0) { - await configCollector.collectAllConfigurations(modulesWithoutCore, directory); - } - - return configCollector.collectedConfig; - } - /** * Get module choices for selection * @param {Set} installedModuleIds - Currently installed module IDs @@ -1014,32 +481,11 @@ class UI { const availableModules = await moduleManager.listAvailable(); const isNewInstallation = installedModuleIds.size === 0; - const moduleChoices = availableModules.map((mod) => ({ + return availableModules.map((mod) => ({ name: mod.name, value: mod.id, checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id), })); - - // Check for custom source folder - const customPath = path.join(process.cwd(), 'bmad-custom-src'); - const hasCustomFolder = await fs.pathExists(customPath); - - // Add custom option at the beginning - if (hasCustomFolder) { - moduleChoices.unshift({ - name: 'šŸ“ Custom: Agents, Workflows, Modules', - value: 'custom', - checked: false, - }); - } else { - moduleChoices.unshift({ - name: 'šŸ“ Custom: Agents, Workflows, Modules (specify path)', - value: 'custom', - checked: false, - }); - } - - return moduleChoices; } /**