feat: v6.0.0-alpha.0 - the future is now

This commit is contained in:
Brian Madison
2025-09-28 23:17:07 -05:00
parent 52f6889089
commit 0a6a3f3015
747 changed files with 52759 additions and 235199 deletions

View File

@@ -0,0 +1,281 @@
const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { XmlHandler } = require('../../../lib/xml-handler');
const { getSourcePath } = require('../../../lib/project-root');
/**
* Base class for IDE-specific setup
* All IDE handlers should extend this class
*/
class BaseIdeSetup {
constructor(name, displayName = null, preferred = false) {
this.name = name;
this.displayName = displayName || name; // Human-readable name for UI
this.preferred = preferred; // Whether this IDE should be shown in preferred list
this.configDir = null; // Override in subclasses
this.rulesDir = null; // Override in subclasses
this.xmlHandler = new XmlHandler();
}
/**
* Main setup method - must be implemented by subclasses
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
throw new Error(`setup() must be implemented by ${this.name} handler`);
}
/**
* Cleanup IDE configuration
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir) {
// Default implementation - can be overridden
if (this.configDir) {
const configPath = path.join(projectDir, this.configDir);
if (await fs.pathExists(configPath)) {
const bmadRulesPath = path.join(configPath, 'bmad');
if (await fs.pathExists(bmadRulesPath)) {
await fs.remove(bmadRulesPath);
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
}
}
}
}
/**
* Get list of agents from BMAD installation
* @param {string} bmadDir - BMAD installation directory
* @returns {Array} List of agent files
*/
async getAgents(bmadDir) {
const agents = [];
// Get core agents
const coreAgentsPath = path.join(bmadDir, 'core', 'agents');
if (await fs.pathExists(coreAgentsPath)) {
const coreAgents = await this.scanDirectory(coreAgentsPath, '.md');
agents.push(
...coreAgents.map((a) => ({
...a,
module: 'core',
})),
);
}
// Get module agents
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg') {
const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
if (await fs.pathExists(moduleAgentsPath)) {
const moduleAgents = await this.scanDirectory(moduleAgentsPath, '.md');
agents.push(
...moduleAgents.map((a) => ({
...a,
module: entry.name,
})),
);
}
}
}
return agents;
}
/**
* Get list of tasks from BMAD installation
* @param {string} bmadDir - BMAD installation directory
* @returns {Array} List of task files
*/
async getTasks(bmadDir) {
const tasks = [];
// Get core tasks
const coreTasksPath = path.join(bmadDir, 'core', 'tasks');
if (await fs.pathExists(coreTasksPath)) {
const coreTasks = await this.scanDirectory(coreTasksPath, '.md');
tasks.push(
...coreTasks.map((t) => ({
...t,
module: 'core',
})),
);
}
// Get module tasks
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg') {
const moduleTasksPath = path.join(bmadDir, entry.name, 'tasks');
if (await fs.pathExists(moduleTasksPath)) {
const moduleTasks = await this.scanDirectory(moduleTasksPath, '.md');
tasks.push(
...moduleTasks.map((t) => ({
...t,
module: entry.name,
})),
);
}
}
}
return tasks;
}
/**
* Scan a directory for files with specific extension
* @param {string} dir - Directory to scan
* @param {string} ext - File extension to match
* @returns {Array} List of file info objects
*/
async scanDirectory(dir, ext) {
const files = [];
if (!(await fs.pathExists(dir))) {
return files;
}
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Recursively scan subdirectories
const subFiles = await this.scanDirectory(fullPath, ext);
files.push(...subFiles);
} else if (entry.isFile() && entry.name.endsWith(ext)) {
files.push({
name: path.basename(entry.name, ext),
path: fullPath,
relativePath: path.relative(dir, fullPath),
filename: entry.name,
});
}
}
return files;
}
/**
* Create IDE command/rule file from agent or task
* @param {string} content - File content
* @param {Object} metadata - File metadata
* @param {string} projectDir - The actual project directory path
* @returns {string} Processed content
*/
processContent(content, metadata = {}, projectDir = null) {
// Replace placeholders
let processed = content;
// Inject activation block for agent files FIRST (before replacements)
if (metadata.name && content.includes('<agent')) {
processed = this.xmlHandler.injectActivationSimple(processed, metadata);
}
// Use the actual project directory path if provided, otherwise default to 'bmad/'
const projectRoot = projectDir ? projectDir + '/' : 'bmad/';
// Common replacements (including in the activation block)
processed = processed.replaceAll('{project-root}', projectRoot);
processed = processed.replaceAll('{module}', metadata.module || 'core');
processed = processed.replaceAll('{agent}', metadata.name || '');
processed = processed.replaceAll('{task}', metadata.name || '');
return processed;
}
/**
* Ensure directory exists
* @param {string} dirPath - Directory path
*/
async ensureDir(dirPath) {
await fs.ensureDir(dirPath);
}
/**
* Write file with content
* @param {string} filePath - File path
* @param {string} content - File content
*/
async writeFile(filePath, content) {
await this.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content, 'utf8');
}
/**
* Copy file from source to destination
* @param {string} source - Source file path
* @param {string} dest - Destination file path
*/
async copyFile(source, dest) {
await this.ensureDir(path.dirname(dest));
await fs.copy(source, dest, { overwrite: true });
}
/**
* Check if path exists
* @param {string} pathToCheck - Path to check
* @returns {boolean} True if path exists
*/
async exists(pathToCheck) {
return await fs.pathExists(pathToCheck);
}
/**
* Alias for exists method
* @param {string} pathToCheck - Path to check
* @returns {boolean} True if path exists
*/
async pathExists(pathToCheck) {
return await fs.pathExists(pathToCheck);
}
/**
* Read file content
* @param {string} filePath - File path
* @returns {string} File content
*/
async readFile(filePath) {
return await fs.readFile(filePath, 'utf8');
}
/**
* Format name as title
* @param {string} name - Name to format
* @returns {string} Formatted title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Create agent configuration file
* @param {string} bmadDir - BMAD installation directory
* @param {Object} agent - Agent information
*/
async createAgentConfig(bmadDir, agent) {
const agentConfigDir = path.join(bmadDir, '_cfg', 'agents');
await this.ensureDir(agentConfigDir);
// Load agent config template
const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
const templateContent = await this.readFile(templatePath);
const configContent = `# Agent Config: ${agent.name}
${templateContent}`;
const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
await this.writeFile(configPath, configContent);
}
}
module.exports = { BaseIdeSetup };

View File

