From 23f650ff4dea1b019fc884414013b51e1eec1cbd Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Thu, 18 Dec 2025 03:22:46 +0800 Subject: [PATCH 1/4] fixed _bmad folder stutter with agent custom files --- tools/cli/installers/lib/core/installer.js | 514 +-- .../cli/installers/lib/core/installer.js.bak | 3204 ----------------- tools/cli/installers/lib/modules/manager.js | 2 +- 3 files changed, 32 insertions(+), 3688 deletions(-) delete mode 100644 tools/cli/installers/lib/core/installer.js.bak diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 3815c27a..5b403972 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -13,12 +13,13 @@ const { XmlHandler } = require('../../../lib/xml-handler'); const { DependencyResolver } = require('./dependency-resolver'); const { ConfigCollector } = require('./config-collector'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); -const { AgentPartyGenerator } = require('../../../lib/agent-party-generator'); const { CLIUtils } = require('../../../lib/cli-utils'); const { ManifestGenerator } = require('./manifest-generator'); const { IdeConfigManager } = require('./ide-config-manager'); const { CustomHandler } = require('../custom/handler'); -const { filterCustomizationData } = require('../../../lib/agent/compiler'); + +// BMAD installation folder name - this is constant and should never change +const BMAD_FOLDER_NAME = '_bmad'; class Installer { constructor() { @@ -34,58 +35,35 @@ class Installer { this.ideConfigManager = new IdeConfigManager(); this.installedFiles = new Set(); // Track all installed files this.ttsInjectedFiles = []; // Track files with TTS injection applied + this.bmadFolderName = BMAD_FOLDER_NAME; } /** * Find the bmad installation directory in a project - * V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml + * Always uses the standard _bmad folder name * Also checks for legacy _cfg folder for migration * @param {string} projectDir - Project directory * @returns {Promise} { bmadDir: string, hasLegacyCfg: boolean } */ async findBmadDir(projectDir) { + const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); + // Check if project directory exists if (!(await fs.pathExists(projectDir))) { // Project doesn't exist yet, return default - return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false }; + return { bmadDir, hasLegacyCfg: false }; } - let bmadDir = null; + // Check for legacy _cfg folder if bmad directory exists let hasLegacyCfg = false; - - try { - const entries = await fs.readdir(projectDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - const bmadPath = path.join(projectDir, entry.name); - - // Check for current _config folder - const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml'); - if (await fs.pathExists(manifestPath)) { - // Found a V6+ installation with current _config folder - return { bmadDir: bmadPath, hasLegacyCfg: false }; - } - - // Check for legacy _cfg folder - const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml'); - if (await fs.pathExists(legacyManifestPath)) { - bmadDir = bmadPath; - hasLegacyCfg = true; - } - } + if (await fs.pathExists(bmadDir)) { + const legacyCfgPath = path.join(bmadDir, '_cfg'); + if (await fs.pathExists(legacyCfgPath)) { + hasLegacyCfg = true; } - } catch { - console.log(chalk.red('Error reading project directory for BMAD installation detection')); } - // If we found a bmad directory (with or without legacy _cfg) - if (bmadDir) { - return { bmadDir, hasLegacyCfg }; - } - - // No V6+ installation found, return default - // This will be used for new installations - return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false }; + return { bmadDir, hasLegacyCfg }; } /** @@ -120,7 +98,7 @@ class Installer { * * 3. Document marker in instructions.md (if applicable) */ - async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) { + async copyFileWithPlaceholderReplacement(sourcePath, targetPath) { // List of text file extensions that should have placeholder replacement const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml']; const ext = path.extname(sourcePath).toLowerCase(); @@ -285,7 +263,7 @@ class Installer { // Check for already configured IDEs const { Detector } = require('./detector'); const detector = new Detector(); - const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad'); + const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); // During full reinstall, use the saved previous IDEs since bmad dir was deleted // Otherwise detect from existing installation @@ -532,18 +510,14 @@ class Installer { } } - // Always use _bmad as the folder name - const bmadFolderName = '_bmad'; - this.bmadFolderName = bmadFolderName; // Store for use in other methods - // Store AgentVibes configuration for injection point processing this.enableAgentVibes = config.enableAgentVibes || false; // Set bmad folder name on module manager and IDE manager for placeholder replacement - this.moduleManager.setBmadFolderName(bmadFolderName); + this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME); this.moduleManager.setCoreConfig(moduleConfigs.core || {}); this.moduleManager.setCustomModulePaths(customModulePaths); - this.ideManager.setBmadFolderName(bmadFolderName); + this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME); // Tool selection will be collected after we determine if it's a reinstall/update/new install @@ -553,14 +527,8 @@ class Installer { // Resolve target directory (path.resolve handles platform differences) const projectDir = path.resolve(config.directory); - let existingBmadDir = null; - let existingBmadFolderName = null; - - if (await fs.pathExists(projectDir)) { - const result = await this.findBmadDir(projectDir); - existingBmadDir = result.bmadDir; - existingBmadFolderName = path.basename(existingBmadDir); - } + // Always use the standard _bmad folder name + const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); // Create a project directory if it doesn't exist (user already confirmed) if (!(await fs.pathExists(projectDir))) { @@ -582,8 +550,6 @@ class Installer { } } - const bmadDir = path.join(projectDir, bmadFolderName); - // Check existing installation spinner.text = 'Checking for existing installation...'; const existingInstall = await this.detector.detect(bmadDir); @@ -1606,7 +1572,7 @@ class Installer { const targetPath = path.join(agentsDir, fileName); if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); + await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath); this.installedFiles.add(targetPath); } } @@ -1622,7 +1588,7 @@ class Installer { const targetPath = path.join(tasksDir, fileName); if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); + await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath); this.installedFiles.add(targetPath); } } @@ -1638,7 +1604,7 @@ class Installer { const targetPath = path.join(toolsDir, fileName); if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); + await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath); this.installedFiles.add(targetPath); } } @@ -1654,7 +1620,7 @@ class Installer { const targetPath = path.join(templatesDir, fileName); if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); + await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath); this.installedFiles.add(targetPath); } } @@ -1669,7 +1635,7 @@ class Installer { await fs.ensureDir(path.dirname(targetPath)); if (await fs.pathExists(dataPath)) { - await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad'); + await this.copyFileWithPlaceholderReplacement(dataPath, targetPath); this.installedFiles.add(targetPath); } } @@ -1759,14 +1725,9 @@ class Installer { } } - // Check if this is a workflow.yaml file - if (file.endsWith('workflow.yaml')) { - await fs.ensureDir(path.dirname(targetFile)); - await this.copyWorkflowYamlStripped(sourceFile, targetFile); - } else { - // Copy the file with placeholder replacement - await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad'); - } + // Copy the file with placeholder replacement + await fs.ensureDir(path.dirname(targetFile)); + await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile); // Track the installed file this.installedFiles.add(targetFile); @@ -1844,7 +1805,7 @@ class Installer { if (!(await fs.pathExists(customizePath))) { const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml'); if (await fs.pathExists(genericTemplatePath)) { - await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad'); + await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`)); } @@ -1853,235 +1814,6 @@ class Installer { } } - /** - * Build standalone agents in bmad/agents/ directory - * @param {string} bmadDir - Path to bmad directory - * @param {string} projectDir - Path to project directory - */ - async buildStandaloneAgents(bmadDir, projectDir) { - const standaloneAgentsPath = path.join(bmadDir, 'agents'); - const cfgAgentsDir = path.join(bmadDir, '_config', 'agents'); - - // Check if standalone agents directory exists - if (!(await fs.pathExists(standaloneAgentsPath))) { - return; - } - - // Get all subdirectories in agents/ - const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true }); - - for (const agentDir of agentDirs) { - if (!agentDir.isDirectory()) continue; - - const agentDirPath = path.join(standaloneAgentsPath, agentDir.name); - - // Find any .agent.yaml file in the directory - const files = await fs.readdir(agentDirPath); - const yamlFile = files.find((f) => f.endsWith('.agent.yaml')); - - if (!yamlFile) continue; - - const agentName = path.basename(yamlFile, '.agent.yaml'); - const sourceYamlPath = path.join(agentDirPath, yamlFile); - const targetMdPath = path.join(agentDirPath, `${agentName}.md`); - const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`); - - // Check for customizations - const customizeExists = await fs.pathExists(customizePath); - let customizedFields = []; - - if (customizeExists) { - const customizeContent = await fs.readFile(customizePath, 'utf8'); - const yaml = require('yaml'); - const customizeYaml = yaml.parse(customizeContent); - - // Detect what fields are customized (similar to rebuildAgentFiles) - if (customizeYaml) { - if (customizeYaml.persona) { - for (const [key, value] of Object.entries(customizeYaml.persona)) { - if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) { - customizedFields.push(`persona.${key}`); - } - } - } - if (customizeYaml.agent?.metadata) { - for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) { - if (value !== '' && value !== null) { - customizedFields.push(`metadata.${key}`); - } - } - } - if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) { - customizedFields.push('critical_actions'); - } - if (customizeYaml.menu && customizeYaml.menu.length > 0) { - customizedFields.push('menu'); - } - } - } - - // Build YAML to XML .md - let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, { - includeMetadata: true, - }); - - // DO NOT replace {project-root} - LLMs understand this placeholder at runtime - // const processedContent = xmlContent.replaceAll('{project-root}', projectDir); - - // Process TTS injection points (pass targetPath for tracking) - xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath); - - // Write the built .md file with POSIX-compliant final newline - const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n'; - await fs.writeFile(targetMdPath, content, 'utf8'); - - // Display result - if (customizedFields.length > 0) { - console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`)); - } else { - console.log(chalk.dim(` Built standalone agent: ${agentName}.md`)); - } - } - } - - /** - * Rebuild agent files from installer source (for compile command) - * @param {string} modulePath - Path to module in bmad/ installation - * @param {string} moduleName - Module name - */ - async rebuildAgentFiles(modulePath, moduleName) { - // Get source agents directory from installer - const sourceAgentsPath = - moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents'); - - if (!(await fs.pathExists(sourceAgentsPath))) { - return; // No source agents to rebuild - } - - // Determine project directory (parent of bmad/ directory) - const bmadDir = path.dirname(modulePath); - const projectDir = path.dirname(bmadDir); - const cfgAgentsDir = path.join(bmadDir, '_config', 'agents'); - const targetAgentsPath = path.join(modulePath, 'agents'); - - // Ensure target directory exists - await fs.ensureDir(targetAgentsPath); - - // Get all YAML agent files from source - const sourceFiles = await fs.readdir(sourceAgentsPath); - - for (const file of sourceFiles) { - if (file.endsWith('.agent.yaml')) { - const agentName = file.replace('.agent.yaml', ''); - const sourceYamlPath = path.join(sourceAgentsPath, file); - const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`); - const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`); - - // Check for customizations - const customizeExists = await fs.pathExists(customizePath); - let customizedFields = []; - - if (customizeExists) { - const customizeContent = await fs.readFile(customizePath, 'utf8'); - const yaml = require('yaml'); - const customizeYaml = yaml.parse(customizeContent); - - // Detect what fields are customized - if (customizeYaml) { - if (customizeYaml.persona) { - for (const [key, value] of Object.entries(customizeYaml.persona)) { - if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) { - customizedFields.push(`persona.${key}`); - } - } - } - if (customizeYaml.agent?.metadata) { - for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) { - if (value !== '' && value !== null) { - customizedFields.push(`metadata.${key}`); - } - } - } - if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) { - customizedFields.push('critical_actions'); - } - if (customizeYaml.memories && customizeYaml.memories.length > 0) { - customizedFields.push('memories'); - } - if (customizeYaml.menu && customizeYaml.menu.length > 0) { - customizedFields.push('menu'); - } - if (customizeYaml.prompts && customizeYaml.prompts.length > 0) { - customizedFields.push('prompts'); - } - } - } - - // Read the YAML content - const yamlContent = await fs.readFile(sourceYamlPath, 'utf8'); - - // Read customize content if exists - let customizeData = {}; - if (customizeExists) { - const customizeContent = await fs.readFile(customizePath, 'utf8'); - const yaml = require('yaml'); - customizeData = yaml.parse(customizeContent); - } - - // Build agent answers from customize data (filter empty values) - const answers = {}; - if (customizeData.persona) { - Object.assign(answers, filterCustomizationData(customizeData.persona)); - } - if (customizeData.agent?.metadata) { - const filteredMetadata = filterCustomizationData(customizeData.agent.metadata); - if (Object.keys(filteredMetadata).length > 0) { - Object.assign(answers, { metadata: filteredMetadata }); - } - } - if (customizeData.critical_actions && customizeData.critical_actions.length > 0) { - answers.critical_actions = customizeData.critical_actions; - } - if (customizeData.memories && customizeData.memories.length > 0) { - answers.memories = customizeData.memories; - } - - const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml'); - let coreConfig = {}; - if (await fs.pathExists(coreConfigPath)) { - const yaml = require('yaml'); - const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8'); - coreConfig = yaml.parse(coreConfigContent); - } - - // Compile using the same compiler as initial installation - const { compileAgent } = require('../../../lib/agent/compiler'); - 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 = 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'; - await fs.writeFile(targetMdPath, content, 'utf8'); - - // Display result with customizations if any - if (customizedFields.length > 0) { - console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`)); - } else { - console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`)); - } - } - } - } - /** * Private: Update core */ @@ -2677,190 +2409,6 @@ class Installer { return { customFiles, modifiedFiles }; } - /** - * Private: Create agent configuration files - * @param {string} bmadDir - BMAD installation directory - * @param {Object} userInfo - User information including name and language - */ - async createAgentConfigs(bmadDir, userInfo = null) { - const agentConfigDir = path.join(bmadDir, '_config', 'agents'); - await fs.ensureDir(agentConfigDir); - - // Get all agents from all modules - const agents = []; - const agentDetails = []; // For manifest generation - - // Check modules for agents (including core) - const entries = await fs.readdir(bmadDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory() && entry.name !== '_config') { - const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents'); - if (await fs.pathExists(moduleAgentsPath)) { - const agentFiles = await fs.readdir(moduleAgentsPath); - for (const agentFile of agentFiles) { - if (agentFile.endsWith('.md')) { - const agentPath = path.join(moduleAgentsPath, agentFile); - const agentContent = await fs.readFile(agentPath, 'utf8'); - - // Skip agents with localskip="true" - const hasLocalSkip = agentContent.match(/]*\slocalskip="true"[^>]*>/); - if (hasLocalSkip) { - continue; // Skip this agent - it should not have been installed - } - - const agentName = path.basename(agentFile, '.md'); - - // Extract any nodes with agentConfig="true" - const agentConfigNodes = this.extractAgentConfigNodes(agentContent); - - agents.push({ - name: agentName, - module: entry.name, - agentConfigNodes: agentConfigNodes, - }); - - // Use shared AgentPartyGenerator to extract details - let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName); - - // Apply config overrides if they exist - if (details) { - const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`); - if (await fs.pathExists(configPath)) { - const configContent = await fs.readFile(configPath, 'utf8'); - details = AgentPartyGenerator.applyConfigOverrides(details, configContent); - } - agentDetails.push(details); - } - } - } - } - } - } - - // Create config file for each agent - let createdCount = 0; - let skippedCount = 0; - - // Load agent config template - const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md'); - const templateContent = await fs.readFile(templatePath, 'utf8'); - - for (const agent of agents) { - const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`); - - // Skip if config file already exists (preserve custom configurations) - if (await fs.pathExists(configPath)) { - skippedCount++; - continue; - } - - // Build config content header - let configContent = `# Agent Config: ${agent.name}\n\n`; - - // Process template and add agent-specific config nodes - let processedTemplate = templateContent; - - // Replace {core:user_name} placeholder with actual user name if available - if (userInfo && userInfo.userName) { - processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName); - } - - // Replace {core:communication_language} placeholder with actual language if available - if (userInfo && userInfo.responseLanguage) { - processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage); - } - - // If this agent has agentConfig nodes, add them after the existing comment - if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) { - // Find the agent-specific configuration nodes comment - const commentPattern = /(\s*)/; - const commentMatch = processedTemplate.match(commentPattern); - - if (commentMatch) { - // Add nodes right after the comment - let agentSpecificNodes = ''; - for (const node of agent.agentConfigNodes) { - agentSpecificNodes += `\n ${node}`; - } - - processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`); - } - } - - configContent += processedTemplate; - - // Ensure POSIX-compliant final newline - if (!configContent.endsWith('\n')) { - configContent += '\n'; - } - - await fs.writeFile(configPath, configContent, 'utf8'); - this.installedFiles.add(configPath); // Track agent config files - createdCount++; - } - - // Generate agent manifest with overrides applied - await this.generateAgentManifest(bmadDir, agentDetails); - - return { total: agents.length, created: createdCount, skipped: skippedCount }; - } - - /** - * Generate agent manifest XML file - * @param {string} bmadDir - BMAD installation directory - * @param {Array} agentDetails - Array of agent details - */ - async generateAgentManifest(bmadDir, agentDetails) { - const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv'); - await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false }); - } - - /** - * Extract nodes with agentConfig="true" from agent content - * @param {string} content - Agent file content - * @returns {Array} Array of XML nodes that should be added to agent config - */ - extractAgentConfigNodes(content) { - const nodes = []; - - try { - // Find all XML nodes with agentConfig="true" - // Match self-closing tags and tags with content - const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g; - const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g; - - // Extract self-closing tags - let match; - while ((match = selfClosingPattern.exec(content)) !== null) { - // Extract just the tag without children (structure only) - const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/); - if (tagMatch) { - const tagName = tagMatch[1]; - const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute - nodes.push(`<${tagName}${attributes}>`); - } - } - - // Extract tags with content - while ((match = withContentPattern.exec(content)) !== null) { - const fullMatch = match[0]; - const tagName = match[1]; - - // Extract opening tag with attributes (removing agentConfig="true") - const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`)); - if (openingTagMatch) { - const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, ''); - // Add empty node structure (no children) - nodes.push(`<${tagName}${attributes}>`); - } - } - } catch (error) { - console.error('Error extracting agentConfig nodes:', error); - } - - return nodes; - } - /** * Handle missing custom module sources interactively * @param {Map} customModuleSources - Map of custom module ID to info @@ -2999,7 +2547,7 @@ class Installer { await this.manifest.addCustomModule(bmadDir, missing.info); validCustomModules.push({ - id: moduleId, + id: missing.id, name: missing.name, path: resolvedPath, info: missing.info, @@ -3013,7 +2561,7 @@ class Installer { case 'remove': { // Extra confirmation for destructive remove console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`)); - console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`)); + console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`)); const { confirm } = await inquirer.prompt([ { diff --git a/tools/cli/installers/lib/core/installer.js.bak b/tools/cli/installers/lib/core/installer.js.bak deleted file mode 100644 index 49b1d62d..00000000 --- a/tools/cli/installers/lib/core/installer.js.bak +++ /dev/null @@ -1,3204 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const chalk = require('chalk'); -const ora = require('ora'); -const inquirer = require('inquirer'); -const { Detector } = require('./detector'); -const { Manifest } = require('./manifest'); -const { ModuleManager } = require('../modules/manager'); -const { IdeManager } = require('../ide/manager'); -const { FileOps } = require('../../../lib/file-ops'); -const { Config } = require('../../../lib/config'); -const { XmlHandler } = require('../../../lib/xml-handler'); -const { DependencyResolver } = require('./dependency-resolver'); -const { ConfigCollector } = require('./config-collector'); -const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); -const { AgentPartyGenerator } = require('../../../lib/agent-party-generator'); -const { CLIUtils } = require('../../../lib/cli-utils'); -const { ManifestGenerator } = require('./manifest-generator'); -const { IdeConfigManager } = require('./ide-config-manager'); -const { CustomHandler } = require('../custom/handler'); -const { filterCustomizationData } = require('../../../lib/agent/compiler'); - -class Installer { - constructor() { - this.detector = new Detector(); - this.manifest = new Manifest(); - this.moduleManager = new ModuleManager(); - this.ideManager = new IdeManager(); - this.fileOps = new FileOps(); - this.config = new Config(); - this.xmlHandler = new XmlHandler(); - this.dependencyResolver = new DependencyResolver(); - this.configCollector = new ConfigCollector(); - this.ideConfigManager = new IdeConfigManager(); - this.installedFiles = new Set(); // Track all installed files - this.ttsInjectedFiles = []; // Track files with TTS injection applied - } - - /** - * Find the bmad installation directory in a project - * V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml - * Also checks for legacy _cfg folder for migration - * @param {string} projectDir - Project directory - * @returns {Promise} { bmadDir: string, hasLegacyCfg: boolean } - */ - async findBmadDir(projectDir) { - // Check if project directory exists - if (!(await fs.pathExists(projectDir))) { - // Project doesn't exist yet, return default - return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false }; - } - - let bmadDir = null; - let hasLegacyCfg = false; - - try { - const entries = await fs.readdir(projectDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - const bmadPath = path.join(projectDir, entry.name); - - // Check for current _config folder - const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml'); - if (await fs.pathExists(manifestPath)) { - // Found a V6+ installation with current _config folder - return { bmadDir: bmadPath, hasLegacyCfg: false }; - } - - // Check for legacy _cfg folder - const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml'); - if (await fs.pathExists(legacyManifestPath)) { - bmadDir = bmadPath; - hasLegacyCfg = true; - } - } - } - } catch { - console.log(chalk.red('Error reading project directory for BMAD installation detection')); - } - - // If we found a bmad directory (with or without legacy _cfg) - if (bmadDir) { - return { bmadDir, hasLegacyCfg }; - } - - // No V6+ installation found, return default - // This will be used for new installations - return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false }; - } - - /** - * @function copyFileWithPlaceholderReplacement - * @intent Copy files from BMAD source to installation directory with dynamic content transformation - * @why Enables installation-time customization: _bmad replacement + optional AgentVibes TTS injection - * @param {string} sourcePath - Absolute path to source file in BMAD repository - * @param {string} targetPath - Absolute path to destination file in user's project - * @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad') - * @returns {Promise} Resolves when file copy and transformation complete - * @sideeffects Writes transformed file to targetPath, creates parent directories if needed - * @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails - * @calledby installCore(), installModule(), IDE installers during file vendoring - * @calls processTTSInjectionPoints(), fs.readFile(), fs.writeFile(), fs.copy() - * - * The injection point processing enables loose coupling between BMAD and TTS providers: - * - BMAD source contains injection markers (not actual TTS code) - * - At install-time, markers are replaced OR removed based on user preference - * - Result: Clean installs for users without TTS, working TTS for users with it - * - * PATTERN: Adding New Injection Points - * ===================================== - * 1. Add HTML comment marker in BMAD source file: - * - * - * 2. Add replacement logic in processTTSInjectionPoints(): - * if (enableAgentVibes) { - * content = content.replace(//g, 'actual code'); - * } else { - * content = content.replace(/\n?/g, ''); - * } - * - * 3. Document marker in instructions.md (if applicable) - */ - async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) { - // List of text file extensions that should have placeholder replacement - const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml']; - const ext = path.extname(sourcePath).toLowerCase(); - - // Check if this is a text file that might contain placeholders - if (textExtensions.includes(ext)) { - try { - // Read the file content - let content = await fs.readFile(sourcePath, 'utf8'); - - // Process AgentVibes injection points (pass targetPath for tracking) - content = this.processTTSInjectionPoints(content, targetPath); - - // Write to target with replaced content - await fs.ensureDir(path.dirname(targetPath)); - await fs.writeFile(targetPath, content, 'utf8'); - } catch { - // If reading as text fails (might be binary despite extension), fall back to regular copy - await fs.copy(sourcePath, targetPath, { overwrite: true }); - } - } else { - // Binary file or other file type - just copy directly - await fs.copy(sourcePath, targetPath, { overwrite: true }); - } - } - - /** - * @function processTTSInjectionPoints - * @intent Transform TTS injection markers based on user's installation choice - * @why Enables optional TTS integration without tight coupling between BMAD and TTS providers - * @param {string} content - Raw file content containing potential injection markers - * @returns {string} Transformed content with markers replaced (if enabled) or stripped (if disabled) - * @sideeffects None - pure transformation function - * @edgecases Returns content unchanged if no markers present, safe to call on all files - * @calledby copyFileWithPlaceholderReplacement() during every file copy operation - * @calls String.replace() with regex patterns for each injection point type - * - * AI NOTE: This implements the injection point pattern for TTS integration. - * Key architectural decisions: - * - * 1. **Why Injection Points vs Direct Integration?** - * - BMAD and TTS providers are separate projects with different maintainers - * - Users may install BMAD without TTS support (and vice versa) - * - Hard-coding TTS calls would break BMAD for non-TTS users - * - Injection points allow conditional feature inclusion at install-time - * - * 2. **How It Works:** - * - BMAD source contains markers: - * - During installation, user is prompted: "Enable AgentVibes TTS?" - * - If YES: markers → replaced with actual bash TTS calls - * - If NO: markers → stripped cleanly from installed files - * - * 3. **State Management:** - * - this.enableAgentVibes set in install() method from config.enableAgentVibes - * - config.enableAgentVibes comes from ui.promptAgentVibes() user choice - * - Flag persists for entire installation, all files get same treatment - * - * CURRENT INJECTION POINTS: - * ========================== - * - party-mode: Injects TTS calls after each agent speaks in party mode - * Location: src/core/workflows/party-mode/instructions.md - * Marker: - * Replacement: Bash call to .claude/hooks/bmad-speak.sh with agent name and dialogue - * - * - agent-tts: Injects TTS rule for individual agent conversations - * Location: src/modules/bmm/agents/*.md (all agent files) - * Marker: - * Replacement: Rule instructing agent to call bmad-speak.sh with agent ID and response - * - * ADDING NEW INJECTION POINTS: - * ============================= - * 1. Add new case in this function: - * content = content.replace( - * //g, - * `code to inject when enabled` - * ); - * - * 2. Add marker to BMAD source file at injection location - * - * 3. Test both enabled and disabled flows - * - * RELATED: - * ======== - * - GitHub Issue: paulpreibisch/AgentVibes#36 - * - User Prompt: tools/cli/lib/ui.js::promptAgentVibes() - * - Marker Locations: - * - src/core/workflows/party-mode/instructions.md:101 - * - src/modules/bmm/agents/*.md (rules sections) - * - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo) - */ - processTTSInjectionPoints(content, targetPath = null) { - // Check if AgentVibes is enabled (set during installation configuration) - const enableAgentVibes = this.enableAgentVibes || false; - - // Check if content contains any TTS injection markers - const hasPartyMode = content.includes(''); - const hasAgentTTS = content.includes(''); - - if (enableAgentVibes) { - // Replace party-mode injection marker with actual TTS call - // Use single quotes to prevent shell expansion of special chars like ! - content = content.replaceAll( - '', - `IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/) - - If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: - - Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\` - - This speaks the dialogue with the agent's unique voice - - Run in background to not block next agent`, - ); - - // Replace agent-tts injection marker with TTS rule for individual agents - content = content.replaceAll( - '', - `- When responding to user messages, speak your responses using TTS: - Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response - Replace {agent-id} with YOUR agent ID from tag at top of this file - Replace {response-text} with the text you just output to the user - IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes - Run in background (&) to avoid blocking`, - ); - - // Track files that had TTS injection applied - if (targetPath && (hasPartyMode || hasAgentTTS)) { - const injectionType = hasPartyMode ? 'party-mode' : 'agent-tts'; - this.ttsInjectedFiles.push({ path: targetPath, type: injectionType }); - } - } else { - // Strip injection markers cleanly when AgentVibes is disabled - content = content.replaceAll(/\n?/g, ''); - content = content.replaceAll(/\n?/g, ''); - } - - return content; - } - - /** - * Collect Tool/IDE configurations after module configuration - * @param {string} projectDir - Project directory - * @param {Array} selectedModules - Selected modules from configuration - * @param {boolean} isFullReinstall - Whether this is a full reinstall - * @param {Array} previousIdes - Previously configured IDEs (for reinstalls) - * @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional) - * @returns {Object} Tool/IDE selection and configurations - */ - async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false, previousIdes = [], preSelectedIdes = null) { - // Use pre-selected IDEs if provided, otherwise prompt - let toolConfig; - if (preSelectedIdes === null) { - // Fallback: prompt for tool selection (backwards compatibility) - const { UI } = require('../../../lib/ui'); - const ui = new UI(); - toolConfig = await ui.promptToolSelection(projectDir, selectedModules); - } else { - // IDEs were already selected during initial prompts - toolConfig = { - ides: preSelectedIdes, - skipIde: !preSelectedIdes || preSelectedIdes.length === 0, - }; - } - - // Check for already configured IDEs - const { Detector } = require('./detector'); - const detector = new Detector(); - const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad'); - - // During full reinstall, use the saved previous IDEs since bmad dir was deleted - // Otherwise detect from existing installation - let previouslyConfiguredIdes; - if (isFullReinstall) { - // During reinstall, treat all IDEs as new (need configuration) - previouslyConfiguredIdes = []; - } else { - const existingInstall = await detector.detect(bmadDir); - previouslyConfiguredIdes = existingInstall.ides || []; - } - - // Load saved IDE configurations for already-configured IDEs - const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir); - - // Collect IDE-specific configurations if any were selected - const ideConfigurations = {}; - - // First, add saved configs for already-configured IDEs - for (const ide of toolConfig.ides || []) { - if (previouslyConfiguredIdes.includes(ide) && savedIdeConfigs[ide]) { - ideConfigurations[ide] = savedIdeConfigs[ide]; - } - } - - if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) { - // Determine which IDEs are newly selected (not previously configured) - const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide)); - - if (newlySelectedIdes.length > 0) { - console.log('\n'); // Add spacing before IDE questions - - for (const ide of newlySelectedIdes) { - // List of IDEs that have interactive prompts - //TODO: Why is this here, hardcoding this list here is bad, fix me! - const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes( - ide, - ); - - if (needsPrompts) { - // Get IDE handler and collect configuration - try { - // Dynamically load the IDE setup module - const ideModule = require(`../ide/${ide}`); - - // Get the setup class (handle different export formats) - let SetupClass; - const className = - ide - .split('-') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join('') + 'Setup'; - - if (ideModule[className]) { - SetupClass = ideModule[className]; - } else if (ideModule.default) { - SetupClass = ideModule.default; - } else { - continue; - } - - const ideSetup = new SetupClass(); - - // Check if this IDE has a collectConfiguration method - if (typeof ideSetup.collectConfiguration === 'function') { - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); - ideConfigurations[ide] = await ideSetup.collectConfiguration({ - selectedModules: selectedModules || [], - projectDir, - bmadDir, - }); - } - } catch { - // IDE doesn't have a setup file or collectConfiguration method - console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`)); - } - } - } - } - - // Log which IDEs are already configured and being kept - const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide)); - if (keptIdes.length > 0) { - console.log(chalk.dim(`\nKeeping existing configuration for: ${keptIdes.join(', ')}`)); - } - } - - return { - ides: toolConfig.ides, - skipIde: toolConfig.skipIde, - configurations: ideConfigurations, - }; - } - - /** - * Main installation method - * @param {Object} config - Installation configuration - * @param {string} config.directory - Target directory - * @param {boolean} config.installCore - Whether to install core - * @param {string[]} config.modules - Modules to install - * @param {string[]} config.ides - IDEs to configure - * @param {boolean} config.skipIde - Skip IDE configuration - */ - async install(originalConfig) { - // Clone config to avoid mutating the caller's object - const config = { ...originalConfig }; - - // Check if core config was already collected in UI - const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0; - - // Only display logo if core config wasn't already collected (meaning we're not continuing from UI) - if (!hasCoreConfig) { - // Display BMAD logo - CLIUtils.displayLogo(); - - // Display welcome message - CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version); - } - - // Note: Legacy V4 detection now happens earlier in UI.promptInstall() - // before any config collection, so we don't need to check again here - - const projectDir = path.resolve(config.directory); - - // If core config was pre-collected (from interactive mode), use it - if (config.coreConfig && Object.keys(config.coreConfig).length > 0) { - this.configCollector.collectedConfig.core = config.coreConfig; - // Also store in allAnswers for cross-referencing - this.configCollector.allAnswers = {}; - for (const [key, value] of Object.entries(config.coreConfig)) { - this.configCollector.allAnswers[`core_${key}`] = value; - } - } - - // 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; - - // For quick update, populate customModulePaths from _customModuleSources - if (config._customModuleSources) { - for (const [moduleId, customInfo] of config._customModuleSources) { - customModulePaths.set(moduleId, customInfo.sourcePath); - } - } - } else { - // For regular updates (modify flow), check manifest for custom module sources - if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) { - for (const customModule of config._existingInstall.customModules) { - // Ensure we have an absolute sourcePath - let absoluteSourcePath = customModule.sourcePath; - - // Check if sourcePath is a cache-relative path (starts with _config) - if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) { - // Convert cache-relative path to absolute path - absoluteSourcePath = path.join(bmadDir, absoluteSourcePath); - } - // If no sourcePath but we have relativePath, convert it - else if (!absoluteSourcePath && customModule.relativePath) { - // relativePath is relative to the project root (parent of bmad dir) - absoluteSourcePath = path.resolve(projectDir, customModule.relativePath); - } - // Ensure sourcePath is absolute for anything else - else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) { - absoluteSourcePath = path.resolve(absoluteSourcePath); - } - - if (absoluteSourcePath) { - customModulePaths.set(customModule.id, absoluteSourcePath); - } - } - } - - // Build custom module paths map from customContent - - // Handle selectedFiles (from existing install path or manual directory input) - if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) { - const customHandler = new CustomHandler(); - for (const customFile of config.customContent.selectedFiles) { - const customInfo = await customHandler.getCustomInfo(customFile, path.resolve(config.directory)); - if (customInfo && customInfo.id) { - customModulePaths.set(customInfo.id, customInfo.path); - } - } - } - - // 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) { - // Get selected cached module IDs (if available) - const selectedCachedIds = config.customContent.selectedCachedModules || []; - // If no selection info, include all cached modules (for backward compatibility) - const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected; - - for (const cachedModule of config.customContent.cachedModules) { - // For cached modules, the path is the cachePath which contains the module.yaml - if ( - cachedModule.id && - cachedModule.cachePath && // Include if selected or if we should include all - (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id)) - ) { - customModulePaths.set(cachedModule.id, cachedModule.cachePath); - } - } - } - - // Get list of all modules including custom modules - // Order: core first, then official modules, then custom modules - const allModulesForConfig = ['core']; - - // Add official modules (excluding core and any custom modules) - const officialModules = (config.modules || []).filter((m) => m !== 'core' && !customModulePaths.has(m)); - allModulesForConfig.push(...officialModules); - - // Add custom modules at the end - for (const [moduleId] of customModulePaths) { - if (!allModulesForConfig.includes(moduleId)) { - allModulesForConfig.push(moduleId); - } - } - - // Check if core was already collected in UI - if (config.coreConfig && Object.keys(config.coreConfig).length > 0) { - // Core already collected, skip it in config collection - const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core'); - moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), { - customModulePaths, - }); - } else { - // Core not collected yet, include it - moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), { - customModulePaths, - }); - } - } - - // Always use _bmad as the folder name - const bmadFolderName = '_bmad'; - this.bmadFolderName = bmadFolderName; // Store for use in other methods - - // Store AgentVibes configuration for injection point processing - this.enableAgentVibes = config.enableAgentVibes || false; - - // 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 - - const spinner = ora('Preparing installation...').start(); - - try { - // Resolve target directory (path.resolve handles platform differences) - const projectDir = path.resolve(config.directory); - - let existingBmadDir = null; - let existingBmadFolderName = null; - - if (await fs.pathExists(projectDir)) { - const result = await this.findBmadDir(projectDir); - existingBmadDir = result.bmadDir; - existingBmadFolderName = path.basename(existingBmadDir); - } - - // Create a project directory if it doesn't exist (user already confirmed) - if (!(await fs.pathExists(projectDir))) { - spinner.text = 'Creating installation directory...'; - try { - // fs.ensureDir handles platform-specific directory creation - // It will recursively create all necessary parent directories - await fs.ensureDir(projectDir); - } catch (error) { - spinner.fail('Failed to create installation directory'); - console.error(chalk.red(`Error: ${error.message}`)); - // More detailed error for common issues - if (error.code === 'EACCES') { - console.error(chalk.red('Permission denied. Check parent directory permissions.')); - } else if (error.code === 'ENOSPC') { - console.error(chalk.red('No space left on device.')); - } - throw new Error(`Cannot create directory: ${projectDir}`); - } - } - - const bmadDir = path.join(projectDir, bmadFolderName); - - // Check existing installation - spinner.text = 'Checking for existing installation...'; - const existingInstall = await this.detector.detect(bmadDir); - - if (existingInstall.installed && !config.force && !config._quickUpdate) { - spinner.stop(); - - // Check if user already decided what to do (from early menu in ui.js) - let action = null; - if (config.actionType === 'update') { - action = 'update'; - } else { - // Fallback: Ask the user (backwards compatibility for other code paths) - console.log(chalk.yellow('\n⚠️ Existing BMAD installation detected')); - console.log(chalk.dim(` Location: ${bmadDir}`)); - console.log(chalk.dim(` Version: ${existingInstall.version}`)); - - const promptResult = await this.promptUpdateAction(); - action = promptResult.action; - } - - if (action === 'update') { - // Store that we're updating for later processing - config._isUpdate = true; - config._existingInstall = existingInstall; - - // Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv) - const existingFilesManifest = await this.readFilesManifest(bmadDir); - const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest); - - config._customFiles = customFiles; - config._modifiedFiles = modifiedFiles; - - // Also check cache directory for custom modules (like quick update does) - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - - for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; - - // Skip if we already have this module from manifest - if (customModulePaths.has(moduleId)) { - continue; - } - - const cachedPath = path.join(cacheDir, moduleId); - - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - customModulePaths.set(moduleId, cachedPath); - } - } - } - - // Update module manager with the new custom module paths from cache - this.moduleManager.setCustomModulePaths(customModulePaths); - } - - // If there are custom files, back them up temporarily - if (customFiles.length > 0) { - const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp'); - await fs.ensureDir(tempBackupDir); - - spinner.start(`Backing up ${customFiles.length} custom files...`); - for (const customFile of customFiles) { - const relativePath = path.relative(bmadDir, customFile); - const backupPath = path.join(tempBackupDir, relativePath); - await fs.ensureDir(path.dirname(backupPath)); - await fs.copy(customFile, backupPath); - } - spinner.succeed(`Backed up ${customFiles.length} custom files`); - - config._tempBackupDir = tempBackupDir; - } - - // For modified files, back them up to temp directory (will be restored as .bak files after install) - if (modifiedFiles.length > 0) { - const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp'); - await fs.ensureDir(tempModifiedBackupDir); - - spinner.start(`Backing up ${modifiedFiles.length} modified files...`); - for (const modifiedFile of modifiedFiles) { - const relativePath = path.relative(bmadDir, modifiedFile.path); - const tempBackupPath = path.join(tempModifiedBackupDir, relativePath); - await fs.ensureDir(path.dirname(tempBackupPath)); - await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); - } - spinner.succeed(`Backed up ${modifiedFiles.length} modified files`); - - config._tempModifiedBackupDir = tempModifiedBackupDir; - } - } - } else if (existingInstall.installed && config._quickUpdate) { - // Quick update mode - automatically treat as update without prompting - spinner.text = 'Preparing quick update...'; - config._isUpdate = true; - config._existingInstall = existingInstall; - - // Detect custom and modified files BEFORE updating - const existingFilesManifest = await this.readFilesManifest(bmadDir); - const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest); - - config._customFiles = customFiles; - config._modifiedFiles = modifiedFiles; - - // Also check cache directory for custom modules (like quick update does) - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - - for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; - - // Skip if we already have this module from manifest - if (customModulePaths.has(moduleId)) { - continue; - } - - const cachedPath = path.join(cacheDir, moduleId); - - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - customModulePaths.set(moduleId, cachedPath); - } - } - } - - // Update module manager with the new custom module paths from cache - this.moduleManager.setCustomModulePaths(customModulePaths); - } - - // Back up custom files - if (customFiles.length > 0) { - const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp'); - await fs.ensureDir(tempBackupDir); - - spinner.start(`Backing up ${customFiles.length} custom files...`); - for (const customFile of customFiles) { - const relativePath = path.relative(bmadDir, customFile); - const backupPath = path.join(tempBackupDir, relativePath); - await fs.ensureDir(path.dirname(backupPath)); - await fs.copy(customFile, backupPath); - } - spinner.succeed(`Backed up ${customFiles.length} custom files`); - config._tempBackupDir = tempBackupDir; - } - - // Back up modified files - if (modifiedFiles.length > 0) { - const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp'); - await fs.ensureDir(tempModifiedBackupDir); - - spinner.start(`Backing up ${modifiedFiles.length} modified files...`); - for (const modifiedFile of modifiedFiles) { - const relativePath = path.relative(bmadDir, modifiedFile.path); - const tempBackupPath = path.join(tempModifiedBackupDir, relativePath); - await fs.ensureDir(path.dirname(tempBackupPath)); - await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); - } - spinner.succeed(`Backed up ${modifiedFiles.length} modified files`); - config._tempModifiedBackupDir = tempModifiedBackupDir; - } - } - - // Now collect tool configurations after we know if it's a reinstall - // Skip for quick update since we already have the IDE list - spinner.stop(); - let toolSelection; - if (config._quickUpdate) { - // Quick update already has IDEs configured, use saved configurations - const preConfiguredIdes = {}; - const savedIdeConfigs = config._savedIdeConfigs || {}; - - for (const ide of config.ides || []) { - // Use saved config if available, otherwise mark as already configured (legacy) - if (savedIdeConfigs[ide]) { - preConfiguredIdes[ide] = savedIdeConfigs[ide]; - } else { - preConfiguredIdes[ide] = { _alreadyConfigured: true }; - } - } - toolSelection = { - ides: config.ides || [], - skipIde: !config.ides || config.ides.length === 0, - configurations: preConfiguredIdes, - }; - } else { - // Pass pre-selected IDEs from early prompt (if available) - // This allows IDE selection to happen before file copying, improving UX - const preSelectedIdes = config.ides && config.ides.length > 0 ? config.ides : null; - toolSelection = await this.collectToolConfigurations( - path.resolve(config.directory), - config.modules, - config._isFullReinstall || false, - config._previouslyConfiguredIdes || [], - preSelectedIdes, - ); - } - - // Merge tool selection into config (for both quick update and regular flow) - config.ides = toolSelection.ides; - config.skipIde = toolSelection.skipIde; - const ideConfigurations = toolSelection.configurations; - - if (spinner.isSpinning) { - spinner.text = 'Continuing installation...'; - } else { - spinner.start('Continuing installation...'); - } - - // Create bmad directory structure - 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'); - } - - const projectRoot = getProjectRoot(); - - // Step 1: Install core module first (if requested) - if (config.installCore) { - spinner.start('Installing BMAD core...'); - await this.installCoreWithDependencies(bmadDir, { core: {} }); - spinner.succeed('Core installed'); - - // Generate core config file - await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} }); - } - - // Custom content is already handled in UI before module selection - let finalCustomContent = config.customContent; - - // Step 3: Prepare modules list including cached custom modules - let allModules = [...(config.modules || [])]; - - // During quick update, we might have custom module sources from the manifest - if (config._customModuleSources) { - // Add custom modules from stored sources - for (const [moduleId, customInfo] of config._customModuleSources) { - if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) { - allModules.push(moduleId); - } - } - } - - // Add cached custom modules - if (finalCustomContent && finalCustomContent.cachedModules) { - for (const cachedModule of finalCustomContent.cachedModules) { - if (!allModules.includes(cachedModule.id)) { - allModules.push(cachedModule.id); - } - } - } - - // Regular custom content from user input (non-cached) - if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) { - // Add custom modules to the installation list - const customHandler = new CustomHandler(); - for (const customFile of finalCustomContent.selectedFiles) { - const customInfo = await customHandler.getCustomInfo(customFile, projectDir); - if (customInfo && customInfo.id) { - allModules.push(customInfo.id); - } - } - } - - // Don't include core again if already installed - if (config.installCore) { - allModules = allModules.filter((m) => m !== 'core'); - } - - 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({ - bmadDir: bmadDir, // Pass bmadDir so we can check cache - }); - - const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, { - verbose: config.verbose, - moduleManager: tempModuleManager, - }); - - spinner.succeed('Dependencies resolved'); - - // Install modules with their dependencies - if (allModules && allModules.length > 0) { - const installedModuleNames = new Set(); - - for (const moduleName of allModules) { - // Skip if already installed - if (installedModuleNames.has(moduleName)) { - continue; - } - installedModuleNames.add(moduleName); - - // Show appropriate message based on whether this is a quick update - const isQuickUpdate = config._quickUpdate || false; - spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`); - - // Check if this is a custom module - let isCustomModule = false; - let customInfo = null; - let useCache = false; - - // First check if we have a cached version - if (finalCustomContent && finalCustomContent.cachedModules) { - const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName); - if (cachedModule) { - isCustomModule = true; - customInfo = { - id: moduleName, - path: cachedModule.cachePath, - config: {}, - }; - useCache = true; - } - } - - // Then check if we have custom module sources from the manifest (for quick update) - if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) { - customInfo = config._customModuleSources.get(moduleName); - isCustomModule = true; - - // Check if this is a cached module (source path starts with _config) - if ( - customInfo.sourcePath && - (customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) - ) { - useCache = true; - // Make sure we have the right path structure - if (!customInfo.path) { - customInfo.path = customInfo.sourcePath; - } - } - } - - // Finally check regular custom content - if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) { - const customHandler = new CustomHandler(); - for (const customFile of finalCustomContent.selectedFiles) { - const info = await customHandler.getCustomInfo(customFile, projectDir); - if (info && info.id === moduleName) { - isCustomModule = true; - customInfo = info; - break; - } - } - } - - if (isCustomModule && customInfo) { - // Custom modules are now installed via ModuleManager just like standard modules - // The custom module path should already be in customModulePaths from earlier setup - if (!customModulePaths.has(moduleName) && customInfo.path) { - customModulePaths.set(moduleName, customInfo.path); - this.moduleManager.setCustomModulePaths(customModulePaths); - } - - const collectedModuleConfig = moduleConfigs[moduleName] || {}; - - // Use ModuleManager to install the custom module - await this.moduleManager.install( - moduleName, - bmadDir, - (filePath) => { - this.installedFiles.add(filePath); - }, - { - isCustom: true, - moduleConfig: collectedModuleConfig, - isQuickUpdate: config._quickUpdate || false, - installer: this, - }, - ); - - // Create module config (include collected config from module.yaml prompts) - await this.generateModuleConfigs(bmadDir, { - [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig }, - }); - } else { - // Regular module installation - // Special case for core module - if (moduleName === 'core') { - await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]); - } else { - await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]); - } - } - - spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`); - } - - // Install partial modules (only dependencies) - for (const [module, files] of Object.entries(resolution.byModule)) { - if (!allModules.includes(module) && module !== 'core') { - const totalFiles = - files.agents.length + - files.tasks.length + - files.tools.length + - files.templates.length + - files.data.length + - files.other.length; - if (totalFiles > 0) { - spinner.start(`Installing ${module} dependencies...`); - await this.installPartialModule(module, bmadDir, files); - spinner.succeed(`${module} dependencies installed`); - } - } - } - } - - // All content is now installed as modules - no separate custom content handling needed - - // Generate clean config.yaml files for each installed module - spinner.start('Generating module configurations...'); - await this.generateModuleConfigs(bmadDir, moduleConfigs); - spinner.succeed('Module configurations generated'); - - // Create agent configuration files - // Note: Legacy createAgentConfigs removed - using YAML customize system instead - // Customize templates are now created in processAgentFiles when building YAML agents - - // Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion) - const cfgDir = path.join(bmadDir, '_config'); - this.installedFiles.add(path.join(cfgDir, 'manifest.yaml')); - this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv')); - this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv')); - this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv')); - - // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup - spinner.start('Generating workflow and agent manifests...'); - const manifestGen = new ManifestGenerator(); - - // For quick update, we need ALL installed modules in the manifest - // Not just the ones being updated - const allModulesForManifest = config._quickUpdate - ? config._existingModules || allModules || [] - : config._preserveModules - ? [...allModules, ...config._preserveModules] - : allModules || []; - - // For regular installs (including when called from quick update), use what we have - let modulesForCsvPreserve; - if (config._quickUpdate) { - // Quick update - use existing modules or fall back to modules being updated - modulesForCsvPreserve = config._existingModules || allModules || []; - } else { - // Regular install - use the modules we're installing plus any preserved ones - modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules; - } - - const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], { - ides: config.ides || [], - preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir - }); - - // Custom modules are now included in the main modules list - no separate tracking needed - - spinner.succeed( - `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`, - ); - - // Configure IDEs and copy documentation - if (!config.skipIde && config.ides && config.ides.length > 0) { - // Filter out any undefined/null values from the IDE list - const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); - - if (validIdes.length === 0) { - console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.')); - } else { - // Check if any IDE might need prompting (no pre-collected config) - const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]); - - if (!needsPrompting) { - spinner.start('Configuring IDEs...'); - } - - // Temporarily suppress console output if not verbose - const originalLog = console.log; - if (!config.verbose) { - console.log = () => { }; - } - - for (const ide of validIdes) { - // Only show spinner if we have pre-collected config (no prompts expected) - if (ideConfigurations[ide] && !needsPrompting) { - spinner.text = `Configuring ${ide}...`; - } else if (!ideConfigurations[ide]) { - // Stop spinner before prompting - if (spinner.isSpinning) { - spinner.stop(); - } - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); - } - - // Pass pre-collected configuration to avoid re-prompting - await this.ideManager.setup(ide, projectDir, bmadDir, { - selectedModules: allModules || [], - preCollectedConfig: ideConfigurations[ide] || null, - verbose: config.verbose, - }); - - // Save IDE configuration for future updates - if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { - await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); - } - - // Restart spinner if we stopped it - if (!ideConfigurations[ide] && !spinner.isSpinning) { - spinner.start('Configuring IDEs...'); - } - } - - // Restore console.log - console.log = originalLog; - - if (spinner.isSpinning) { - spinner.succeed(`Configured: ${validIdes.join(', ')}`); - } else { - console.log(chalk.green(`✓ Configured: ${validIdes.join(', ')}`)); - } - } - } - - // Run module-specific installers after IDE setup - spinner.start('Running module-specific installers...'); - - // Create a conditional logger based on verbose mode - const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose; - const moduleLogger = { - log: (msg) => (verboseMode ? console.log(msg) : {}), // Only log in verbose mode - error: (msg) => console.error(msg), // Always show errors - warn: (msg) => console.warn(msg), // Always show warnings - }; - - // Run core module installer if core was installed - if (config.installCore || resolution.byModule.core) { - spinner.text = 'Running core module installer...'; - - await this.moduleManager.runModuleInstaller('core', bmadDir, { - installedIDEs: config.ides || [], - moduleConfig: moduleConfigs.core || {}, - coreConfig: moduleConfigs.core || {}, - logger: moduleLogger, - }); - } - - // Run installers for user-selected modules - if (config.modules && config.modules.length > 0) { - for (const moduleName of config.modules) { - spinner.text = `Running ${moduleName} module installer...`; - - // Pass installed IDEs and module config to module installer - await this.moduleManager.runModuleInstaller(moduleName, bmadDir, { - installedIDEs: config.ides || [], - moduleConfig: moduleConfigs[moduleName] || {}, - coreConfig: moduleConfigs.core || {}, - logger: moduleLogger, - }); - } - } - - spinner.succeed('Module-specific installers completed'); - - // Note: Manifest files are already created by ManifestGenerator above - // No need to create legacy manifest.csv anymore - - // If this was an update, restore custom files - let customFiles = []; - let modifiedFiles = []; - if (config._isUpdate) { - if (config._customFiles && config._customFiles.length > 0) { - spinner.start(`Restoring ${config._customFiles.length} custom files...`); - - for (const originalPath of config._customFiles) { - const relativePath = path.relative(bmadDir, originalPath); - const backupPath = path.join(config._tempBackupDir, relativePath); - - if (await fs.pathExists(backupPath)) { - await fs.ensureDir(path.dirname(originalPath)); - await fs.copy(backupPath, originalPath, { overwrite: true }); - } - } - - // Clean up temp backup - if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) { - await fs.remove(config._tempBackupDir); - } - - spinner.succeed(`Restored ${config._customFiles.length} custom files`); - customFiles = config._customFiles; - } - - if (config._modifiedFiles && config._modifiedFiles.length > 0) { - modifiedFiles = config._modifiedFiles; - - // Restore modified files as .bak files - if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { - spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`); - - for (const modifiedFile of modifiedFiles) { - const relativePath = path.relative(bmadDir, modifiedFile.path); - const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath); - const bakPath = modifiedFile.path + '.bak'; - - if (await fs.pathExists(tempBackupPath)) { - await fs.ensureDir(path.dirname(bakPath)); - await fs.copy(tempBackupPath, bakPath, { overwrite: true }); - } - } - - // Clean up temp backup - await fs.remove(config._tempModifiedBackupDir); - - spinner.succeed(`Restored ${modifiedFiles.length} modified files as .bak`); - } - } - } - - spinner.stop(); - - // Report custom and modified files if any were found - if (customFiles.length > 0) { - console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`)); - } - - if (modifiedFiles.length > 0) { - console.log(chalk.yellow(`\n⚠️ User modified files detected: ${modifiedFiles.length}`)); - console.log( - chalk.dim( - '\nThese user modified files have been updated with the new version, search the project for .bak files that had your customizations.', - ), - ); - console.log(chalk.dim('Remove these .bak files it no longer needed\n')); - } - - // Display completion message - const { UI } = require('../../../lib/ui'); - const ui = new UI(); - ui.showInstallSummary({ - path: bmadDir, - modules: config.modules, - ides: config.ides, - customFiles: customFiles.length > 0 ? customFiles : undefined, - ttsInjectedFiles: this.enableAgentVibes && this.ttsInjectedFiles.length > 0 ? this.ttsInjectedFiles : undefined, - agentVibesEnabled: this.enableAgentVibes || false, - }); - - return { - success: true, - path: bmadDir, - modules: config.modules, - ides: config.ides, - needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled, - projectDir: projectDir, - }; - } catch (error) { - spinner.fail('Installation failed'); - throw error; - } - } - - /** - * Update existing installation - */ - async update(config) { - const spinner = ora('Checking installation...').start(); - - try { - const projectDir = path.resolve(config.directory); - const { bmadDir } = await this.findBmadDir(projectDir); - const existingInstall = await this.detector.detect(bmadDir); - - if (!existingInstall.installed) { - spinner.fail('No BMAD installation found'); - throw new Error(`No BMAD installation found at ${bmadDir}`); - } - - spinner.text = 'Analyzing update requirements...'; - - // Compare versions and determine what needs updating - const currentVersion = existingInstall.version; - const newVersion = require(path.join(getProjectRoot(), 'package.json')).version; - - // Check for custom modules with missing sources before update - const customModuleSources = new Map(); - - // Check manifest for backward compatibility - if (existingInstall.customModules) { - for (const customModule of existingInstall.customModules) { - customModuleSources.set(customModule.id, customModule); - } - } - - // Also check cache directory - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - - for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; - - // Skip if we already have this module - if (customModuleSources.has(moduleId)) { - continue; - } - - const cachedPath = path.join(cacheDir, moduleId); - - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - customModuleSources.set(moduleId, { - id: moduleId, - name: moduleId, - sourcePath: path.join('_config', 'custom', moduleId), // Relative path - cached: true, - }); - } - } - } - } - - if (customModuleSources.size > 0) { - spinner.stop(); - console.log(chalk.yellow('\nChecking custom module sources before update...')); - - const projectRoot = getProjectRoot(); - await this.handleMissingCustomSources( - customModuleSources, - bmadDir, - projectRoot, - 'update', - existingInstall.modules.map((m) => m.id), - ); - - spinner.start('Preparing update...'); - } - - if (config.dryRun) { - spinner.stop(); - console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n')); - console.log(chalk.bold('Current version:'), currentVersion); - console.log(chalk.bold('New version:'), newVersion); - console.log(chalk.bold('Core:'), existingInstall.hasCore ? 'Will be updated' : 'Not installed'); - - if (existingInstall.modules.length > 0) { - console.log(chalk.bold('\nModules to update:')); - for (const mod of existingInstall.modules) { - console.log(` - ${mod.id}`); - } - } - return; - } - - // Perform actual update - if (existingInstall.hasCore) { - spinner.text = 'Updating core...'; - await this.updateCore(bmadDir, config.force); - } - - for (const module of existingInstall.modules) { - spinner.text = `Updating module: ${module.id}...`; - await this.moduleManager.update(module.id, bmadDir, config.force); - } - - // Update manifest - spinner.text = 'Updating manifest...'; - await this.manifest.update(bmadDir, { - version: newVersion, - updateDate: new Date().toISOString(), - }); - - spinner.succeed('Update complete'); - return { success: true }; - } catch (error) { - spinner.fail('Update failed'); - throw error; - } - } - - /** - * Get installation status - */ - async getStatus(directory) { - const projectDir = path.resolve(directory); - const { bmadDir } = await this.findBmadDir(projectDir); - return await this.detector.detect(bmadDir); - } - - /** - * Get available modules - */ - async getAvailableModules() { - return await this.moduleManager.listAvailable(); - } - - /** - * Uninstall BMAD - */ - async uninstall(directory) { - const projectDir = path.resolve(directory); - const { bmadDir } = await this.findBmadDir(projectDir); - - if (await fs.pathExists(bmadDir)) { - await fs.remove(bmadDir); - } - - // Clean up IDE configurations - await this.ideManager.cleanup(projectDir); - - return { success: true }; - } - - /** - * Private: Create directory structure - */ - async createDirectoryStructure(bmadDir) { - await fs.ensureDir(bmadDir); - await fs.ensureDir(path.join(bmadDir, '_config')); - await fs.ensureDir(path.join(bmadDir, '_config', 'agents')); - await fs.ensureDir(path.join(bmadDir, '_config', 'custom')); - } - - /** - * Generate clean config.yaml files for each installed module - * @param {string} bmadDir - BMAD installation directory - * @param {Object} moduleConfigs - Collected configuration values - */ - async generateModuleConfigs(bmadDir, moduleConfigs) { - const yaml = require('yaml'); - - // Extract core config values to share with other modules - const coreConfig = moduleConfigs.core || {}; - - // Get all installed module directories - const entries = await fs.readdir(bmadDir, { withFileTypes: true }); - const installedModules = entries - .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs') - .map((entry) => entry.name); - - // Generate config.yaml for each installed module - for (const moduleName of installedModules) { - const modulePath = path.join(bmadDir, moduleName); - - // Get module-specific config or use empty object if none - const config = moduleConfigs[moduleName] || {}; - - if (await fs.pathExists(modulePath)) { - const configPath = path.join(modulePath, 'config.yaml'); - - // Create header - const packageJson = require(path.join(getProjectRoot(), 'package.json')); - const header = `# ${moduleName.toUpperCase()} Module Configuration -# Generated by BMAD installer -# Version: ${packageJson.version} -# Date: ${new Date().toISOString()} - -`; - - // For non-core modules, add core config values directly - let finalConfig = { ...config }; - let coreSection = ''; - - if (moduleName !== 'core' && coreConfig && Object.keys(coreConfig).length > 0) { - // Add core values directly to the module config - // These will be available for reference in the module - finalConfig = { - ...config, - ...coreConfig, // Spread core config values directly into the module config - }; - - // Create a comment section to identify core values - coreSection = '\n# Core Configuration Values\n'; - } - - // Clean the config to remove any non-serializable values (like functions) - const cleanConfig = structuredClone(finalConfig); - - // Convert config to YAML - let yamlContent = yaml.stringify(cleanConfig, { - indent: 2, - lineWidth: 0, - minContentWidth: 0, - }); - - // If we have core values, reorganize the YAML to group them with their comment - if (coreSection && moduleName !== 'core') { - // Split the YAML into lines - const lines = yamlContent.split('\n'); - const moduleConfigLines = []; - const coreConfigLines = []; - - // Separate module-specific and core config lines - for (const line of lines) { - const key = line.split(':')[0].trim(); - if (Object.prototype.hasOwnProperty.call(coreConfig, key)) { - coreConfigLines.push(line); - } else { - moduleConfigLines.push(line); - } - } - - // Rebuild YAML with module config first, then core config with comment - yamlContent = moduleConfigLines.join('\n'); - if (coreConfigLines.length > 0) { - yamlContent += coreSection + coreConfigLines.join('\n'); - } - } - - // Write the clean config file with POSIX-compliant final newline - const content = header + yamlContent; - await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8'); - - // Track the config file in installedFiles - this.installedFiles.add(configPath); - } - } - } - - /** - * Install core with resolved dependencies - * @param {string} bmadDir - BMAD installation directory - * @param {Object} coreFiles - Core files to install - */ - async installCoreWithDependencies(bmadDir, coreFiles) { - const sourcePath = getModulePath('core'); - const targetPath = path.join(bmadDir, 'core'); - await this.installCore(bmadDir); - } - - /** - * Install module with resolved dependencies - * @param {string} moduleName - Module name - * @param {string} bmadDir - BMAD installation directory - * @param {Object} moduleFiles - Module files to install - */ - async installModuleWithDependencies(moduleName, bmadDir, moduleFiles) { - // Get module configuration for conditional installation - const moduleConfig = this.configCollector.collectedConfig[moduleName] || {}; - - // Use existing module manager for full installation with file tracking - // Note: Module-specific installers are called separately after IDE setup - await this.moduleManager.install( - moduleName, - bmadDir, - (filePath) => { - this.installedFiles.add(filePath); - }, - { - skipModuleInstaller: true, // We'll run it later after IDE setup - moduleConfig: moduleConfig, // Pass module config for conditional filtering - installer: this, - }, - ); - - // Process agent files to build YAML agents and create customize templates - const modulePath = path.join(bmadDir, moduleName); - await this.processAgentFiles(modulePath, moduleName); - - // Dependencies are already included in full module install - } - - /** - * Install partial module (only dependencies needed by other modules) - */ - async installPartialModule(moduleName, bmadDir, files) { - const sourceBase = getModulePath(moduleName); - const targetBase = path.join(bmadDir, moduleName); - - // Create module directory - await fs.ensureDir(targetBase); - - // Copy only the required dependency files - if (files.agents && files.agents.length > 0) { - const agentsDir = path.join(targetBase, 'agents'); - await fs.ensureDir(agentsDir); - - for (const agentPath of files.agents) { - const fileName = path.basename(agentPath); - const sourcePath = path.join(sourceBase, 'agents', fileName); - const targetPath = path.join(agentsDir, fileName); - - if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); - this.installedFiles.add(targetPath); - } - } - } - - if (files.tasks && files.tasks.length > 0) { - const tasksDir = path.join(targetBase, 'tasks'); - await fs.ensureDir(tasksDir); - - for (const taskPath of files.tasks) { - const fileName = path.basename(taskPath); - const sourcePath = path.join(sourceBase, 'tasks', fileName); - const targetPath = path.join(tasksDir, fileName); - - if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); - this.installedFiles.add(targetPath); - } - } - } - - if (files.tools && files.tools.length > 0) { - const toolsDir = path.join(targetBase, 'tools'); - await fs.ensureDir(toolsDir); - - for (const toolPath of files.tools) { - const fileName = path.basename(toolPath); - const sourcePath = path.join(sourceBase, 'tools', fileName); - const targetPath = path.join(toolsDir, fileName); - - if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); - this.installedFiles.add(targetPath); - } - } - } - - if (files.templates && files.templates.length > 0) { - const templatesDir = path.join(targetBase, 'templates'); - await fs.ensureDir(templatesDir); - - for (const templatePath of files.templates) { - const fileName = path.basename(templatePath); - const sourcePath = path.join(sourceBase, 'templates', fileName); - const targetPath = path.join(templatesDir, fileName); - - if (await fs.pathExists(sourcePath)) { - await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad'); - this.installedFiles.add(targetPath); - } - } - } - - if (files.data && files.data.length > 0) { - for (const dataPath of files.data) { - // Preserve directory structure for data files - const relative = path.relative(sourceBase, dataPath); - const targetPath = path.join(targetBase, relative); - - await fs.ensureDir(path.dirname(targetPath)); - - if (await fs.pathExists(dataPath)) { - await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad'); - this.installedFiles.add(targetPath); - } - } - } - - // Create a marker file to indicate this is a partial installation - const markerPath = path.join(targetBase, '.partial'); - await fs.writeFile( - markerPath, - `This module contains only dependencies required by other modules.\nInstalled: ${new Date().toISOString()}\n`, - ); - } - - /** - * Private: Install core - * @param {string} bmadDir - BMAD installation directory - */ - async installCore(bmadDir) { - const sourcePath = getModulePath('core'); - const targetPath = path.join(bmadDir, 'core'); - - // Copy core files (skip .agent.yaml files like modules do) - await this.copyCoreFiles(sourcePath, targetPath); - - // Compile agents using the same compiler as modules - const { ModuleManager } = require('../modules/manager'); - const moduleManager = new ModuleManager(); - await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this); - - // Process agent files to inject activation block - await this.processAgentFiles(targetPath, 'core'); - } - - /** - * Copy core files (similar to copyModuleWithFiltering but for core) - * @param {string} sourcePath - Source path - * @param {string} targetPath - Target path - */ - async copyCoreFiles(sourcePath, targetPath) { - // Get all files in source - const files = await this.getFileList(sourcePath); - - for (const file of files) { - // Skip sub-modules directory - these are IDE-specific and handled separately - if (file.startsWith('sub-modules/')) { - continue; - } - - // Skip sidecar directories - they are handled separately during agent compilation - if ( - path - .dirname(file) - .split('/') - .some((dir) => dir.toLowerCase().includes('sidecar')) - ) { - continue; - } - - // Skip _module-installer directory - it's only needed at install time - if (file.startsWith('_module-installer/') || file === 'module.yaml') { - continue; - } - - // Skip config.yaml templates - we'll generate clean ones with actual values - if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) { - continue; - } - - // Skip .agent.yaml files - they will be compiled separately - if (file.endsWith('.agent.yaml')) { - continue; - } - - const sourceFile = path.join(sourcePath, file); - const targetFile = path.join(targetPath, file); - - // Check if this is an agent file - if (file.startsWith('agents/') && file.endsWith('.md')) { - // Read the file to check for localskip - const content = await fs.readFile(sourceFile, 'utf8'); - - // Check for localskip="true" in the agent tag - const agentMatch = content.match(/]*\slocalskip="true"[^>]*>/); - if (agentMatch) { - console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`)); - continue; // Skip this agent - } - } - - // Check if this is a workflow.yaml file - if (file.endsWith('workflow.yaml')) { - await fs.ensureDir(path.dirname(targetFile)); - await this.copyWorkflowYamlStripped(sourceFile, targetFile); - } else { - // Copy the file with placeholder replacement - await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad'); - } - - // Track the installed file - this.installedFiles.add(targetFile); - } - } - - /** - * Get list of all files in a directory recursively - * @param {string} dir - Directory path - * @param {string} baseDir - Base directory for relative paths - * @returns {Array} List of relative file paths - */ - async getFileList(dir, baseDir = dir) { - const files = []; - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - // Skip _module-installer directories - if (entry.name === '_module-installer') { - continue; - } - const subFiles = await this.getFileList(fullPath, baseDir); - files.push(...subFiles); - } else { - files.push(path.relative(baseDir, fullPath)); - } - } - - return files; - } - - /** - * Process agent files to build YAML agents and inject activation blocks - * @param {string} modulePath - Path to module in bmad/ installation - * @param {string} moduleName - Module name - */ - async processAgentFiles(modulePath, moduleName) { - const agentsPath = path.join(modulePath, 'agents'); - - // Check if agents directory exists - if (!(await fs.pathExists(agentsPath))) { - return; // No agents to process - } - - // Determine project directory (parent of bmad/ directory) - const bmadDir = path.dirname(modulePath); - const cfgAgentsDir = path.join(bmadDir, '_config', 'agents'); - - // Ensure _config/agents directory exists - await fs.ensureDir(cfgAgentsDir); - - // Get all agent files - const agentFiles = await fs.readdir(agentsPath); - - for (const agentFile of agentFiles) { - // Skip .agent.yaml files - they should already be compiled by compileModuleAgents - if (agentFile.endsWith('.agent.yaml')) { - continue; - } - - // Only process .md files (already compiled from YAML) - if (!agentFile.endsWith('.md')) { - continue; - } - - const agentName = agentFile.replace('.md', ''); - const mdPath = path.join(agentsPath, agentFile); - const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`); - - // For .md files that are already compiled, we don't need to do much - // Just ensure the customize template exists - if (!(await fs.pathExists(customizePath))) { - const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml'); - if (await fs.pathExists(genericTemplatePath)) { - await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad'); - if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`)); - } - } - } - } - } - - /** - * Build standalone agents in bmad/agents/ directory - * @param {string} bmadDir - Path to bmad directory - * @param {string} projectDir - Path to project directory - */ - async buildStandaloneAgents(bmadDir, projectDir) { - const standaloneAgentsPath = path.join(bmadDir, 'agents'); - const cfgAgentsDir = path.join(bmadDir, '_config', 'agents'); - - // Check if standalone agents directory exists - if (!(await fs.pathExists(standaloneAgentsPath))) { - return; - } - - // Get all subdirectories in agents/ - const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true }); - - for (const agentDir of agentDirs) { - if (!agentDir.isDirectory()) continue; - - const agentDirPath = path.join(standaloneAgentsPath, agentDir.name); - - // Find any .agent.yaml file in the directory - const files = await fs.readdir(agentDirPath); - const yamlFile = files.find((f) => f.endsWith('.agent.yaml')); - - if (!yamlFile) continue; - - const agentName = path.basename(yamlFile, '.agent.yaml'); - const sourceYamlPath = path.join(agentDirPath, yamlFile); - const targetMdPath = path.join(agentDirPath, `${agentName}.md`); - const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`); - - // Check for customizations - const customizeExists = await fs.pathExists(customizePath); - let customizedFields = []; - - if (customizeExists) { - const customizeContent = await fs.readFile(customizePath, 'utf8'); - const yaml = require('yaml'); - const customizeYaml = yaml.parse(customizeContent); - - // Detect what fields are customized (similar to rebuildAgentFiles) - if (customizeYaml) { - if (customizeYaml.persona) { - for (const [key, value] of Object.entries(customizeYaml.persona)) { - if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) { - customizedFields.push(`persona.${key}`); - } - } - } - if (customizeYaml.agent?.metadata) { - for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) { - if (value !== '' && value !== null) { - customizedFields.push(`metadata.${key}`); - } - } - } - if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) { - customizedFields.push('critical_actions'); - } - if (customizeYaml.menu && customizeYaml.menu.length > 0) { - customizedFields.push('menu'); - } - } - } - - // Build YAML to XML .md - let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, { - includeMetadata: true, - }); - - // DO NOT replace {project-root} - LLMs understand this placeholder at runtime - // const processedContent = xmlContent.replaceAll('{project-root}', projectDir); - - // Process TTS injection points (pass targetPath for tracking) - xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath); - - // Write the built .md file with POSIX-compliant final newline - const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n'; - await fs.writeFile(targetMdPath, content, 'utf8'); - - // Display result - if (customizedFields.length > 0) { - console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`)); - } else { - console.log(chalk.dim(` Built standalone agent: ${agentName}.md`)); - } - } - } - - /** - * Rebuild agent files from installer source (for compile command) - * @param {string} modulePath - Path to module in bmad/ installation - * @param {string} moduleName - Module name - */ - async rebuildAgentFiles(modulePath, moduleName) { - // Get source agents directory from installer - const sourceAgentsPath = - moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents'); - - if (!(await fs.pathExists(sourceAgentsPath))) { - return; // No source agents to rebuild - } - - // Determine project directory (parent of bmad/ directory) - const bmadDir = path.dirname(modulePath); - const projectDir = path.dirname(bmadDir); - const cfgAgentsDir = path.join(bmadDir, '_config', 'agents'); - const targetAgentsPath = path.join(modulePath, 'agents'); - - // Ensure target directory exists - await fs.ensureDir(targetAgentsPath); - - // Get all YAML agent files from source - const sourceFiles = await fs.readdir(sourceAgentsPath); - - for (const file of sourceFiles) { - if (file.endsWith('.agent.yaml')) { - const agentName = file.replace('.agent.yaml', ''); - const sourceYamlPath = path.join(sourceAgentsPath, file); - const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`); - const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`); - - // Check for customizations - const customizeExists = await fs.pathExists(customizePath); - let customizedFields = []; - - if (customizeExists) { - const customizeContent = await fs.readFile(customizePath, 'utf8'); - const yaml = require('yaml'); - const customizeYaml = yaml.parse(customizeContent); - - // Detect what fields are customized - if (customizeYaml) { - if (customizeYaml.persona) { - for (const [key, value] of Object.entries(customizeYaml.persona)) { - if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) { - customizedFields.push(`persona.${key}`); - } - } - } - if (customizeYaml.agent?.metadata) { - for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) { - if (value !== '' && value !== null) { - customizedFields.push(`metadata.${key}`); - } - } - } - if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) { - customizedFields.push('critical_actions'); - } - if (customizeYaml.memories && customizeYaml.memories.length > 0) { - customizedFields.push('memories'); - } - if (customizeYaml.menu && customizeYaml.menu.length > 0) { - customizedFields.push('menu'); - } - if (customizeYaml.prompts && customizeYaml.prompts.length > 0) { - customizedFields.push('prompts'); - } - } - } - - // Read the YAML content - const yamlContent = await fs.readFile(sourceYamlPath, 'utf8'); - - // Read customize content if exists - let customizeData = {}; - if (customizeExists) { - const customizeContent = await fs.readFile(customizePath, 'utf8'); - const yaml = require('yaml'); - customizeData = yaml.parse(customizeContent); - } - - // Build agent answers from customize data (filter empty values) - const answers = {}; - if (customizeData.persona) { - Object.assign(answers, filterCustomizationData(customizeData.persona)); - } - if (customizeData.agent?.metadata) { - const filteredMetadata = filterCustomizationData(customizeData.agent.metadata); - if (Object.keys(filteredMetadata).length > 0) { - Object.assign(answers, { metadata: filteredMetadata }); - } - } - if (customizeData.critical_actions && customizeData.critical_actions.length > 0) { - answers.critical_actions = customizeData.critical_actions; - } - if (customizeData.memories && customizeData.memories.length > 0) { - answers.memories = customizeData.memories; - } - - const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml'); - let coreConfig = {}; - if (await fs.pathExists(coreConfigPath)) { - const yaml = require('yaml'); - const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8'); - coreConfig = yaml.parse(coreConfigContent); - } - - // Compile using the same compiler as initial installation - const { compileAgent } = require('../../../lib/agent/compiler'); - 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 = 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'; - await fs.writeFile(targetMdPath, content, 'utf8'); - - // Display result with customizations if any - if (customizedFields.length > 0) { - console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`)); - } else { - console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`)); - } - } - } - } - - /** - * Compile/rebuild all agents and tasks for quick updates - * @param {Object} config - Compilation configuration - * @returns {Object} Compilation results - */ - async compileAgents(config) { - try { - const projectDir = path.resolve(config.directory); - const { bmadDir } = await this.findBmadDir(projectDir); - - // Check if bmad directory exists - if (!(await fs.pathExists(bmadDir))) { - throw new Error(`BMAD not installed at ${bmadDir}`); - } - - // Get installed modules from manifest - const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml'); - let installedModules = []; - let manifest = null; - if (await fs.pathExists(manifestPath)) { - const manifestContent = await fs.readFile(manifestPath, 'utf8'); - const yaml = require('yaml'); - manifest = yaml.parse(manifestContent); - installedModules = manifest.modules || []; - } - - // Check for custom modules with missing sources - if (manifest && manifest.customModules && manifest.customModules.length > 0) { - console.log(chalk.yellow('\nChecking custom module sources before compilation...')); - - const customModuleSources = new Map(); - for (const customModule of manifest.customModules) { - customModuleSources.set(customModule.id, customModule); - } - - const projectRoot = getProjectRoot(); - await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules); - } - - let agentCount = 0; - let taskCount = 0; - - // Process all modules in bmad directory - const entries = await fs.readdir(bmadDir, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs') { - const modulePath = path.join(bmadDir, entry.name); - - // Special handling for standalone agents in bmad/agents/ directory - if (entry.name === 'agents') { - await this.buildStandaloneAgents(bmadDir, projectDir); - - // Count standalone agents - const standaloneAgentsPath = path.join(bmadDir, 'agents'); - const standaloneAgentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true }); - for (const agentDir of standaloneAgentDirs) { - if (agentDir.isDirectory()) { - const agentDirPath = path.join(standaloneAgentsPath, agentDir.name); - const agentFiles = await fs.readdir(agentDirPath); - agentCount += agentFiles.filter((f) => f.endsWith('.md') && !f.endsWith('.agent.yaml')).length; - } - } - } else { - // Rebuild module agents from installer source - const agentsPath = path.join(modulePath, 'agents'); - if (await fs.pathExists(agentsPath)) { - await this.rebuildAgentFiles(modulePath, entry.name); - const agentFiles = await fs.readdir(agentsPath); - agentCount += agentFiles.filter((f) => f.endsWith('.md')).length; - } - - // Count tasks (already built) - const tasksPath = path.join(modulePath, 'tasks'); - if (await fs.pathExists(tasksPath)) { - const taskFiles = await fs.readdir(tasksPath); - taskCount += taskFiles.filter((f) => f.endsWith('.md')).length; - } - } - } - } - - // Update IDE configurations using the existing IDE list from manifest - if (manifest && manifest.ides && manifest.ides.length > 0) { - for (const ide of manifest.ides) { - await this.ideManager.setup(ide, projectDir, bmadDir, { - selectedModules: installedModules, - skipModuleInstall: true, // Skip module installation, just update IDE files - verbose: config.verbose, - preCollectedConfig: { _alreadyConfigured: true }, // Skip all interactive prompts during compile - }); - } - console.log(chalk.green('✓ IDE configurations updated')); - } else { - console.log(chalk.yellow('⚠️ No IDEs configured. Skipping IDE update.')); - } - return { agentCount, taskCount }; - } catch (error) { - throw error; - } - } - - /** - * Private: Update core - */ - async updateCore(bmadDir, force = false) { - const sourcePath = getModulePath('core'); - const targetPath = path.join(bmadDir, 'core'); - - if (force) { - await fs.remove(targetPath); - await this.installCore(bmadDir); - } else { - // Selective update - preserve user modifications - await this.fileOps.syncDirectory(sourcePath, targetPath); - - // Recompile agents (#1133) - const { ModuleManager } = require('../modules/manager'); - const moduleManager = new ModuleManager(); - await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir, this); - await this.processAgentFiles(targetPath, 'core'); - } - } - - /** - * Quick update method - preserves all settings and only prompts for new config fields - * @param {Object} config - Configuration with directory - * @returns {Object} Update result - */ - async quickUpdate(config) { - const ora = require('ora'); - const spinner = ora('Starting quick update...').start(); - - try { - const projectDir = path.resolve(config.directory); - const { bmadDir } = await this.findBmadDir(projectDir); - - // Check if bmad directory exists - if (!(await fs.pathExists(bmadDir))) { - spinner.fail('No BMAD installation found'); - throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`); - } - - spinner.text = 'Detecting installed modules and configuration...'; - - // Detect existing installation - const existingInstall = await this.detector.detect(bmadDir); - const installedModules = existingInstall.modules.map((m) => m.id); - const configuredIdes = existingInstall.ides || []; - const projectRoot = path.dirname(bmadDir); - - // Get custom module sources from cache - const customModuleSources = new Map(); - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - - for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; - - // Skip if we already have this module from manifest - if (customModuleSources.has(moduleId)) { - continue; - } - - const cachedPath = path.join(cacheDir, moduleId); - - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - // For quick update, we always rebuild from cache - customModuleSources.set(moduleId, { - id: moduleId, - name: moduleId, // We'll read the actual name if needed - sourcePath: cachedPath, - cached: true, // Flag to indicate this is from cache - }); - } - } - } - } - - // Load saved IDE configurations - const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir); - - // Get available modules (what we have source for) - const availableModulesData = await this.moduleManager.listAvailable(); - const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules]; - - // Add custom modules from manifest if their sources exist - for (const [moduleId, customModule] of customModuleSources) { - // Use the absolute sourcePath - const sourcePath = customModule.sourcePath; - - // Check if source exists at the recorded path - if ( - sourcePath && - (await fs.pathExists(sourcePath)) && // Add to available modules if not already there - !availableModules.some((m) => m.id === moduleId) - ) { - availableModules.push({ - id: moduleId, - name: customModule.name || moduleId, - path: sourcePath, - isCustom: true, - fromManifest: true, - }); - } - } - - // Handle missing custom module sources using shared method - const customModuleResult = await this.handleMissingCustomSources( - customModuleSources, - bmadDir, - projectRoot, - 'update', - installedModules, - ); - - const { validCustomModules, keptModulesWithoutSources } = customModuleResult; - - const customModulesFromManifest = validCustomModules.map((m) => ({ - ...m, - isCustom: true, - hasUpdate: true, - })); - - const allAvailableModules = [...availableModules, ...customModulesFromManifest]; - const availableModuleIds = new Set(allAvailableModules.map((m) => m.id)); - - // Core module is special - never include it in update flow - const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core'); - - // Only update modules that are BOTH installed AND available (we have source for) - const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id)); - const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id)); - - // Add custom modules that were kept without sources to the skipped modules - // This ensures their agents are preserved in the manifest - for (const keptModule of keptModulesWithoutSources) { - if (!skippedModules.includes(keptModule)) { - skippedModules.push(keptModule); - } - } - - spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`); - - if (skippedModules.length > 0) { - console.log(chalk.yellow(`⚠️ Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`)); - } - - // Load existing configs and collect new fields (if any) - console.log(chalk.cyan('\n📋 Checking for new configuration options...')); - await this.configCollector.loadExistingConfig(projectDir); - - let promptedForNewFields = false; - - // Check core config for new fields - const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true); - if (corePrompted) { - promptedForNewFields = true; - } - - // Check each module we're updating for new fields (NOT skipped modules) - for (const moduleName of modulesToUpdate) { - const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true); - if (modulePrompted) { - promptedForNewFields = true; - } - } - - if (!promptedForNewFields) { - console.log(chalk.green('✓ All configuration is up to date, no new options to configure')); - } - - // Add metadata - this.configCollector.collectedConfig._meta = { - version: require(path.join(getProjectRoot(), 'package.json')).version, - installDate: new Date().toISOString(), - lastModified: new Date().toISOString(), - }; - - // Build the config object for the installer - const installConfig = { - directory: projectDir, - installCore: true, - modules: modulesToUpdate, // Only update modules we have source for - ides: configuredIdes, - skipIde: configuredIdes.length === 0, - coreConfig: this.configCollector.collectedConfig.core, - actionType: 'install', // Use regular install flow - _quickUpdate: true, // Flag to skip certain prompts - _preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them - _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer - _customModuleSources: customModuleSources, // Pass custom module sources for updates - _existingModules: installedModules, // Pass all installed modules for manifest generation - }; - - // Call the standard install method - const result = await this.install(installConfig); - - // Only succeed the spinner if it's still spinning - // (install method might have stopped it if folder name changed) - if (spinner.isSpinning) { - spinner.succeed('Quick update complete!'); - } - - return { - success: true, - moduleCount: modulesToUpdate.length + 1, // +1 for core - hadNewFields: promptedForNewFields, - modules: ['core', ...modulesToUpdate], - skippedModules: skippedModules, - ides: configuredIdes, - }; - } catch (error) { - spinner.fail('Quick update failed'); - throw error; - } - } - - /** - * Compile agents with customizations only - * @param {Object} config - Configuration with directory - * @returns {Object} Compilation result - */ - async compileAgents(config) { - const ora = require('ora'); - const chalk = require('chalk'); - const { ModuleManager } = require('../modules/manager'); - const { getSourcePath } = require('../../../lib/project-root'); - - const spinner = ora('Recompiling agents with customizations...').start(); - - try { - const projectDir = path.resolve(config.directory); - const { bmadDir } = await this.findBmadDir(projectDir); - - // Check if bmad directory exists - if (!(await fs.pathExists(bmadDir))) { - spinner.fail('No BMAD installation found'); - throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`); - } - - // Detect existing installation - const existingInstall = await this.detector.detect(bmadDir); - const installedModules = existingInstall.modules.map((m) => m.id); - - // Initialize module manager - const moduleManager = new ModuleManager(); - moduleManager.setBmadFolderName(path.basename(bmadDir)); - - let totalAgentCount = 0; - - // Get custom module sources from cache - const customModuleSources = new Map(); - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - - for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; - const cachedPath = path.join(cacheDir, moduleId); - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - - // Check if this is actually a custom module - if (await fs.pathExists(moduleYamlPath)) { - customModuleSources.set(moduleId, cachedPath); - } - } - } - } - - // Process each installed module - for (const moduleId of installedModules) { - spinner.text = `Recompiling agents in ${moduleId}...`; - - // Get source path - let sourcePath; - if (moduleId === 'core') { - sourcePath = getSourcePath('core'); - } else { - // First check if it's in the custom cache - if (customModuleSources.has(moduleId)) { - sourcePath = customModuleSources.get(moduleId); - } else { - sourcePath = await moduleManager.findModuleSource(moduleId); - } - } - - if (!sourcePath) { - console.log(chalk.yellow(` Warning: Source not found for module ${moduleId}, skipping...`)); - continue; - } - - const targetPath = path.join(bmadDir, moduleId); - - // Compile agents for this module - await moduleManager.compileModuleAgents(sourcePath, targetPath, moduleId, bmadDir, this); - - // Count agents (rough estimate based on files) - const agentsPath = path.join(targetPath, 'agents'); - if (await fs.pathExists(agentsPath)) { - const agentFiles = await fs.readdir(agentsPath); - const agentCount = agentFiles.filter(f => f.endsWith('.md')).length; - totalAgentCount += agentCount; - } - } - - spinner.succeed('Agent recompilation complete!'); - - return { - success: true, - agentCount: totalAgentCount, - modules: installedModules, - }; - } catch (error) { - spinner.fail('Agent recompilation failed'); - throw error; - } - } - - /** - * Private: Prompt for update action - */ - async promptUpdateAction() { - const inquirer = require('inquirer'); - return await inquirer.prompt([ - { - type: 'list', - name: 'action', - message: 'What would you like to do?', - choices: [{ name: 'Update existing installation', value: 'update' }], - }, - ]); - } - - /** - * Handle legacy BMAD v4 migration with automatic backup - * @param {string} projectDir - Project directory - * @param {Object} legacyV4 - Legacy V4 detection result with offenders array - */ - async handleLegacyV4Migration(projectDir, legacyV4) { - console.log(chalk.yellow.bold('\n⚠️ Legacy BMAD v4 detected')); - console.log(chalk.dim('The installer found legacy artefacts in your project.\n')); - - // Separate _bmad* folders (auto-backup) from other offending paths (manual cleanup) - const bmadFolders = legacyV4.offenders.filter((p) => { - const name = path.basename(p); - return name.startsWith('_bmad'); // Only dot-prefixed folders get auto-backed up - }); - const otherOffenders = legacyV4.offenders.filter((p) => { - const name = path.basename(p); - return !name.startsWith('_bmad'); // Everything else is manual cleanup - }); - - const inquirer = require('inquirer'); - - // Show warning for other offending paths FIRST - if (otherOffenders.length > 0) { - console.log(chalk.yellow('⚠️ Recommended cleanup:')); - console.log(chalk.dim('It is recommended to remove the following items before proceeding:\n')); - for (const p of otherOffenders) console.log(chalk.dim(` - ${p}`)); - - console.log(chalk.cyan('\nCleanup commands you can copy/paste:')); - console.log(chalk.dim('macOS/Linux:')); - for (const p of otherOffenders) console.log(chalk.dim(` rm -rf '${p}'`)); - console.log(chalk.dim('Windows:')); - for (const p of otherOffenders) console.log(chalk.dim(` rmdir /S /Q "${p}"`)); - - const { cleanedUp } = await inquirer.prompt([ - { - type: 'confirm', - name: 'cleanedUp', - message: 'Have you completed the recommended cleanup? (You can proceed without it, but it is recommended)', - default: false, - }, - ]); - - if (cleanedUp) { - console.log(chalk.green('✓ Cleanup acknowledged\n')); - } else { - console.log(chalk.yellow('⚠️ Proceeding without recommended cleanup\n')); - } - } - - // Handle _bmad* folders with automatic backup - if (bmadFolders.length > 0) { - console.log(chalk.cyan('The following legacy folders will be moved to v4-backup:')); - for (const p of bmadFolders) console.log(chalk.dim(` - ${p}`)); - - const { proceed } = await inquirer.prompt([ - { - type: 'confirm', - name: 'proceed', - message: 'Proceed with backing up legacy v4 folders?', - default: true, - }, - ]); - - if (proceed) { - const backupDir = path.join(projectDir, 'v4-backup'); - await fs.ensureDir(backupDir); - - for (const folder of bmadFolders) { - const folderName = path.basename(folder); - const backupPath = path.join(backupDir, folderName); - - // If backup already exists, add timestamp - let finalBackupPath = backupPath; - if (await fs.pathExists(backupPath)) { - const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-').split('T')[0]; - finalBackupPath = path.join(backupDir, `${folderName}-${timestamp}`); - } - - await fs.move(folder, finalBackupPath, { overwrite: false }); - console.log(chalk.green(`✓ Moved ${folderName} to ${path.relative(projectDir, finalBackupPath)}`)); - } - } else { - throw new Error('Installation cancelled by user'); - } - } - } - - /** - * Read files-manifest.csv - * @param {string} bmadDir - BMAD installation directory - * @returns {Array} Array of file entries from files-manifest.csv - */ - async readFilesManifest(bmadDir) { - const filesManifestPath = path.join(bmadDir, '_config', 'files-manifest.csv'); - if (!(await fs.pathExists(filesManifestPath))) { - return []; - } - - try { - const content = await fs.readFile(filesManifestPath, 'utf8'); - const lines = content.split('\n'); - const files = []; - - for (let i = 1; i < lines.length; i++) { - // Skip header - const line = lines[i].trim(); - if (!line) continue; - - // Parse CSV line properly handling quoted values - const parts = []; - let current = ''; - let inQuotes = false; - - for (const char of line) { - if (char === '"') { - inQuotes = !inQuotes; - } else if (char === ',' && !inQuotes) { - parts.push(current); - current = ''; - } else { - current += char; - } - } - parts.push(current); // Add last part - - if (parts.length >= 4) { - files.push({ - type: parts[0], - name: parts[1], - module: parts[2], - path: parts[3], - hash: parts[4] || null, // Hash may not exist in old manifests - }); - } - } - - return files; - } catch (error) { - console.warn('Warning: Could not read files-manifest.csv:', error.message); - return []; - } - } - - /** - * Detect custom and modified files - * @param {string} bmadDir - BMAD installation directory - * @param {Array} existingFilesManifest - Previous files from files-manifest.csv - * @returns {Object} Object with customFiles and modifiedFiles arrays - */ - async detectCustomFiles(bmadDir, existingFilesManifest) { - const customFiles = []; - const modifiedFiles = []; - - // Memory is always in _bmad/_memory - const bmadMemoryPath = '_memory'; - - // Check if the manifest has hashes - if not, we can't detect modifications - let manifestHasHashes = false; - if (existingFilesManifest && existingFilesManifest.length > 0) { - manifestHasHashes = existingFilesManifest.some((f) => f.hash); - } - - // Build map of previously installed files from files-manifest.csv with their hashes - const installedFilesMap = new Map(); - for (const fileEntry of existingFilesManifest) { - if (fileEntry.path) { - const absolutePath = path.join(bmadDir, fileEntry.path); - installedFilesMap.set(path.normalize(absolutePath), { - hash: fileEntry.hash, - relativePath: fileEntry.path, - }); - } - } - - // Recursively scan bmadDir for all files - const scanDirectory = async (dir) => { - try { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - // Skip certain directories - if (entry.name === 'node_modules' || entry.name === '.git') { - continue; - } - await scanDirectory(fullPath); - } else if (entry.isFile()) { - const normalizedPath = path.normalize(fullPath); - const fileInfo = installedFilesMap.get(normalizedPath); - - // Skip certain system files that are auto-generated - const relativePath = path.relative(bmadDir, fullPath); - const fileName = path.basename(fullPath); - - // Skip _config directory EXCEPT for modified agent customizations - if (relativePath.startsWith('_config/') || relativePath.startsWith('_config\\')) { - // Special handling for .customize.yaml files - only preserve if modified - if (relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml')) { - // Check if the customization file has been modified from manifest - const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml'); - if (await fs.pathExists(manifestPath)) { - const crypto = require('node:crypto'); - const currentContent = await fs.readFile(fullPath, 'utf8'); - const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex'); - - const yaml = require('yaml'); - const manifestContent = await fs.readFile(manifestPath, 'utf8'); - const manifestData = yaml.parse(manifestContent); - const originalHash = manifestData.agentCustomizations?.[relativePath]; - - // Only add to customFiles if hash differs (user modified) - if (originalHash && currentHash !== originalHash) { - customFiles.push(fullPath); - } - } - } - continue; - } - - if (relativePath.startsWith(bmadMemoryPath + '/') && path.dirname(relativePath).includes('-sidecar')) { - continue; - } - - // Skip config.yaml files - these are regenerated on each install/update - if (fileName === 'config.yaml') { - continue; - } - - if (!fileInfo) { - // File not in manifest = custom file - // EXCEPT: Agent .md files in module folders are generated files, not custom - // Only treat .md files under _config/agents/ as custom - if (!(fileName.endsWith('.md') && relativePath.includes('/agents/') && !relativePath.startsWith('_config/'))) { - customFiles.push(fullPath); - } - } else if (manifestHasHashes && fileInfo.hash) { - // File in manifest with hash - check if it was modified - const currentHash = await this.manifest.calculateFileHash(fullPath); - if (currentHash && currentHash !== fileInfo.hash) { - // Hash changed = file was modified - modifiedFiles.push({ - path: fullPath, - relativePath: fileInfo.relativePath, - }); - } - } - } - } - } catch { - // Ignore errors scanning directories - } - }; - - await scanDirectory(bmadDir); - return { customFiles, modifiedFiles }; - } - - /** - * Private: Create agent configuration files - * @param {string} bmadDir - BMAD installation directory - * @param {Object} userInfo - User information including name and language - */ - async createAgentConfigs(bmadDir, userInfo = null) { - const agentConfigDir = path.join(bmadDir, '_config', 'agents'); - await fs.ensureDir(agentConfigDir); - - // Get all agents from all modules - const agents = []; - const agentDetails = []; // For manifest generation - - // Check modules for agents (including core) - const entries = await fs.readdir(bmadDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory() && entry.name !== '_config') { - const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents'); - if (await fs.pathExists(moduleAgentsPath)) { - const agentFiles = await fs.readdir(moduleAgentsPath); - for (const agentFile of agentFiles) { - if (agentFile.endsWith('.md')) { - const agentPath = path.join(moduleAgentsPath, agentFile); - const agentContent = await fs.readFile(agentPath, 'utf8'); - - // Skip agents with localskip="true" - const hasLocalSkip = agentContent.match(/]*\slocalskip="true"[^>]*>/); - if (hasLocalSkip) { - continue; // Skip this agent - it should not have been installed - } - - const agentName = path.basename(agentFile, '.md'); - - // Extract any nodes with agentConfig="true" - const agentConfigNodes = this.extractAgentConfigNodes(agentContent); - - agents.push({ - name: agentName, - module: entry.name, - agentConfigNodes: agentConfigNodes, - }); - - // Use shared AgentPartyGenerator to extract details - let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName); - - // Apply config overrides if they exist - if (details) { - const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`); - if (await fs.pathExists(configPath)) { - const configContent = await fs.readFile(configPath, 'utf8'); - details = AgentPartyGenerator.applyConfigOverrides(details, configContent); - } - agentDetails.push(details); - } - } - } - } - } - } - - // Create config file for each agent - let createdCount = 0; - let skippedCount = 0; - - // Load agent config template - const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md'); - const templateContent = await fs.readFile(templatePath, 'utf8'); - - for (const agent of agents) { - const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`); - - // Skip if config file already exists (preserve custom configurations) - if (await fs.pathExists(configPath)) { - skippedCount++; - continue; - } - - // Build config content header - let configContent = `# Agent Config: ${agent.name}\n\n`; - - // Process template and add agent-specific config nodes - let processedTemplate = templateContent; - - // Replace {core:user_name} placeholder with actual user name if available - if (userInfo && userInfo.userName) { - processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName); - } - - // Replace {core:communication_language} placeholder with actual language if available - if (userInfo && userInfo.responseLanguage) { - processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage); - } - - // If this agent has agentConfig nodes, add them after the existing comment - if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) { - // Find the agent-specific configuration nodes comment - const commentPattern = /(\s*)/; - const commentMatch = processedTemplate.match(commentPattern); - - if (commentMatch) { - // Add nodes right after the comment - let agentSpecificNodes = ''; - for (const node of agent.agentConfigNodes) { - agentSpecificNodes += `\n ${node}`; - } - - processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`); - } - } - - configContent += processedTemplate; - - // Ensure POSIX-compliant final newline - if (!configContent.endsWith('\n')) { - configContent += '\n'; - } - - await fs.writeFile(configPath, configContent, 'utf8'); - this.installedFiles.add(configPath); // Track agent config files - createdCount++; - } - - // Generate agent manifest with overrides applied - await this.generateAgentManifest(bmadDir, agentDetails); - - return { total: agents.length, created: createdCount, skipped: skippedCount }; - } - - /** - * Generate agent manifest XML file - * @param {string} bmadDir - BMAD installation directory - * @param {Array} agentDetails - Array of agent details - */ - async generateAgentManifest(bmadDir, agentDetails) { - const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv'); - await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false }); - } - - /** - * Extract nodes with agentConfig="true" from agent content - * @param {string} content - Agent file content - * @returns {Array} Array of XML nodes that should be added to agent config - */ - extractAgentConfigNodes(content) { - const nodes = []; - - try { - // Find all XML nodes with agentConfig="true" - // Match self-closing tags and tags with content - const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g; - const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g; - - // Extract self-closing tags - let match; - while ((match = selfClosingPattern.exec(content)) !== null) { - // Extract just the tag without children (structure only) - const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/); - if (tagMatch) { - const tagName = tagMatch[1]; - const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute - nodes.push(`<${tagName}${attributes}>`); - } - } - - // Extract tags with content - while ((match = withContentPattern.exec(content)) !== null) { - const fullMatch = match[0]; - const tagName = match[1]; - - // Extract opening tag with attributes (removing agentConfig="true") - const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`)); - if (openingTagMatch) { - const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, ''); - // Add empty node structure (no children) - nodes.push(`<${tagName}${attributes}>`); - } - } - } catch (error) { - console.error('Error extracting agentConfig nodes:', error); - } - - return nodes; - } - - /** - * Handle missing custom module sources interactively - * @param {Map} customModuleSources - Map of custom module ID to info - * @param {string} bmadDir - BMAD directory - * @param {string} projectRoot - Project root directory - * @param {string} operation - Current operation ('update', 'compile', etc.) - * @param {Array} installedModules - Array of installed module IDs (will be modified) - * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array - */ - async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) { - const validCustomModules = []; - const keptModulesWithoutSources = []; // Track modules kept without sources - const customModulesWithMissingSources = []; - - // Check which sources exist - for (const [moduleId, customInfo] of customModuleSources) { - if (await fs.pathExists(customInfo.sourcePath)) { - validCustomModules.push({ - id: moduleId, - name: customInfo.name, - path: customInfo.sourcePath, - info: customInfo, - }); - } else { - // For cached modules that are missing, we just skip them without prompting - if (customInfo.cached) { - // Skip cached modules without prompting - keptModulesWithoutSources.push({ - id: moduleId, - name: customInfo.name, - cached: true, - }); - } else { - customModulesWithMissingSources.push({ - id: moduleId, - name: customInfo.name, - sourcePath: customInfo.sourcePath, - relativePath: customInfo.relativePath, - info: customInfo, - }); - } - } - } - - // If no missing sources, return immediately - if (customModulesWithMissingSources.length === 0) { - return { - validCustomModules, - keptModulesWithoutSources: [], - }; - } - - // Stop any spinner for interactive prompts - const currentSpinner = ora(); - if (currentSpinner.isSpinning) { - currentSpinner.stop(); - } - - console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`)); - - const inquirer = require('inquirer'); - let keptCount = 0; - let updatedCount = 0; - let removedCount = 0; - - for (const missing of customModulesWithMissingSources) { - console.log(chalk.dim(` • ${missing.name} (${missing.id})`)); - console.log(chalk.dim(` Original source: ${missing.relativePath}`)); - console.log(chalk.dim(` Full path: ${missing.sourcePath}`)); - - const choices = [ - { - name: 'Keep installed (will not be processed)', - value: 'keep', - short: 'Keep', - }, - { - name: 'Specify new source location', - value: 'update', - short: 'Update', - }, - ]; - - // Only add remove option if not just compiling agents - if (operation !== 'compile-agents') { - choices.push({ - name: '⚠️ REMOVE module completely (destructive!)', - value: 'remove', - short: 'Remove', - }); - } - - const { action } = await inquirer.prompt([ - { - type: 'list', - name: 'action', - message: `How would you like to handle "${missing.name}"?`, - choices, - }, - ]); - - switch (action) { - case 'update': { - const { newSourcePath } = await inquirer.prompt([ - { - type: 'input', - name: 'newSourcePath', - message: 'Enter the new path to the custom module:', - default: missing.sourcePath, - validate: async (input) => { - if (!input || input.trim() === '') { - return 'Please enter a path'; - } - const expandedPath = path.resolve(input.trim()); - if (!(await fs.pathExists(expandedPath))) { - return 'Path does not exist'; - } - // Check if it looks like a valid module - const moduleYamlPath = path.join(expandedPath, 'module.yaml'); - const agentsPath = path.join(expandedPath, 'agents'); - const workflowsPath = path.join(expandedPath, 'workflows'); - - if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) { - return 'Path does not appear to contain a valid custom module'; - } - return true; - }, - }, - ]); - - // Update the source in manifest - const resolvedPath = path.resolve(newSourcePath.trim()); - missing.info.sourcePath = resolvedPath; - // Remove relativePath - we only store absolute sourcePath now - delete missing.info.relativePath; - await this.manifest.addCustomModule(bmadDir, missing.info); - - validCustomModules.push({ - id: moduleId, - name: missing.name, - path: resolvedPath, - info: missing.info, - }); - - updatedCount++; - console.log(chalk.green(`✓ Updated source location`)); - - break; - } - case 'remove': { - // Extra confirmation for destructive remove - console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`)); - console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`)); - - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: chalk.red.bold('Are you absolutely sure you want to delete this module?'), - default: false, - }, - ]); - - if (confirm) { - const { typedConfirm } = await inquirer.prompt([ - { - type: 'input', - name: 'typedConfirm', - message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'), - validate: (input) => { - if (input !== 'DELETE') { - return chalk.red('You must type "DELETE" exactly to proceed'); - } - return true; - }, - }, - ]); - - if (typedConfirm === 'DELETE') { - // Remove the module from filesystem and manifest - const modulePath = path.join(bmadDir, moduleId); - if (await fs.pathExists(modulePath)) { - const fsExtra = require('fs-extra'); - await fsExtra.remove(modulePath); - console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`)); - } - - await this.manifest.removeModule(bmadDir, moduleId); - await this.manifest.removeCustomModule(bmadDir, moduleId); - console.log(chalk.yellow(` ✓ Removed from manifest`)); - - // Also remove from installedModules list - if (installedModules && installedModules.includes(moduleId)) { - const index = installedModules.indexOf(moduleId); - if (index !== -1) { - installedModules.splice(index, 1); - } - } - - removedCount++; - console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`)); - } else { - console.log(chalk.dim(' Removal cancelled - module will be kept')); - keptCount++; - } - } else { - console.log(chalk.dim(' Removal cancelled - module will be kept')); - keptCount++; - } - - break; - } - case 'keep': { - keptCount++; - keptModulesWithoutSources.push(moduleId); - console.log(chalk.dim(` Module will be kept as-is`)); - - break; - } - // No default - } - } - - // Show summary - if (keptCount > 0 || updatedCount > 0 || removedCount > 0) { - console.log(chalk.dim(`\nSummary for custom modules with missing sources:`)); - if (keptCount > 0) console.log(chalk.dim(` • ${keptCount} module(s) kept as-is`)); - if (updatedCount > 0) console.log(chalk.dim(` • ${updatedCount} module(s) updated with new sources`)); - if (removedCount > 0) console.log(chalk.red(` • ${removedCount} module(s) permanently deleted`)); - } - - return { - validCustomModules, - keptModulesWithoutSources, - }; - } -} - -module.exports = { Installer }; diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index e54bf3be..4844f243 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -731,7 +731,7 @@ class ModuleManager { async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir, installer = null) { const sourceAgentsPath = path.join(sourcePath, 'agents'); const targetAgentsPath = path.join(targetPath, 'agents'); - const cfgAgentsDir = path.join(bmadDir, '_bmad', '_config', 'agents'); + const cfgAgentsDir = path.join(bmadDir, '_config', 'agents'); // Check if agents directory exists in source if (!(await fs.pathExists(sourceAgentsPath))) { From 5c756b640434977587632e04dced27ab770dc2a4 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Thu, 18 Dec 2025 12:52:10 +0800 Subject: [PATCH 2/4] chore: bump version to 6.0.0-alpha.19 Bug fix: - Fixed _bmad folder stutter with agent custom files - Removed unnecessary backup file causing installer bloat - Improved path handling for agent customizations --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e938d15b..a757f51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [6.0.0-alpha.19] + +**Release: December 18, 2025** + +### 🐛 Bug Fixes + +**Installer Stability:** + +- **Fixed \_bmad Folder Stutter**: Resolved issue with duplicate \_bmad folder creation when applying agent custom files +- **Cleaner Installation**: Removed unnecessary backup file that was causing bloat in the installer +- **Streamlined Agent Customization**: Fixed path handling for agent custom files to prevent folder duplication + +### 📊 Statistics + +- **3 files changed** with critical fix +- **3,688 lines removed** by eliminating backup files +- **Improved installer performance** and stability + +--- + ## [6.0.0-alpha.18] **Release: December 18, 2025** diff --git a/package.json b/package.json index 738aba72..f9694ae6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "bmad-method", - "version": "6.0.0-alpha.18", + "version": "6.0.0-alpha.19", "description": "Breakthrough Method of Agile AI-driven Development", "keywords": [ "agile", From 2da9aebaa8bc055231c52bce871b14b839799424 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Thu, 18 Dec 2025 00:58:54 -0700 Subject: [PATCH 3/4] docs: add DigitalOcean sponsor attribution (#1162) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7a271e6d..cca9b121 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,8 @@ MIT License - See [LICENSE](LICENSE) for details. **Trademarks:** BMad™ and BMAD-METHOD™ are trademarks of BMad Code, LLC. +Supported by:  DigitalOcean + ---

