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
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...');

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
*/
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);
}
}