@@ -0,0 +1,271 @@
const path = require('node:path');
const os = require('node:os');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const inquirer = require('inquirer');
/**
* Auggie CLI setup handler
* Allows flexible installation of agents to multiple locations
*/
class AuggieSetup extends BaseIdeSetup {
constructor() {
super('auggie', 'Auggie CLI');
this.defaultLocations = [
{ name: 'Project Directory (.auggie/commands)', value: '.auggie/commands', checked: true },
{ name: 'User Home (~/.auggie/commands)', value: path.join(os.homedir(), '.auggie', 'commands') },
{ name: 'Custom Location', value: 'custom' },
];
}
/**
* Collect configuration choices before installation
* @param {Object} options - Configuration options
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const response = await inquirer.prompt([
{
type: 'checkbox',
name: 'locations',
message: 'Select Auggie CLI installation locations:',
choices: this.defaultLocations,
validate: (answers) => {
if (answers.length === 0) {
return 'Please select at least one location';
}
return true;
},
},
]);
const locations = [];
for (const loc of response.locations) {
if (loc === 'custom') {
const custom = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter custom path for Auggie commands:',
validate: (input) => {
if (!input.trim()) {
return 'Path cannot be empty';
}
return true;
},
},
]);
locations.push(custom.path);
} else {
locations.push(loc);
}
}
return { auggieLocations: locations };
}
/**
* Setup Auggie CLI configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Use pre-collected configuration if available
const config = options.preCollectedConfig || {};
const locations = await this.getInstallLocations(projectDir, { ...options, auggieLocations: config.auggieLocations });
if (locations.length === 0) {
console.log(chalk.yellow('No locations selected. Skipping Auggie CLI setup.'));
return { success: false, reason: 'no-locations' };
}
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
let totalInstalled = 0;
// Install to each selected location
for (const location of locations) {
console.log(chalk.dim(`\n Installing to: ${location}`));
const agentsDir = path.join(location, 'agents');
const tasksDir = path.join(location, 'tasks');
await this.ensureDir(agentsDir);
await this.ensureDir(tasksDir);
// Install agents
for (const agent of agents) {
const content = await this.readFile(agent.path);
const commandContent = this.createAgentCommand(agent, content);
const targetPath = path.join(agentsDir, `${agent.module}-${agent.name}.md`);
await this.writeFile(targetPath, commandContent);
totalInstalled++;
}
// Install tasks
for (const task of tasks) {
const content = await this.readFile(task.path);
const commandContent = this.createTaskCommand(task, content);
const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`);
await this.writeFile(targetPath, commandContent);
totalInstalled++;
}
console.log(chalk.green(` ✓ Installed ${agents.length} agents and ${tasks.length} tasks`));
}
console.log(chalk.green(`\n${this.name} configured:`));
console.log(chalk.dim(` - ${totalInstalled} total commands installed`));
console.log(chalk.dim(` - ${locations.length} location(s) configured`));
return {
success: true,
commands: totalInstalled,
locations: locations.length,
};
}
/**
* Get installation locations from user
*/
async getInstallLocations(projectDir, options) {
if (options.auggieLocations) {
// Process the pre-collected locations to resolve relative paths
const processedLocations = [];
for (const loc of options.auggieLocations) {
if (loc === '.auggie/commands') {
// Relative to project directory
processedLocations.push(path.join(projectDir, loc));
} else {
processedLocations.push(loc);
}
}
return processedLocations;
}
const response = await inquirer.prompt([
{
type: 'checkbox',
name: 'locations',
message: 'Select Auggie CLI installation locations:',
choices: this.defaultLocations,
validate: (answers) => {
if (answers.length === 0) {
return 'Please select at least one location';
}
return true;
},
},
]);
const locations = [];
for (const loc of response.locations) {
if (loc === 'custom') {
const custom = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter custom path for Auggie commands:',
validate: (input) => {
if (!input.trim()) {
return 'Path cannot be empty';
}
return true;
},
},
]);
locations.push(custom.path);
} else if (loc.startsWith('.auggie')) {
// Relative to project directory
locations.push(path.join(projectDir, loc));
} else {
locations.push(loc);
}
}
return locations;
}
/**
* Create agent command content
*/
createAgentCommand(agent, content) {
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
return `# ${title} Agent
## Activation
Type \`@${agent.name}\` to activate this agent.
${content}
## Module
BMAD ${agent.module.toUpperCase()} module
`;
}
/**
* Create task command content
*/
createTaskCommand(task, content) {
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
return `# ${taskName} Task
## Activation
Type \`@task-${task.name}\` to execute this task.
${content}
## Module
BMAD ${task.module.toUpperCase()} module
`;
}
/**
* Cleanup Auggie configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
// Check common locations
const locations = [path.join(os.homedir(), '.auggie', 'commands'), path.join(projectDir, '.auggie', 'commands')];
for (const location of locations) {
const agentsDir = path.join(location, 'agents');
const tasksDir = path.join(location, 'tasks');
if (await fs.pathExists(agentsDir)) {
// Remove only BMAD files (those with module prefix)
const files = await fs.readdir(agentsDir);
for (const file of files) {
if (file.includes('-') && file.endsWith('.md')) {
await fs.remove(path.join(agentsDir, file));
}
}
}
if (await fs.pathExists(tasksDir)) {
const files = await fs.readdir(tasksDir);
for (const file of files) {
if (file.includes('-') && file.endsWith('.md')) {
await fs.remove(path.join(tasksDir, file));
}
}
}
}
console.log(chalk.dim('Cleaned up Auggie CLI configurations'));
}
}
module.exports = { AuggieSetup };

View File

@@ -0,0 +1,625 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { WorkflowCommandGenerator } = require('./workflow-command-generator');
/**
* Claude Code IDE setup handler
*/
class ClaudeCodeSetup extends BaseIdeSetup {
constructor() {
super('claude-code', 'Claude Code', true); // preferred IDE
this.configDir = '.claude';
this.commandsDir = 'commands';
this.agentsDir = 'agents';
}
/**
* Collect configuration choices before installation
* @param {Object} options - Configuration options
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const config = {
subagentChoices: null,
installLocation: null,
};
const sourceModulesPath = getSourcePath('modules');
const modules = options.selectedModules || [];
for (const moduleName of modules) {
// Check for Claude Code sub-module injection config in SOURCE directory
const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml');
if (await this.exists(injectionConfigPath)) {
const fs = require('fs-extra');
const yaml = require('js-yaml');
try {
// Load injection configuration
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
const injectionConfig = yaml.load(configContent);
// Ask about subagents if they exist and we haven't asked yet
if (injectionConfig.subagents && !config.subagentChoices) {
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
if (config.subagentChoices.install !== 'none') {
// Ask for installation location
const inquirer = require('inquirer');
const locationAnswer = await inquirer.prompt([
{
type: 'list',
name: 'location',
message: 'Where would you like to install Claude Code subagents?',
choices: [
{ name: 'Project level (.claude/agents/)', value: 'project' },
{ name: 'User level (~/.claude/agents/)', value: 'user' },
],
default: 'project',
},
]);
config.installLocation = locationAnswer.location;
}
}
} catch (error) {
console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
}
}
}
return config;
}
/**
* Setup Claude Code IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
// Store project directory for use in processContent
this.projectDir = projectDir;
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .claude/commands directory structure
const claudeDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(claudeDir, this.commandsDir);
const bmadCommandsDir = path.join(commandsDir, 'bmad');
await this.ensureDir(bmadCommandsDir);
// Get agents and tasks from SOURCE, not installed location
// This ensures we process files with {project-root} placeholders intact
const sourceDir = getSourcePath('modules');
const agents = await this.getAgentsFromSource(sourceDir, options.selectedModules || []);
const tasks = await this.getTasksFromSource(sourceDir, options.selectedModules || []);
// Create directories for each module
const modules = new Set();
for (const item of [...agents, ...tasks]) modules.add(item.module);
for (const module of modules) {
await this.ensureDir(path.join(bmadCommandsDir, module));
await this.ensureDir(path.join(bmadCommandsDir, module, 'agents'));
await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks'));
}
// Process and copy agents
let agentCount = 0;
for (const agent of agents) {
const content = await this.readAndProcess(agent.path, {
module: agent.module,
name: agent.name,
});
const targetPath = path.join(bmadCommandsDir, agent.module, 'agents', `${agent.name}.md`);
await this.writeFile(targetPath, content);
agentCount++;
}
// Process and copy tasks
let taskCount = 0;
for (const task of tasks) {
const content = await this.readAndProcess(task.path, {
module: task.module,
name: task.name,
});
const targetPath = path.join(bmadCommandsDir, task.module, 'tasks', `${task.name}.md`);
await this.writeFile(targetPath, content);
taskCount++;
}
// Process Claude Code specific injections for installed modules
// Use pre-collected configuration if available
if (options.preCollectedConfig) {
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
} else {
await this.processModuleInjections(projectDir, bmadDir, options);
}
// Skip CLAUDE.md creation - let user manage their own CLAUDE.md file
// await this.createClaudeConfig(projectDir, modules);
// Generate workflow commands from manifest (if it exists)
const workflowGen = new WorkflowCommandGenerator();
const workflowResult = await workflowGen.generateWorkflowCommands(projectDir, bmadDir);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`));
console.log(chalk.dim(` - ${taskCount} tasks installed`));
if (workflowResult.generated > 0) {
console.log(chalk.dim(` - ${workflowResult.generated} workflow commands generated`));
}
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`));
return {
success: true,
agents: agentCount,
tasks: taskCount,
};
}
// Method removed - CLAUDE.md file management left to user
/**
* Read and process file content
*/
async readAndProcess(filePath, metadata) {
const fs = require('fs-extra');
const content = await fs.readFile(filePath, 'utf8');
return this.processContent(content, metadata);
}
/**
* Override processContent to use the actual project directory path
*/
processContent(content, metadata = {}) {
// Use the base class method with the actual project directory
return super.processContent(content, metadata, this.projectDir);
}
/**
* Get agents from source modules (not installed location)
*/
async getAgentsFromSource(sourceDir, selectedModules) {
const fs = require('fs-extra');
const agents = [];
// Add core agents
const corePath = getModulePath('core');
if (await fs.pathExists(path.join(corePath, 'agents'))) {
const coreAgents = await this.getAgentsFromDir(path.join(corePath, 'agents'), 'core');
agents.push(...coreAgents);
}
// Add module agents
for (const moduleName of selectedModules) {
const modulePath = path.join(sourceDir, moduleName);
const agentsPath = path.join(modulePath, 'agents');
if (await fs.pathExists(agentsPath)) {
const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName);
agents.push(...moduleAgents);
}
}
return agents;
}
/**
* Get tasks from source modules (not installed location)
*/
async getTasksFromSource(sourceDir, selectedModules) {
const fs = require('fs-extra');
const tasks = [];
// Add core tasks
const corePath = getModulePath('core');
if (await fs.pathExists(path.join(corePath, 'tasks'))) {
const coreTasks = await this.getTasksFromDir(path.join(corePath, 'tasks'), 'core');
tasks.push(...coreTasks);
}
// Add module tasks
for (const moduleName of selectedModules) {
const modulePath = path.join(sourceDir, moduleName);
const tasksPath = path.join(modulePath, 'tasks');
if (await fs.pathExists(tasksPath)) {
const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName);
tasks.push(...moduleTasks);
}
}
return tasks;
}
/**
* Get agents from a specific directory
*/
async getAgentsFromDir(dirPath, moduleName) {
const fs = require('fs-extra');
const agents = [];
const files = await fs.readdir(dirPath);
for (const file of files) {
if (file.endsWith('.md')) {
const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8');
// Skip web-only agents
if (content.includes('localskip="true"')) {
continue;
}
agents.push({
path: filePath,
name: file.replace('.md', ''),
module: moduleName,
});
}
}
return agents;
}
/**
* Get tasks from a specific directory
*/
async getTasksFromDir(dirPath, moduleName) {
const fs = require('fs-extra');
const tasks = [];
const files = await fs.readdir(dirPath);
for (const file of files) {
if (file.endsWith('.md')) {
tasks.push({
path: path.join(dirPath, file),
name: file.replace('.md', ''),
module: moduleName,
});
}
}
return tasks;
}
/**
* Process module injections with pre-collected configuration
*/
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
const fs = require('fs-extra');
const yaml = require('js-yaml');
// Get list of installed modules
const modules = options.selectedModules || [];
const { subagentChoices, installLocation } = preCollectedConfig;
// Get the actual source directory (not the installation directory)
const sourceModulesPath = getSourcePath('modules');
for (const moduleName of modules) {
// Check for Claude Code sub-module injection config in SOURCE directory
const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml');
if (await this.exists(injectionConfigPath)) {
try {
// Load injection configuration
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
const config = yaml.load(configContent);
// Process content injections based on user choices
if (config.injections && subagentChoices && subagentChoices.install !== 'none') {
for (const injection of config.injections) {
// Check if this injection is related to a selected subagent
if (this.shouldInject(injection, subagentChoices)) {
await this.injectContent(projectDir, injection, subagentChoices);
}
}
}
// Copy selected subagents
if (config.subagents && subagentChoices && subagentChoices.install !== 'none') {
await this.copySelectedSubagents(
projectDir,
path.dirname(injectionConfigPath),
config.subagents,
subagentChoices,
installLocation,
);
}
} catch (error) {
console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
}
}
}
}
/**
* Process Claude Code specific injections for installed modules
* Looks for injections.yaml in each module's claude-code sub-module
*/
async processModuleInjections(projectDir, bmadDir, options) {
const fs = require('fs-extra');
const yaml = require('js-yaml');
const inquirer = require('inquirer');
// Get list of installed modules
const modules = options.selectedModules || [];
let subagentChoices = null;
let installLocation = null;
// Get the actual source directory (not the installation directory)
const sourceModulesPath = getSourcePath('modules');
for (const moduleName of modules) {
// Check for Claude Code sub-module injection config in SOURCE directory
const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml');
if (await this.exists(injectionConfigPath)) {
console.log(chalk.cyan(`\nConfiguring ${moduleName} Claude Code features...`));
try {
// Load injection configuration
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
const config = yaml.load(configContent);
// Ask about subagents if they exist and we haven't asked yet
if (config.subagents && !subagentChoices) {
subagentChoices = await this.promptSubagentInstallation(config.subagents);
if (subagentChoices.install !== 'none') {
// Ask for installation location
const locationAnswer = await inquirer.prompt([
{
type: 'list',
name: 'location',
message: 'Where would you like to install Claude Code subagents?',
choices: [
{ name: 'Project level (.claude/agents/)', value: 'project' },
{ name: 'User level (~/.claude/agents/)', value: 'user' },
],
default: 'project',
},
]);
installLocation = locationAnswer.location;
}
}
// Process content injections based on user choices
if (config.injections && subagentChoices && subagentChoices.install !== 'none') {
for (const injection of config.injections) {
// Check if this injection is related to a selected subagent
if (this.shouldInject(injection, subagentChoices)) {
await this.injectContent(projectDir, injection, subagentChoices);
}
}
}
// Copy selected subagents
if (config.subagents && subagentChoices && subagentChoices.install !== 'none') {
await this.copySelectedSubagents(
projectDir,
path.dirname(injectionConfigPath),
config.subagents,
subagentChoices,
installLocation,
);
}
} catch (error) {
console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
}
}
}
}
/**
* Prompt user for subagent installation preferences
*/
async promptSubagentInstallation(subagentConfig) {
const inquirer = require('inquirer');
// First ask if they want to install subagents
const { install } = await inquirer.prompt([
{
type: 'list',
name: 'install',
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
choices: [
{ name: 'Yes, install all subagents', value: 'all' },
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
{ name: 'No, skip subagent installation', value: 'none' },
],
default: 'all',
},
]);
if (install === 'selective') {
// Show list of available subagents with descriptions
const subagentInfo = {
'market-researcher.md': 'Market research and competitive analysis',
'requirements-analyst.md': 'Requirements extraction and validation',
'technical-evaluator.md': 'Technology stack evaluation',
'epic-optimizer.md': 'Epic and story breakdown optimization',
'document-reviewer.md': 'Document quality review',
};
const { selected } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selected',
message: 'Select subagents to install:',
choices: subagentConfig.files.map((file) => ({
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
value: file,
checked: true,
})),
},
]);
return { install: 'selective', selected };
}
return { install };
}
/**
* Check if an injection should be applied based on user choices
*/
shouldInject(injection, subagentChoices) {
// If user chose no subagents, no injections
if (subagentChoices.install === 'none') {
return false;
}
// If user chose all subagents, all injections apply
if (subagentChoices.install === 'all') {
return true;
}
// For selective installation, check the 'requires' field
if (subagentChoices.install === 'selective') {
// If injection requires 'any' subagent and user selected at least one
if (injection.requires === 'any' && subagentChoices.selected.length > 0) {
return true;
}
// Check if the required subagent was selected
if (injection.requires) {
const requiredAgent = injection.requires + '.md';
return subagentChoices.selected.includes(requiredAgent);
}
// Fallback: check if injection mentions a selected agent
const selectedAgentNames = subagentChoices.selected.map((f) => f.replace('.md', ''));
for (const agentName of selectedAgentNames) {
if (injection.point && injection.point.includes(agentName)) {
return true;
}
}
}
return false;
}
/**
* Inject content at specified point in file
*/
async injectContent(projectDir, injection, subagentChoices = null) {
const fs = require('fs-extra');
const targetPath = path.join(projectDir, injection.file);
if (await this.exists(targetPath)) {
let content = await fs.readFile(targetPath, 'utf8');
const marker = `<!-- IDE-INJECT-POINT: ${injection.point} -->`;
if (content.includes(marker)) {
let injectionContent = injection.content;
// Filter content if selective subagents chosen
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
injectionContent = this.filterAgentInstructions(injection.content, subagentChoices.selected);
}
content = content.replace(marker, injectionContent);
await fs.writeFile(targetPath, content);
console.log(chalk.dim(` Injected: ${injection.point}${injection.file}`));
}
}
}
/**
* Filter agent instructions to only include selected subagents
*/
filterAgentInstructions(content, selectedFiles) {
const selectedAgents = selectedFiles.map((f) => f.replace('.md', ''));
const lines = content.split('\n');
const filteredLines = [];
let includeNextLine = true;
for (const line of lines) {
// Always include structural lines
if (line.includes('<llm') || line.includes('</llm>')) {
filteredLines.push(line);
includeNextLine = true;
}
// Check if line mentions a subagent
else if (line.includes('subagent')) {
let shouldInclude = false;
for (const agent of selectedAgents) {
if (line.includes(agent)) {
shouldInclude = true;
break;
}
}
if (shouldInclude) {
filteredLines.push(line);
}
}
// Include general instructions
else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) {
filteredLines.push(line);
}
}
// Only return content if we have actual instructions
if (filteredLines.length > 2) {
// More than just llm tags
return filteredLines.join('\n');
}
return ''; // Return empty if no relevant content
}
/**
* Copy selected subagents to appropriate Claude agents directory
*/
async copySelectedSubagents(projectDir, moduleClaudeDir, subagentConfig, choices, location) {
const fs = require('fs-extra');
const sourceDir = path.join(moduleClaudeDir, subagentConfig.source);
// Determine target directory based on user choice
let targetDir;
if (location === 'user') {
targetDir = path.join(require('node:os').homedir(), '.claude', 'agents');
console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`));
} else {
targetDir = path.join(projectDir, '.claude', 'agents');
console.log(chalk.dim(` Installing subagents to project: .claude/agents/`));
}
// Ensure target directory exists
await this.ensureDir(targetDir);
// Determine which files to copy
let filesToCopy = [];
if (choices.install === 'all') {
filesToCopy = subagentConfig.files;
} else if (choices.install === 'selective') {
filesToCopy = choices.selected;
}
// Copy selected subagent files
for (const file of filesToCopy) {
const sourcePath = path.join(sourceDir, file);
const targetPath = path.join(targetDir, file);
if (await this.exists(sourcePath)) {
await fs.copyFile(sourcePath, targetPath);
console.log(chalk.green(` ✓ Installed: ${file.replace('.md', '')}`));
}
}
if (filesToCopy.length > 0) {
console.log(chalk.dim(` Total subagents installed: ${filesToCopy.length}`));
}
}
}
module.exports = { ClaudeCodeSetup };

View File

@@ -0,0 +1,301 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const inquirer = require('inquirer');
/**
* Cline IDE setup handler
* Creates rules in .clinerules directory with ordering support
*/
class ClineSetup extends BaseIdeSetup {
constructor() {
super('cline', 'Cline');
this.configDir = '.clinerules';
this.defaultOrder = {
core: 10,
bmm: 20,
cis: 30,
other: 99,
};
}
/**
* Collect configuration choices before installation
* @param {Object} options - Configuration options
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const response = await inquirer.prompt([
{
type: 'list',
name: 'ordering',
message: 'How should BMAD rules be ordered in Cline?',
choices: [
{ name: 'By module (core first, then modules)', value: 'module' },
{ name: 'By importance (dev agents first)', value: 'importance' },
{ name: 'Alphabetical (simple A-Z ordering)', value: 'alphabetical' },
{ name: "Custom (I'll reorder manually)", value: 'custom' },
],
default: 'module',
},
]);
return { ordering: response.ordering };
}
/**
* Setup Cline IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .clinerules directory
const clineRulesDir = path.join(projectDir, this.configDir);
await this.ensureDir(clineRulesDir);
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Use pre-collected configuration if available
const config = options.preCollectedConfig || {};
const orderingStrategy = config.ordering || options.ordering || 'module';
// Process agents as rules with ordering
let ruleCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const order = this.getOrder(agent, orderingStrategy);
const processedContent = this.createAgentRule(agent, content, projectDir);
// Use numeric prefix for ordering
const prefix = order.toString().padStart(2, '0');
const targetPath = path.join(clineRulesDir, `${prefix}-${agent.module}-${agent.name}.md`);
await this.writeFile(targetPath, processedContent);
ruleCount++;
}
// Process tasks with ordering
for (const task of tasks) {
const content = await this.readFile(task.path);
const order = this.getTaskOrder(task, orderingStrategy);
const processedContent = this.createTaskRule(task, content);
// Tasks get higher order numbers to appear after agents
const prefix = (order + 50).toString().padStart(2, '0');
const targetPath = path.join(clineRulesDir, `${prefix}-task-${task.module}-${task.name}.md`);
await this.writeFile(targetPath, processedContent);
ruleCount++;
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${ruleCount} rules created in ${path.relative(projectDir, clineRulesDir)}`));
console.log(chalk.dim(` - Ordering: ${orderingStrategy}`));
// Important message about toggle system
console.log(chalk.yellow('\n ⚠️ IMPORTANT: Cline Toggle System'));
console.log(chalk.cyan(' Rules are OFF by default to avoid context pollution'));
console.log(chalk.dim(' To use BMAD agents:'));
console.log(chalk.dim(' 1. Click rules icon below chat input'));
console.log(chalk.dim(' 2. Toggle ON the specific agent you need'));
console.log(chalk.dim(' 3. Type @{agent-name} to activate'));
console.log(chalk.dim(' 4. Toggle OFF when done to free context'));
console.log(chalk.dim('\n 💡 Best practice: Only enable 1-2 agents at a time'));
return {
success: true,
rules: ruleCount,
ordering: orderingStrategy,
};
}
/**
* Ask user about rule ordering strategy
*/
async askOrderingStrategy() {
const response = await inquirer.prompt([
{
type: 'list',
name: 'ordering',
message: 'How should BMAD rules be ordered in Cline?',
choices: [
{ name: 'By module (core first, then modules)', value: 'module' },
{ name: 'By importance (dev agents first)', value: 'importance' },
{ name: 'Alphabetical (simple A-Z ordering)', value: 'alphabetical' },
{ name: "Custom (I'll reorder manually)", value: 'custom' },
],
default: 'module',
},
]);
return response.ordering;
}
/**
* Get order number for an agent based on strategy
*/
getOrder(agent, strategy) {
switch (strategy) {
case 'module': {
return this.defaultOrder[agent.module] || this.defaultOrder.other;
}
case 'importance': {
// Prioritize certain agent types
if (agent.name.includes('dev') || agent.name.includes('code')) return 10;
if (agent.name.includes('architect') || agent.name.includes('design')) return 15;
if (agent.name.includes('test') || agent.name.includes('qa')) return 20;
if (agent.name.includes('doc') || agent.name.includes('write')) return 25;
if (agent.name.includes('review')) return 30;
return 40;
}
case 'alphabetical': {
// Use a fixed number, files will sort alphabetically by name
return 50;
}
default: {
// 'custom' or any other value - user will reorder manually
return 99;
}
}
}
/**
* Get order number for a task
*/
getTaskOrder(task, strategy) {
// Tasks always come after agents
return this.getOrder(task, strategy) + 50;
}
/**
* Create rule content for an agent
*/
createAgentRule(agent, content, projectDir) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
// Extract YAML content
const yamlMatch = content.match(/```ya?ml\r?\n([\s\S]*?)```/);
const yamlContent = yamlMatch ? yamlMatch[1] : content;
// Get relative path
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
let ruleContent = `# ${title} Agent
This rule defines the ${title} persona and project standards.
## Role Definition
When the user types \`@${agent.name}\`, adopt this persona and follow these guidelines:
\`\`\`yaml
${yamlContent}
\`\`\`
## Project Standards
- Always maintain consistency with project documentation in BMAD directories
- Follow the agent's specific guidelines and constraints
- Update relevant project files when making changes
- Reference the complete agent definition in [${relativePath}](${relativePath})
## Usage
Type \`@${agent.name}\` to activate this ${title} persona.
## Module
Part of the BMAD ${agent.module.toUpperCase()} module.
`;
return ruleContent;
}
/**
* Create rule content for a task
*/
createTaskRule(task, content) {
// Extract task name
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
let ruleContent = `# ${taskName} Task
This rule defines the ${taskName} task workflow.
## Task Workflow
When this task is referenced, execute the following steps:
${content}
## Project Integration
- This task follows BMAD Method standards
- Ensure all outputs align with project conventions
- Update relevant documentation after task completion
## Usage
Reference with \`@task-${task.name}\` to access this workflow.
## Module
Part of the BMAD ${task.module.toUpperCase()} module.
`;
return ruleContent;
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup Cline configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const clineRulesDir = path.join(projectDir, this.configDir);
if (await fs.pathExists(clineRulesDir)) {
// Remove all numbered BMAD rules
const files = await fs.readdir(clineRulesDir);
let removed = 0;
for (const file of files) {
// Check if it matches our naming pattern (XX-module-name.md)
if (/^\d{2}-.*\.md$/.test(file)) {
const filePath = path.join(clineRulesDir, file);
const content = await fs.readFile(filePath, 'utf8');
// Verify it's a BMAD rule
if (content.includes('BMAD') && content.includes('Module')) {
await fs.remove(filePath);
removed++;
}
}
}
console.log(chalk.dim(`Removed ${removed} BMAD rules from Cline`));
}
}
}
module.exports = { ClineSetup };

