diff --git a/tools/cli/commands/build.js b/tools/cli/commands/build.js index ec5c6dec..54d24c9a 100644 --- a/tools/cli/commands/build.js +++ b/tools/cli/commands/build.js @@ -166,8 +166,66 @@ async function buildAllAgents(projectDir, force = false) { let builtCount = 0; let skippedCount = 0; - // First, build standalone agents in bmad/agents/ - const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); + // Detect .bmad folder name (could be .bmad or bmad) + const bmadFolder = (await fs.pathExists(path.join(projectDir, '.bmad'))) ? '.bmad' : 'bmad'; + const bmadDir = path.join(projectDir, bmadFolder); + + // Build agents from ALL module directories in .bmad/ (including custom, hde, etc.) + if (await fs.pathExists(bmadDir)) { + console.log(chalk.cyan('\nScanning all modules in .bmad/...')); + const moduleEntries = await fs.readdir(bmadDir, { withFileTypes: true }); + + for (const moduleEntry of moduleEntries) { + // Skip special directories + if (!moduleEntry.isDirectory() || moduleEntry.name === '_cfg' || moduleEntry.name === 'docs') { + continue; + } + + const modulePath = path.join(bmadDir, moduleEntry.name); + const agentsPath = path.join(modulePath, 'agents'); + + // Check if this module has an agents/ directory + if (!(await fs.pathExists(agentsPath))) { + continue; + } + + console.log(chalk.cyan(`\nBuilding agents in ${moduleEntry.name} module...`)); + const agentFiles = await fs.readdir(agentsPath); + + for (const file of agentFiles) { + if (!file.endsWith('.agent.yaml')) { + continue; + } + + const agentName = file.replace('.agent.yaml', ''); + const agentYamlPath = path.join(agentsPath, file); + const outputPath = path.join(agentsPath, `${agentName}.md`); + + // Check if rebuild needed + if (!force && (await fs.pathExists(outputPath))) { + const needsRebuild = await checkIfNeedsRebuild(agentYamlPath, outputPath, projectDir, agentName); + if (!needsRebuild) { + console.log(chalk.dim(` ${agentName}: up to date`)); + skippedCount++; + continue; + } + } + + console.log(chalk.cyan(` Building ${agentName}...`)); + + const customizePath = path.join(bmadDir, '_cfg', 'agents', `${moduleEntry.name}-${agentName}.customize.yaml`); + const customizeExists = await fs.pathExists(customizePath); + + await builder.buildAgent(agentYamlPath, customizeExists ? customizePath : null, outputPath, { includeMetadata: true }); + + console.log(chalk.green(` ✓ ${agentName} (${moduleEntry.name})`)); + builtCount++; + } + } + } + + // Also build standalone agents in bmad/agents/ (top-level, for backward compatibility) + const standaloneAgentsDir = path.join(projectDir, bmadFolder, 'agents'); if (await fs.pathExists(standaloneAgentsDir)) { console.log(chalk.cyan('\nBuilding standalone agents...')); const agentDirs = await fs.readdir(standaloneAgentsDir); @@ -205,7 +263,7 @@ async function buildAllAgents(projectDir, force = false) { console.log(chalk.cyan(` Building standalone agent ${agentName}...`)); - const customizePath = path.join(projectDir, 'bmad', '_cfg', 'agents', `${agentName}.customize.yaml`); + const customizePath = path.join(projectDir, bmadFolder, '_cfg', 'agents', `${agentName}.customize.yaml`); const customizeExists = await fs.pathExists(customizePath); await builder.buildAgent(agentYamlPath, customizeExists ? customizePath : null, outputPath, { includeMetadata: true }); @@ -275,8 +333,52 @@ async function checkBuildStatus(projectDir) { const needsRebuild = []; const upToDate = []; - // Check standalone agents in bmad/agents/ - const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); + // Detect .bmad folder name (could be .bmad or bmad) + const bmadFolder = (await fs.pathExists(path.join(projectDir, '.bmad'))) ? '.bmad' : 'bmad'; + const bmadDir = path.join(projectDir, bmadFolder); + + // Check agents in ALL module directories in .bmad/ + if (await fs.pathExists(bmadDir)) { + const moduleEntries = await fs.readdir(bmadDir, { withFileTypes: true }); + + for (const moduleEntry of moduleEntries) { + // Skip special directories + if (!moduleEntry.isDirectory() || moduleEntry.name === '_cfg' || moduleEntry.name === 'docs') { + continue; + } + + const modulePath = path.join(bmadDir, moduleEntry.name); + const agentsPath = path.join(modulePath, 'agents'); + + // Check if this module has an agents/ directory + if (!(await fs.pathExists(agentsPath))) { + continue; + } + + const agentFiles = await fs.readdir(agentsPath); + + for (const file of agentFiles) { + if (!file.endsWith('.agent.yaml')) { + continue; + } + + const agentName = file.replace('.agent.yaml', ''); + const agentYamlPath = path.join(agentsPath, file); + const outputPath = path.join(agentsPath, `${agentName}.md`); + + if (!(await fs.pathExists(outputPath))) { + needsRebuild.push(`${agentName} (${moduleEntry.name})`); + } else if (await checkIfNeedsRebuild(agentYamlPath, outputPath, projectDir, agentName)) { + needsRebuild.push(`${agentName} (${moduleEntry.name})`); + } else { + upToDate.push(`${agentName} (${moduleEntry.name})`); + } + } + } + } + + // Check standalone agents in bmad/agents/ (top-level) + const standaloneAgentsDir = path.join(projectDir, bmadFolder, 'agents'); if (await fs.pathExists(standaloneAgentsDir)) { const agentDirs = await fs.readdir(standaloneAgentsDir); @@ -406,8 +508,42 @@ async function checkIfNeedsRebuild(yamlPath, outputPath, projectDir, agentName) * List available agents */ async function listAvailableAgents(projectDir) { - // List standalone agents first - const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); + // Detect .bmad folder name (could be .bmad or bmad) + const bmadFolder = (await fs.pathExists(path.join(projectDir, '.bmad'))) ? '.bmad' : 'bmad'; + const bmadDir = path.join(projectDir, bmadFolder); + + // List agents from ALL module directories in .bmad/ + if (await fs.pathExists(bmadDir)) { + console.log(chalk.dim(' Module agents:')); + const moduleEntries = await fs.readdir(bmadDir, { withFileTypes: true }); + + for (const moduleEntry of moduleEntries) { + // Skip special directories + if (!moduleEntry.isDirectory() || moduleEntry.name === '_cfg' || moduleEntry.name === 'docs') { + continue; + } + + const modulePath = path.join(bmadDir, moduleEntry.name); + const agentsPath = path.join(modulePath, 'agents'); + + // Check if this module has an agents/ directory + if (!(await fs.pathExists(agentsPath))) { + continue; + } + + const agentFiles = await fs.readdir(agentsPath); + + for (const file of agentFiles) { + if (file.endsWith('.agent.yaml')) { + const agentName = file.replace('.agent.yaml', ''); + console.log(chalk.dim(` - ${agentName} (${moduleEntry.name})`)); + } + } + } + } + + // List standalone agents + const standaloneAgentsDir = path.join(projectDir, bmadFolder, 'agents'); if (await fs.pathExists(standaloneAgentsDir)) { console.log(chalk.dim(' Standalone agents:')); const agentDirs = await fs.readdir(standaloneAgentsDir); diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index c43a197b..db5ee46e 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1792,7 +1792,17 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: // Rebuild module agents from installer source const agentsPath = path.join(modulePath, 'agents'); if (await fs.pathExists(agentsPath)) { - await this.rebuildAgentFiles(modulePath, entry.name); + // Check if this module has source in the installer + const sourceAgentsPath = + entry.name === 'core' + ? path.join(getModulePath('core'), 'agents') + : path.join(getSourcePath(`modules/${entry.name}`), 'agents'); + + // Only rebuild if source exists in installer, otherwise skip (for custom modules) + if (await fs.pathExists(sourceAgentsPath)) { + await this.rebuildAgentFiles(modulePath, entry.name); + } + const agentFiles = await fs.readdir(agentsPath); agentCount += agentFiles.filter((f) => f.endsWith('.md')).length; } @@ -1817,9 +1827,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: spinner.succeed('No custom agents found to rebuild'); } - // Skip full manifest regeneration during compileAgents to preserve custom agents - // Custom agents are already added to manifests during individual installation - // Only regenerate YAML manifest for IDE updates if needed + // Detect installed modules for manifest regeneration and IDE configuration + spinner.start('Regenerating manifests...'); + const existingInstall = await this.detector.detect(bmadDir); + const installedModules = existingInstall.modules.map((m) => m.id); + + // Regenerate manifests to include all discovered content (including custom) + const { ManifestGenerator } = require('./manifest-generator'); + const manifestGen = new ManifestGenerator(); + + // Get existing IDE list from current manifest const existingManifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml'); let existingIdes = []; if (await fs.pathExists(existingManifestPath)) { @@ -1829,6 +1846,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice: existingIdes = manifest.ides || []; } + await manifestGen.generateManifests(bmadDir, installedModules, [], { + ides: existingIdes, + preservedModules: [], + }); + spinner.succeed('Manifests regenerated'); + // Update IDE configurations using the existing IDE list from manifest if (existingIdes && existingIdes.length > 0) { spinner.start('Updating IDE configurations...'); diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 644fd494..1020bea6 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -87,20 +87,24 @@ class ManifestGenerator { } /** - * Collect all workflows from core and selected modules + * Collect all workflows from ALL directories in bmad installation * Scans the INSTALLED bmad directory, not the source */ async collectWorkflows(selectedModules) { this.workflows = []; - // Use updatedModules which already includes deduplicated 'core' + selectedModules - for (const moduleName of this.updatedModules) { - const modulePath = path.join(this.bmadDir, moduleName); + // Scan all directories under bmad installation + const entries = await fs.readdir(this.bmadDir, { withFileTypes: true }); - if (await fs.pathExists(modulePath)) { - const moduleWorkflows = await this.getWorkflowsFromPath(modulePath, moduleName); - this.workflows.push(...moduleWorkflows); + for (const entry of entries) { + // Skip special directories that don't contain modules + if (!entry.isDirectory() || entry.name === '_cfg' || entry.name === 'docs') { + continue; } + + const modulePath = path.join(this.bmadDir, entry.name); + const moduleWorkflows = await this.getWorkflowsFromPath(modulePath, entry.name); + this.workflows.push(...moduleWorkflows); } } @@ -184,23 +188,32 @@ class ManifestGenerator { } /** - * Collect all agents from core and selected modules + * Collect all agents from ALL directories in bmad installation * Scans the INSTALLED bmad directory, not the source */ async collectAgents(selectedModules) { this.agents = []; - // Use updatedModules which already includes deduplicated 'core' + selectedModules - for (const moduleName of this.updatedModules) { - const agentsPath = path.join(this.bmadDir, moduleName, 'agents'); + // Scan all directories under bmad installation + const entries = await fs.readdir(this.bmadDir, { withFileTypes: true }); + for (const entry of entries) { + // Skip special directories that don't contain modules + if (!entry.isDirectory() || entry.name === '_cfg' || entry.name === 'docs') { + continue; + } + + const modulePath = path.join(this.bmadDir, entry.name); + + // Check for agents/ subdirectory in this module + const agentsPath = path.join(modulePath, 'agents'); if (await fs.pathExists(agentsPath)) { - const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName); + const moduleAgents = await this.getAgentsFromDir(agentsPath, entry.name); this.agents.push(...moduleAgents); } } - // Get standalone agents from bmad/agents/ directory + // Also check for standalone agents in bmad/agents/ directory (top-level) const standaloneAgentsDir = path.join(this.bmadDir, 'agents'); if (await fs.pathExists(standaloneAgentsDir)) { const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true }); @@ -292,18 +305,27 @@ class ManifestGenerator { } /** - * Collect all tasks from core and selected modules + * Collect all tasks from ALL directories in bmad installation * Scans the INSTALLED bmad directory, not the source */ async collectTasks(selectedModules) { this.tasks = []; - // Use updatedModules which already includes deduplicated 'core' + selectedModules - for (const moduleName of this.updatedModules) { - const tasksPath = path.join(this.bmadDir, moduleName, 'tasks'); + // Scan all directories under bmad installation + const entries = await fs.readdir(this.bmadDir, { withFileTypes: true }); + for (const entry of entries) { + // Skip special directories that don't contain modules + if (!entry.isDirectory() || entry.name === '_cfg' || entry.name === 'docs') { + continue; + } + + const modulePath = path.join(this.bmadDir, entry.name); + + // Check for tasks/ subdirectory in this module + const tasksPath = path.join(modulePath, 'tasks'); if (await fs.pathExists(tasksPath)) { - const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName); + const moduleTasks = await this.getTasksFromDir(tasksPath, entry.name); this.tasks.push(...moduleTasks); } }