diff --git a/tools/cli/installers/lib/ide/antigravity.js b/tools/cli/installers/lib/ide/antigravity.js index 7af2e41b..73464f0d 100644 --- a/tools/cli/installers/lib/ide/antigravity.js +++ b/tools/cli/installers/lib/ide/antigravity.js @@ -150,9 +150,10 @@ class AntigravitySetup extends BaseIdeSetup { // Write workflow-command artifacts with FLATTENED naming using shared utility 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 taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir); + const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, bmadWorkflowsDir); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed`)); diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js index cf7dedcd..0ddff285 100644 --- a/tools/cli/installers/lib/ide/claude-code.js +++ b/tools/cli/installers/lib/ide/claude-code.js @@ -3,9 +3,7 @@ const fs = require('fs-extra'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); const { loadModuleInjectionConfig, shouldApplyInjection, @@ -18,10 +16,14 @@ const prompts = require('../../../lib/prompts'); /** * 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 { constructor() { - super('claude-code', 'Claude Code', true); // preferred IDE + super('claude-code', 'Claude Code', true); this.configDir = '.claude'; this.commandsDir = 'commands'; this.agentsDir = 'agents'; @@ -29,7 +31,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { /** * Prompt for subagent installation location - * @returns {Promise} Selected location ('project' or 'user') */ async promptInstallLocation() { 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 - * @param {string} projectDir - Project directory */ async cleanup(projectDir) { 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)) { const entries = await fs.readdir(commandsDir); let removedCount = 0; @@ -102,72 +66,41 @@ class ClaudeCodeSetup extends BaseIdeSetup { 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 - * @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 + console.log(`[DEBUG CLAUDE-CODE] setup called! projectDir=${projectDir}`); this.projectDir = projectDir; console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Clean up old BMAD installation first await this.cleanup(projectDir); - // Create .claude/commands directory structure const claudeDir = path.join(projectDir, this.configDir); const commandsDir = path.join(claudeDir, this.commandsDir); await this.ensureDir(commandsDir); - // Use underscore format: files written directly to commands dir (no bmad subfolder) - // Creates: .claude/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); + // Use the unified installer for standard artifacts + const installer = new UnifiedInstaller(this.bmadFolderName); + console.log(`[DEBUG CLAUDE-CODE] About to call installer.install, targetDir=${commandsDir}`); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: commandsDir, + namingStyle: NamingStyle.FLAT_COLON, + templateType: TemplateType.CLAUDE, + }, + options.selectedModules || [], + ); + console.log(`[DEBUG CLAUDE-CODE] installer.install done, counts=`, counts); // 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) { - // IDE is already configured from previous installation, skip prompting - // Just process with default/existing configuration await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {}); } else if (options.preCollectedConfig) { await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig); @@ -175,43 +108,24 @@ class ClaudeCodeSetup extends BaseIdeSetup { 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.dim(` - ${agentCount} agents installed`)); - if (workflowCommandCount > 0) { - console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`)); + console.log(chalk.dim(` - ${counts.agents} agents installed`)); + if (counts.workflows > 0) { + console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`)); } - if (taskToolResult.generated > 0) { + if (counts.tasks + counts.tools > 0) { console.log( - chalk.dim( - ` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`, - ), + 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: agentCount, + agents: counts.agents, }; } - // Method removed - CLAUDE.md file management left to user - /** * Read and process file content */ @@ -224,7 +138,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { * Override processContent to keep {project-root} placeholder */ processContent(content, metadata = {}) { - // Use the base class method WITHOUT projectDir to preserve {project-root} placeholder return super.processContent(content, metadata); } @@ -234,14 +147,12 @@ class ClaudeCodeSetup extends BaseIdeSetup { async getAgentsFromSource(sourceDir, selectedModules) { const agents = []; - // Add core agents const corePath = getModulePath('core'); if (await fs.pathExists(path.join(corePath, 'agents'))) { const coreAgents = await 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'); @@ -259,11 +170,9 @@ class ClaudeCodeSetup extends BaseIdeSetup { * Process module injections with pre-collected configuration */ async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) { - // Get list of installed modules const modules = options.selectedModules || []; const { subagentChoices, installLocation } = preCollectedConfig; - // Get the actual source directory (not the installation directory) await this.processModuleInjectionsInternal({ projectDir, modules, @@ -276,15 +185,12 @@ class ClaudeCodeSetup extends BaseIdeSetup { /** * 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) { - // 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 { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ projectDir, modules, @@ -303,6 +209,7 @@ class ClaudeCodeSetup extends BaseIdeSetup { } async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) { + console.log(`[DEBUG CLAUDE-CODE] processModuleInjectionsInternal called! modules=${modules.join(',')}`); let choices = subagentChoices; let location = installLocation; @@ -346,7 +253,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { * Prompt user for subagent installation preferences */ async promptSubagentInstallation(subagentConfig) { - // First ask if they want to install subagents const install = await prompts.select({ message: 'Would you like to install Claude Code subagents for enhanced functionality?', choices: [ @@ -358,7 +264,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { }); 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', @@ -395,7 +300,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { 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 = filterAgentInstructions(injection.content, subagentChoices.selected); } @@ -413,7 +317,6 @@ class ClaudeCodeSetup extends BaseIdeSetup { async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) { const os = require('node:os'); - // Determine target directory based on user choice let targetDir; if (location === 'user') { 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/`)); } - // Ensure target directory exists await this.ensureDir(targetDir); const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); @@ -458,17 +360,12 @@ class ClaudeCodeSetup extends BaseIdeSetup { /** * 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) { const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project + return null; } await this.ensureDir(commandsDir); @@ -490,8 +387,6 @@ You must fully embody this agent's persona and follow all activation instruction `; - // Use underscore format: bmad_custom_fred-commit-poet.md - // Written directly to commands dir (no bmad subfolder) const launcherName = customAgentColonName(agentName); const launcherPath = path.join(commandsDir, launcherName); await this.writeFile(launcherPath, launcherContent); diff --git a/tools/cli/installers/lib/ide/cline.js b/tools/cli/installers/lib/ide/cline.js index f2109d88..4c317048 100644 --- a/tools/cli/installers/lib/ide/cline.js +++ b/tools/cli/installers/lib/ide/cline.js @@ -123,6 +123,7 @@ class ClineSetup extends BaseIdeSetup { artifacts.push({ type: 'task', module: task.module, + path: task.path, sourcePath: task.path, relativePath: path.join(task.module, 'tasks', `${task.name}.md`), content, diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index b632d4b7..ea3870ab 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -3,25 +3,22 @@ const fs = require('fs-extra'); const os = require('node:os'); const chalk = require('chalk'); const { BaseIdeSetup } = require('./_base-ide'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); -const { getTasksFromBmad } = require('./shared/bmad-artifacts'); -const { toDashPath, customAgentDashName } = require('./shared/path-utils'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); +const { customAgentDashName } = require('./shared/path-utils'); const prompts = require('../../../lib/prompts'); /** * Codex setup handler (CLI mode) + * + * Uses UnifiedInstaller for all artifact installation. */ class CodexSetup extends BaseIdeSetup { constructor() { - super('codex', 'Codex', true); // preferred IDE + super('codex', 'Codex', true); } /** * Collect configuration choices before installation - * @param {Object} options - Configuration options - * @returns {Object} Collected configuration */ async collectConfiguration(options = {}) { let confirmed = false; @@ -43,7 +40,6 @@ class CodexSetup extends BaseIdeSetup { default: 'global', }); - // Display detailed instructions for the chosen option console.log(''); if (installLocation === 'project') { console.log(this.getProjectSpecificInstructions()); @@ -51,7 +47,6 @@ class CodexSetup extends BaseIdeSetup { console.log(this.getGlobalInstructions()); } - // Confirm the choice confirmed = await prompts.confirm({ message: 'Proceed with this installation option?', default: true, @@ -67,78 +62,48 @@ class CodexSetup extends BaseIdeSetup { /** * 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}...`)); - // 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 { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options); - const destDir = this.getCodexPromptDir(projectDir, installLocation); + await fs.ensureDir(destDir); await this.clearOldBmadFiles(destDir); - // Collect artifacts and write using underscore format - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - const agentCount = await agentGen.writeDashArtifacts(destDir, agentArtifacts); - - const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); - const taskArtifacts = []; - for (const task of tasks) { - const content = await this.readAndProcessWithProject( - task.path, - { - 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; + // Use the unified installer - so much simpler! + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: destDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.CODEX, + }, + options.selectedModules || [], + ); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - Mode: CLI`)); - console.log(chalk.dim(` - ${counts.agents} agents exported`)); - console.log(chalk.dim(` - ${counts.tasks} tasks exported`)); - console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`)); - if (counts.workflowLaunchers > 0) { - console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`)); + console.log(chalk.dim(` - ${counts.agents} agents installed`)); + if (counts.workflows > 0) { + console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`)); } - 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}`)); return { success: true, - mode, - artifacts, - counts, + mode: 'cli', + ...counts, destination: destDir, - written, installLocation, }; } @@ -147,7 +112,6 @@ class CodexSetup extends BaseIdeSetup { * Detect Codex installation by checking for BMAD prompt exports */ async detect(projectDir) { - // Check both global and project-specific locations const globalDir = this.getCodexPromptDir(null, 'global'); const projectDir_local = projectDir || process.cwd(); const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project'); @@ -171,63 +135,6 @@ class CodexSetup extends BaseIdeSetup { 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') { if (location === 'project' && projectDir) { return path.join(projectDir, '.codex', 'prompts'); @@ -235,19 +142,6 @@ class CodexSetup extends BaseIdeSetup { 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) { if (!(await fs.pathExists(destDir))) { 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 - * @returns {string} Instructions text */ - getGlobalInstructions(destDir) { + getGlobalInstructions() { const lines = [ '', 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.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.bold.cyan('═'.repeat(70)), @@ -303,11 +191,8 @@ class CodexSetup extends BaseIdeSetup { /** * 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 commonLines = [ @@ -316,7 +201,7 @@ class CodexSetup extends BaseIdeSetup { chalk.bold.yellow(' Project-Specific Codex Configuration'), chalk.bold.cyan('═'.repeat(70)), '', - chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '/.codex/prompts'), + chalk.white(' Prompts will be installed to: ') + chalk.cyan('/.codex/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]; - return lines.join('\n'); } @@ -358,7 +242,6 @@ class CodexSetup extends BaseIdeSetup { * Cleanup Codex configuration */ async cleanup(projectDir = null) { - // Clean both global and project-specific locations const globalDir = this.getCodexPromptDir(null, 'global'); await this.clearOldBmadFiles(globalDir); @@ -370,11 +253,6 @@ class CodexSetup extends BaseIdeSetup { /** * 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) { const destDir = this.getCodexPromptDir(projectDir, 'project'); @@ -397,7 +275,6 @@ You must fully embody this agent's persona and follow all activation instruction `; - // Use underscore format: bmad_custom_fred-commit-poet.md const fileName = customAgentDashName(agentName); const launcherPath = path.join(destDir, fileName); await fs.writeFile(launcherPath, launcherContent, 'utf8'); diff --git a/tools/cli/installers/lib/ide/cursor.js b/tools/cli/installers/lib/ide/cursor.js index 771bba72..72e43ec1 100644 --- a/tools/cli/installers/lib/ide/cursor.js +++ b/tools/cli/installers/lib/ide/cursor.js @@ -1,31 +1,77 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); const { customAgentColonName } = require('./shared/path-utils'); /** * Cursor IDE setup handler + * + * Uses the UnifiedInstaller - all the complex artifact collection + * and writing logic is now centralized. */ class CursorSetup extends BaseIdeSetup { constructor() { - super('cursor', 'Cursor', true); // preferred IDE + super('cursor', 'Cursor', true); this.configDir = '.cursor'; this.rulesDir = 'rules'; this.commandsDir = 'commands'; } /** - * Cleanup old BMAD installation before reinstalling - * @param {string} projectDir - Project directory + * Setup Cursor IDE configuration + */ + 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) { const fs = require('fs-extra'); 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)) { const entries = await fs.readdir(commandsDir); 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 - * @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) { const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project + return null; } 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. 1. LOAD the FULL agent file from @${agentPath} @@ -135,20 +117,9 @@ class CursorSetup extends BaseIdeSetup { `; - // 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 launcherPath = path.join(commandsDir, launcherName); - await this.writeFile(launcherPath, commandContent); + await this.writeFile(launcherPath, launcherContent); return { path: launcherPath, diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index fd5f45d5..8483308b 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -9,54 +9,10 @@ const { toColonName, toColonPath, toDashPath } = require('./path-utils'); */ class TaskToolCommandGenerator { /** - * Generate task and tool commands from manifest CSVs - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {string} baseCommandsDir - Optional base commands directory (defaults to .claude/commands/bmad) + * REMOVED: Old generateTaskToolCommands method that created nested structure. + * This was causing bugs where files were written to wrong directories. + * Use generateColonTaskToolCommands() or generateDashTaskToolCommands() instead. */ - 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 @@ -93,10 +49,16 @@ Follow all instructions in the ${type} file exactly as written. } const csvContent = await fs.readFile(manifestPath, 'utf8'); - return csv.parse(csvContent, { + const tasks = csv.parse(csvContent, { columns: 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'); - return csv.parse(csvContent, { + const tools = csv.parse(csvContent, { columns: 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; + // DEBUG: Log parameters + console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}`); + // Generate command files for tasks for (const task of standaloneTasks) { const commandContent = this.generateCommandContent(task, 'task'); // Use underscore format: bmad_bmm_name.md const flatName = toColonName(task.module, 'tasks', task.name); const commandPath = path.join(baseCommandsDir, flatName); + console.log(`[DEBUG generateColonTaskToolCommands] Writing task ${task.name} to: ${commandPath}`); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); generatedCount++; @@ -186,7 +158,7 @@ Follow all instructions in the ${type} file exactly as written. // Generate command files for tasks for (const task of standaloneTasks) { 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 commandPath = path.join(baseCommandsDir, flatName); 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 for (const tool of standaloneTools) { 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 commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); diff --git a/tools/cli/installers/lib/ide/shared/unified-installer.js b/tools/cli/installers/lib/ide/shared/unified-installer.js new file mode 100644 index 00000000..98097320 --- /dev/null +++ b/tools/cli/installers/lib/ide/shared/unified-installer.js @@ -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} selectedModules - Modules to install + * @returns {Promise} 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, +}; diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index ebf8b7f5..18b7d1a2 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -14,44 +14,10 @@ class WorkflowCommandGenerator { } /** - * Generate workflow commands from the manifest CSV - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory + * REMOVED: Old generateWorkflowCommands method that created nested structure. + * This was hardcoded to .claude/commands/bmad and caused bugs. + * 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) { const workflows = await this.loadWorkflowManifest(bmadDir);