View File

@@ -0,0 +1,267 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const inquirer = require('inquirer');
/**
* Codex setup handler (supports both CLI and Web)
* Creates comprehensive AGENTS.md file in project root
*/
class CodexSetup extends BaseIdeSetup {
constructor() {
super('codex', 'Codex', true); // preferred IDE
this.agentsFile = 'AGENTS.md';
}
/**
* Collect configuration choices before installation
* @param {Object} options - Configuration options
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const response = await inquirer.prompt([
{
type: 'list',
name: 'mode',
message: 'Select Codex deployment mode:',
choices: [
{ name: 'CLI (Command-line interface)', value: 'cli' },
{ name: 'Web (Browser-based interface)', value: 'web' },
],
default: 'cli',
},
]);
return { codexMode: response.mode };
}
/**
* Setup Codex configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Use pre-collected configuration if available
const config = options.preCollectedConfig || {};
const mode = config.codexMode || options.codexMode || 'cli';
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Create AGENTS.md content
const content = this.createAgentsDocument(agents, tasks, mode);
// Write AGENTS.md file
const agentsPath = path.join(projectDir, this.agentsFile);
await this.writeFile(agentsPath, content);
// Handle mode-specific setup
if (mode === 'web') {
await this.setupWebMode(projectDir);
} else {
await this.setupCliMode(projectDir);
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - Mode: ${mode === 'web' ? 'Web' : 'CLI'}`));
console.log(chalk.dim(` - ${agents.length} agents documented`));
console.log(chalk.dim(` - ${tasks.length} tasks documented`));
console.log(chalk.dim(` - Agents file: ${this.agentsFile}`));
return {
success: true,
mode,
agents: agents.length,
tasks: tasks.length,
};
}
/**
* Select Codex mode (CLI or Web)
*/
async selectMode() {
const response = await inquirer.prompt([
{
type: 'list',
name: 'mode',
message: 'Select Codex deployment mode:',
choices: [
{ name: 'CLI (Command-line interface)', value: 'cli' },
{ name: 'Web (Browser-based interface)', value: 'web' },
],
default: 'cli',
},
]);
return response.mode;
}
/**
* Create comprehensive agents document
*/
createAgentsDocument(agents, tasks, mode) {
let content = `# BMAD Method - Agent Directory
This document contains all available BMAD agents and tasks for use with Codex ${mode === 'web' ? 'Web' : 'CLI'}.
## Quick Start
${
mode === 'web'
? `Access agents through the web interface:
1. Navigate to the Agents section
2. Select an agent to activate
3. The agent persona will be active for your session`
: `Activate agents in CLI:
1. Reference agents using \`@{agent-name}\`
2. Execute tasks using \`@task-{task-name}\`
3. Agents remain active for the conversation`
}
---
## Available Agents
`;
// Group agents by module
const agentsByModule = {};
for (const agent of agents) {
if (!agentsByModule[agent.module]) {
agentsByModule[agent.module] = [];
}
agentsByModule[agent.module].push(agent);
}
// Document each module's agents
for (const [module, moduleAgents] of Object.entries(agentsByModule)) {
content += `### ${module.toUpperCase()} Module\n\n`;
for (const agent of moduleAgents) {
const agentContent = this.readFileSync(agent.path);
const titleMatch = agentContent.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
const iconMatch = agentContent.match(/icon="([^"]+)"/);
const icon = iconMatch ? iconMatch[1] : '🤖';
const whenToUseMatch = agentContent.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
content += `#### ${icon} ${title} (\`@${agent.name}\`)\n\n`;
content += `**When to use:** ${whenToUse}\n\n`;
content += `**Activation:** Type \`@${agent.name}\` to activate this agent.\n\n`;
}
}
content += `---
## Available Tasks
`;
// Group tasks by module
const tasksByModule = {};
for (const task of tasks) {
if (!tasksByModule[task.module]) {
tasksByModule[task.module] = [];
}
tasksByModule[task.module].push(task);
}
// Document each module's tasks
for (const [module, moduleTasks] of Object.entries(tasksByModule)) {
content += `### ${module.toUpperCase()} Module Tasks\n\n`;
for (const task of moduleTasks) {
const taskContent = this.readFileSync(task.path);
const nameMatch = taskContent.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
content += `- **${taskName}** (\`@task-${task.name}\`)\n`;
}
content += '\n';
}
content += `---
## Usage Guidelines
1. **One agent at a time**: Activate a single agent for focused assistance
2. **Task execution**: Tasks are one-time workflows, not persistent personas
3. **Module organization**: Agents and tasks are grouped by their source module
4. **Context preservation**: ${mode === 'web' ? 'Sessions maintain agent context' : 'Conversations maintain agent context'}
---
*Generated by BMAD Method installer for Codex ${mode === 'web' ? 'Web' : 'CLI'}*
`;
return content;
}
/**
* Read file synchronously (for document generation)
*/
readFileSync(filePath) {
const fs = require('node:fs');
try {
return fs.readFileSync(filePath, 'utf8');
} catch {
return '';
}
}
/**
* Setup for CLI mode
*/
async setupCliMode(projectDir) {
// CLI mode - ensure .gitignore includes AGENTS.md if needed
const fs = require('fs-extra');
const gitignorePath = path.join(projectDir, '.gitignore');
if (await fs.pathExists(gitignorePath)) {
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
if (!gitignoreContent.includes('AGENTS.md')) {
// User can decide whether to track this file
console.log(chalk.dim(' Note: Consider adding AGENTS.md to .gitignore if desired'));
}
}
}
/**
* Setup for Web mode
*/
async setupWebMode(projectDir) {
// Web mode - add to .gitignore to avoid committing
const fs = require('fs-extra');
const gitignorePath = path.join(projectDir, '.gitignore');
if (await fs.pathExists(gitignorePath)) {
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
if (!gitignoreContent.includes('AGENTS.md')) {
await fs.appendFile(gitignorePath, '\n# Codex Web agents file\nAGENTS.md\n');
console.log(chalk.dim(' Added AGENTS.md to .gitignore for web deployment'));
}
}
}
/**
* Cleanup Codex configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const agentsPath = path.join(projectDir, this.agentsFile);
if (await fs.pathExists(agentsPath)) {
await fs.remove(agentsPath);
console.log(chalk.dim('Removed AGENTS.md file'));
}
}
}
module.exports = { CodexSetup };

View File

@@ -0,0 +1,204 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* Crush IDE setup handler
* Creates commands in .crush/commands/ directory structure
*/
class CrushSetup extends BaseIdeSetup {
constructor() {
super('crush', 'Crush');
this.configDir = '.crush';
this.commandsDir = 'commands';
}
/**
* Setup Crush IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .crush/commands/bmad directory structure
const crushDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(crushDir, this.commandsDir, 'bmad');
const agentsDir = path.join(commandsDir, 'agents');
const tasksDir = path.join(commandsDir, 'tasks');
await this.ensureDir(agentsDir);
await this.ensureDir(tasksDir);
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Setup agents as commands
let agentCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const commandContent = this.createAgentCommand(agent, content, projectDir);
const targetPath = path.join(agentsDir, `${agent.module}-${agent.name}.md`);
await this.writeFile(targetPath, commandContent);
agentCount++;
}
// Setup tasks as commands
let taskCount = 0;
for (const task of tasks) {
const content = await this.readFile(task.path);
const commandContent = this.createTaskCommand(task, content);
const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`);
await this.writeFile(targetPath, commandContent);
taskCount++;
}
// Create module-specific subdirectories for better organization
await this.organizeByModule(commandsDir, agents, tasks, bmadDir);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agent commands created`));
console.log(chalk.dim(` - ${taskCount} task commands created`));
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
console.log(chalk.dim('\n Commands can be accessed via Crush command palette'));
return {
success: true,
agents: agentCount,
tasks: taskCount,
};
}
/**
* Organize commands by module
*/
async organizeByModule(commandsDir, agents, tasks, bmadDir) {
// Get unique modules
const modules = new Set();
for (const agent of agents) modules.add(agent.module);
for (const task of tasks) modules.add(task.module);
// Create module directories
for (const module of modules) {
const moduleDir = path.join(commandsDir, module);
const moduleAgentsDir = path.join(moduleDir, 'agents');
const moduleTasksDir = path.join(moduleDir, 'tasks');
await this.ensureDir(moduleAgentsDir);
await this.ensureDir(moduleTasksDir);
// Copy module-specific agents
const moduleAgents = agents.filter((a) => a.module === module);
for (const agent of moduleAgents) {
const content = await this.readFile(agent.path);
const commandContent = this.createAgentCommand(agent, content, bmadDir);
const targetPath = path.join(moduleAgentsDir, `${agent.name}.md`);
await this.writeFile(targetPath, commandContent);
}
// Copy module-specific tasks
const moduleTasks = tasks.filter((t) => t.module === module);
for (const task of moduleTasks) {
const content = await this.readFile(task.path);
const commandContent = this.createTaskCommand(task, content);
const targetPath = path.join(moduleTasksDir, `${task.name}.md`);
await this.writeFile(targetPath, commandContent);
}
}
}
/**
* Create agent command content
*/
createAgentCommand(agent, content, projectDir) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
const iconMatch = content.match(/icon="([^"]+)"/);
const icon = iconMatch ? iconMatch[1] : '🤖';
// Get relative path
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
let commandContent = `# /${agent.name} Command
When this command is used, adopt the following agent persona:
## ${icon} ${title} Agent
${content}
## Command Usage
This command activates the ${title} agent from the BMAD ${agent.module.toUpperCase()} module.
## File Reference
Complete agent definition: [${relativePath}](${relativePath})
## Module
Part of the BMAD ${agent.module.toUpperCase()} module.
`;
return commandContent;
}
/**
* Create task command content
*/
createTaskCommand(task, content) {
// Extract task name
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
let commandContent = `# /task-${task.name} Command
When this command is used, execute the following task:
## ${taskName} Task
${content}
## Command Usage
This command executes the ${taskName} task from the BMAD ${task.module.toUpperCase()} module.
## Module
Part of the BMAD ${task.module.toUpperCase()} module.
`;
return commandContent;
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup Crush configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
if (await fs.pathExists(bmadCommandsDir)) {
await fs.remove(bmadCommandsDir);
console.log(chalk.dim(`Removed BMAD commands from Crush`));
}
}
}
module.exports = { CrushSetup };

