Compare commits

...

3 Commits

Author SHA1 Message Date
Ziyu Huang 0c6f47ce82
Merge e9b75bd3cb into fe0817f590 2025-12-01 07:33:48 +01:00
Ziyu Huang e9b75bd3cb Enhance build command to scan all .bmad/ modules
- Update buildAllAgents() to scan all directories in .bmad/
- Update checkBuildStatus() to check all modules
- Update listAvailableAgents() to list all modules
- Auto-detect .bmad or bmad folder name
- Skip special directories (_cfg, docs)

This makes 'bmad build --all' discover custom modules (custom/, hde/, etc.)
and builds their agents automatically, matching the behavior of the fixed
install/compile workflow.

Related to #990
2025-11-28 07:06:09 +08:00
Ziyu Huang 1af87338ae 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
2025-11-28 06:45:38 +08:00
3 changed files with 210 additions and 29 deletions

View File

@ -166,8 +166,66 @@ async function buildAllAgents(projectDir, force = false) {
let builtCount = 0; let builtCount = 0;
let skippedCount = 0; let skippedCount = 0;
// First, build standalone agents in bmad/agents/ // Detect .bmad folder name (could be .bmad or bmad)
const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); 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)) { if (await fs.pathExists(standaloneAgentsDir)) {
console.log(chalk.cyan('\nBuilding standalone agents...')); console.log(chalk.cyan('\nBuilding standalone agents...'));
const agentDirs = await fs.readdir(standaloneAgentsDir); const agentDirs = await fs.readdir(standaloneAgentsDir);
@ -205,7 +263,7 @@ async function buildAllAgents(projectDir, force = false) {
console.log(chalk.cyan(` Building standalone agent ${agentName}...`)); 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); const customizeExists = await fs.pathExists(customizePath);
await builder.buildAgent(agentYamlPath, customizeExists ? customizePath : null, outputPath, { includeMetadata: true }); await builder.buildAgent(agentYamlPath, customizeExists ? customizePath : null, outputPath, { includeMetadata: true });
@ -275,8 +333,52 @@ async function checkBuildStatus(projectDir) {
const needsRebuild = []; const needsRebuild = [];
const upToDate = []; const upToDate = [];
// Check standalone agents in bmad/agents/ // Detect .bmad folder name (could be .bmad or bmad)
const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); 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)) { if (await fs.pathExists(standaloneAgentsDir)) {
const agentDirs = await fs.readdir(standaloneAgentsDir); const agentDirs = await fs.readdir(standaloneAgentsDir);
@ -406,8 +508,42 @@ async function checkIfNeedsRebuild(yamlPath, outputPath, projectDir, agentName)
* List available agents * List available agents
*/ */
async function listAvailableAgents(projectDir) { async function listAvailableAgents(projectDir) {
// List standalone agents first // Detect .bmad folder name (could be .bmad or bmad)
const standaloneAgentsDir = path.join(projectDir, 'bmad', 'agents'); 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)) { if (await fs.pathExists(standaloneAgentsDir)) {
console.log(chalk.dim(' Standalone agents:')); console.log(chalk.dim(' Standalone agents:'));
const agentDirs = await fs.readdir(standaloneAgentsDir); const agentDirs = await fs.readdir(standaloneAgentsDir);

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