From 1af87338ae60c4de1cd36d2b15c185e5d0e7bb8b Mon Sep 17 00:00:00 2001 From: Ziyu Huang Date: Fri, 28 Nov 2025 06:45:38 +0800 Subject: [PATCH] Fix #990: Enable custom module discovery in installer - Fix 'installedModules is not defined' error in compileAgents() - Add manifest regeneration during compilation to discover custom content - Change manifest generator to scan all directories dynamically - Fix ManifestGenerator import to use destructuring - Handle custom modules without installer source gracefully This enables custom workflows/agents/tasks in .bmad/custom/ to be automatically discovered and generate proper .claude/commands/ files. Changes: - installer.js: Add manifest regeneration, fix imports, handle custom modules - manifest-generator.js: Scan all directories instead of hardcoded list Fixes #990 --- tools/cli/installers/lib/core/installer.js | 31 ++++++++-- .../installers/lib/core/manifest-generator.js | 58 +++++++++++++------ 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 6e6fbab9..aec9a7c7 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1787,7 +1787,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; } @@ -1812,9 +1822,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)) { @@ -1824,6 +1841,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 1dbb8ea6..a38ef864 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); } } @@ -175,23 +179,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 }); @@ -283,18 +296,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); } }