View File

@@ -0,0 +1,224 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* Cursor IDE setup handler
*/
class CursorSetup extends BaseIdeSetup {
constructor() {
super('cursor', 'Cursor', true); // preferred IDE
this.configDir = '.cursor';
this.rulesDir = 'rules';
}
/**
* Setup Cursor IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .cursor/rules directory structure
const cursorDir = path.join(projectDir, this.configDir);
const rulesDir = path.join(cursorDir, this.rulesDir);
const bmadRulesDir = path.join(rulesDir, 'bmad');
await this.ensureDir(bmadRulesDir);
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Create directories for each module
const modules = new Set();
for (const item of [...agents, ...tasks]) modules.add(item.module);
for (const module of modules) {
await this.ensureDir(path.join(bmadRulesDir, module));
await this.ensureDir(path.join(bmadRulesDir, module, 'agents'));
await this.ensureDir(path.join(bmadRulesDir, module, 'tasks'));
}
// Process and copy agents
let agentCount = 0;
for (const agent of agents) {
const content = await this.readAndProcess(agent.path, {
module: agent.module,
name: agent.name,
});
const targetPath = path.join(bmadRulesDir, agent.module, 'agents', `${agent.name}.mdc`);
await this.writeFile(targetPath, content);
agentCount++;
}
// Process and copy tasks
let taskCount = 0;
for (const task of tasks) {
const content = await this.readAndProcess(task.path, {
module: task.module,
name: task.name,
});
const targetPath = path.join(bmadRulesDir, task.module, 'tasks', `${task.name}.mdc`);
await this.writeFile(targetPath, content);
taskCount++;
}
// Create BMAD index file (but NOT .cursorrules - user manages that)
await this.createBMADIndex(bmadRulesDir, agents, tasks, modules);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`));
console.log(chalk.dim(` - ${taskCount} tasks installed`));
console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, bmadRulesDir)}`));
return {
success: true,
agents: agentCount,
tasks: taskCount,
};
}
/**
* Create BMAD index file for easy navigation
*/
async createBMADIndex(bmadRulesDir, agents, tasks, modules) {
const indexPath = path.join(bmadRulesDir, 'index.mdc');
let content = `---
description: BMAD Method - Master Index
globs:
alwaysApply: true
---
# BMAD Method - Cursor Rules Index
This is the master index for all BMAD agents and tasks available in your project.
## Installation Complete!
BMAD rules have been installed to: \`.cursor/rules/bmad/\`
**Note:** BMAD does not modify your \`.cursorrules\` file. You manage that separately.
## How to Use
- Reference specific agents: @bmad/{module}/agents/{agent-name}
- Reference specific tasks: @bmad/{module}/tasks/{task-name}
- Reference entire modules: @bmad/{module}
- Reference this index: @bmad/index
## Available Modules
`;
for (const module of modules) {
content += `### ${module.toUpperCase()}\n\n`;
// List agents for this module
const moduleAgents = agents.filter((a) => a.module === module);
if (moduleAgents.length > 0) {
content += `**Agents:**\n`;
for (const agent of moduleAgents) {
content += `- @bmad/${module}/agents/${agent.name} - ${agent.name}\n`;
}
content += '\n';
}
// List tasks for this module
const moduleTasks = tasks.filter((t) => t.module === module);
if (moduleTasks.length > 0) {
content += `**Tasks:**\n`;
for (const task of moduleTasks) {
content += `- @bmad/${module}/tasks/${task.name} - ${task.name}\n`;
}
content += '\n';
}
}
content += `
## Quick Reference
- All BMAD rules are Manual type - reference them explicitly when needed
- Agents provide persona-based assistance with specific expertise
- Tasks are reusable workflows for common operations
- Each agent includes an activation block for proper initialization
## Configuration
BMAD rules are configured as Manual rules (alwaysApply: false) to give you control
over when they're included in your context. Reference them explicitly when you need
specific agent expertise or task workflows.
`;
await this.writeFile(indexPath, content);
}
/**
* Read and process file content
*/
async readAndProcess(filePath, metadata) {
const fs = require('fs-extra');
const content = await fs.readFile(filePath, 'utf8');
return this.processContent(content, metadata);
}
/**
* Override processContent to add MDC metadata header for Cursor
* @param {string} content - File content
* @param {Object} metadata - File metadata
* @returns {string} Processed content with MDC header
*/
processContent(content, metadata = {}) {
// First apply base processing (includes activation injection for agents)
let processed = super.processContent(content, metadata);
// Determine the type and description based on content
const isAgent = content.includes('<agent');
const isTask = content.includes('<task');
let description = '';
let globs = '';
if (isAgent) {
// Extract agent title if available
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
// Manual rules for agents don't need globs
globs = '';
} else if (isTask) {
// Extract task name if available
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`;
// Tasks might be auto-attached to certain file types
globs = '';
} else {
description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`;
globs = '';
}
// Create MDC metadata header
const mdcHeader = `---
description: ${description}
globs: ${globs}
alwaysApply: false
---
`;
// Add the MDC header to the processed content
return mdcHeader + processed;
}
}
module.exports = { CursorSetup };

