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
This commit is contained in:
Ziyu Huang 2025-11-28 06:45:38 +08:00
parent 355ccebca2
commit 1af87338ae
2 changed files with 67 additions and 22 deletions

View File

@ -1787,7 +1787,17 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Rebuild module agents from installer source // Rebuild module agents from installer source
const agentsPath = path.join(modulePath, 'agents'); const agentsPath = path.join(modulePath, 'agents');
if (await fs.pathExists(agentsPath)) { 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); const agentFiles = await fs.readdir(agentsPath);
agentCount += agentFiles.filter((f) => f.endsWith('.md')).length; 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'); spinner.succeed('No custom agents found to rebuild');
} }
// Skip full manifest regeneration during compileAgents to preserve custom agents // Detect installed modules for manifest regeneration and IDE configuration
// Custom agents are already added to manifests during individual installation spinner.start('Regenerating manifests...');
// Only regenerate YAML manifest for IDE updates if needed 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'); const existingManifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
let existingIdes = []; let existingIdes = [];
if (await fs.pathExists(existingManifestPath)) { 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 || []; 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 // Update IDE configurations using the existing IDE list from manifest
if (existingIdes && existingIdes.length > 0) { if (existingIdes && existingIdes.length > 0) {
spinner.start('Updating IDE configurations...'); spinner.start('Updating IDE configurations...');

View File

@ -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 * Scans the INSTALLED bmad directory, not the source
*/ */
async collectWorkflows(selectedModules) { async collectWorkflows(selectedModules) {
this.workflows = []; this.workflows = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Scan all directories under bmad installation
for (const moduleName of this.updatedModules) { const entries = await fs.readdir(this.bmadDir, { withFileTypes: true });
const modulePath = path.join(this.bmadDir, moduleName);
if (await fs.pathExists(modulePath)) { for (const entry of entries) {
const moduleWorkflows = await this.getWorkflowsFromPath(modulePath, moduleName); // Skip special directories that don't contain modules
this.workflows.push(...moduleWorkflows); 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 * Scans the INSTALLED bmad directory, not the source
*/ */
async collectAgents(selectedModules) { async collectAgents(selectedModules) {
this.agents = []; this.agents = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Scan all directories under bmad installation
for (const moduleName of this.updatedModules) { const entries = await fs.readdir(this.bmadDir, { withFileTypes: true });
const agentsPath = path.join(this.bmadDir, moduleName, 'agents');
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)) { if (await fs.pathExists(agentsPath)) {
const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName); const moduleAgents = await this.getAgentsFromDir(agentsPath, entry.name);
this.agents.push(...moduleAgents); 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'); const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
if (await fs.pathExists(standaloneAgentsDir)) { if (await fs.pathExists(standaloneAgentsDir)) {
const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true }); 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 * Scans the INSTALLED bmad directory, not the source
*/ */
async collectTasks(selectedModules) { async collectTasks(selectedModules) {
this.tasks = []; this.tasks = [];
// Use updatedModules which already includes deduplicated 'core' + selectedModules // Scan all directories under bmad installation
for (const moduleName of this.updatedModules) { const entries = await fs.readdir(this.bmadDir, { withFileTypes: true });
const tasksPath = path.join(this.bmadDir, moduleName, 'tasks');
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)) { if (await fs.pathExists(tasksPath)) {
const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName); const moduleTasks = await this.getTasksFromDir(tasksPath, entry.name);
this.tasks.push(...moduleTasks); this.tasks.push(...moduleTasks);
} }
} }