From e39aa33eea15ec3c9c7c6776fa9d4b8e8971501b Mon Sep 17 00:00:00 2001 From: sjennings Date: Thu, 18 Dec 2025 02:14:18 -0600 Subject: [PATCH 4/4] fix(bmgd): add workflow status update to game-architecture completion (#1161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(bmgd): add workflow status update to game-architecture completion The game-architecture workflow was not updating the bmgd-workflow-status.yaml file on completion, unlike other BMGD workflows (narrative, brainstorm-game). Changes: - Add step 4 "Update Workflow Status" to update create-architecture status - Renumber subsequent steps (5-8 → 6-9) - Add success metric for workflow status update - Add failure condition for missing status update * feat(bmgd): add generate-project-context workflow for game development Adds a new workflow to create optimized project-context.md files for AI agent consistency in game development projects. New workflow files: - workflow.md: Main workflow entry point - project-context-template.md: Template for context file - steps/step-01-discover.md: Context discovery & initialization - steps/step-02-generate.md: Rules generation with A/P/C menus - steps/step-03-complete.md: Finalization & optimization Integration: - Added generate-project-context trigger to game-architect agent menu - Added project context creation option to game-architecture completion step - Renumbered steps 6-9 → 7-10 to accommodate new step 6 Adapted from BMM generate-project-context with game-specific: - Engine patterns (Unity, Unreal, Godot) - Performance and frame budget rules - Platform-specific requirements - Game testing patterns --------- Co-authored-by: Scott Jennings Co-authored-by: Brian --- .../bmgd/agents/game-architect.agent.yaml | 4 + .../steps/step-09-complete.md | 66 +++- .../project-context-template.md | 20 + .../steps/step-01-discover.md | 201 ++++++++++ .../steps/step-02-generate.md | 373 ++++++++++++++++++ .../steps/step-03-complete.md | 279 +++++++++++++ .../generate-project-context/workflow.md | 48 +++ 7 files changed, 985 insertions(+), 6 deletions(-) create mode 100644 src/modules/bmgd/workflows/3-technical/generate-project-context/project-context-template.md create mode 100644 src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-01-discover.md create mode 100644 src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-02-generate.md create mode 100644 src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-03-complete.md create mode 100644 src/modules/bmgd/workflows/3-technical/generate-project-context/workflow.md diff --git a/src/modules/bmgd/agents/game-architect.agent.yaml b/src/modules/bmgd/agents/game-architect.agent.yaml index 8e218901..3b28024d 100644 --- a/src/modules/bmgd/agents/game-architect.agent.yaml +++ b/src/modules/bmgd/agents/game-architect.agent.yaml @@ -33,6 +33,10 @@ agent: exec: "{project-root}/_bmad/bmgd/workflows/3-technical/game-architecture/workflow.md" description: Produce a Scale Adaptive Game Architecture + - trigger: generate-project-context + exec: "{project-root}/_bmad/bmgd/workflows/3-technical/generate-project-context/workflow.md" + description: Create optimized project-context.md for AI agent consistency + - trigger: correct-course workflow: "{project-root}/_bmad/bmgd/workflows/4-production/correct-course/workflow.yaml" description: Course Correction Analysis (when implementation is off-track) diff --git a/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md b/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md index 7e71161e..51f022e3 100644 --- a/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md +++ b/src/modules/bmgd/workflows/3-technical/game-architecture/steps/step-09-complete.md @@ -12,6 +12,7 @@ outputFile: '{output_folder}/game-architecture.md' # Handoff References epicWorkflow: '{project-root}/_bmad/bmgd/workflows/4-production/epic-workflow/workflow.yaml' +projectContextWorkflow: '{project-root}/_bmad/bmgd/workflows/3-technical/generate-project-context/workflow.md' --- # Step 9: Completion @@ -131,7 +132,17 @@ platform: '{{platform}}' --- ```` -### 4. Present Completion Summary +### 4. Update Workflow Status + +**If not in standalone mode:** + +Load `{output_folder}/bmgd-workflow-status.yaml` and: + +- Update `create-architecture` status to the output file path +- Preserve all comments and structure +- Determine next workflow in sequence + +### 5. Present Completion Summary "**Architecture Complete!** @@ -158,9 +169,50 @@ platform: '{{platform}}' **Document saved to:** `{outputFile}` -Do you want to review or adjust anything before we finalize?" +Do you want to review or adjust anything before we finalize? -### 5. Handle Review Requests +**Optional Enhancement: Project Context File** + +Would you like to create a `project-context.md` file? This is a concise, optimized guide for AI agents that captures: + +- Critical engine-specific rules they might miss +- Specific patterns and conventions for your game project +- Performance and optimization requirements +- Anti-patterns and edge cases to avoid + +{if_existing_project_context} +I noticed you already have a project context file. Would you like to update it with your new architectural decisions? +{else} +This file helps ensure AI agents implement game code consistently with your project's unique requirements and patterns. +{/if_existing_project_context} + +**Create/Update project context?** [Y/N]" + +### 6. Handle Project Context Creation Choice + +If user responds 'Y' or 'yes' to creating/updating project context: + +"Excellent choice! Let me launch the Generate Project Context workflow to create a comprehensive guide for AI agents. + +This will help ensure consistent implementation by capturing: + +- Engine-specific patterns and rules +- Performance and optimization conventions from your architecture +- Testing and quality standards +- Anti-patterns to avoid + +The workflow will collaborate with you to create an optimized `project-context.md` file that AI agents will read before implementing any game code." + +**Execute the Generate Project Context workflow:** + +- Load and execute: `{projectContextWorkflow}` +- The workflow will handle discovery, generation, and completion of the project context file +- After completion, return here for final handoff + +If user responds 'N' or 'no': +"Understood! Your architecture is complete and ready for implementation. You can always create a project context file later using the Generate Project Context workflow if needed." + +### 7. Handle Review Requests **If user wants to review:** @@ -179,7 +231,7 @@ Or type 'all' to see the complete document." **Show requested section and allow edits.** -### 6. Present Next Steps Menu +### 8. Present Next Steps Menu **After user confirms completion:** @@ -204,7 +256,7 @@ Or type 'all' to see the complete document." 2. Proceed to Epic creation workflow 3. Exit workflow" -### 7. Handle User Selection +### 9. Handle User Selection Based on user choice: @@ -224,7 +276,7 @@ Based on user choice: - Confirm document is saved and complete - Exit workflow gracefully -### 8. Provide Handoff Guidance +### 10. Provide Handoff Guidance **For Epic Creation handoff:** @@ -270,6 +322,7 @@ This is the final step. Ensure: - Development setup is complete - Document status updated to 'complete' - Frontmatter shows all steps completed +- Workflow status updated (if tracking) - User has clear next steps - Document saved and ready for AI agent consumption @@ -278,6 +331,7 @@ This is the final step. Ensure: - Missing executive summary - Incomplete development setup - Frontmatter not updated +- Status not updated when tracking - No clear next steps provided - User left without actionable guidance diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/project-context-template.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/project-context-template.md new file mode 100644 index 00000000..b9e4d3bc --- /dev/null +++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/project-context-template.md @@ -0,0 +1,20 @@ +--- +project_name: '{{project_name}}' +user_name: '{{user_name}}' +date: '{{date}}' +sections_completed: [] +--- + +# Project Context for AI Agents + +_This file contains critical rules and patterns that AI agents must follow when implementing game code in this project. Focus on unobvious details that agents might otherwise miss._ + +--- + +## Technology Stack & Versions + +_Documented after discovery phase_ + +## Critical Implementation Rules + +_Documented after discovery phase_ diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-01-discover.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-01-discover.md new file mode 100644 index 00000000..a92db901 --- /dev/null +++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-01-discover.md @@ -0,0 +1,201 @@ +# Step 1: Context Discovery & Initialization + +## MANDATORY EXECUTION RULES (READ FIRST): + +- NEVER generate content without user input +- ALWAYS treat this as collaborative discovery between technical peers +- YOU ARE A FACILITATOR, not a content generator +- FOCUS on discovering existing project context and technology stack +- IDENTIFY critical implementation rules that AI agents need +- ABSOLUTELY NO TIME ESTIMATES + +## EXECUTION PROTOCOLS: + +- Show your analysis before taking any action +- Read existing project files to understand current context +- Initialize document and update frontmatter +- FORBIDDEN to load next step until discovery is complete + +## CONTEXT BOUNDARIES: + +- Variables from workflow.md are available in memory +- Focus on existing project files and architecture decisions +- Look for patterns, conventions, and unique requirements +- Prioritize rules that prevent implementation mistakes + +## YOUR TASK: + +Discover the project's game engine, technology stack, existing patterns, and critical implementation rules that AI agents must follow when writing game code. + +## DISCOVERY SEQUENCE: + +### 1. Check for Existing Project Context + +First, check if project context already exists: + +- Look for file at `{output_folder}/project-context.md` +- If exists: Read complete file to understand existing rules +- Present to user: "Found existing project context with {number_of_sections} sections. Would you like to update this or create a new one?" + +### 2. Discover Game Engine & Technology Stack + +Load and analyze project files to identify technologies: + +**Architecture Document:** + +- Look for `{output_folder}/game-architecture.md` or `{output_folder}/architecture.md` +- Extract engine choice with specific version (Unity, Unreal, Godot, custom) +- Note architectural decisions that affect implementation + +**Engine-Specific Files:** + +- Unity: Check for `ProjectSettings/ProjectVersion.txt`, `Packages/manifest.json` +- Unreal: Check for `.uproject` files, `Config/DefaultEngine.ini` +- Godot: Check for `project.godot`, `export_presets.cfg` +- Custom: Check for engine config files, build scripts + +**Package/Dependency Files:** + +- Unity: `Packages/manifest.json`, NuGet packages +- Unreal: `.Build.cs` files, plugin configs +- Godot: `addons/` directory, GDExtension configs +- Web-based: `package.json`, `requirements.txt` + +**Configuration Files:** + +- Build tool configs +- Linting and formatting configs +- Testing configurations +- CI/CD pipeline configs + +### 3. Identify Existing Code Patterns + +Search through existing codebase for patterns: + +**Naming Conventions:** + +- Script/class naming patterns +- Asset naming conventions +- Scene/level naming patterns +- Test file naming patterns + +**Code Organization:** + +- How components/scripts are structured +- Where utilities and helpers are placed +- How systems are organized +- Folder hierarchy patterns + +**Engine-Specific Patterns:** + +- Unity: MonoBehaviour patterns, ScriptableObject usage, serialization rules +- Unreal: Actor/Component patterns, Blueprint integration, UE macros +- Godot: Node patterns, signal usage, autoload patterns + +### 4. Extract Critical Implementation Rules + +Look for rules that AI agents might miss: + +**Engine-Specific Rules:** + +- Unity: Assembly definitions, Unity lifecycle methods, coroutine patterns +- Unreal: UPROPERTY/UFUNCTION usage, garbage collection rules, tick patterns +- Godot: `_ready` vs `_enter_tree`, node ownership, scene instancing + +**Performance Rules:** + +- Frame budget constraints +- Memory allocation patterns +- Hot path optimization requirements +- Object pooling patterns + +**Platform-Specific Rules:** + +- Target platform constraints +- Input handling conventions +- Platform-specific code patterns +- Build configuration rules + +**Testing Rules:** + +- Test structure requirements +- Mock usage conventions +- Integration vs unit test boundaries +- Play mode vs edit mode testing + +### 5. Initialize Project Context Document + +Based on discovery, create or update the context document: + +#### A. Fresh Document Setup (if no existing context) + +Copy template from `{installed_path}/project-context-template.md` to `{output_folder}/project-context.md` +Initialize frontmatter with: + +```yaml +--- +project_name: '{{project_name}}' +user_name: '{{user_name}}' +date: '{{date}}' +sections_completed: ['technology_stack'] +existing_patterns_found: { { number_of_patterns_discovered } } +--- +``` + +#### B. Existing Document Update + +Load existing context and prepare for updates +Set frontmatter `sections_completed` to track what will be updated + +### 6. Present Discovery Summary + +Report findings to user: + +"Welcome {{user_name}}! I've analyzed your game project for {{project_name}} to discover the context that AI agents need. + +**Game Engine & Stack Discovered:** +{{engine_and_version}} +{{list_of_technologies_with_versions}} + +**Existing Patterns Found:** + +- {{number_of_patterns}} implementation patterns +- {{number_of_conventions}} coding conventions +- {{number_of_rules}} critical rules + +**Key Areas for Context Rules:** + +- {{area_1}} (e.g., Engine lifecycle and patterns) +- {{area_2}} (e.g., Performance and optimization) +- {{area_3}} (e.g., Platform-specific requirements) + +{if_existing_context} +**Existing Context:** Found {{sections}} sections already defined. We can update or add to these. +{/if_existing_context} + +Ready to create/update your project context. This will help AI agents implement game code consistently with your project's standards. + +[C] Continue to context generation" + +## SUCCESS METRICS: + +- Existing project context properly detected and handled +- Game engine and technology stack accurately identified with versions +- Critical implementation patterns discovered +- Project context document properly initialized +- Discovery findings clearly presented to user +- User ready to proceed with context generation + +## FAILURE MODES: + +- Not checking for existing project context before creating new one +- Missing critical engine versions or configurations +- Overlooking important coding patterns or conventions +- Not initializing frontmatter properly +- Not presenting clear discovery summary to user + +## NEXT STEP: + +After user selects [C] to continue, load `./step-02-generate.md` to collaboratively generate the specific project context rules. + +Remember: Do NOT proceed to step-02 until user explicitly selects [C] from the menu and discovery is confirmed! diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-02-generate.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-02-generate.md new file mode 100644 index 00000000..75e978cb --- /dev/null +++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-02-generate.md @@ -0,0 +1,373 @@ +# Step 2: Context Rules Generation + +## MANDATORY EXECUTION RULES (READ FIRST): + +- NEVER generate content without user input +- ALWAYS treat this as collaborative discovery between technical peers +- YOU ARE A FACILITATOR, not a content generator +- FOCUS on unobvious rules that AI agents need to be reminded of +- KEEP CONTENT LEAN - optimize for LLM context efficiency +- ABSOLUTELY NO TIME ESTIMATES + +## EXECUTION PROTOCOLS: + +- Show your analysis before taking any action +- Focus on specific, actionable rules rather than general advice +- Present A/P/C menu after each major rule category +- ONLY save when user chooses C (Continue) +- Update frontmatter with completed sections +- FORBIDDEN to load next step until all sections are complete + +## COLLABORATION MENUS (A/P/C): + +This step will generate content and present choices for each rule category: + +- **A (Advanced Elicitation)**: Use discovery protocols to explore nuanced implementation rules +- **P (Party Mode)**: Bring multiple perspectives to identify critical edge cases +- **C (Continue)**: Save the current rules and proceed to next category + +## PROTOCOL INTEGRATION: + +- When 'A' selected: Execute {project-root}/\_bmad/core/tasks/advanced-elicitation.xml +- When 'P' selected: Execute {project-root}/\_bmad/core/workflows/party-mode +- PROTOCOLS always return to display this step's A/P/C menu after the A or P have completed +- User accepts/rejects protocol changes before proceeding + +## CONTEXT BOUNDARIES: + +- Discovery results from step-1 are available +- Game engine and existing patterns are identified +- Focus on rules that prevent implementation mistakes +- Prioritize unobvious details that AI agents might miss + +## YOUR TASK: + +Collaboratively generate specific, critical rules that AI agents must follow when implementing game code in this project. + +## CONTEXT GENERATION SEQUENCE: + +### 1. Technology Stack & Versions + +Document the exact technology stack from discovery: + +**Core Technologies:** +Based on user skill level, present findings: + +**Expert Mode:** +"Technology stack from your architecture and project files: +{{exact_technologies_with_versions}} + +Any critical version constraints I should document for agents?" + +**Intermediate Mode:** +"I found your technology stack: + +**Game Engine:** +{{engine_with_version}} + +**Key Dependencies:** +{{important_dependencies_with_versions}} + +Are there any version constraints or compatibility notes agents should know about?" + +**Beginner Mode:** +"Here are the technologies you're using: + +**Game Engine:** +{{friendly_description_of_engine}} + +**Important Notes:** +{{key_things_agents_need_to_know_about_versions}} + +Should I document any special version rules or compatibility requirements?" + +### 2. Engine-Specific Rules + +Focus on unobvious engine patterns agents might miss: + +**Unity Rules (if applicable):** +"Based on your Unity project, I notice some specific patterns: + +**Lifecycle Rules:** +{{unity_lifecycle_patterns}} + +**Serialization Rules:** +{{serialization_requirements}} + +**Assembly Definitions:** +{{assembly_definition_rules}} + +**Coroutine/Async Patterns:** +{{async_patterns}} + +Are these patterns correct? Any other Unity-specific rules agents should follow?" + +**Unreal Rules (if applicable):** +"Based on your Unreal project, I notice some specific patterns: + +**UPROPERTY/UFUNCTION Rules:** +{{macro_usage_patterns}} + +**Blueprint Integration:** +{{blueprint_rules}} + +**Garbage Collection:** +{{gc_patterns}} + +**Tick Patterns:** +{{tick_optimization_rules}} + +Are these patterns correct? Any other Unreal-specific rules agents should follow?" + +**Godot Rules (if applicable):** +"Based on your Godot project, I notice some specific patterns: + +**Node Lifecycle:** +{{node_lifecycle_patterns}} + +**Signal Usage:** +{{signal_conventions}} + +**Scene Instancing:** +{{scene_patterns}} + +**Autoload Patterns:** +{{autoload_rules}} + +Are these patterns correct? Any other Godot-specific rules agents should follow?" + +### 3. Performance Rules + +Document performance-critical patterns: + +**Frame Budget Rules:** +"Your game has these performance requirements: + +**Target Frame Rate:** +{{target_fps}} + +**Frame Budget:** +{{milliseconds_per_frame}} + +**Critical Systems:** +{{systems_that_must_meet_budget}} + +**Hot Path Rules:** +{{hot_path_patterns}} + +Any other performance rules agents must follow?" + +**Memory Management:** +"Memory patterns for your project: + +**Allocation Rules:** +{{allocation_patterns}} + +**Pooling Requirements:** +{{object_pooling_rules}} + +**Asset Loading:** +{{asset_loading_patterns}} + +Are there memory constraints agents should know about?" + +### 4. Code Organization Rules + +Document project structure and organization: + +**Folder Structure:** +"Your project organization: + +**Script Organization:** +{{script_folder_structure}} + +**Asset Organization:** +{{asset_folder_patterns}} + +**Scene/Level Organization:** +{{scene_organization}} + +Any organization rules agents must follow?" + +**Naming Conventions:** +"Your naming patterns: + +**Script/Class Names:** +{{class_naming_patterns}} + +**Asset Names:** +{{asset_naming_patterns}} + +**Variable/Method Names:** +{{variable_naming_patterns}} + +Any other naming rules?" + +### 5. Testing Rules + +Focus on testing patterns that ensure consistency: + +**Test Structure Rules:** +"Your testing setup shows these patterns: + +**Test Organization:** +{{test_file_organization}} + +**Test Categories:** +{{unit_vs_integration_boundaries}} + +**Mocking Patterns:** +{{mock_usage_conventions}} + +**Play Mode Testing:** +{{play_mode_test_patterns}} + +Are there testing rules agents should always follow?" + +### 6. Platform & Build Rules + +Document platform-specific requirements: + +**Target Platforms:** +"Your platform configuration: + +**Primary Platform:** +{{primary_platform}} + +**Platform-Specific Code:** +{{platform_conditional_patterns}} + +**Build Configurations:** +{{build_config_rules}} + +**Input Handling:** +{{input_abstraction_patterns}} + +Any platform rules agents must know?" + +### 7. Critical Don't-Miss Rules + +Identify rules that prevent common mistakes: + +**Anti-Patterns to Avoid:** +"Based on your codebase, here are critical things agents must NOT do: + +{{critical_anti_patterns_with_examples}} + +**Edge Cases:** +{{specific_edge_cases_agents_should_handle}} + +**Common Gotchas:** +{{engine_specific_gotchas}} + +**Performance Traps:** +{{performance_patterns_to_avoid}} + +Are there other 'gotchas' agents should know about?" + +### 8. Generate Context Content + +For each category, prepare lean content for the project context file: + +#### Content Structure: + +```markdown +## Technology Stack & Versions + +{{concise_technology_list_with_exact_versions}} + +## Critical Implementation Rules + +### Engine-Specific Rules + +{{bullet_points_of_engine_rules}} + +### Performance Rules + +{{bullet_points_of_performance_requirements}} + +### Code Organization Rules + +{{bullet_points_of_organization_patterns}} + +### Testing Rules + +{{bullet_points_of_testing_requirements}} + +### Platform & Build Rules + +{{bullet_points_of_platform_requirements}} + +### Critical Don't-Miss Rules + +{{bullet_points_of_anti_patterns_and_gotchas}} +``` + +### 9. Present Content and Menu + +After each category, show the generated rules and present choices: + +"I've drafted the {{category_name}} rules for your project context. + +**Here's what I'll add:** + +[Show the complete markdown content for this category] + +**What would you like to do?** +[A] Advanced Elicitation - Explore nuanced rules for this category +[P] Party Mode - Review from different implementation perspectives +[C] Continue - Save these rules and move to next category" + +### 10. Handle Menu Selection + +#### If 'A' (Advanced Elicitation): + +- Execute advanced-elicitation.xml with current category rules +- Process enhanced rules that come back +- Ask user: "Accept these enhanced rules for {{category}}? (y/n)" +- If yes: Update content, then return to A/P/C menu +- If no: Keep original content, then return to A/P/C menu + +#### If 'P' (Party Mode): + +- Execute party-mode workflow with category rules context +- Process collaborative insights on implementation patterns +- Ask user: "Accept these changes to {{category}} rules? (y/n)" +- If yes: Update content, then return to A/P/C menu +- If no: Keep original content, then return to A/P/C menu + +#### If 'C' (Continue): + +- Save the current category content to project context file +- Update frontmatter: `sections_completed: [...]` +- Proceed to next category or step-03 if complete + +## APPEND TO PROJECT CONTEXT: + +When user selects 'C' for a category, append the content directly to `{output_folder}/project-context.md` using the structure from step 8. + +## SUCCESS METRICS: + +- All critical technology versions accurately documented +- Engine-specific rules cover unobvious patterns +- Performance rules capture project-specific requirements +- Code organization rules maintain project standards +- Testing rules ensure consistent test quality +- Platform rules prevent cross-platform issues +- Content is lean and optimized for LLM context +- A/P/C menu presented and handled correctly for each category + +## FAILURE MODES: + +- Including obvious rules that agents already know +- Making content too verbose for LLM context efficiency +- Missing critical anti-patterns or edge cases +- Not getting user validation for each rule category +- Not documenting exact versions and configurations +- Not presenting A/P/C menu after content generation + +## NEXT STEP: + +After completing all rule categories and user selects 'C' for the final category, load `./step-03-complete.md` to finalize the project context file. + +Remember: Do NOT proceed to step-03 until all categories are complete and user explicitly selects 'C' for each! diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-03-complete.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-03-complete.md new file mode 100644 index 00000000..e87e1382 --- /dev/null +++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/steps/step-03-complete.md @@ -0,0 +1,279 @@ +# Step 3: Context Completion & Finalization + +## MANDATORY EXECUTION RULES (READ FIRST): + +- NEVER generate content without user input +- ALWAYS treat this as collaborative completion between technical peers +- YOU ARE A FACILITATOR, not a content generator +- FOCUS on finalizing a lean, LLM-optimized project context +- ENSURE all critical rules are captured and actionable +- ABSOLUTELY NO TIME ESTIMATES + +## EXECUTION PROTOCOLS: + +- Show your analysis before taking any action +- Review and optimize content for LLM context efficiency +- Update frontmatter with completion status +- NO MORE STEPS - this is the final step + +## CONTEXT BOUNDARIES: + +- All rule categories from step-2 are complete +- Technology stack and versions are documented +- Focus on final review, optimization, and completion +- Ensure the context file is ready for AI agent consumption + +## YOUR TASK: + +Complete the project context file, optimize it for LLM efficiency, and provide guidance for usage and maintenance. + +## COMPLETION SEQUENCE: + +### 1. Review Complete Context File + +Read the entire project context file and analyze: + +**Content Analysis:** + +- Total length and readability for LLMs +- Clarity and specificity of rules +- Coverage of all critical areas +- Actionability of each rule + +**Structure Analysis:** + +- Logical organization of sections +- Consistency of formatting +- Absence of redundant or obvious information +- Optimization for quick scanning + +### 2. Optimize for LLM Context + +Ensure the file is lean and efficient: + +**Content Optimization:** + +- Remove any redundant rules or obvious information +- Combine related rules into concise bullet points +- Use specific, actionable language +- Ensure each rule provides unique value + +**Formatting Optimization:** + +- Use consistent markdown formatting +- Implement clear section hierarchy +- Ensure scannability with strategic use of bolding +- Maintain readability while maximizing information density + +### 3. Final Content Structure + +Ensure the final structure follows this optimized format: + +```markdown +# Project Context for AI Agents + +_This file contains critical rules and patterns that AI agents must follow when implementing game code in this project. Focus on unobvious details that agents might otherwise miss._ + +--- + +## Technology Stack & Versions + +{{concise_technology_list}} + +## Critical Implementation Rules + +### Engine-Specific Rules + +{{engine_rules}} + +### Performance Rules + +{{performance_requirements}} + +### Code Organization Rules + +{{organization_patterns}} + +### Testing Rules + +{{testing_requirements}} + +### Platform & Build Rules + +{{platform_requirements}} + +### Critical Don't-Miss Rules + +{{anti_patterns_and_gotchas}} + +--- + +## Usage Guidelines + +**For AI Agents:** + +- Read this file before implementing any game code +- Follow ALL rules exactly as documented +- When in doubt, prefer the more restrictive option +- Update this file if new patterns emerge + +**For Humans:** + +- Keep this file lean and focused on agent needs +- Update when technology stack changes +- Review quarterly for outdated rules +- Remove rules that become obvious over time + +Last Updated: {{date}} +``` + +### 4. Present Completion Summary + +Based on user skill level, present the completion: + +**Expert Mode:** +"Project context complete. Optimized for LLM consumption with {{rule_count}} critical rules across {{section_count}} sections. + +File saved to: `{output_folder}/project-context.md` + +Ready for AI agent integration." + +**Intermediate Mode:** +"Your project context is complete and optimized for AI agents! + +**What we created:** + +- {{rule_count}} critical implementation rules +- Technology stack with exact versions +- Engine-specific patterns and conventions +- Performance and optimization guidelines +- Testing and platform requirements + +**Key benefits:** + +- AI agents will implement consistently with your standards +- Reduced context switching and implementation errors +- Clear guidance for unobvious project requirements + +**Next steps:** + +- AI agents should read this file before implementing +- Update as your project evolves +- Review periodically for optimization" + +**Beginner Mode:** +"Excellent! Your project context guide is ready! + +**What this does:** +Think of this as a 'rules of the road' guide for AI agents working on your game. It ensures they all follow the same patterns and avoid common mistakes. + +**What's included:** + +- Exact engine and technology versions to use +- Critical coding rules they might miss +- Performance and optimization standards +- Testing and platform requirements + +**How AI agents use it:** +They read this file before writing any code, ensuring everything they create follows your project's standards perfectly. + +Your project context is saved and ready to help agents implement consistently!" + +### 5. Final File Updates + +Update the project context file with completion information: + +**Frontmatter Update:** + +```yaml +--- +project_name: '{{project_name}}' +user_name: '{{user_name}}' +date: '{{date}}' +sections_completed: + ['technology_stack', 'engine_rules', 'performance_rules', 'organization_rules', 'testing_rules', 'platform_rules', 'anti_patterns'] +status: 'complete' +rule_count: { { total_rules } } +optimized_for_llm: true +--- +``` + +**Add Usage Section:** +Append the usage guidelines from step 3 to complete the document. + +### 6. Completion Validation + +Final checks before completion: + +**Content Validation:** + +- All critical technology versions documented +- Engine-specific rules are specific and actionable +- Performance rules capture project requirements +- Code organization rules maintain standards +- Testing rules ensure consistency +- Platform rules prevent cross-platform issues +- Anti-pattern rules prevent common mistakes + +**Format Validation:** + +- Content is lean and optimized for LLMs +- Structure is logical and scannable +- No redundant or obvious information +- Consistent formatting throughout + +### 7. Completion Message + +Present final completion to user: + +"**Project Context Generation Complete!** + +Your optimized project context file is ready at: +`{output_folder}/project-context.md` + +**Context Summary:** + +- {{rule_count}} critical rules for AI agents +- {{section_count}} comprehensive sections +- Optimized for LLM context efficiency +- Ready for immediate agent integration + +**Key Benefits:** + +- Consistent implementation across all AI agents +- Reduced common mistakes and edge cases +- Clear guidance for project-specific patterns +- Minimal LLM context usage + +**Next Steps:** + +1. AI agents will automatically read this file when implementing +2. Update this file when your technology stack or patterns evolve +3. Review quarterly to optimize and remove outdated rules + +Your project context will help ensure high-quality, consistent game implementation across all development work. Great work capturing your project's critical implementation requirements!" + +## SUCCESS METRICS: + +- Complete project context file with all critical rules +- Content optimized for LLM context efficiency +- All technology versions and patterns documented +- File structure is logical and scannable +- Usage guidelines included for agents and humans +- Frontmatter properly updated with completion status +- User provided with clear next steps and benefits + +## FAILURE MODES: + +- Final content is too verbose for LLM consumption +- Missing critical implementation rules or patterns +- Not optimizing content for agent readability +- Not providing clear usage guidelines +- Frontmatter not properly updated +- Not validating file completion before ending + +## WORKFLOW COMPLETE: + +This is the final step of the Generate Project Context workflow. The user now has a comprehensive, optimized project context file that will ensure consistent, high-quality game implementation across all AI agents working on the project. + +The project context file serves as the critical "rules of the road" that agents need to implement game code consistently with the project's standards and patterns. diff --git a/src/modules/bmgd/workflows/3-technical/generate-project-context/workflow.md b/src/modules/bmgd/workflows/3-technical/generate-project-context/workflow.md new file mode 100644 index 00000000..8eb8945c --- /dev/null +++ b/src/modules/bmgd/workflows/3-technical/generate-project-context/workflow.md @@ -0,0 +1,48 @@ +--- +name: generate-project-context +description: Creates a concise project-context.md file with critical rules and patterns that AI agents must follow when implementing game code. Optimized for LLM context efficiency. +--- + +# Generate Project Context Workflow + +**Goal:** Create a concise, optimized `project-context.md` file containing critical rules, patterns, and guidelines that AI agents must follow when implementing game code. This file focuses on unobvious details that LLMs need to be reminded of. + +**Your Role:** You are a technical facilitator working with a peer to capture the essential implementation rules that will ensure consistent, high-quality game code generation across all AI agents working on the project. + +--- + +## WORKFLOW ARCHITECTURE + +This uses **micro-file architecture** for disciplined execution: + +- Each step is a self-contained file with embedded rules +- Sequential progression with user control at each step +- Document state tracked in frontmatter +- Focus on lean, LLM-optimized content generation +- You NEVER proceed to a step file if the current step file indicates the user must approve and indicate continuation. + +--- + +## INITIALIZATION + +### Configuration Loading + +Load config from `{project-root}/_bmad/bmgd/config.yaml` and resolve: + +- `project_name`, `output_folder`, `user_name` +- `communication_language`, `document_output_language`, `game_dev_experience` +- `date` as system-generated current datetime + +### Paths + +- `installed_path` = `{project-root}/_bmad/bmgd/workflows/3-technical/generate-project-context` +- `template_path` = `{installed_path}/project-context-template.md` +- `output_file` = `{output_folder}/project-context.md` + +--- + +## EXECUTION + +Load and execute `steps/step-01-discover.md` to begin the workflow. + +**Note:** Input document discovery and initialization protocols are handled in step-01-discover.md.