View File

@@ -0,0 +1,160 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* Gemini CLI setup handler
* Creates TOML files in .gemini/commands/ structure
*/
class GeminiSetup extends BaseIdeSetup {
constructor() {
super('gemini', 'Gemini CLI', true); // preferred IDE
this.configDir = '.gemini';
this.commandsDir = 'commands';
}
/**
* Setup Gemini CLI configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .gemini/commands/agents and .gemini/commands/tasks directories
const geminiDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(geminiDir, this.commandsDir);
const agentsDir = path.join(commandsDir, 'agents');
const tasksDir = path.join(commandsDir, 'tasks');
await this.ensureDir(agentsDir);
await this.ensureDir(tasksDir);
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Install agents as TOML files
let agentCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const tomlContent = this.createAgentToml(agent, content, bmadDir);
const tomlPath = path.join(agentsDir, `${agent.name}.toml`);
await this.writeFile(tomlPath, tomlContent);
agentCount++;
console.log(chalk.green(` ✓ Added agent: /bmad:agents:${agent.name}`));
}
// Install tasks as TOML files
let taskCount = 0;
for (const task of tasks) {
const content = await this.readFile(task.path);
const tomlContent = this.createTaskToml(task, content, bmadDir);
const tomlPath = path.join(tasksDir, `${task.name}.toml`);
await this.writeFile(tomlPath, tomlContent);
taskCount++;
console.log(chalk.green(` ✓ Added task: /bmad:tasks:${task.name}`));
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`));
console.log(chalk.dim(` - ${taskCount} tasks configured`));
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
console.log(chalk.dim(` - Agent activation: /bmad:agents:{agent-name}`));
console.log(chalk.dim(` - Task activation: /bmad:tasks:{task-name}`));
return {
success: true,
agents: agentCount,
tasks: taskCount,
};
}
/**
* Create agent TOML content
*/
createAgentToml(agent, content, bmadDir) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
// Get relative path from project root to agent file
const relativePath = path.relative(process.cwd(), agent.path).replaceAll('\\', '/');
// Create TOML content
const tomlContent = `description = "Activates the ${title} agent from the BMad Method."
prompt = """
CRITICAL: You are now the BMad '${title}' agent. Adopt its persona and capabilities as defined in the following configuration.
Read and internalize the full agent definition, following all instructions and maintaining this persona until explicitly told to switch or exit.
@${relativePath}
"""
`;
return tomlContent;
}
/**
* Create task TOML content
*/
createTaskToml(task, content, bmadDir) {
// Extract task name from XML if available
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
// Get relative path from project root to task file
const relativePath = path.relative(process.cwd(), task.path).replaceAll('\\', '/');
// Create TOML content
const tomlContent = `description = "Executes the ${taskName} task from the BMad Method."
prompt = """
Execute the following BMad Method task workflow:
@${relativePath}
Follow all instructions and complete the task as defined.
"""
`;
return tomlContent;
}
/**
* Cleanup Gemini configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
const agentsDir = path.join(commandsDir, 'agents');
const tasksDir = path.join(commandsDir, 'tasks');
// Remove BMAD TOML files
if (await fs.pathExists(agentsDir)) {
const files = await fs.readdir(agentsDir);
for (const file of files) {
if (file.endsWith('.toml')) {
await fs.remove(path.join(agentsDir, file));
}
}
}
if (await fs.pathExists(tasksDir)) {
const files = await fs.readdir(tasksDir);
for (const file of files) {
if (file.endsWith('.toml')) {
await fs.remove(path.join(tasksDir, file));
}
}
}
console.log(chalk.dim(`Removed BMAD configuration from Gemini CLI`));
}
}
module.exports = { GeminiSetup };

View File

@@ -0,0 +1,289 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const inquirer = require('inquirer');
/**
* GitHub Copilot setup handler
* Creates chat modes in .github/chatmodes/ and configures VS Code settings
*/
class GitHubCopilotSetup extends BaseIdeSetup {
constructor() {
super('github-copilot', 'GitHub Copilot', true); // preferred IDE
this.configDir = '.github';
this.chatmodesDir = 'chatmodes';
this.vscodeDir = '.vscode';
}
/**
* Collect configuration choices before installation
* @param {Object} options - Configuration options
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const config = {};
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
const response = await inquirer.prompt([
{
type: 'list',
name: 'configChoice',
message: 'How would you like to configure VS Code settings?',
choices: [
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
{ name: 'Configure each setting manually', value: 'manual' },
{ name: 'Skip settings configuration', value: 'skip' },
],
default: 'defaults',
},
]);
config.vsCodeConfig = response.configChoice;
if (response.configChoice === 'manual') {
config.manualSettings = await inquirer.prompt([
{
type: 'input',
name: 'maxRequests',
message: 'Maximum requests per session (1-50)?',
default: '15',
validate: (input) => {
const num = parseInt(input);
return (num >= 1 && num <= 50) || 'Enter 1-50';
},
},
{
type: 'confirm',
name: 'runTasks',
message: 'Allow running workspace tasks?',
default: true,
},
{
type: 'confirm',
name: 'mcpDiscovery',
message: 'Enable MCP server discovery?',
default: true,
},
{
type: 'confirm',
name: 'autoFix',
message: 'Enable automatic error fixing?',
default: true,
},
{
type: 'confirm',
name: 'autoApprove',
message: 'Auto-approve tools (less secure)?',
default: false,
},
]);
}
return config;
}
/**
* Setup GitHub Copilot configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Configure VS Code settings using pre-collected config if available
const config = options.preCollectedConfig || {};
await this.configureVsCodeSettings(projectDir, { ...options, ...config });
// Create .github/chatmodes directory
const githubDir = path.join(projectDir, this.configDir);
const chatmodesDir = path.join(githubDir, this.chatmodesDir);
await this.ensureDir(chatmodesDir);
// Get agents
const agents = await this.getAgents(bmadDir);
// Create chat mode files
let modeCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const chatmodeContent = this.createChatmodeContent(agent, content);
const targetPath = path.join(chatmodesDir, `${agent.module}-${agent.name}.chatmode.md`);
await this.writeFile(targetPath, chatmodeContent);
modeCount++;
console.log(chalk.green(` ✓ Created chat mode: ${agent.module}-${agent.name}`));
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${modeCount} chat modes created`));
console.log(chalk.dim(` - Chat modes directory: ${path.relative(projectDir, chatmodesDir)}`));
console.log(chalk.dim(` - VS Code settings configured`));
console.log(chalk.dim('\n Chat modes available in VS Code Chat view'));
return {
success: true,
chatmodes: modeCount,
settings: true,
};
}
/**
* Configure VS Code settings for GitHub Copilot
*/
async configureVsCodeSettings(projectDir, options) {
const fs = require('fs-extra');
const vscodeDir = path.join(projectDir, this.vscodeDir);
const settingsPath = path.join(vscodeDir, 'settings.json');
await this.ensureDir(vscodeDir);
// Read existing settings
let existingSettings = {};
if (await fs.pathExists(settingsPath)) {
try {
const content = await fs.readFile(settingsPath, 'utf8');
existingSettings = JSON.parse(content);
console.log(chalk.yellow(' Found existing .vscode/settings.json'));
} catch {
console.warn(chalk.yellow(' Could not parse settings.json, creating new'));
}
}
// Use pre-collected configuration or skip if not available
let configChoice = options.vsCodeConfig;
if (!configChoice) {
// If no pre-collected config, skip configuration
console.log(chalk.yellow(' ⚠ No configuration collected, skipping VS Code settings'));
return;
}
if (configChoice === 'skip') {
console.log(chalk.yellow(' ⚠ Skipping VS Code settings'));
return;
}
let bmadSettings = {};
if (configChoice === 'defaults') {
bmadSettings = {
'chat.agent.enabled': true,
'chat.agent.maxRequests': 15,
'github.copilot.chat.agent.runTasks': true,
'chat.mcp.discovery.enabled': true,
'github.copilot.chat.agent.autoFix': true,
'chat.tools.autoApprove': false,
};
console.log(chalk.green(' ✓ Using recommended defaults'));
} else {
// Manual configuration - use pre-collected settings
const manual = options.manualSettings || {};
bmadSettings = {
'chat.agent.enabled': true,
'chat.agent.maxRequests': parseInt(manual.maxRequests || 15),
'github.copilot.chat.agent.runTasks': manual.runTasks === undefined ? true : manual.runTasks,
'chat.mcp.discovery.enabled': manual.mcpDiscovery === undefined ? true : manual.mcpDiscovery,
'github.copilot.chat.agent.autoFix': manual.autoFix === undefined ? true : manual.autoFix,
'chat.tools.autoApprove': manual.autoApprove || false,
};
}
// Merge settings (existing take precedence)
const mergedSettings = { ...bmadSettings, ...existingSettings };
// Write settings
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2));
console.log(chalk.green(' ✓ VS Code settings configured'));
}
/**
* Create chat mode content
*/
createChatmodeContent(agent, content) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
const description = whenToUseMatch ? whenToUseMatch[1] : `Activates the ${title} agent persona.`;
// Available GitHub Copilot tools
const tools = [
'changes',
'codebase',
'fetch',
'findTestFiles',
'githubRepo',
'problems',
'usages',
'editFiles',
'runCommands',
'runTasks',
'runTests',
'search',
'searchResults',
'terminalLastCommand',
'terminalSelection',
'testFailure',
];
let chatmodeContent = `---
description: "${description.replaceAll('"', String.raw`\"`)}"
tools: ${JSON.stringify(tools)}
---
# ${title} Agent
${content}
## Module
Part of the BMAD ${agent.module.toUpperCase()} module.
`;
return chatmodeContent;
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup GitHub Copilot configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const chatmodesDir = path.join(projectDir, this.configDir, this.chatmodesDir);
if (await fs.pathExists(chatmodesDir)) {
// Remove BMAD chat modes
const files = await fs.readdir(chatmodesDir);
let removed = 0;
for (const file of files) {
if (file.endsWith('.chatmode.md')) {
const filePath = path.join(chatmodesDir, file);
const content = await fs.readFile(filePath, 'utf8');
if (content.includes('BMAD') && content.includes('Module')) {
await fs.remove(filePath);
removed++;
}
}
}
console.log(chalk.dim(`Removed ${removed} BMAD chat modes from GitHub Copilot`));
}
}
}
module.exports = { GitHubCopilotSetup };

View File

@@ -0,0 +1,142 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* iFlow CLI setup handler
* Creates commands in .iflow/commands/ directory structure
*/
class IFlowSetup extends BaseIdeSetup {
constructor() {
super('iflow', 'iFlow CLI');
this.configDir = '.iflow';
this.commandsDir = 'commands';
}
/**
* Setup iFlow CLI configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .iflow/commands/bmad directory structure
const iflowDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
const agentsDir = path.join(commandsDir, 'agents');
const tasksDir = path.join(commandsDir, 'tasks');
await this.ensureDir(agentsDir);
await this.ensureDir(tasksDir);
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Setup agents as commands
let agentCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const commandContent = this.createAgentCommand(agent, content);
const targetPath = path.join(agentsDir, `${agent.module}-${agent.name}.md`);
await this.writeFile(targetPath, commandContent);
agentCount++;
}
// Setup tasks as commands
let taskCount = 0;
for (const task of tasks) {
const content = await this.readFile(task.path);
const commandContent = this.createTaskCommand(task, content);
const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`);
await this.writeFile(targetPath, commandContent);
taskCount++;
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agent commands created`));
console.log(chalk.dim(` - ${taskCount} task commands created`));
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
return {
success: true,
agents: agentCount,
tasks: taskCount,
};
}
/**
* Create agent command content
*/
createAgentCommand(agent, content) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
let commandContent = `# /${agent.name} Command
When this command is used, adopt the following agent persona:
## ${title} Agent
${content}
## Usage
This command activates the ${title} agent from the BMAD ${agent.module.toUpperCase()} module.
## Module
Part of the BMAD ${agent.module.toUpperCase()} module.
`;
return commandContent;
}
/**
* Create task command content
*/
createTaskCommand(task, content) {
// Extract task name
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
let commandContent = `# /task-${task.name} Command
When this command is used, execute the following task:
## ${taskName} Task
${content}
## Usage
This command executes the ${taskName} task from the BMAD ${task.module.toUpperCase()} module.
## Module
Part of the BMAD ${task.module.toUpperCase()} module.
`;
return commandContent;
}
/**
* Cleanup iFlow configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
if (await fs.pathExists(bmadCommandsDir)) {
await fs.remove(bmadCommandsDir);
console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`));
}
}
}
module.exports = { IFlowSetup };

