folder rename from .bmad to _bmad

This commit is contained in:
Brian Madison
2025-12-13 16:22:34 +08:00
parent 0c873638ab
commit 25c79e3fe5
375 changed files with 1421 additions and 2745 deletions

View File

@@ -135,7 +135,7 @@ class Detector {
}
/**
* Detect legacy installation (.bmad-method, .bmm, .cis)
* Detect legacy installation (_bmad-method, .bmm, .cis)
* @param {string} projectDir - Project directory to check
* @returns {Object} Legacy installation details
*/
@@ -147,8 +147,8 @@ class Detector {
paths: [],
};
// Check for legacy core (.bmad-method)
const legacyCorePath = path.join(projectDir, '.bmad-method');
// Check for legacy core (_bmad-method)
const legacyCorePath = path.join(projectDir, '_bmad-method');
if (await fs.pathExists(legacyCorePath)) {
result.hasLegacy = true;
result.legacyCore = true;
@@ -161,7 +161,7 @@ class Detector {
if (
entry.isDirectory() &&
entry.name.startsWith('.') &&
entry.name !== '.bmad-method' &&
entry.name !== '_bmad-method' &&
!entry.name.startsWith('.git') &&
!entry.name.startsWith('.vscode') &&
!entry.name.startsWith('.idea')
@@ -204,7 +204,7 @@ class Detector {
/**
* Detect legacy BMAD v4 footprints (case-sensitive path checks)
* V4 used .bmad-method as default folder name
* V4 used _bmad-method as default folder name
* V6+ uses configurable folder names and ALWAYS has _cfg/manifest.yaml with installation.version
* @param {string} projectDir - Project directory to check
* @returns {{ hasLegacyV4: boolean, offenders: string[] }}

View File

@@ -1,23 +1,3 @@
/**
* File: tools/cli/installers/lib/core/installer.js
*
* BMAD Method - Business Model Agile Development Method
* Repository: https://github.com/paulpreibisch/BMAD-METHOD
*
* Copyright (c) 2025 Paul Preibisch
* Licensed under the Apache License, Version 2.0
*
* ---
*
* @fileoverview Core BMAD installation orchestrator with AgentVibes injection point support
* @context Manages complete BMAD installation flow including core agents, modules, IDE configs, and optional TTS integration
* @architecture Orchestrator pattern - coordinates Detector, ModuleManager, IdeManager, and file operations to build complete BMAD installation
* @dependencies fs-extra, ora, chalk, detector.js, module-manager.js, ide-manager.js, config.js
* @entrypoints Called by install.js command via installer.install(config)
* @patterns Injection point processing (AgentVibes), placeholder replacement (.bmad), module dependency resolution
* @related GitHub AgentVibes#34 (injection points), ui.js (user prompts), copyFileWithPlaceholderReplacement()
*/
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
@@ -32,13 +12,11 @@ const { Config } = require('../../../lib/config');
const { XmlHandler } = require('../../../lib/xml-handler');
const { DependencyResolver } = require('./dependency-resolver');
const { ConfigCollector } = require('./config-collector');
// processInstallation no longer needed - LLMs understand {project-root}
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
const { CLIUtils } = require('../../../lib/cli-utils');
const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager');
const { replaceAgentSidecarFolders } = require('./post-install-sidecar-replacement');
const { CustomHandler } = require('../custom/handler');
class Installer {
@@ -67,7 +45,7 @@ class Installer {
// Check if project directory exists
if (!(await fs.pathExists(projectDir))) {
// Project doesn't exist yet, return default
return path.join(projectDir, '.bmad');
return path.join(projectDir, '_bmad');
}
// V6+ strategy: Look for ANY directory with _cfg/manifest.yaml
@@ -89,13 +67,13 @@ class Installer {
// No V6+ installation found, return default
// This will be used for new installations
return path.join(projectDir, '.bmad');
return path.join(projectDir, '_bmad');
}
/**
* @function copyFileWithPlaceholderReplacement
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
* @why Enables installation-time customization: .bmad replacement + optional AgentVibes TTS injection
* @why Enables installation-time customization: _bmad replacement + optional AgentVibes TTS injection
* @param {string} sourcePath - Absolute path to source file in BMAD repository
* @param {string} targetPath - Absolute path to destination file in user's project
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
@@ -472,8 +450,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
});
}
// Always use .bmad as the folder name
const bmadFolderName = '.bmad';
// Always use _bmad as the folder name
const bmadFolderName = '_bmad';
this.bmadFolderName = bmadFolderName; // Store for use in other methods
// Store AgentVibes configuration for injection point processing
@@ -602,7 +580,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// If there are custom files, back them up temporarily
if (customFiles.length > 0) {
const tempBackupDir = path.join(projectDir, '.bmad-custom-backup-temp');
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
await fs.ensureDir(tempBackupDir);
spinner.start(`Backing up ${customFiles.length} custom files...`);
@@ -619,7 +597,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// For modified files, back them up to temp directory (will be restored as .bak files after install)
if (modifiedFiles.length > 0) {
const tempModifiedBackupDir = path.join(projectDir, '.bmad-modified-backup-temp');
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
await fs.ensureDir(tempModifiedBackupDir);
console.log(chalk.yellow(`\nDEBUG: Backing up ${modifiedFiles.length} modified files to temp location`));
@@ -653,7 +631,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Back up custom files
if (customFiles.length > 0) {
const tempBackupDir = path.join(projectDir, '.bmad-custom-backup-temp');
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
await fs.ensureDir(tempBackupDir);
spinner.start(`Backing up ${customFiles.length} custom files...`);
@@ -669,7 +647,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Back up modified files
if (modifiedFiles.length > 0) {
const tempModifiedBackupDir = path.join(projectDir, '.bmad-modified-backup-temp');
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
await fs.ensureDir(tempModifiedBackupDir);
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
@@ -1316,29 +1294,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
console.log(chalk.dim('Review the .bak files to see your changes and merge if needed.\n'));
}
// Reinstall custom agents from _cfg/custom/agents/ sources
const customAgentResults = await this.reinstallCustomAgents(projectDir, bmadDir);
if (customAgentResults.count > 0) {
console.log(chalk.green(`\n✓ Reinstalled ${customAgentResults.count} custom agent${customAgentResults.count > 1 ? 's' : ''}`));
for (const agent of customAgentResults.agents) {
console.log(chalk.dim(` - ${agent}`));
}
}
// Replace {agent_sidecar_folder} placeholders in all agent files
console.log(chalk.dim('\n Configuring agent sidecar folders...'));
const sidecarResults = await replaceAgentSidecarFolders(bmadDir);
if (sidecarResults.filesReplaced > 0) {
console.log(
chalk.green(
` ✓ Updated ${sidecarResults.filesReplaced} agent file(s) with ${sidecarResults.totalReplacements} sidecar reference(s)`,
),
);
} else {
console.log(chalk.dim(' No agent sidecar references found'));
}
// Display completion message
const { UI } = require('../../../lib/ui');
const ui = new UI();
@@ -1852,7 +1807,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Create customize template if it doesn't exist
if (!(await fs.pathExists(customizePath))) {
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
@@ -1868,8 +1823,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Replace .bmad with actual folder name
xmlContent = xmlContent.replaceAll('.bmad', this.bmadFolderName || 'bmad');
// Replace _bmad with actual folder name
xmlContent = xmlContent.replaceAll('_bmad', this.bmadFolderName || 'bmad');
// Replace {agent_sidecar_folder} if configured
const coreConfig = this.configCollector.collectedConfig.core || {};
@@ -1916,7 +1871,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Resolve path variables
const resolvedSidecarFolder = agentSidecarFolder
.replaceAll('{project-root}', projectDir)
.replaceAll('.bmad', this.bmadFolderName || 'bmad');
.replaceAll('_bmad', this.bmadFolderName || 'bmad');
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
@@ -1942,20 +1897,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
console.log(chalk.dim(` Built agent: ${agentName}.md${hasSidecar ? ' (with sidecar)' : ''}`));
}
// Handle legacy .md agents - inject activation if needed
else if (agentFile.endsWith('.md')) {
const agentPath = path.join(agentsPath, agentFile);
let content = await fs.readFile(agentPath, 'utf8');
// Check if content has agent XML and no activation block
if (content.includes('<agent') && !content.includes('<activation')) {
// Inject the activation block using XML handler
content = this.xmlHandler.injectActivationSimple(content);
// Ensure POSIX-compliant final newline
const finalContent = content.endsWith('\n') ? content : content + '\n';
await fs.writeFile(agentPath, finalContent, 'utf8');
}
}
}
}
@@ -2169,23 +2110,18 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
* @returns {Object} Compilation results
*/
async compileAgents(config) {
const ora = require('ora');
const spinner = ora('Starting agent compilation...').start();
try {
const projectDir = path.resolve(config.directory);
const bmadDir = await this.findBmadDir(projectDir);
// Check if bmad directory exists
if (!(await fs.pathExists(bmadDir))) {
spinner.fail('No BMAD installation found');
throw new Error(`BMAD not installed at ${bmadDir}`);
}
// Check for custom modules with missing sources
const manifest = await this.manifest.read(bmadDir);
if (manifest && manifest.customModules && manifest.customModules.length > 0) {
spinner.stop();
console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
const customModuleSources = new Map();
@@ -2196,15 +2132,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const projectRoot = getProjectRoot();
const installedModules = manifest.modules || [];
await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
spinner.start('Rebuilding agent files...');
}
let agentCount = 0;
let taskCount = 0;
// Process all modules in bmad directory
spinner.text = 'Rebuilding agent files...';
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
@@ -2213,7 +2146,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Special handling for standalone agents in bmad/agents/ directory
if (entry.name === 'agents') {
spinner.text = 'Building standalone agents...';
await this.buildStandaloneAgents(bmadDir, projectDir);
// Count standalone agents
@@ -2245,16 +2177,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Reinstall custom agents from _cfg/custom/agents/ sources
spinner.start('Rebuilding custom agents...');
const customAgentResults = await this.reinstallCustomAgents(projectDir, bmadDir);
if (customAgentResults.count > 0) {
spinner.succeed(`Rebuilt ${customAgentResults.count} custom agent${customAgentResults.count > 1 ? 's' : ''}`);
agentCount += customAgentResults.count;
} else {
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
@@ -2269,36 +2191,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Update IDE configurations using the existing IDE list from manifest
if (existingIdes && existingIdes.length > 0) {
spinner.start('Updating IDE configurations...');
for (const ide of existingIdes) {
spinner.text = `Updating ${ide}...`;
// Stop spinner before IDE setup to prevent blocking any potential prompts
// However, we pass _alreadyConfigured to skip all prompts during compile
spinner.stop();
await this.ideManager.setup(ide, projectDir, bmadDir, {
selectedModules: installedModules,
skipModuleInstall: true, // Skip module installation, just update IDE files
verbose: config.verbose,
preCollectedConfig: { _alreadyConfigured: true }, // Skip all interactive prompts during compile
});
// Restart spinner for next IDE
if (existingIdes.indexOf(ide) < existingIdes.length - 1) {
spinner.start('Updating IDE configurations...');
}
}
console.log(chalk.green('✓ IDE configurations updated'));
} else {
console.log(chalk.yellow('⚠️ No IDEs configured. Skipping IDE update.'));
}
return { agentCount, taskCount };
} catch (error) {
spinner.fail('Compilation failed');
throw error;
}
}
@@ -2610,19 +2516,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
lastModified: new Date().toISOString(),
};
const existingBmadFolderName = path.basename(bmadDir);
const newBmadFolderName = this.configCollector.collectedConfig.core?.bmad_folder || existingBmadFolderName;
if (existingBmadFolderName === newBmadFolderName) {
// Normal quick update - start the spinner
console.log(chalk.cyan('Updating BMAD installation...'));
} else {
// Folder name has changed - stop spinner and let install() handle it
spinner.stop();
console.log(chalk.yellow(`\n⚠️ Folder name will change: ${existingBmadFolderName}${newBmadFolderName}`));
console.log(chalk.yellow('The installer will handle the folder migration.\n'));
}
// Build the config object for the installer
const installConfig = {
directory: projectDir,
@@ -2690,14 +2583,14 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
console.log(chalk.yellow.bold('\n⚠ Legacy BMAD v4 detected'));
console.log(chalk.dim('The installer found legacy artefacts in your project.\n'));
// Separate .bmad* folders (auto-backup) from other offending paths (manual cleanup)
// Separate _bmad* folders (auto-backup) from other offending paths (manual cleanup)
const bmadFolders = legacyV4.offenders.filter((p) => {
const name = path.basename(p);
return name.startsWith('.bmad'); // Only dot-prefixed folders get auto-backed up
return name.startsWith('_bmad'); // Only dot-prefixed folders get auto-backed up
});
const otherOffenders = legacyV4.offenders.filter((p) => {
const name = path.basename(p);
return !name.startsWith('.bmad'); // Everything else is manual cleanup
return !name.startsWith('_bmad'); // Everything else is manual cleanup
});
const inquirer = require('inquirer');
@@ -2730,7 +2623,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Handle .bmad* folders with automatic backup
// Handle _bmad* folders with automatic backup
if (bmadFolders.length > 0) {
console.log(chalk.cyan('The following legacy folders will be moved to v4-backup:'));
for (const p of bmadFolders) console.log(chalk.dim(` - ${p}`));
@@ -2846,7 +2739,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (fileEntry.path) {
// Paths are relative to bmadDir. Legacy manifests incorrectly prefixed 'bmad/' -
// strip it if present. This is safe because no real path inside bmadDir would
// start with 'bmad/' (you'd never have .bmad/bmad/... as an actual structure).
// start with 'bmad/' (you'd never have _bmad/bmad/... as an actual structure).
const relativePath = fileEntry.path.startsWith('bmad/') ? fileEntry.path.slice(5) : fileEntry.path;
const absolutePath = path.join(bmadDir, relativePath);
installedFilesMap.set(path.normalize(absolutePath), {
@@ -3099,165 +2992,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
return nodes;
}
/**
* Reinstall custom agents from backup and source locations
* This preserves custom agents across quick updates/reinstalls
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @returns {Object} Result with count and agent names
*/
async reinstallCustomAgents(projectDir, bmadDir) {
const {
discoverAgents,
loadAgentConfig,
extractManifestData,
addToManifest,
createIdeSlashCommands,
updateManifestYaml,
} = require('../../../lib/agent/installer');
const { compileAgent } = require('../../../lib/agent/compiler');
const results = { count: 0, agents: [] };
// Check multiple locations for custom agents
const sourceLocations = [
path.join(bmadDir, '_cfg', 'custom', 'agents'), // Backup location
path.join(bmadDir, 'custom', 'src', 'agents'), // BMAD folder source location
path.join(projectDir, 'custom', 'src', 'agents'), // Project root source location
];
let foundAgents = [];
let processedAgents = new Set(); // Track to avoid duplicates
// Discover agents from all locations
for (const location of sourceLocations) {
if (await fs.pathExists(location)) {
const agents = discoverAgents(location);
// Only add agents we haven't processed yet
const newAgents = agents.filter((agent) => !processedAgents.has(agent.name));
foundAgents.push(...newAgents);
for (const agent of newAgents) processedAgents.add(agent.name);
}
}
if (foundAgents.length === 0) {
return results;
}
try {
const customAgentsDir = path.join(bmadDir, 'custom', 'agents');
await fs.ensureDir(customAgentsDir);
const manifestFile = path.join(bmadDir, '_cfg', 'agent-manifest.csv');
const manifestYamlFile = path.join(bmadDir, '_cfg', 'manifest.yaml');
for (const agent of foundAgents) {
try {
const agentConfig = loadAgentConfig(agent.yamlFile);
const finalAgentName = agent.name; // Already named correctly from save
// Determine agent type from the name (e.g., "fred-commit-poet" → "commit-poet")
let agentType = finalAgentName;
const parts = finalAgentName.split('-');
if (parts.length >= 2) {
// Try to extract type (last part or last two parts)
// For "fred-commit-poet", we want "commit-poet"
// This is heuristic - could be improved with metadata storage
agentType = parts.slice(-2).join('-'); // Take last 2 parts as type
}
// Create target directory - use relative path if agent is in a subdirectory
const agentTargetDir = agent.relativePath
? path.join(customAgentsDir, agent.relativePath)
: path.join(customAgentsDir, finalAgentName);
await fs.ensureDir(agentTargetDir);
// Calculate paths
const compiledFileName = `${finalAgentName}.md`;
const compiledPath = path.join(agentTargetDir, compiledFileName);
const relativePath = path.relative(projectDir, compiledPath);
// Compile with embedded defaults (answers are already in defaults section)
const { xml, metadata } = compileAgent(
await fs.readFile(agent.yamlFile, 'utf8'),
agentConfig.defaults || {},
finalAgentName,
relativePath,
{ config: config.coreConfig },
);
// Write compiled agent
await fs.writeFile(compiledPath, xml, 'utf8');
// Backup source YAML to _cfg/custom/agents if not already there
const cfgAgentsBackupDir = path.join(bmadDir, '_cfg', 'custom', 'agents');
await fs.ensureDir(cfgAgentsBackupDir);
const backupYamlPath = path.join(cfgAgentsBackupDir, `${finalAgentName}.agent.yaml`);
// Only backup if source is not already in backup location
if (agent.yamlFile !== backupYamlPath) {
await fs.copy(agent.yamlFile, backupYamlPath);
}
// Copy sidecar files for agents with hasSidecar flag
if (agentConfig.hasSidecar === true && agent.type === 'expert') {
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
// Get agent sidecar folder from config or use default
const agentSidecarFolder = config.coreConfig?.agent_sidecar_folder;
// Resolve path variables
const resolvedSidecarFolder = agentSidecarFolder.replaceAll('{project-root}', projectDir).replaceAll('.bmad', bmadDir);
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, finalAgentName);
await fs.ensureDir(agentSidecarDir);
// Copy sidecar files (preserve existing, add new)
const sidecarResult = copyAgentSidecarFiles(agent.path, agentSidecarDir, agent.yamlFile);
if (sidecarResult.copied.length > 0 || sidecarResult.preserved.length > 0) {
console.log(chalk.dim(` Sidecar: ${sidecarResult.copied.length} new, ${sidecarResult.preserved.length} preserved`));
}
}
// Update manifest CSV
if (await fs.pathExists(manifestFile)) {
// Preserve YAML metadata for persona name, but override id for filename
const manifestMetadata = {
...metadata,
id: relativePath, // Use the compiled agent path for id
name: metadata.name || finalAgentName, // Use YAML metadata.name (persona name) or fallback
title: metadata.title, // Use YAML title
icon: metadata.icon, // Use YAML icon
};
const manifestData = extractManifestData(xml, manifestMetadata, relativePath, 'custom');
manifestData.name = finalAgentName; // Use filename for the name field
manifestData.path = relativePath;
addToManifest(manifestFile, manifestData);
}
// Create IDE slash commands (async function)
await createIdeSlashCommands(projectDir, finalAgentName, relativePath, metadata);
// Update manifest.yaml
if (await fs.pathExists(manifestYamlFile)) {
updateManifestYaml(manifestYamlFile, finalAgentName, agentType);
}
results.count++;
results.agents.push(finalAgentName);
} catch (agentError) {
console.log(chalk.yellow(` ⚠️ Failed to reinstall ${agent.name}: ${agentError.message}`));
}
}
} catch (error) {
console.log(chalk.yellow(` ⚠️ Error reinstalling custom agents: ${error.message}`));
}
return results;
}
/**
* Copy IDE-specific documentation to BMAD docs
* @param {Array} ides - List of selected IDEs

View File

@@ -23,7 +23,7 @@ class ManifestGenerator {
/**
* Generate all manifests for the installation
* @param {string} bmadDir - .bmad
* @param {string} bmadDir - _bmad
* @param {Array} selectedModules - Selected modules for installation
* @param {Array} installedFiles - All installed files (optional, for hash tracking)
*/
@@ -47,7 +47,7 @@ class ManifestGenerator {
// But all modules should be included in the final manifest
this.preservedModules = [...new Set([...preservedModules, ...selectedModules, ...installedModules])]; // Include all installed modules
this.bmadDir = bmadDir;
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '.bmad' or 'bmad')
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
this.allInstalledFiles = installedFiles;
if (!Object.prototype.hasOwnProperty.call(options, 'ides')) {

View File

@@ -1,79 +0,0 @@
/**
* Post-installation sidecar folder replacement utility
* Replaces {agent_sidecar_folder} placeholders in all installed agents
*/
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('yaml');
const glob = require('glob');
const chalk = require('chalk');
/**
* Replace {agent_sidecar_folder} placeholders in all agent files
* @param {string} bmadDir - Path to .bmad directory
* @returns {Object} Statistics about replacements made
*/
async function replaceAgentSidecarFolders(bmadDir) {
const results = {
filesScanned: 0,
filesReplaced: 0,
totalReplacements: 0,
errors: [],
};
try {
// Load core config to get agent_sidecar_folder value
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
if (!(await fs.pathExists(coreConfigPath))) {
throw new Error(`Core config not found at ${coreConfigPath}`);
}
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
const coreConfig = yaml.parse(coreConfigContent);
const agentSidecarFolder = coreConfig.agent_sidecar_folder;
// Use the literal value from config, don't resolve the placeholders
console.log(chalk.dim(`\n Replacing {agent_sidecar_folder} with: ${agentSidecarFolder}`));
// Find all agent .md files
const agentPattern = path.join(bmadDir, '**/*.md');
const agentFiles = glob.sync(agentPattern);
for (const agentFile of agentFiles) {
results.filesScanned++;
try {
let content = await fs.readFile(agentFile, 'utf8');
// Check if file contains {agent_sidecar_folder}
if (content.includes('{agent_sidecar_folder}')) {
// Replace all occurrences
const originalContent = content;
content = content.replaceAll('{agent_sidecar_folder}', agentSidecarFolder);
// Only write if content changed
if (content !== originalContent) {
await fs.writeFile(agentFile, content, 'utf8');
const replacementCount = (originalContent.match(/{agent_sidecar_folder}/g) || []).length;
results.filesReplaced++;
results.totalReplacements += replacementCount;
console.log(chalk.dim(` ✓ Replaced ${replacementCount} occurrence(s) in ${path.relative(bmadDir, agentFile)}`));
}
}
} catch (error) {
results.errors.push(`Error processing ${agentFile}: ${error.message}`);
}
}
return results;
} catch (error) {
results.errors.push(`Fatal error: ${error.message}`);
return results;
}
}
module.exports = { replaceAgentSidecarFolders };

View File

@@ -316,7 +316,7 @@ class CustomHandler {
// Create customize template if it doesn't exist
if (!(await fs.pathExists(customizePath))) {
const { getSourcePath } = require('../../../lib/project-root');
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
// Copy with placeholder replacement
let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
@@ -355,7 +355,7 @@ class CustomHandler {
const projectDir = path.dirname(bmadDir);
const resolvedSidecarFolder = config.agent_sidecar_folder
.replaceAll('{project-root}', projectDir)
.replaceAll('.bmad', path.basename(bmadDir));
.replaceAll('_bmad', path.basename(bmadDir));
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);

View File

@@ -31,18 +31,11 @@ class BaseIdeSetup {
/**
* Get the agent command activation header from the central template
* @returns {string} The activation header text (without XML tags)
* @returns {string} The activation header text
*/
async getAgentCommandHeader() {
const headerPath = path.join(getSourcePath(), 'src', 'utility', 'models', 'agent-command-header.md');
try {
const content = await fs.readFile(headerPath, 'utf8');
// Strip the <critical> tags to get plain text
return content.replaceAll(/<critical>|<\/critical>/g, '').trim();
} catch {
// Fallback if file doesn't exist
return "You must fully embody this agent's persona and follow all activation instructions, steps and rules exactly as specified. NEVER break character until given an exit command.";
}
const headerPath = path.join(getSourcePath(), 'src', 'utility', 'agent-components', 'agent-command-header.md');
return await fs.readFile(headerPath, 'utf8');
}
/**
@@ -527,26 +520,26 @@ class BaseIdeSetup {
}
/**
* Write file with content (replaces .bmad placeholder)
* Write file with content (replaces _bmad placeholder)
* @param {string} filePath - File path
* @param {string} content - File content
*/
async writeFile(filePath, content) {
// Replace .bmad placeholder if present
if (typeof content === 'string' && content.includes('.bmad')) {
content = content.replaceAll('.bmad', this.bmadFolderName);
// Replace _bmad placeholder if present
if (typeof content === 'string' && content.includes('_bmad')) {
content = content.replaceAll('_bmad', this.bmadFolderName);
}
// Replace escape sequence .bmad with literal .bmad
if (typeof content === 'string' && content.includes('.bmad')) {
content = content.replaceAll('.bmad', '.bmad');
// Replace escape sequence _bmad with literal _bmad
if (typeof content === 'string' && content.includes('_bmad')) {
content = content.replaceAll('_bmad', '_bmad');
}
await this.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content, 'utf8');
}
/**
* Copy file from source to destination (replaces .bmad placeholder in text files)
* Copy file from source to destination (replaces _bmad placeholder in text files)
* @param {string} source - Source file path
* @param {string} dest - Destination file path
*/
@@ -563,14 +556,14 @@ class BaseIdeSetup {
// Read the file content
let content = await fs.readFile(source, 'utf8');
// Replace .bmad placeholder with actual folder name
if (content.includes('.bmad')) {
content = content.replaceAll('.bmad', this.bmadFolderName);
// Replace _bmad placeholder with actual folder name
if (content.includes('_bmad')) {
content = content.replaceAll('_bmad', this.bmadFolderName);
}
// Replace escape sequence .bmad with literal .bmad
if (content.includes('.bmad')) {
content = content.replaceAll('.bmad', '.bmad');
// Replace escape sequence _bmad with literal _bmad
if (content.includes('_bmad')) {
content = content.replaceAll('_bmad', '_bmad');
}
// Write to dest with replaced content

View File

@@ -119,7 +119,7 @@ class AntigravitySetup extends BaseIdeSetup {
await this.ensureDir(bmadWorkflowsDir);
// Generate agent launchers using AgentCommandGenerator
// This creates small launcher files that reference the actual agents in .bmad/
// This creates small launcher files that reference the actual agents in _bmad/
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);

View File

@@ -118,7 +118,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
await this.ensureDir(bmadCommandsDir);
// Generate agent launchers using AgentCommandGenerator
// This creates small launcher files that reference the actual agents in .bmad/
// This creates small launcher files that reference the actual agents in _bmad/
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);

View File

@@ -265,8 +265,8 @@ class CodexSetup extends BaseIdeSetup {
'',
chalk.white(' /prompts installed globally to your HOME DIRECTORY.'),
'',
chalk.yellow(' ⚠️ These prompts reference a specific .bmad path'),
chalk.dim(" To use with other projects, you'd need to copy the .bmad dir"),
chalk.yellow(' ⚠️ These prompts reference a specific _bmad path'),
chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"),
'',
chalk.green(' ✓ You can now use /commands in Codex CLI'),
chalk.dim(' Example: /bmad-bmm-agents-pm'),

View File

@@ -174,8 +174,8 @@ ${contentWithoutFrontmatter}
// Note: {user_name} and other {config_values} are left as-is for runtime substitution by Gemini
const tomlContent = template
.replaceAll('{{title}}', title)
.replaceAll('{.bmad}', '.bmad')
.replaceAll('{.bmad}', this.bmadFolderName)
.replaceAll('{_bmad}', '_bmad')
.replaceAll('{_bmad}', this.bmadFolderName)
.replaceAll('{{module}}', agent.module)
.replaceAll('{{name}}', agent.name);
@@ -196,8 +196,8 @@ ${contentWithoutFrontmatter}
// Replace template variables
const tomlContent = template
.replaceAll('{{taskName}}', taskName)
.replaceAll('{.bmad}', '.bmad')
.replaceAll('{.bmad}', this.bmadFolderName)
.replaceAll('{_bmad}', '_bmad')
.replaceAll('{_bmad}', this.bmadFolderName)
.replaceAll('{{module}}', task.module)
.replaceAll('{{filename}}', task.filename);

View File

@@ -45,11 +45,11 @@ class RooSetup extends BaseIdeSetup {
continue;
}
// Read the actual agent file from .bmad for metadata extraction (installed agents are .md files)
// Read the actual agent file from _bmad for metadata extraction (installed agents are .md files)
const agentPath = path.join(bmadDir, artifact.module, 'agents', `${artifact.name}.md`);
const content = await this.readFile(agentPath);
// Create command file that references the actual .bmad agent
// Create command file that references the actual _bmad agent
await this.createCommandFile({ module: artifact.module, name: artifact.name, path: agentPath }, content, commandPath, projectDir);
addedCount++;

View File

@@ -65,8 +65,8 @@ class AgentCommandGenerator {
.replaceAll('{{module}}', agent.module)
.replaceAll('{{path}}', agentPathInModule)
.replaceAll('{{description}}', agent.description || `${agent.name} agent`)
.replaceAll('.bmad', this.bmadFolderName)
.replaceAll('.bmad', '.bmad');
.replaceAll('_bmad', this.bmadFolderName)
.replaceAll('_bmad', '_bmad');
}
/**

View File

@@ -109,7 +109,7 @@ class WorkflowCommandGenerator {
// Convert source path to installed path
// From: /Users/.../src/modules/bmm/workflows/.../workflow.yaml
// To: {project-root}/.bmad/bmm/workflows/.../workflow.yaml
// To: {project-root}/_bmad/bmm/workflows/.../workflow.yaml
let workflowPath = workflow.path;
// Extract the relative path from source
@@ -131,8 +131,8 @@ class WorkflowCommandGenerator {
.replaceAll('{{module}}', workflow.module)
.replaceAll('{{description}}', workflow.description)
.replaceAll('{{workflow_path}}', workflowPath)
.replaceAll('.bmad', this.bmadFolderName)
.replaceAll('.bmad', '.bmad');
.replaceAll('_bmad', this.bmadFolderName)
.replaceAll('_bmad', '_bmad');
}
/**

View File

@@ -6,7 +6,7 @@ description: '{{description}}'
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
<agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from @.bmad/{{module}}/agents/{{path}}
1. LOAD the FULL agent file from @_bmad/{{module}}/agents/{{path}}
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
3. Execute ALL activation steps exactly as written in the agent file
4. Follow the agent's persona and menu system precisely

View File

@@ -3,12 +3,12 @@ prompt = """
CRITICAL: You are now the BMad '{{title}}' agent.
PRE-FLIGHT CHECKLIST:
1. [ ] IMMEDIATE ACTION: Load and parse @{.bmad}/{{module}}/config.yaml - store ALL config values in memory for use throughout the session.
2. [ ] IMMEDIATE ACTION: Read and internalize the full agent definition at @{.bmad}/{{module}}/agents/{{name}}.md.
1. [ ] IMMEDIATE ACTION: Load and parse @{_bmad}/{{module}}/config.yaml - store ALL config values in memory for use throughout the session.
2. [ ] IMMEDIATE ACTION: Read and internalize the full agent definition at @{_bmad}/{{module}}/agents/{{name}}.md.
3. [ ] CONFIRM: The user's name from config is {user_name}.
Only after all checks are complete, greet the user by name and display the menu.
Acknowledge this checklist is complete in your first response.
AGENT DEFINITION: @{.bmad}/{{module}}/agents/{{name}}.md
AGENT DEFINITION: @{_bmad}/{{module}}/agents/{{name}}.md
"""

View File

@@ -3,10 +3,10 @@ prompt = """
Execute the following BMad Method task workflow:
PRE-FLIGHT CHECKLIST:
1. [ ] IMMEDIATE ACTION: Load and parse @{.bmad}/{{module}}/config.yaml.
2. [ ] IMMEDIATE ACTION: Read and load the task definition at @{.bmad}/{{module}}/tasks/{{filename}}.
1. [ ] IMMEDIATE ACTION: Load and parse @{_bmad}/{{module}}/config.yaml.
2. [ ] IMMEDIATE ACTION: Read and load the task definition at @{_bmad}/{{module}}/tasks/{{filename}}.
Follow all instructions and complete the task as defined.
TASK DEFINITION: @{.bmad}/{{module}}/tasks/{{filename}}
TASK DEFINITION: @{_bmad}/{{module}}/tasks/{{filename}}
"""

View File

@@ -5,7 +5,7 @@ description: '{{description}}'
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
<steps CRITICAL="TRUE">
1. Always LOAD the FULL @.bmad/core/tasks/workflow.xml
1. Always LOAD the FULL @_bmad/core/tasks/workflow.xml
2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config @{{workflow_path}}
3. Pass the yaml path {{workflow_path}} as 'workflow-config' parameter to the workflow.xml instructions
4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions

View File

@@ -47,7 +47,7 @@ class ModuleManager {
}
/**
* Copy a file and replace .bmad placeholder with actual folder name
* Copy a file and replace _bmad placeholder with actual folder name
* @param {string} sourcePath - Source file path
* @param {string} targetPath - Target file path
*/
@@ -62,14 +62,14 @@ class ModuleManager {
// Read the file content
let content = await fs.readFile(sourcePath, 'utf8');
// Replace escape sequence .bmad with literal .bmad
if (content.includes('.bmad')) {
content = content.replaceAll('.bmad', '.bmad');
// Replace escape sequence _bmad with literal _bmad
if (content.includes('_bmad')) {
content = content.replaceAll('_bmad', '_bmad');
}
// Replace .bmad placeholder with actual folder name
if (content.includes('.bmad')) {
content = content.replaceAll('.bmad', this.bmadFolderName);
// Replace _bmad placeholder with actual folder name
if (content.includes('_bmad')) {
content = content.replaceAll('_bmad', this.bmadFolderName);
}
// Write to target with replaced content
@@ -695,8 +695,8 @@ class ModuleManager {
// IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML
// Otherwise parsing will fail on the placeholder
yamlContent = yamlContent.replaceAll('.bmad', '.bmad');
yamlContent = yamlContent.replaceAll('.bmad', this.bmadFolderName);
yamlContent = yamlContent.replaceAll('_bmad', '_bmad');
yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName);
try {
// First check if web_bundle exists by parsing
@@ -815,7 +815,7 @@ class ModuleManager {
// Create customize template if it doesn't exist
if (!(await fs.pathExists(customizePath))) {
const { getSourcePath } = require('../../../lib/project-root');
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
@@ -853,9 +853,9 @@ class ModuleManager {
// Compile with customizations if any
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config: this.coreConfig });
// Replace .bmad placeholder if needed
if (xml.includes('.bmad') && this.bmadFolderName) {
const processedXml = xml.replaceAll('.bmad', this.bmadFolderName);
// Replace _bmad placeholder if needed
if (xml.includes('_bmad') && this.bmadFolderName) {
const processedXml = xml.replaceAll('_bmad', this.bmadFolderName);
await fs.writeFile(targetMdPath, processedXml, 'utf8');
} else {
await fs.writeFile(targetMdPath, xml, 'utf8');
@@ -872,7 +872,7 @@ class ModuleManager {
const projectDir = path.dirname(bmadDir);
const resolvedSidecarFolder = agentSidecarFolder
.replaceAll('{project-root}', projectDir)
.replaceAll('.bmad', path.basename(bmadDir));
.replaceAll('_bmad', path.basename(bmadDir));
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
@@ -931,28 +931,23 @@ class ModuleManager {
* @param {string} moduleName - Module name
*/
async processAgentFiles(modulePath, moduleName) {
const agentsPath = path.join(modulePath, 'agents');
// Check if agents directory exists
if (!(await fs.pathExists(agentsPath))) {
return; // No agents to process
}
// Get all agent MD files recursively
const agentFiles = await this.findAgentMdFiles(agentsPath);
for (const agentFile of agentFiles) {
if (!agentFile.endsWith('.md')) continue;
let content = await fs.readFile(agentFile, 'utf8');
// Check if content has agent XML and no activation block
if (content.includes('<agent') && !content.includes('<activation')) {
// Inject the activation block using XML handler
content = this.xmlHandler.injectActivationSimple(content);
await fs.writeFile(agentFile, content, 'utf8');
}
}
// const agentsPath = path.join(modulePath, 'agents');
// // Check if agents directory exists
// if (!(await fs.pathExists(agentsPath))) {
// return; // No agents to process
// }
// // Get all agent MD files recursively
// const agentFiles = await this.findAgentMdFiles(agentsPath);
// for (const agentFile of agentFiles) {
// if (!agentFile.endsWith('.md')) continue;
// let content = await fs.readFile(agentFile, 'utf8');
// // Check if content has agent XML and no activation block
// if (content.includes('<agent') && !content.includes('<activation')) {
// // Inject the activation block using XML handler
// content = this.xmlHandler.injectActivationSimple(content);
// await fs.writeFile(agentFile, content, 'utf8');
// }
// }
}
/**
@@ -1030,10 +1025,10 @@ class ModuleManager {
const installWorkflowPath = item['workflow-install']; // Where to copy TO
// Parse SOURCE workflow path
// Handle both .bmad placeholder and hardcoded 'bmad'
// Example: {project-root}/.bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
// Handle both _bmad placeholder and hardcoded 'bmad'
// Example: {project-root}/_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
// Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:\.bmad)\/([^/]+)\/workflows\/(.+)/);
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!sourceMatch) {
console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`));
continue;
@@ -1042,9 +1037,9 @@ class ModuleManager {
const [, sourceModule, sourceWorkflowSubPath] = sourceMatch;
// Parse INSTALL workflow path
// Handle.bmad
// Example: {project-root}/.bmad/bmgd/workflows/4-production/create-story/workflow.yaml
const installMatch = installWorkflowPath.match(/\{project-root\}\/(\.bmad)\/([^/]+)\/workflows\/(.+)/);
// Handle_bmad
// Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.yaml
const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!installMatch) {
console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`));
continue;
@@ -1096,9 +1091,9 @@ class ModuleManager {
async updateWorkflowConfigSource(workflowYamlPath, newModuleName) {
let yamlContent = await fs.readFile(workflowYamlPath, 'utf8');
// Replace config_source: "{project-root}/.bmad/OLD_MODULE/config.yaml"
// with config_source: "{project-root}/.bmad/NEW_MODULE/config.yaml"
// Note: At this point .bmad has already been replaced with actual folder name
// Replace config_source: "{project-root}/_bmad/OLD_MODULE/config.yaml"
// with config_source: "{project-root}/_bmad/NEW_MODULE/config.yaml"
// Note: At this point _bmad has already been replaced with actual folder name
const configSourcePattern = /config_source:\s*["']?\{project-root\}\/[^/]+\/[^/]+\/config\.yaml["']?/g;
const newConfigSource = `config_source: "{project-root}/${this.bmadFolderName}/${newModuleName}/config.yaml"`;