claude cline codex installers use central function

This commit is contained in:
Brian Madison
2026-01-24 03:02:59 -06:00
parent 9b8ce69f37
commit 7074395bdd
8 changed files with 490 additions and 478 deletions

View File

@@ -150,9 +150,10 @@ class AntigravitySetup extends BaseIdeSetup {
// Write workflow-command artifacts with FLATTENED naming using shared utility // Write workflow-command artifacts with FLATTENED naming using shared utility
const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts); const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts);
// Generate task and tool commands from manifests (if they exist) // Generate task and tool commands using FLAT naming (not nested!)
// Use the new generateDashTaskToolCommands method with explicit target directory
const taskToolGen = new TaskToolCommandGenerator(); const taskToolGen = new TaskToolCommandGenerator();
const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir); const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, bmadWorkflowsDir);
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`)); console.log(chalk.dim(` - ${agentCount} agents installed`));

View File

@@ -3,9 +3,7 @@ const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { const {
loadModuleInjectionConfig, loadModuleInjectionConfig,
shouldApplyInjection, shouldApplyInjection,
@@ -18,10 +16,14 @@ const prompts = require('../../../lib/prompts');
/** /**
* Claude Code IDE setup handler * Claude Code IDE setup handler
*
* Uses UnifiedInstaller for standard artifact installation,
* plus Claude-specific subagent injection handling.
*/ */
console.log(`[DEBUG CLAUDE-CODE] Module loaded!`);
class ClaudeCodeSetup extends BaseIdeSetup { class ClaudeCodeSetup extends BaseIdeSetup {
constructor() { constructor() {
super('claude-code', 'Claude Code', true); // preferred IDE super('claude-code', 'Claude Code', true);
this.configDir = '.claude'; this.configDir = '.claude';
this.commandsDir = 'commands'; this.commandsDir = 'commands';
this.agentsDir = 'agents'; this.agentsDir = 'agents';
@@ -29,7 +31,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
/** /**
* Prompt for subagent installation location * Prompt for subagent installation location
* @returns {Promise<string>} Selected location ('project' or 'user')
*/ */
async promptInstallLocation() { async promptInstallLocation() {
return prompts.select({ return prompts.select({
@@ -42,57 +43,20 @@ class ClaudeCodeSetup extends BaseIdeSetup {
}); });
} }
// /**
// * 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 yaml = require('yaml');
// try {
// // Load injection configuration
// const configContent = await fs.readFile(injectionConfigPath, 'utf8');
// const injectionConfig = yaml.parse(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') {
// config.installLocation = await this.promptInstallLocation();
// }
// }
// } catch (error) {
// console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
// }
// }
// }
// return config;
// }
/** /**
* Cleanup old BMAD installation before reinstalling * Cleanup old BMAD installation before reinstalling
* @param {string} projectDir - Project directory
*/ */
async cleanup(projectDir) { async cleanup(projectDir) {
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats) // Remove ANY bmad folder or files at any level
const bmadPath = path.join(commandsDir, 'bmad');
if (await fs.pathExists(bmadPath)) {
await fs.remove(bmadPath);
console.log(chalk.dim(` Removed old bmad folder from ${this.name}`));
}
// Also remove any bmad* files at root level
if (await fs.pathExists(commandsDir)) { if (await fs.pathExists(commandsDir)) {
const entries = await fs.readdir(commandsDir); const entries = await fs.readdir(commandsDir);
let removedCount = 0; let removedCount = 0;
@@ -102,72 +66,41 @@ class ClaudeCodeSetup extends BaseIdeSetup {
removedCount++; removedCount++;
} }
} }
// Also remove legacy bmad folder if it exists
const bmadFolder = path.join(commandsDir, 'bmad');
if (await fs.pathExists(bmadFolder)) {
await fs.remove(bmadFolder);
console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`));
}
}
}
/**
* Clean up legacy folder structure (module/type/name.md) if it exists
* This can be called after migration to remove old nested directories
* @param {string} projectDir - Project directory
*/
async cleanupLegacyFolders(projectDir) {
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
if (!(await fs.pathExists(commandsDir))) {
return;
}
// Remove legacy bmad folder if it exists
const bmadFolder = path.join(commandsDir, 'bmad');
if (await fs.pathExists(bmadFolder)) {
await fs.remove(bmadFolder);
console.log(chalk.dim(` Removed legacy bmad folder from ${this.name}`));
} }
} }
/** /**
* Setup Claude Code IDE configuration * 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 = {}) { async setup(projectDir, bmadDir, options = {}) {
// Store project directory for use in processContent console.log(`[DEBUG CLAUDE-CODE] setup called! projectDir=${projectDir}`);
this.projectDir = projectDir; this.projectDir = projectDir;
console.log(chalk.cyan(`Setting up ${this.name}...`)); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Clean up old BMAD installation first
await this.cleanup(projectDir); await this.cleanup(projectDir);
// Create .claude/commands directory structure
const claudeDir = path.join(projectDir, this.configDir); const claudeDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(claudeDir, this.commandsDir); const commandsDir = path.join(claudeDir, this.commandsDir);
await this.ensureDir(commandsDir); await this.ensureDir(commandsDir);
// Use underscore format: files written directly to commands dir (no bmad subfolder) // Use the unified installer for standard artifacts
// Creates: .claude/commands/bmad_bmm_pm.md const installer = new UnifiedInstaller(this.bmadFolderName);
console.log(`[DEBUG CLAUDE-CODE] About to call installer.install, targetDir=${commandsDir}`);
// Generate agent launchers using AgentCommandGenerator const counts = await installer.install(
// This creates small launcher files that reference the actual agents in _bmad/ projectDir,
const agentGen = new AgentCommandGenerator(this.bmadFolderName); bmadDir,
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); {
targetDir: commandsDir,
// Write agent launcher files using flat underscore naming namingStyle: NamingStyle.FLAT_COLON,
// Creates files like: bmad_bmm_pm.md templateType: TemplateType.CLAUDE,
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts); },
options.selectedModules || [],
);
console.log(`[DEBUG CLAUDE-CODE] installer.install done, counts=`, counts);
// Process Claude Code specific injections for installed modules // Process Claude Code specific injections for installed modules
// Use pre-collected configuration if available, or skip if already configured
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) { if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
// IDE is already configured from previous installation, skip prompting
// Just process with default/existing configuration
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {}); await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {});
} else if (options.preCollectedConfig) { } else if (options.preCollectedConfig) {
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig); await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
@@ -175,43 +108,24 @@ class ClaudeCodeSetup extends BaseIdeSetup {
await this.processModuleInjections(projectDir, bmadDir, options); 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(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
// Write workflow-command artifacts using flat underscore naming
// Creates files like: bmad_bmm_correct-course.md
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
// Generate task and tool commands from manifests (if they exist)
const taskToolGen = new TaskToolCommandGenerator();
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`)); console.log(chalk.dim(` - ${counts.agents} agents installed`));
if (workflowCommandCount > 0) { if (counts.workflows > 0) {
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`)); console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
} }
if (taskToolResult.generated > 0) { if (counts.tasks + counts.tools > 0) {
console.log( console.log(
chalk.dim( chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
),
); );
} }
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
return { return {
success: true, success: true,
agents: agentCount, agents: counts.agents,
}; };
} }
// Method removed - CLAUDE.md file management left to user
/** /**
* Read and process file content * Read and process file content
*/ */
@@ -224,7 +138,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
* Override processContent to keep {project-root} placeholder * Override processContent to keep {project-root} placeholder
*/ */
processContent(content, metadata = {}) { processContent(content, metadata = {}) {
// Use the base class method WITHOUT projectDir to preserve {project-root} placeholder
return super.processContent(content, metadata); return super.processContent(content, metadata);
} }
@@ -234,14 +147,12 @@ class ClaudeCodeSetup extends BaseIdeSetup {
async getAgentsFromSource(sourceDir, selectedModules) { async getAgentsFromSource(sourceDir, selectedModules) {
const agents = []; const agents = [];
// Add core agents
const corePath = getModulePath('core'); const corePath = getModulePath('core');
if (await fs.pathExists(path.join(corePath, 'agents'))) { if (await fs.pathExists(path.join(corePath, 'agents'))) {
const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core'); const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core');
agents.push(...coreAgents); agents.push(...coreAgents);
} }
// Add module agents
for (const moduleName of selectedModules) { for (const moduleName of selectedModules) {
const modulePath = path.join(sourceDir, moduleName); const modulePath = path.join(sourceDir, moduleName);
const agentsPath = path.join(modulePath, 'agents'); const agentsPath = path.join(modulePath, 'agents');
@@ -259,11 +170,9 @@ class ClaudeCodeSetup extends BaseIdeSetup {
* Process module injections with pre-collected configuration * Process module injections with pre-collected configuration
*/ */
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) { async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
// Get list of installed modules
const modules = options.selectedModules || []; const modules = options.selectedModules || [];
const { subagentChoices, installLocation } = preCollectedConfig; const { subagentChoices, installLocation } = preCollectedConfig;
// Get the actual source directory (not the installation directory)
await this.processModuleInjectionsInternal({ await this.processModuleInjectionsInternal({
projectDir, projectDir,
modules, modules,
@@ -276,15 +185,12 @@ class ClaudeCodeSetup extends BaseIdeSetup {
/** /**
* Process Claude Code specific injections for installed modules * 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) { async processModuleInjections(projectDir, bmadDir, options) {
// Get list of installed modules
const modules = options.selectedModules || []; const modules = options.selectedModules || [];
let subagentChoices = null; let subagentChoices = null;
let installLocation = null; let installLocation = null;
// Get the actual source directory (not the installation directory)
const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({
projectDir, projectDir,
modules, modules,
@@ -303,6 +209,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
} }
async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) { async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) {
console.log(`[DEBUG CLAUDE-CODE] processModuleInjectionsInternal called! modules=${modules.join(',')}`);
let choices = subagentChoices; let choices = subagentChoices;
let location = installLocation; let location = installLocation;
@@ -346,7 +253,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
* Prompt user for subagent installation preferences * Prompt user for subagent installation preferences
*/ */
async promptSubagentInstallation(subagentConfig) { async promptSubagentInstallation(subagentConfig) {
// First ask if they want to install subagents
const install = await prompts.select({ const install = await prompts.select({
message: 'Would you like to install Claude Code subagents for enhanced functionality?', message: 'Would you like to install Claude Code subagents for enhanced functionality?',
choices: [ choices: [
@@ -358,7 +264,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
}); });
if (install === 'selective') { if (install === 'selective') {
// Show list of available subagents with descriptions
const subagentInfo = { const subagentInfo = {
'market-researcher.md': 'Market research and competitive analysis', 'market-researcher.md': 'Market research and competitive analysis',
'requirements-analyst.md': 'Requirements extraction and validation', 'requirements-analyst.md': 'Requirements extraction and validation',
@@ -395,7 +300,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
if (content.includes(marker)) { if (content.includes(marker)) {
let injectionContent = injection.content; let injectionContent = injection.content;
// Filter content if selective subagents chosen
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') { if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected); injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected);
} }
@@ -413,7 +317,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) { async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) {
const os = require('node:os'); const os = require('node:os');
// Determine target directory based on user choice
let targetDir; let targetDir;
if (location === 'user') { if (location === 'user') {
targetDir = path.join(os.homedir(), '.claude', 'agents'); targetDir = path.join(os.homedir(), '.claude', 'agents');
@@ -423,7 +326,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
console.log(chalk.dim(` Installing subagents to project: .claude/agents/`)); console.log(chalk.dim(` Installing subagents to project: .claude/agents/`));
} }
// Ensure target directory exists
await this.ensureDir(targetDir); await this.ensureDir(targetDir);
const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices);
@@ -458,17 +360,12 @@ class ClaudeCodeSetup extends BaseIdeSetup {
/** /**
* Install a custom agent launcher for Claude Code * Install a custom agent launcher for Claude Code
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created command
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
if (!(await this.exists(path.join(projectDir, this.configDir)))) { if (!(await this.exists(path.join(projectDir, this.configDir)))) {
return null; // IDE not configured for this project return null;
} }
await this.ensureDir(commandsDir); await this.ensureDir(commandsDir);
@@ -490,8 +387,6 @@ You must fully embody this agent's persona and follow all activation instruction
</agent-activation> </agent-activation>
`; `;
// Use underscore format: bmad_custom_fred-commit-poet.md
// Written directly to commands dir (no bmad subfolder)
const launcherName = customAgentColonName(agentName); const launcherName = customAgentColonName(agentName);
const launcherPath = path.join(commandsDir, launcherName); const launcherPath = path.join(commandsDir, launcherName);
await this.writeFile(launcherPath, launcherContent); await this.writeFile(launcherPath, launcherContent);

View File

@@ -123,6 +123,7 @@ class ClineSetup extends BaseIdeSetup {
artifacts.push({ artifacts.push({
type: 'task', type: 'task',
module: task.module, module: task.module,
path: task.path,
sourcePath: task.path, sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`), relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content, content,

View File

@@ -3,25 +3,22 @@ const fs = require('fs-extra');
const os = require('node:os'); const os = require('node:os');
const chalk = require('chalk'); const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { customAgentDashName } = require('./shared/path-utils');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
/** /**
* Codex setup handler (CLI mode) * Codex setup handler (CLI mode)
*
* Uses UnifiedInstaller for all artifact installation.
*/ */
class CodexSetup extends BaseIdeSetup { class CodexSetup extends BaseIdeSetup {
constructor() { constructor() {
super('codex', 'Codex', true); // preferred IDE super('codex', 'Codex', true);
} }
/** /**
* Collect configuration choices before installation * Collect configuration choices before installation
* @param {Object} options - Configuration options
* @returns {Object} Collected configuration
*/ */
async collectConfiguration(options = {}) { async collectConfiguration(options = {}) {
let confirmed = false; let confirmed = false;
@@ -43,7 +40,6 @@ class CodexSetup extends BaseIdeSetup {
default: 'global', default: 'global',
}); });
// Display detailed instructions for the chosen option
console.log(''); console.log('');
if (installLocation === 'project') { if (installLocation === 'project') {
console.log(this.getProjectSpecificInstructions()); console.log(this.getProjectSpecificInstructions());
@@ -51,7 +47,6 @@ class CodexSetup extends BaseIdeSetup {
console.log(this.getGlobalInstructions()); console.log(this.getGlobalInstructions());
} }
// Confirm the choice
confirmed = await prompts.confirm({ confirmed = await prompts.confirm({
message: 'Proceed with this installation option?', message: 'Proceed with this installation option?',
default: true, default: true,
@@ -67,78 +62,48 @@ class CodexSetup extends BaseIdeSetup {
/** /**
* Setup Codex configuration * Setup Codex configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/ */
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`)); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Always use CLI mode
const mode = 'cli';
// Get installation location from pre-collected config or default to global
const installLocation = options.preCollectedConfig?.installLocation || 'global'; const installLocation = options.preCollectedConfig?.installLocation || 'global';
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
const destDir = this.getCodexPromptDir(projectDir, installLocation); const destDir = this.getCodexPromptDir(projectDir, installLocation);
await fs.ensureDir(destDir); await fs.ensureDir(destDir);
await this.clearOldBmadFiles(destDir); await this.clearOldBmadFiles(destDir);
// Collect artifacts and write using underscore format // Use the unified installer - so much simpler!
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const installer = new UnifiedInstaller(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const counts = await installer.install(
const agentCount = await agentGen.writeDashArtifacts(destDir, agentArtifacts); projectDir,
bmadDir,
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); {
const taskArtifacts = []; targetDir: destDir,
for (const task of tasks) { namingStyle: NamingStyle.FLAT_DASH,
const content = await this.readAndProcessWithProject( templateType: TemplateType.CODEX,
task.path, },
{ options.selectedModules || [],
module: task.module, );
name: task.name,
},
projectDir,
);
taskArtifacts.push({
type: 'task',
module: task.module,
sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content,
});
}
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
// Also write tasks using underscore format
const ttGen = new TaskToolCommandGenerator();
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
const written = agentCount + workflowCount + tasksWritten;
console.log(chalk.green(`${this.name} configured:`)); console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - Mode: CLI`)); console.log(chalk.dim(` - Mode: CLI`));
console.log(chalk.dim(` - ${counts.agents} agents exported`)); console.log(chalk.dim(` - ${counts.agents} agents installed`));
console.log(chalk.dim(` - ${counts.tasks} tasks exported`)); if (counts.workflows > 0) {
console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`)); console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
if (counts.workflowLaunchers > 0) {
console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`));
} }
console.log(chalk.dim(` - ${written} Codex prompt files written`)); if (counts.tasks + counts.tools > 0) {
console.log(
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
);
}
console.log(chalk.dim(` - ${counts.total} Codex prompt files written`));
console.log(chalk.dim(` - Destination: ${destDir}`)); console.log(chalk.dim(` - Destination: ${destDir}`));
return { return {
success: true, success: true,
mode, mode: 'cli',
artifacts, ...counts,
counts,
destination: destDir, destination: destDir,
written,
installLocation, installLocation,
}; };
} }
@@ -147,7 +112,6 @@ class CodexSetup extends BaseIdeSetup {
* Detect Codex installation by checking for BMAD prompt exports * Detect Codex installation by checking for BMAD prompt exports
*/ */
async detect(projectDir) { async detect(projectDir) {
// Check both global and project-specific locations
const globalDir = this.getCodexPromptDir(null, 'global'); const globalDir = this.getCodexPromptDir(null, 'global');
const projectDir_local = projectDir || process.cwd(); const projectDir_local = projectDir || process.cwd();
const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project'); const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project');
@@ -171,63 +135,6 @@ class CodexSetup extends BaseIdeSetup {
return false; return false;
} }
/**
* Collect Claude-style artifacts for Codex export.
* Returns the normalized artifact list for further processing.
*/
async collectClaudeArtifacts(projectDir, bmadDir, options = {}) {
const selectedModules = options.selectedModules || [];
const artifacts = [];
// Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
for (const artifact of agentArtifacts) {
artifacts.push({
type: 'agent',
module: artifact.module,
sourcePath: artifact.sourcePath,
relativePath: artifact.relativePath,
content: artifact.content,
});
}
const tasks = await getTasksFromBmad(bmadDir, selectedModules);
for (const task of tasks) {
const content = await this.readAndProcessWithProject(
task.path,
{
module: task.module,
name: task.name,
},
projectDir,
);
artifacts.push({
type: 'task',
module: task.module,
sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content,
});
}
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
artifacts.push(...workflowArtifacts);
return {
artifacts,
counts: {
agents: agentArtifacts.length,
tasks: tasks.length,
workflows: workflowCounts.commands,
workflowLaunchers: workflowCounts.launchers,
},
};
}
getCodexPromptDir(projectDir = null, location = 'global') { getCodexPromptDir(projectDir = null, location = 'global') {
if (location === 'project' && projectDir) { if (location === 'project' && projectDir) {
return path.join(projectDir, '.codex', 'prompts'); return path.join(projectDir, '.codex', 'prompts');
@@ -235,19 +142,6 @@ class CodexSetup extends BaseIdeSetup {
return path.join(os.homedir(), '.codex', 'prompts'); return path.join(os.homedir(), '.codex', 'prompts');
} }
async flattenAndWriteArtifacts(artifacts, destDir) {
let written = 0;
for (const artifact of artifacts) {
const flattenedName = this.flattenFilename(artifact.relativePath);
const targetPath = path.join(destDir, flattenedName);
await fs.writeFile(targetPath, artifact.content);
written++;
}
return written;
}
async clearOldBmadFiles(destDir) { async clearOldBmadFiles(destDir) {
if (!(await fs.pathExists(destDir))) { if (!(await fs.pathExists(destDir))) {
return; return;
@@ -270,16 +164,10 @@ class CodexSetup extends BaseIdeSetup {
} }
} }
async readAndProcessWithProject(filePath, metadata, projectDir) {
const content = await fs.readFile(filePath, 'utf8');
return super.processContent(content, metadata, projectDir);
}
/** /**
* Get instructions for global installation * Get instructions for global installation
* @returns {string} Instructions text
*/ */
getGlobalInstructions(destDir) { getGlobalInstructions() {
const lines = [ const lines = [
'', '',
chalk.bold.cyan('═'.repeat(70)), chalk.bold.cyan('═'.repeat(70)),
@@ -292,7 +180,7 @@ class CodexSetup extends BaseIdeSetup {
chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"), chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"),
'', '',
chalk.green(' ✓ You can now use /commands in Codex CLI'), chalk.green(' ✓ You can now use /commands in Codex CLI'),
chalk.dim(' Example: /bmad_bmm_pm'), chalk.dim(' Example: /bmad-bmm-pm'),
chalk.dim(' Type / to see all available commands'), chalk.dim(' Type / to see all available commands'),
'', '',
chalk.bold.cyan('═'.repeat(70)), chalk.bold.cyan('═'.repeat(70)),
@@ -303,11 +191,8 @@ class CodexSetup extends BaseIdeSetup {
/** /**
* Get instructions for project-specific installation * Get instructions for project-specific installation
* @param {string} projectDir - Optional project directory
* @param {string} destDir - Optional destination directory
* @returns {string} Instructions text
*/ */
getProjectSpecificInstructions(projectDir = null, destDir = null) { getProjectSpecificInstructions() {
const isWindows = os.platform() === 'win32'; const isWindows = os.platform() === 'win32';
const commonLines = [ const commonLines = [
@@ -316,7 +201,7 @@ class CodexSetup extends BaseIdeSetup {
chalk.bold.yellow(' Project-Specific Codex Configuration'), chalk.bold.yellow(' Project-Specific Codex Configuration'),
chalk.bold.cyan('═'.repeat(70)), chalk.bold.cyan('═'.repeat(70)),
'', '',
chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '<project>/.codex/prompts'), chalk.white(' Prompts will be installed to: ') + chalk.cyan('<project>/.codex/prompts'),
'', '',
chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'), chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'),
'', '',
@@ -350,7 +235,6 @@ class CodexSetup extends BaseIdeSetup {
]; ];
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines]; const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];
return lines.join('\n'); return lines.join('\n');
} }
@@ -358,7 +242,6 @@ class CodexSetup extends BaseIdeSetup {
* Cleanup Codex configuration * Cleanup Codex configuration
*/ */
async cleanup(projectDir = null) { async cleanup(projectDir = null) {
// Clean both global and project-specific locations
const globalDir = this.getCodexPromptDir(null, 'global'); const globalDir = this.getCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(globalDir); await this.clearOldBmadFiles(globalDir);
@@ -370,11 +253,6 @@ class CodexSetup extends BaseIdeSetup {
/** /**
* Install a custom agent launcher for Codex * Install a custom agent launcher for Codex
* @param {string} projectDir - Project directory (not used, Codex installs to home)
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created command
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const destDir = this.getCodexPromptDir(projectDir, 'project'); const destDir = this.getCodexPromptDir(projectDir, 'project');
@@ -397,7 +275,6 @@ You must fully embody this agent's persona and follow all activation instruction
</agent-activation> </agent-activation>
`; `;
// Use underscore format: bmad_custom_fred-commit-poet.md
const fileName = customAgentDashName(agentName); const fileName = customAgentDashName(agentName);
const launcherPath = path.join(destDir, fileName); const launcherPath = path.join(destDir, fileName);
await fs.writeFile(launcherPath, launcherContent, 'utf8'); await fs.writeFile(launcherPath, launcherContent, 'utf8');

View File

@@ -1,31 +1,77 @@
const path = require('node:path'); const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
const { customAgentColonName } = require('./shared/path-utils'); const { customAgentColonName } = require('./shared/path-utils');
/** /**
* Cursor IDE setup handler * Cursor IDE setup handler
*
* Uses the UnifiedInstaller - all the complex artifact collection
* and writing logic is now centralized.
*/ */
class CursorSetup extends BaseIdeSetup { class CursorSetup extends BaseIdeSetup {
constructor() { constructor() {
super('cursor', 'Cursor', true); // preferred IDE super('cursor', 'Cursor', true);
this.configDir = '.cursor'; this.configDir = '.cursor';
this.rulesDir = 'rules'; this.rulesDir = 'rules';
this.commandsDir = 'commands'; this.commandsDir = 'commands';
} }
/** /**
* Cleanup old BMAD installation before reinstalling * Setup Cursor IDE configuration
* @param {string} projectDir - Project directory */
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Clean up old BMAD installation first
await this.cleanup(projectDir);
// Create .cursor/commands directory
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
await this.ensureDir(commandsDir);
// Use the unified installer
const installer = new UnifiedInstaller(this.bmadFolderName);
const counts = await installer.install(
projectDir,
bmadDir,
{
targetDir: commandsDir,
namingStyle: NamingStyle.FLAT_COLON,
templateType: TemplateType.CURSOR,
},
options.selectedModules || [],
);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${counts.agents} agents installed`));
if (counts.workflows > 0) {
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
}
if (counts.tasks + counts.tools > 0) {
console.log(
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
);
}
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
return {
success: true,
agents: counts.agents,
tasks: counts.tasks,
tools: counts.tools,
workflows: counts.workflows,
};
}
/**
* Cleanup old BMAD installation
*/ */
async cleanup(projectDir) { async cleanup(projectDir) {
const fs = require('fs-extra'); const fs = require('fs-extra');
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
if (await fs.pathExists(commandsDir)) { if (await fs.pathExists(commandsDir)) {
const entries = await fs.readdir(commandsDir); const entries = await fs.readdir(commandsDir);
for (const entry of entries) { for (const entry of entries) {
@@ -42,88 +88,24 @@ class CursorSetup extends BaseIdeSetup {
} }
} }
/**
* 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}...`));
// Clean up old BMAD installation first
await this.cleanup(projectDir);
// Create .cursor/commands directory structure
const cursorDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(cursorDir, this.commandsDir);
await this.ensureDir(commandsDir);
// Use underscore format: files written directly to commands dir (no bmad subfolder)
// Creates: .cursor/commands/bmad_bmm_pm.md
// Generate agent launchers using AgentCommandGenerator
// This creates small launcher files that reference the actual agents in _bmad/
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Write agent launcher files using flat underscore naming
// Creates files like: bmad_bmm_pm.md
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
// Generate workflow commands from manifest (if it exists)
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
// Write workflow-command artifacts using flat underscore naming
// Creates files like: bmad_bmm_correct-course.md
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
// Generate task and tool commands from manifests (if they exist)
const taskToolGen = new TaskToolCommandGenerator();
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`));
if (workflowCommandCount > 0) {
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
}
if (taskToolResult.generated > 0) {
console.log(
chalk.dim(
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
),
);
}
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
return {
success: true,
agents: agentCount,
tasks: taskToolResult.tasks || 0,
tools: taskToolResult.tools || 0,
workflows: workflowCommandCount,
};
}
/** /**
* Install a custom agent launcher for Cursor * Install a custom agent launcher for Cursor
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created command
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
if (!(await this.exists(path.join(projectDir, this.configDir)))) { if (!(await this.exists(path.join(projectDir, this.configDir)))) {
return null; // IDE not configured for this project return null;
} }
await this.ensureDir(commandsDir); await this.ensureDir(commandsDir);
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. const launcherContent = `---
name: '${agentName}'
description: '${agentName} agent'
---
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
<agent-activation CRITICAL="TRUE"> <agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from @${agentPath} 1. LOAD the FULL agent file from @${agentPath}
@@ -135,20 +117,9 @@ class CursorSetup extends BaseIdeSetup {
</agent-activation> </agent-activation>
`; `;
// Cursor uses YAML frontmatter matching Claude Code format
const commandContent = `---
name: '${agentName}'
description: '${agentName} agent'
---
${launcherContent}
`;
// Use underscore format: bmad_custom_fred-commit-poet.md
// Written directly to commands dir (no bmad subfolder)
const launcherName = customAgentColonName(agentName); const launcherName = customAgentColonName(agentName);
const launcherPath = path.join(commandsDir, launcherName); const launcherPath = path.join(commandsDir, launcherName);
await this.writeFile(launcherPath, commandContent); await this.writeFile(launcherPath, launcherContent);
return { return {
path: launcherPath, path: launcherPath,

View File

@@ -9,54 +9,10 @@ const { toColonName, toColonPath, toDashPath } = require('./path-utils');
*/ */
class TaskToolCommandGenerator { class TaskToolCommandGenerator {
/** /**
* Generate task and tool commands from manifest CSVs * REMOVED: Old generateTaskToolCommands method that created nested structure.
* @param {string} projectDir - Project directory * This was causing bugs where files were written to wrong directories.
* @param {string} bmadDir - BMAD installation directory * Use generateColonTaskToolCommands() or generateDashTaskToolCommands() instead.
* @param {string} baseCommandsDir - Optional base commands directory (defaults to .claude/commands/bmad)
*/ */
async generateTaskToolCommands(projectDir, bmadDir, baseCommandsDir = null) {
const tasks = await this.loadTaskManifest(bmadDir);
const tools = await this.loadToolManifest(bmadDir);
// Filter to only standalone items
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
// Base commands directory - use provided or default to Claude Code structure
const commandsDir = baseCommandsDir || path.join(projectDir, '.claude', 'commands', 'bmad');
let generatedCount = 0;
// Generate command files for tasks
for (const task of standaloneTasks) {
const moduleTasksDir = path.join(commandsDir, task.module, 'tasks');
await fs.ensureDir(moduleTasksDir);
const commandContent = this.generateCommandContent(task, 'task');
const commandPath = path.join(moduleTasksDir, `${task.name}.md`);
await fs.writeFile(commandPath, commandContent);
generatedCount++;
}
// Generate command files for tools
for (const tool of standaloneTools) {
const moduleToolsDir = path.join(commandsDir, tool.module, 'tools');
await fs.ensureDir(moduleToolsDir);
const commandContent = this.generateCommandContent(tool, 'tool');
const commandPath = path.join(moduleToolsDir, `${tool.name}.md`);
await fs.writeFile(commandPath, commandContent);
generatedCount++;
}
return {
generated: generatedCount,
tasks: standaloneTasks.length,
tools: standaloneTools.length,
};
}
/** /**
* Generate command content for a task or tool * Generate command content for a task or tool
@@ -93,10 +49,16 @@ Follow all instructions in the ${type} file exactly as written.
} }
const csvContent = await fs.readFile(manifestPath, 'utf8'); const csvContent = await fs.readFile(manifestPath, 'utf8');
return csv.parse(csvContent, { const tasks = csv.parse(csvContent, {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true,
}); });
// Filter out README files
return tasks.filter((task) => {
const nameLower = task.name.toLowerCase();
return !nameLower.includes('readme') && task.name !== 'README';
});
} }
/** /**
@@ -110,10 +72,16 @@ Follow all instructions in the ${type} file exactly as written.
} }
const csvContent = await fs.readFile(manifestPath, 'utf8'); const csvContent = await fs.readFile(manifestPath, 'utf8');
return csv.parse(csvContent, { const tools = csv.parse(csvContent, {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true,
}); });
// Filter out README files
return tools.filter((tool) => {
const nameLower = tool.name.toLowerCase();
return !nameLower.includes('readme') && tool.name !== 'README';
});
} }
/** /**
@@ -135,12 +103,16 @@ Follow all instructions in the ${type} file exactly as written.
let generatedCount = 0; let generatedCount = 0;
// DEBUG: Log parameters
console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}`);
// Generate command files for tasks // Generate command files for tasks
for (const task of standaloneTasks) { for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task'); const commandContent = this.generateCommandContent(task, 'task');
// Use underscore format: bmad_bmm_name.md // Use underscore format: bmad_bmm_name.md
const flatName = toColonName(task.module, 'tasks', task.name); const flatName = toColonName(task.module, 'tasks', task.name);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
console.log(`[DEBUG generateColonTaskToolCommands] Writing task ${task.name} to: ${commandPath}`);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
await fs.writeFile(commandPath, commandContent); await fs.writeFile(commandPath, commandContent);
generatedCount++; generatedCount++;
@@ -186,7 +158,7 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tasks // Generate command files for tasks
for (const task of standaloneTasks) { for (const task of standaloneTasks) {
const commandContent = this.generateCommandContent(task, 'task'); const commandContent = this.generateCommandContent(task, 'task');
// Use underscore format: bmad_bmm_name.md // Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath)
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));
@@ -197,7 +169,7 @@ Follow all instructions in the ${type} file exactly as written.
// Generate command files for tools // Generate command files for tools
for (const tool of standaloneTools) { for (const tool of standaloneTools) {
const commandContent = this.generateCommandContent(tool, 'tool'); const commandContent = this.generateCommandContent(tool, 'tool');
// Use underscore format: bmad_bmm_name.md // Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath)
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
const commandPath = path.join(baseCommandsDir, flatName); const commandPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(commandPath)); await fs.ensureDir(path.dirname(commandPath));

View File

@@ -0,0 +1,329 @@
/**
* Unified BMAD Installer for all IDEs
*
* Replaces the fractured, duplicated setup logic across all IDE handlers.
* All IDEs do the same thing:
* 1. Collect agents, workflows, tasks, tools from the same sources
* 2. Write them to a target directory
* 3. Use a naming convention (flat-colon, flat-dash, or nested)
*
* The only differences between IDEs are:
* - target directory (e.g., .claude/commands/, .cursor/rules/)
* - naming style (underscore vs dash vs nested)
* - template/frontmatter (some need YAML, some need custom frontmatter)
*/
const path = require('node:path');
const fs = require('fs-extra');
const { AgentCommandGenerator } = require('./agent-command-generator');
const { WorkflowCommandGenerator } = require('./workflow-command-generator');
const { TaskToolCommandGenerator } = require('./task-tool-command-generator');
const { toColonPath, toDashPath } = require('./path-utils');
/**
* Naming styles
*/
const NamingStyle = {
FLAT_COLON: 'flat-colon', // bmad_bmm_agent_pm.md (Windows-compatible)
FLAT_DASH: 'flat-dash', // bmad-bmm-agent-pm.md
NESTED: 'nested', // bmad/bmm/agents/pm.md (OLD, deprecated)
};
/**
* Template types for different IDE frontmatter/formatting
*/
const TemplateType = {
CLAUDE: 'claude', // YAML frontmatter with name/description
CURSOR: 'cursor', // Same as Claude
CODEX: 'codex', // No frontmatter, direct content
CLINE: 'cline', // No frontmatter, direct content
WINDSURF: 'windsurf', // YAML with auto_execution_mode
AUGMENT: 'augment', // YAML frontmatter
};
/**
* Unified installer configuration
* @typedef {Object} UnifiedInstallConfig
* @property {string} targetDir - Full path to target directory
* @property {NamingStyle} namingStyle - How to name files
* @property {TemplateType} templateType - What template format to use
* @property {boolean} includeNestedStructure - For NESTED style, create subdirectories
* @property {Function} [customTemplateFn] - Optional custom template function
*/
/**
* Unified BMAD Installer
*/
class UnifiedInstaller {
constructor(bmadFolderName = 'bmad') {
this.bmadFolderName = bmadFolderName;
}
/**
* Install BMAD artifacts for an IDE
*
* @param {string} projectDir - Project root directory
* @param {string} bmadDir - BMAD installation directory (_bmad)
* @param {UnifiedInstallConfig} config - Installation configuration
* @param {Array<string>} selectedModules - Modules to install
* @returns {Promise<Object>} Installation result with counts
*/
async install(projectDir, bmadDir, config, selectedModules = []) {
const {
targetDir,
namingStyle = NamingStyle.FLAT_COLON,
templateType = TemplateType.CLAUDE,
includeNestedStructure = false,
customTemplateFn = null,
} = config;
// Clean up any existing BMAD files in target directory
await this.cleanupBmadFiles(targetDir);
// Ensure target directory exists
await fs.ensureDir(targetDir);
// Count results
const counts = {
agents: 0,
workflows: 0,
tasks: 0,
tools: 0,
total: 0,
};
// 1. Install Agents
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
counts.agents = await this.writeArtifacts(agentArtifacts, targetDir, namingStyle, templateType, customTemplateFn, 'agent');
// 2. Install Workflows (filter out README artifacts)
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
const workflowArtifactsFiltered = workflowArtifacts.filter((a) => {
const name = path.basename(a.relativePath || '');
return name.toLowerCase() !== 'readme.md' && !name.toLowerCase().startsWith('readme-');
});
counts.workflows = await this.writeArtifacts(
workflowArtifactsFiltered,
targetDir,
namingStyle,
templateType,
customTemplateFn,
'workflow',
);
// 3. Install Tasks and Tools from manifest CSV (standalone items)
const ttGen = new TaskToolCommandGenerator();
console.log(`[DEBUG] About to call TaskToolCommandGenerator, namingStyle=${namingStyle}, targetDir=${targetDir}`);
// For now, ALWAYS use flat structure - nested is deprecated
// TODO: Remove nested branch entirely after verification
const taskToolResult =
namingStyle === NamingStyle.FLAT_DASH
? await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir)
: await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir);
counts.tasks = taskToolResult.tasks || 0;
counts.tools = taskToolResult.tools || 0;
counts.total = counts.agents + counts.workflows + counts.tasks + counts.tools;
return counts;
}
/**
* Clean up any existing BMAD files in target directory
*/
async cleanupBmadFiles(targetDir) {
if (!(await fs.pathExists(targetDir))) {
return;
}
// Recursively find and remove any bmad* files or directories
const entries = await fs.readdir(targetDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('bmad')) {
const entryPath = path.join(targetDir, entry.name);
await fs.remove(entryPath);
}
}
}
/**
* Write artifacts with specified naming style and template
*/
async writeArtifacts(artifacts, targetDir, namingStyle, templateType, customTemplateFn, artifactType) {
console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}`);
let written = 0;
for (const artifact of artifacts) {
// Determine target path based on naming style
let targetPath;
let content = artifact.content;
console.log(`[DEBUG] writeArtifacts processing: relativePath=${artifact.relativePath}, name=${artifact.name}`);
if (namingStyle === NamingStyle.FLAT_COLON) {
const flatName = toColonPath(artifact.relativePath);
targetPath = path.join(targetDir, flatName);
} else if (namingStyle === NamingStyle.FLAT_DASH) {
const flatName = toDashPath(artifact.relativePath);
targetPath = path.join(targetDir, flatName);
} else {
// Fallback: treat as flat even if NESTED specified
const flatName = toColonPath(artifact.relativePath);
targetPath = path.join(targetDir, flatName);
}
// Apply template transformations if needed
if (customTemplateFn) {
content = customTemplateFn(artifact, content, templateType);
} else {
content = this.applyTemplate(artifact, content, templateType);
}
await fs.ensureDir(path.dirname(targetPath));
await fs.writeFile(targetPath, content, 'utf8');
written++;
}
return written;
}
/**
* Apply template/frontmatter based on type
*/
applyTemplate(artifact, content, templateType) {
switch (templateType) {
case TemplateType.CLAUDE:
case TemplateType.CURSOR: {
// Already has YAML frontmatter from generator
return content;
}
case TemplateType.CODEX:
case TemplateType.CLINE: {
// No frontmatter needed, content as-is
return content;
}
case TemplateType.WINDSURF: {
// Add Windsurf-specific frontmatter
return this.addWindsurfFrontmatter(artifact, content);
}
case TemplateType.AUGMENT: {
// Add Augment frontmatter
return this.addAugmentFrontmatter(artifact, content);
}
default: {
return content;
}
}
}
/**
* Add Windsurf frontmatter with auto_execution_mode
*/
addWindsurfFrontmatter(artifact, content) {
// Remove existing frontmatter if present
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
// Determine auto_execution_mode based on type
let autoExecMode = '1'; // default for workflows
if (artifact.type === 'agent') {
autoExecMode = '3';
} else if (artifact.type === 'task' || artifact.type === 'tool') {
autoExecMode = '2';
}
const name = artifact.name || artifact.displayName || 'workflow';
const frontmatter = `---
description: ${name}
auto_execution_mode: ${autoExecMode}
---
`;
return frontmatter + contentWithoutFrontmatter;
}
/**
* Add Augment frontmatter
*/
addAugmentFrontmatter(artifact, content) {
// Augment uses simple YAML frontmatter
const name = artifact.name || artifact.displayName || 'workflow';
const frontmatter = `---
description: ${name}
---
`;
// Only add if not already present
if (!content.startsWith('---')) {
return frontmatter + content;
}
return content;
}
/**
* Get tasks from manifest CSV
*/
async getTasksFromManifest(bmadDir) {
const csv = require('csv-parse/sync');
const manifestPath = path.join(bmadDir, '_config', 'task-manifest.csv');
if (!(await fs.pathExists(manifestPath))) {
return [];
}
const csvContent = await fs.readFile(manifestPath, 'utf8');
const tasks = csv.parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
// Filter for standalone only
return tasks
.filter((t) => t.standalone === 'true' || t.standalone === true)
.map((t) => ({
...t,
content: null, // Will be read from path when writing
}));
}
/**
* Get tools from manifest CSV
*/
async getToolsFromManifest(bmadDir) {
const csv = require('csv-parse/sync');
const manifestPath = path.join(bmadDir, '_config', 'tool-manifest.csv');
if (!(await fs.pathExists(manifestPath))) {
return [];
}
const csvContent = await fs.readFile(manifestPath, 'utf8');
const tools = csv.parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
// Filter for standalone only
return tools
.filter((t) => t.standalone === 'true' || t.standalone === true)
.map((t) => ({
...t,
content: null, // Will be read from path when writing
}));
}
}
module.exports = {
UnifiedInstaller,
NamingStyle,
TemplateType,
};

View File

@@ -14,44 +14,10 @@ class WorkflowCommandGenerator {
} }
/** /**
* Generate workflow commands from the manifest CSV * REMOVED: Old generateWorkflowCommands method that created nested structure.
* @param {string} projectDir - Project directory * This was hardcoded to .claude/commands/bmad and caused bugs.
* @param {string} bmadDir - BMAD installation directory * Use collectWorkflowArtifacts() + writeColonArtifacts/writeDashArtifacts() instead.
*/ */
async generateWorkflowCommands(projectDir, bmadDir) {
const workflows = await this.loadWorkflowManifest(bmadDir);
if (!workflows) {
console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.'));
return { generated: 0 };
}
// ALL workflows now generate commands - no standalone filtering
const allWorkflows = workflows;
// 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 allWorkflows) {
const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
await fs.ensureDir(moduleWorkflowsDir);
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
const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows);
await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows);
return { generated: generatedCount };
}
async collectWorkflowArtifacts(bmadDir) { async collectWorkflowArtifacts(bmadDir) {
const workflows = await this.loadWorkflowManifest(bmadDir); const workflows = await this.loadWorkflowManifest(bmadDir);