View File

@@ -0,0 +1,171 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* KiloCode IDE setup handler
* Creates custom modes in .kilocodemodes file (similar to Roo)
*/
class KiloSetup extends BaseIdeSetup {
constructor() {
super('kilo', 'Kilo Code');
this.configFile = '.kilocodemodes';
}
/**
* Setup KiloCode IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Check for existing .kilocodemodes file
const kiloModesPath = path.join(projectDir, this.configFile);
let existingModes = [];
let existingContent = '';
if (await this.pathExists(kiloModesPath)) {
existingContent = await this.readFile(kiloModesPath);
// Parse existing modes
const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g);
for (const match of modeMatches) {
existingModes.push(match[1]);
}
console.log(chalk.yellow(`Found existing .kilocodemodes file with ${existingModes.length} modes`));
}
// Get agents
const agents = await this.getAgents(bmadDir);
// Create modes content
let newModesContent = '';
let addedCount = 0;
let skippedCount = 0;
for (const agent of agents) {
const slug = `bmad-${agent.module}-${agent.name}`;
// Skip if already exists
if (existingModes.includes(slug)) {
console.log(chalk.dim(` Skipping ${slug} - already exists`));
skippedCount++;
continue;
}
const content = await this.readFile(agent.path);
const modeEntry = this.createModeEntry(agent, content, projectDir);
newModesContent += modeEntry;
addedCount++;
}
// Build final content
let finalContent = '';
if (existingContent) {
finalContent = existingContent.trim() + '\n' + newModesContent;
} else {
finalContent = 'customModes:\n' + newModesContent;
}
// Write .kilocodemodes file
await this.writeFile(kiloModesPath, finalContent);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${addedCount} modes added`));
if (skippedCount > 0) {
console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`));
}
console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode'));
return {
success: true,
modes: addedCount,
skipped: skippedCount,
};
}
/**
* Create a mode entry for an agent
*/
createModeEntry(agent, content, projectDir) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
const iconMatch = content.match(/icon="([^"]+)"/);
const icon = iconMatch ? iconMatch[1] : '🤖';
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
const roleDefinitionMatch = content.match(/roleDefinition="([^"]+)"/);
const roleDefinition = roleDefinitionMatch
? roleDefinitionMatch[1]
: `You are a ${title} specializing in ${title.toLowerCase()} tasks.`;
// Get relative path
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
// Build mode entry (KiloCode uses same schema as Roo)
const slug = `bmad-${agent.module}-${agent.name}`;
let modeEntry = ` - slug: ${slug}\n`;
modeEntry += ` name: '${icon} ${title}'\n`;
modeEntry += ` roleDefinition: ${roleDefinition}\n`;
modeEntry += ` whenToUse: ${whenToUse}\n`;
modeEntry += ` customInstructions: CRITICAL Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
modeEntry += ` groups:\n`;
modeEntry += ` - read\n`;
modeEntry += ` - edit\n`;
return modeEntry;
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup KiloCode configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const kiloModesPath = path.join(projectDir, this.configFile);
if (await fs.pathExists(kiloModesPath)) {
const content = await fs.readFile(kiloModesPath, 'utf8');
// Remove BMAD modes only
const lines = content.split('\n');
const filteredLines = [];
let skipMode = false;
let removedCount = 0;
for (const line of lines) {
if (/^\s*- slug: bmad-/.test(line)) {
skipMode = true;
removedCount++;
} else if (skipMode && /^\s*- slug: /.test(line)) {
skipMode = false;
}
if (!skipMode) {
filteredLines.push(line);
}
}
await fs.writeFile(kiloModesPath, filteredLines.join('\n'));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
}
}
}
module.exports = { KiloSetup };

View File

@@ -0,0 +1,203 @@
const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
/**
* IDE Manager - handles IDE-specific setup
* Dynamically discovers and loads IDE handlers
*/
class IdeManager {
constructor() {
this.handlers = new Map();
this.loadHandlers();
}
/**
* Dynamically load all IDE handlers from directory
*/
loadHandlers() {
const ideDir = __dirname;
try {
// Get all JS files in the IDE directory
const files = fs.readdirSync(ideDir).filter((file) => {
// Skip base class, manager, utility files (starting with _), and helper modules
return file.endsWith('.js') && !file.startsWith('_') && file !== 'manager.js' && file !== 'workflow-command-generator.js';
});
// Sort alphabetically for consistent ordering
files.sort();
for (const file of files) {
const moduleName = path.basename(file, '.js');
try {
const modulePath = path.join(ideDir, file);
const HandlerModule = require(modulePath);
// Get the first exported class (handles various export styles)
const HandlerClass = HandlerModule.default || HandlerModule[Object.keys(HandlerModule)[0]];
if (HandlerClass) {
const instance = new HandlerClass();
// Use the name property from the instance (set in constructor)
this.handlers.set(instance.name, instance);
}
} catch (error) {
console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`));
}
}
} catch (error) {
console.error(chalk.red('Failed to load IDE handlers:'), error.message);
}
}
/**
* Get all available IDEs with their metadata
* @returns {Array} Array of IDE information objects
*/
getAvailableIdes() {
const ides = [];
for (const [key, handler] of this.handlers) {
ides.push({
value: key,
name: handler.displayName || handler.name || key,
preferred: handler.preferred || false,
});
}
// Sort: preferred first, then alphabetical
ides.sort((a, b) => {
if (a.preferred && !b.preferred) return -1;
if (!a.preferred && b.preferred) return 1;
// Ensure both names exist before comparing
const nameA = a.name || '';
const nameB = b.name || '';
return nameA.localeCompare(nameB);
});
return ides;
}
/**
* Get preferred IDEs
* @returns {Array} Array of preferred IDE information
*/
getPreferredIdes() {
return this.getAvailableIdes().filter((ide) => ide.preferred);
}
/**
* Get non-preferred IDEs
* @returns {Array} Array of non-preferred IDE information
*/
getOtherIdes() {
return this.getAvailableIdes().filter((ide) => !ide.preferred);
}
/**
* Setup IDE configuration
* @param {string} ideName - Name of the IDE
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(ideName, projectDir, bmadDir, options = {}) {
const handler = this.handlers.get(ideName.toLowerCase());
if (!handler) {
console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported`));
console.log(chalk.dim('Supported IDEs:', [...this.handlers.keys()].join(', ')));
return { success: false, reason: 'unsupported' };
}
try {
await handler.setup(projectDir, bmadDir, options);
return { success: true, ide: ideName };
} catch (error) {
console.error(chalk.red(`Failed to setup ${ideName}:`), error.message);
return { success: false, error: error.message };
}
}
/**
* Cleanup IDE configurations
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir) {
const results = [];
for (const [name, handler] of this.handlers) {
try {
await handler.cleanup(projectDir);
results.push({ ide: name, success: true });
} catch (error) {
results.push({ ide: name, success: false, error: error.message });
}
}
return results;
}
/**
* Get list of supported IDEs
* @returns {Array} List of supported IDE names
*/
getSupportedIdes() {
return [...this.handlers.keys()];
}
/**
* Check if an IDE is supported
* @param {string} ideName - Name of the IDE
* @returns {boolean} True if IDE is supported
*/
isSupported(ideName) {
return this.handlers.has(ideName.toLowerCase());
}
/**
* Detect installed IDEs
* @param {string} projectDir - Project directory
* @returns {Array} List of detected IDEs
*/
async detectInstalledIdes(projectDir) {
const detected = [];
// Check for IDE-specific directories
const ideChecks = {
cursor: '.cursor',
'claude-code': '.claude',
windsurf: '.windsurf',
cline: '.clinerules',
roo: '.roomodes',
trae: '.trae',
kilo: '.kilocodemodes',
gemini: '.gemini',
qwen: '.qwen',
crush: '.crush',
iflow: '.iflow',
auggie: '.auggie',
'github-copilot': '.github/chatmodes',
vscode: '.vscode',
idea: '.idea',
};
for (const [ide, dir] of Object.entries(ideChecks)) {
const idePath = path.join(projectDir, dir);
if (await fs.pathExists(idePath)) {
detected.push(ide);
}
}
// Check for AGENTS.md (Codex)
if (await fs.pathExists(path.join(projectDir, 'AGENTS.md'))) {
detected.push('codex');
}
return detected;
}
}
module.exports = { IdeManager };

View File

@@ -0,0 +1,188 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* Qwen Code setup handler
* Creates concatenated QWEN.md file in .qwen/bmad-method/ (similar to Gemini)
*/
class QwenSetup extends BaseIdeSetup {
constructor() {
super('qwen', 'Qwen Code');
this.configDir = '.qwen';
this.bmadDir = 'bmad-method';
}
/**
* Setup Qwen Code configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .qwen/bmad-method directory
const qwenDir = path.join(projectDir, this.configDir);
const bmadMethodDir = path.join(qwenDir, this.bmadDir);
await this.ensureDir(bmadMethodDir);
// Update existing settings.json if present
await this.updateSettings(qwenDir);
// Clean up old agents directory if exists
await this.cleanupOldAgents(qwenDir);
// Get agents
const agents = await this.getAgents(bmadDir);
// Create concatenated content for all agents
let concatenatedContent = `# BMAD Method - Qwen Code Configuration
This file contains all BMAD agents configured for use with Qwen Code.
Agents can be activated by typing \`*{agent-name}\` in your prompts.
---
`;
let agentCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const agentSection = this.createAgentSection(agent, content, projectDir);
concatenatedContent += agentSection;
concatenatedContent += '\n\n---\n\n';
agentCount++;
console.log(chalk.green(` ✓ Added agent: *${agent.name}`));
}
// Write QWEN.md
const qwenMdPath = path.join(bmadMethodDir, 'QWEN.md');
await this.writeFile(qwenMdPath, concatenatedContent);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`));
console.log(chalk.dim(` - Configuration file: ${path.relative(projectDir, qwenMdPath)}`));
console.log(chalk.dim(` - Agents activated with: *{agent-name}`));
return {
success: true,
agents: agentCount,
};
}
/**
* Update settings.json to remove old agent references
*/
async updateSettings(qwenDir) {
const fs = require('fs-extra');
const settingsPath = path.join(qwenDir, 'settings.json');
if (await fs.pathExists(settingsPath)) {
try {
const settingsContent = await fs.readFile(settingsPath, 'utf8');
const settings = JSON.parse(settingsContent);
let updated = false;
// Remove agent file references from contextFileName
if (settings.contextFileName && Array.isArray(settings.contextFileName)) {
const originalLength = settings.contextFileName.length;
settings.contextFileName = settings.contextFileName.filter((fileName) => !fileName.startsWith('agents/'));
if (settings.contextFileName.length !== originalLength) {
updated = true;
}
}
if (updated) {
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
console.log(chalk.green(' ✓ Updated .qwen/settings.json'));
}
} catch (error) {
console.warn(chalk.yellow(' ⚠ Could not update settings.json:'), error.message);
}
}
}
/**
* Clean up old agents directory
*/
async cleanupOldAgents(qwenDir) {
const fs = require('fs-extra');
const agentsDir = path.join(qwenDir, 'agents');
if (await fs.pathExists(agentsDir)) {
await fs.remove(agentsDir);
console.log(chalk.green(' ✓ Removed old agents directory'));
}
}
/**
* Create agent section for concatenated file
*/
createAgentSection(agent, content, projectDir) {
// Extract metadata
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
// Extract YAML content
const yamlMatch = content.match(/```ya?ml\r?\n([\s\S]*?)```/);
const yamlContent = yamlMatch ? yamlMatch[1] : content;
// Get relative path
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
let section = `# ${agent.name.toUpperCase()} Agent Rule
This rule is triggered when the user types \`*${agent.name}\` and activates the ${title} agent persona.
## Agent Activation
CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:
\`\`\`yaml
${yamlContent}
\`\`\`
## File Reference
The complete agent definition is available in [${relativePath}](${relativePath}).
## Usage
When the user types \`*${agent.name}\`, activate this ${title} persona and follow all instructions defined in the YAML configuration above.
## Module
Part of the BMAD ${agent.module.toUpperCase()} module.`;
return section;
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup Qwen configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const bmadMethodDir = path.join(projectDir, this.configDir, this.bmadDir);
if (await fs.pathExists(bmadMethodDir)) {
await fs.remove(bmadMethodDir);
console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`));
}
}
}
module.exports = { QwenSetup };

View File

@@ -0,0 +1,288 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const inquirer = require('inquirer');
/**
* Roo IDE setup handler
* Creates custom modes in .roomodes file
*/
class RooSetup extends BaseIdeSetup {
constructor() {
super('roo', 'Roo Code');
this.configFile = '.roomodes';
this.defaultPermissions = {
dev: {
description: 'Development files',
fileRegex: String.raw`.*\.(js|jsx|ts|tsx|py|java|cpp|c|h|cs|go|rs|php|rb|swift)$`,
},
config: {
description: 'Configuration files',
fileRegex: String.raw`.*\.(json|yaml|yml|toml|xml|ini|env|config)$`,
},
docs: {
description: 'Documentation files',
fileRegex: String.raw`.*\.(md|mdx|rst|txt|doc|docx)$`,
},
styles: {
description: 'Style and design files',
fileRegex: String.raw`.*\.(css|scss|sass|less|stylus)$`,
},
all: {
description: 'All files',
fileRegex: '.*',
},
};
}
/**
* Collect configuration choices before installation
* @param {Object} options - Configuration options
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const response = await inquirer.prompt([
{
type: 'list',
name: 'permissions',
message: 'Select default file edit permissions for BMAD agents:',
choices: [
{ name: 'Development files only (js, ts, py, etc.)', value: 'dev' },
{ name: 'Configuration files only (json, yaml, xml, etc.)', value: 'config' },
{ name: 'Documentation files only (md, txt, doc, etc.)', value: 'docs' },
{ name: 'All files (unrestricted access)', value: 'all' },
{ name: 'Custom per agent (will be configured individually)', value: 'custom' },
],
default: 'dev',
},
]);
return { permissions: response.permissions };
}
/**
* Setup Roo IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Check for existing .roomodes file
const roomodesPath = path.join(projectDir, this.configFile);
let existingModes = [];
let existingContent = '';
if (await this.pathExists(roomodesPath)) {
existingContent = await this.readFile(roomodesPath);
// Parse existing modes to avoid duplicates
const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g);
for (const match of modeMatches) {
existingModes.push(match[1]);
}
console.log(chalk.yellow(`Found existing .roomodes file with ${existingModes.length} modes`));
}
// Get agents
const agents = await this.getAgents(bmadDir);
// Use pre-collected configuration if available
const config = options.preCollectedConfig || {};
let permissionChoice = config.permissions || options.permissions || 'dev';
// Create modes content
let newModesContent = '';
let addedCount = 0;
let skippedCount = 0;
for (const agent of agents) {
const slug = `bmad-${agent.module}-${agent.name}`;
// Skip if already exists
if (existingModes.includes(slug)) {
console.log(chalk.dim(` Skipping ${slug} - already exists`));
skippedCount++;
continue;
}
const content = await this.readFile(agent.path);
const modeEntry = this.createModeEntry(agent, content, permissionChoice, projectDir);
newModesContent += modeEntry;
addedCount++;
console.log(chalk.green(` ✓ Added mode: ${slug}`));
}
// Build final content
let finalContent = '';
if (existingContent) {
// Append to existing content
finalContent = existingContent.trim() + '\n' + newModesContent;
} else {
// Create new .roomodes file
finalContent = 'customModes:\n' + newModesContent;
}
// Write .roomodes file
await this.writeFile(roomodesPath, finalContent);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${addedCount} modes added`));
if (skippedCount > 0) {
console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`));
}
console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim(` - Permission level: ${permissionChoice}`));
console.log(chalk.dim('\n Modes will be available when you open this project in Roo Code'));
return {
success: true,
modes: addedCount,
skipped: skippedCount,
};
}
/**
* Ask user about permission configuration
*/
async askPermissions() {
const response = await inquirer.prompt([
{
type: 'list',
name: 'permissions',
message: 'Select default file edit permissions for BMAD agents:',
choices: [
{ name: 'Development files only (js, ts, py, etc.)', value: 'dev' },
{ name: 'Configuration files only (json, yaml, xml, etc.)', value: 'config' },
{ name: 'Documentation files only (md, txt, doc, etc.)', value: 'docs' },
{ name: 'All files (unrestricted access)', value: 'all' },
{ name: 'Custom per agent (will be configured individually)', value: 'custom' },
],
default: 'dev',
},
]);
return response.permissions;
}
/**
* Create a mode entry for an agent
*/
createModeEntry(agent, content, permissionChoice, projectDir) {
// Extract metadata from agent content
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
const iconMatch = content.match(/icon="([^"]+)"/);
const icon = iconMatch ? iconMatch[1] : '🤖';
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
const roleDefinitionMatch = content.match(/roleDefinition="([^"]+)"/);
const roleDefinition = roleDefinitionMatch
? roleDefinitionMatch[1]
: `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`;
// Get relative path
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
// Determine permissions
const permissions = this.getPermissionsForAgent(agent, permissionChoice);
// Build mode entry
const slug = `bmad-${agent.module}-${agent.name}`;
let modeEntry = ` - slug: ${slug}\n`;
modeEntry += ` name: '${icon} ${title}'\n`;
if (permissions && permissions.description) {
modeEntry += ` description: '${permissions.description}'\n`;
}
modeEntry += ` roleDefinition: ${roleDefinition}\n`;
modeEntry += ` whenToUse: ${whenToUse}\n`;
modeEntry += ` customInstructions: CRITICAL Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
modeEntry += ` groups:\n`;
modeEntry += ` - read\n`;
if (permissions && permissions.fileRegex) {
modeEntry += ` - - edit\n`;
modeEntry += ` - fileRegex: ${permissions.fileRegex}\n`;
modeEntry += ` description: ${permissions.description}\n`;
} else {
modeEntry += ` - edit\n`;
}
return modeEntry;
}
/**
* Get permissions configuration for an agent
*/
getPermissionsForAgent(agent, permissionChoice) {
if (permissionChoice === 'custom') {
// Custom logic based on agent name/module
if (agent.name.includes('dev') || agent.name.includes('code')) {
return this.defaultPermissions.dev;
} else if (agent.name.includes('doc') || agent.name.includes('write')) {
return this.defaultPermissions.docs;
} else if (agent.name.includes('config') || agent.name.includes('setup')) {
return this.defaultPermissions.config;
} else if (agent.name.includes('style') || agent.name.includes('css')) {
return this.defaultPermissions.styles;
}
// Default to all for custom agents
return this.defaultPermissions.all;
}
return this.defaultPermissions[permissionChoice] || null;
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup Roo configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const roomodesPath = path.join(projectDir, this.configFile);
if (await fs.pathExists(roomodesPath)) {
const content = await fs.readFile(roomodesPath, 'utf8');
// Remove BMAD modes only
const lines = content.split('\n');
const filteredLines = [];
let skipMode = false;
let removedCount = 0;
for (const line of lines) {
if (/^\s*- slug: bmad-/.test(line)) {
skipMode = true;
removedCount++;
} else if (skipMode && /^\s*- slug: /.test(line)) {
skipMode = false;
}
if (!skipMode) {
filteredLines.push(line);
}
}
// Write back filtered content
await fs.writeFile(roomodesPath, filteredLines.join('\n'));
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .roomodes`));
}
}
}
module.exports = { RooSetup };

View File

@@ -0,0 +1,182 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* Trae IDE setup handler
*/
class TraeSetup extends BaseIdeSetup {
constructor() {
super('trae', 'Trae');
this.configDir = '.trae';
this.rulesDir = 'rules';
}
/**
* Setup Trae IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .trae/rules directory
const traeDir = path.join(projectDir, this.configDir);
const rulesDir = path.join(traeDir, this.rulesDir);
await this.ensureDir(rulesDir);
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Process agents as rules
let ruleCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const processedContent = this.createAgentRule(agent, content, bmadDir, projectDir);
const targetPath = path.join(rulesDir, `${agent.module}-${agent.name}.md`);
await this.writeFile(targetPath, processedContent);
ruleCount++;
}
// Process tasks as rules
for (const task of tasks) {
const content = await this.readFile(task.path);
const processedContent = this.createTaskRule(task, content);
const targetPath = path.join(rulesDir, `task-${task.module}-${task.name}.md`);
await this.writeFile(targetPath, processedContent);
ruleCount++;
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${ruleCount} rules created`));
console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, rulesDir)}`));
console.log(chalk.dim(` - Agents can be activated with @{agent-name}`));
return {
success: true,
rules: ruleCount,
};
}
/**
* Create rule content for an agent
*/
createAgentRule(agent, content, bmadDir, projectDir) {
// Extract metadata from agent content
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
const iconMatch = content.match(/icon="([^"]+)"/);
const icon = iconMatch ? iconMatch[1] : '🤖';
// Extract YAML content if available
const yamlMatch = content.match(/```ya?ml\r?\n([\s\S]*?)```/);
const yamlContent = yamlMatch ? yamlMatch[1] : content;
// Calculate relative path for reference
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
let ruleContent = `# ${title} Agent Rule
This rule is triggered when the user types \`@${agent.name}\` and activates the ${title} agent persona.
## Agent Activation
CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:
\`\`\`yaml
${yamlContent}
\`\`\`
## File Reference
The complete agent definition is available in [${relativePath}](${relativePath}).
## Usage
When the user types \`@${agent.name}\`, activate this ${title} persona and follow all instructions defined in the YAML configuration above.
## Module
Part of the BMAD ${agent.module.toUpperCase()} module.
`;
return ruleContent;
}
/**
* Create rule content for a task
*/
createTaskRule(task, content) {
// Extract task name from content
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
let ruleContent = `# ${taskName} Task Rule
This rule defines the ${taskName} task workflow.
## Task Definition
When this task is triggered, execute the following workflow:
${content}
## Usage
Reference this task with \`@task-${task.name}\` to execute the defined workflow.
## Module
Part of the BMAD ${task.module.toUpperCase()} module.
`;
return ruleContent;
}
/**
* Format agent/task name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Cleanup Trae configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const rulesPath = path.join(projectDir, this.configDir, this.rulesDir);
if (await fs.pathExists(rulesPath)) {
// Only remove BMAD rules
const files = await fs.readdir(rulesPath);
let removed = 0;
for (const file of files) {
if (file.endsWith('.md')) {
const filePath = path.join(rulesPath, file);
const content = await fs.readFile(filePath, 'utf8');
// Check if it's a BMAD rule
if (content.includes('BMAD') && content.includes('module')) {
await fs.remove(filePath);
removed++;
}
}
}
console.log(chalk.dim(`Removed ${removed} BMAD rules from Trae`));
}
}
}
module.exports = { TraeSetup };

View File

@@ -0,0 +1,149 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
/**
* Windsurf IDE setup handler
*/
class WindsurfSetup extends BaseIdeSetup {
constructor() {
super('windsurf', 'Windsurf', true); // preferred IDE
this.configDir = '.windsurf';
this.workflowsDir = 'workflows';
}
/**
* Setup Windsurf IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .windsurf/workflows directory structure
const windsurfDir = path.join(projectDir, this.configDir);
const workflowsDir = path.join(windsurfDir, this.workflowsDir);
await this.ensureDir(workflowsDir);
// Get agents and tasks
const agents = await this.getAgents(bmadDir);
const tasks = await this.getTasks(bmadDir);
// Create directories for each module
const modules = new Set();
for (const item of [...agents, ...tasks]) modules.add(item.module);
for (const module of modules) {
await this.ensureDir(path.join(workflowsDir, module));
await this.ensureDir(path.join(workflowsDir, module, 'agents'));
await this.ensureDir(path.join(workflowsDir, module, 'tasks'));
}
// Process agents as workflows with organized structure
let agentCount = 0;
for (const agent of agents) {
const content = await this.readFile(agent.path);
const processedContent = this.createWorkflowContent(agent, content);
// Organized path: module/agents/agent-name.md
const targetPath = path.join(workflowsDir, agent.module, 'agents', `${agent.name}.md`);
await this.writeFile(targetPath, processedContent);
agentCount++;
}
// Process tasks as workflows with organized structure
let taskCount = 0;
for (const task of tasks) {
const content = await this.readFile(task.path);
const processedContent = this.createTaskWorkflowContent(task, content);
// Organized path: module/tasks/task-name.md
const targetPath = path.join(workflowsDir, task.module, 'tasks', `${task.name}.md`);
await this.writeFile(targetPath, processedContent);
taskCount++;
}
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`));
console.log(chalk.dim(` - ${taskCount} tasks installed`));
console.log(chalk.dim(` - Organized in modules: ${[...modules].join(', ')}`));
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`));
// Provide additional configuration hints
if (options.showHints !== false) {
console.log(chalk.dim('\n Windsurf workflow settings:'));
console.log(chalk.dim(' - auto_execution_mode: 3 (recommended for agents)'));
console.log(chalk.dim(' - auto_execution_mode: 2 (recommended for tasks)'));
console.log(chalk.dim(' - Workflows can be triggered via the Windsurf menu'));
}
return {
success: true,
agents: agentCount,
tasks: taskCount,
};
}
/**
* Create workflow content for an agent
*/
createWorkflowContent(agent, content) {
// Create simple Windsurf frontmatter matching original format
let workflowContent = `---
description: ${agent.name}
auto_execution_mode: 3
---
${content}`;
return workflowContent;
}
/**
* Create workflow content for a task
*/
createTaskWorkflowContent(task, content) {
// Create simple Windsurf frontmatter matching original format
let workflowContent = `---
description: task-${task.name}
auto_execution_mode: 2
---
${content}`;
return workflowContent;
}
/**
* Cleanup Windsurf configuration
*/
async cleanup(projectDir) {
const fs = require('fs-extra');
const windsurfPath = path.join(projectDir, this.configDir, this.workflowsDir);
if (await fs.pathExists(windsurfPath)) {
// Only remove BMAD workflows, not all workflows
const files = await fs.readdir(windsurfPath);
let removed = 0;
for (const file of files) {
if (file.includes('-') && file.endsWith('.md')) {
const filePath = path.join(windsurfPath, file);
const content = await fs.readFile(filePath, 'utf8');
// Check if it's a BMAD workflow
if (content.includes('tags: [bmad')) {
await fs.remove(filePath);
removed++;
}
}
}
console.log(chalk.dim(`Removed ${removed} BMAD workflows from Windsurf`));
}
}
}
module.exports = { WindsurfSetup };

View File

@@ -0,0 +1,162 @@
const path = require('node:path');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const chalk = require('chalk');
/**
* Generates Claude Code command files for each workflow in the manifest
*/
class WorkflowCommandGenerator {
constructor() {
this.templatePath = path.join(__dirname, 'workflow-command-template.md');
}
/**
* Generate workflow commands from the manifest CSV
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
*/
async generateWorkflowCommands(projectDir, bmadDir) {
const manifestPath = path.join(bmadDir, '_cfg', 'workflow-manifest.csv');
if (!(await fs.pathExists(manifestPath))) {
console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.'));
return { generated: 0 };
}
// Read and parse the CSV manifest
const csvContent = await fs.readFile(manifestPath, 'utf8');
const workflows = csv.parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
// Base commands directory
const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad');
let generatedCount = 0;
// Generate a command file for each workflow, organized by module
for (const workflow of workflows) {
// Create module directory structure: commands/bmad/{module}/workflows/
const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
await fs.ensureDir(moduleWorkflowsDir);
// Use just the workflow name as filename (no prefix)
const commandContent = await this.generateCommandContent(workflow, bmadDir);
const commandPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`);
await fs.writeFile(commandPath, commandContent);
generatedCount++;
}
// Also create a workflow launcher README in each module
await this.createModuleWorkflowLaunchers(baseCommandsDir, workflows, bmadDir);
return { generated: generatedCount };
}
/**
* Generate command content for a workflow
*/
async generateCommandContent(workflow, bmadDir) {
// Load the template
const template = await fs.readFile(this.templatePath, 'utf8');
// Convert source path to installed path
// From: /Users/.../src/modules/bmm/workflows/.../workflow.yaml
// To: {project-root}/bmad/bmm/workflows/.../workflow.yaml
let workflowPath = workflow.path;
// Extract the relative path from source
if (workflowPath.includes('/src/modules/')) {
const match = workflowPath.match(/\/src\/modules\/(.+)/);
if (match) {
workflowPath = `{project-root}/bmad/${match[1]}`;
}
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
workflowPath = `{project-root}/bmad/core/${match[1]}`;
}
}
// Replace template variables
return template
.replaceAll('{{name}}', workflow.name)
.replaceAll('{{module}}', workflow.module)
.replaceAll('{{description}}', workflow.description)
.replaceAll('{{workflow_path}}', workflowPath)
.replaceAll('{{interactive}}', workflow.interactive)
.replaceAll('{{author}}', workflow.author || 'BMAD');
}
/**
* Create workflow launcher files for each module
*/
async createModuleWorkflowLaunchers(baseCommandsDir, workflows, bmadDir) {
// Group workflows by module
const workflowsByModule = {};
for (const workflow of workflows) {
if (!workflowsByModule[workflow.module]) {
workflowsByModule[workflow.module] = [];
}
// Convert path for display
let workflowPath = workflow.path;
if (workflowPath.includes('/src/modules/')) {
const match = workflowPath.match(/\/src\/modules\/(.+)/);
if (match) {
workflowPath = `{project-root}/bmad/${match[1]}`;
}
} else if (workflowPath.includes('/src/core/')) {
const match = workflowPath.match(/\/src\/core\/(.+)/);
if (match) {
workflowPath = `{project-root}/bmad/core/${match[1]}`;
}
}
workflowsByModule[workflow.module].push({
...workflow,
displayPath: workflowPath,
});
}
// Create a launcher file for each module
for (const [module, moduleWorkflows] of Object.entries(workflowsByModule)) {
let content = `# ${module.toUpperCase()} Workflows
## Available Workflows in ${module}
`;
for (const workflow of moduleWorkflows) {
content += `**${workflow.name}**\n`;
content += `- Path: \`${workflow.displayPath}\`\n`;
content += `- ${workflow.description}\n\n`;
}
content += `
## Execution
When running any workflow:
1. LOAD {project-root}/bmad/core/tasks/workflow.md
2. Pass the workflow path as 'workflow-config' parameter
3. Follow workflow.md instructions EXACTLY
4. Save outputs after EACH section
## Modes
- Normal: Full interaction
- #yolo: Skip optional steps
`;
// Write module-specific launcher
const moduleWorkflowsDir = path.join(baseCommandsDir, module, 'workflows');
await fs.ensureDir(moduleWorkflowsDir);
const launcherPath = path.join(moduleWorkflowsDir, 'README.md');
await fs.writeFile(launcherPath, content);
}
}
}
module.exports = { WorkflowCommandGenerator };

View File

@@ -0,0 +1,11 @@
# {{name}}
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 {project-root}/bmad/core/tasks/workflow.md
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.md instructions
4. Follow workflow.md instructions EXACTLY as written
5. Save outputs after EACH section when generating any documents from templates
</steps>