From 7074395bdd70e238de96fd55826d5751353e0d5d Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 03:02:59 -0600 Subject: [PATCH 01/11] claude cline codex installers use central function --- tools/cli/installers/lib/ide/antigravity.js | 5 +- tools/cli/installers/lib/ide/claude-code.js | 179 ++-------- tools/cli/installers/lib/ide/cline.js | 1 + tools/cli/installers/lib/ide/codex.js | 189 ++-------- tools/cli/installers/lib/ide/cursor.js | 151 ++++---- .../ide/shared/task-tool-command-generator.js | 74 ++-- .../lib/ide/shared/unified-installer.js | 329 ++++++++++++++++++ .../ide/shared/workflow-command-generator.js | 40 +-- 8 files changed, 490 insertions(+), 478 deletions(-) create mode 100644 tools/cli/installers/lib/ide/shared/unified-installer.js 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); From cf6cf779bb0416bf0923fe7b17b502cb34c444f4 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 10:55:01 -0600 Subject: [PATCH 02/11] Fix auggie installer to use UnifiedInstaller with flat files - Replace individual generators with UnifiedInstaller class - Remove nested folder structure (.augment/commands/bmad/agents/, etc.) - Now installs flat files to .augment/commands/ (e.g., bmad_bmm_agent_pm.md) - Use NamingStyle.FLAT_COLON and TemplateType.AUGMENT - Remove createTaskCommand, createToolCommand, createWorkflowCommand methods - Simplify cleanup() and update installCustomAgentLauncher() for flat structure --- tools/cli/installers/lib/ide/auggie.js | 188 ++++--------------------- 1 file changed, 29 insertions(+), 159 deletions(-) diff --git a/tools/cli/installers/lib/ide/auggie.js b/tools/cli/installers/lib/ide/auggie.js index 04e08788..2a65e57d 100644 --- a/tools/cli/installers/lib/ide/auggie.js +++ b/tools/cli/installers/lib/ide/auggie.js @@ -2,8 +2,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); /** * Auggie CLI setup handler @@ -13,6 +12,7 @@ class AuggieSetup extends BaseIdeSetup { constructor() { super('auggie', 'Auggie CLI'); this.detectionPaths = ['.augment']; + this.installer = new UnifiedInstaller(this.bmadFolderName); } /** @@ -24,168 +24,39 @@ class AuggieSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Always use project directory - const location = path.join(projectDir, '.augment', 'commands'); + // Use flat file structure in .augment/commands/ + const targetDir = path.join(projectDir, '.augment', 'commands'); - // Clean up old BMAD installation first - await this.cleanup(projectDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Get tasks, tools, and workflows (ALL workflows now generate commands) - const tasks = await this.getTasks(bmadDir, true); - const tools = await this.getTools(bmadDir, true); - - // Get ALL workflows using the new workflow command generator - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Convert workflow artifacts to expected format - const workflows = workflowArtifacts - .filter((artifact) => artifact.type === 'workflow-command') - .map((artifact) => ({ - module: artifact.module, - name: path.basename(artifact.relativePath, '.md'), - path: artifact.sourcePath, - content: artifact.content, - })); - - const bmadCommandsDir = path.join(location, 'bmad'); - const agentsDir = path.join(bmadCommandsDir, 'agents'); - const tasksDir = path.join(bmadCommandsDir, 'tasks'); - const toolsDir = path.join(bmadCommandsDir, 'tools'); - const workflowsDir = path.join(bmadCommandsDir, 'workflows'); - - await this.ensureDir(agentsDir); - await this.ensureDir(tasksDir); - await this.ensureDir(toolsDir); - await this.ensureDir(workflowsDir); - - // Install agent launchers - for (const artifact of agentArtifacts) { - const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`); - await this.writeFile(targetPath, artifact.content); - } - - // Install tasks - for (const task of tasks) { - const content = await this.readFile(task.path); - const commandContent = this.createTaskCommand(task, content); - - const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - } - - // Install tools - for (const tool of tools) { - const content = await this.readFile(tool.path); - const commandContent = this.createToolCommand(tool, content); - - const targetPath = path.join(toolsDir, `${tool.module}-${tool.name}.md`); - await this.writeFile(targetPath, commandContent); - } - - // Install workflows (already generated commands) - for (const workflow of workflows) { - // Use the pre-generated workflow command content - const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`); - await this.writeFile(targetPath, workflow.content); - } - - const totalInstalled = agentArtifacts.length + tasks.length + tools.length + workflows.length; + // Install using UnifiedInstaller + const counts = await this.installer.install(projectDir, bmadDir, { + targetDir, + namingStyle: NamingStyle.FLAT_COLON, + templateType: TemplateType.AUGMENT, + includeNestedStructure: false, + }, options.selectedModules || []); console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentArtifacts.length} agents installed`)); - console.log(chalk.dim(` - ${tasks.length} tasks installed`)); - console.log(chalk.dim(` - ${tools.length} tools installed`)); - console.log(chalk.dim(` - ${workflows.length} workflows installed`)); - console.log(chalk.dim(` - Location: ${path.relative(projectDir, location)}`)); + console.log(chalk.dim(` - ${counts.agents} agents installed`)); + console.log(chalk.dim(` - ${counts.tasks} tasks installed`)); + console.log(chalk.dim(` - ${counts.tools} tools installed`)); + console.log(chalk.dim(` - ${counts.workflows} workflows installed`)); + console.log(chalk.dim(` - Location: ${path.relative(projectDir, targetDir)}`)); console.log(chalk.yellow(`\n 💡 Tip: Add 'model: gpt-4o' to command frontmatter to specify AI model`)); return { success: true, - agents: agentArtifacts.length, - tasks: tasks.length, - tools: tools.length, - workflows: workflows.length, + ...counts, }; } - /** - * Create task command content - */ - createTaskCommand(task, content) { - const nameMatch = content.match(/name="([^"]+)"/); - const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); - - return `--- -description: "Execute the ${taskName} task" ---- - -# ${taskName} Task - -${content} - -## Module -BMAD ${task.module.toUpperCase()} module -`; - } - - /** - * Create tool command content - */ - createToolCommand(tool, content) { - const nameMatch = content.match(/name="([^"]+)"/); - const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name); - - return `--- -description: "Use the ${toolName} tool" ---- - -# ${toolName} Tool - -${content} - -## Module -BMAD ${tool.module.toUpperCase()} module -`; - } - - /** - * Create workflow command content - */ - createWorkflowCommand(workflow, content) { - const description = workflow.description || `Execute the ${workflow.name} workflow`; - - return `--- -description: "${description}" ---- - -# ${workflow.name} Workflow - -${content} - -## Module -BMAD ${workflow.module.toUpperCase()} module -`; - } - /** * Cleanup Auggie configuration + * Removes bmad* files from .augment/commands/ */ async cleanup(projectDir) { - const fs = require('fs-extra'); - - // Only clean up project directory - const location = path.join(projectDir, '.augment', 'commands'); - const bmadDir = path.join(location, 'bmad'); - - if (await fs.pathExists(bmadDir)) { - await fs.remove(bmadDir); - console.log(chalk.dim(` Removed old BMAD commands`)); - } + const targetDir = path.join(projectDir, '.augment', 'commands'); + await this.installer.cleanupBmadFiles(targetDir); + console.log(chalk.dim(` Removed old BMAD commands`)); } /** @@ -197,15 +68,13 @@ BMAD ${workflow.module.toUpperCase()} module * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - // Auggie uses .augment/commands directory - const location = path.join(projectDir, '.augment', 'commands'); - const bmadCommandsDir = path.join(location, 'bmad'); - const agentsDir = path.join(bmadCommandsDir, 'agents'); + // Auggie uses .augment/commands directory with flat structure + const targetDir = path.join(projectDir, '.augment', 'commands'); - // Create .augment/commands/bmad/agents directory if it doesn't exist - await fs.ensureDir(agentsDir); + // Create .augment/commands directory if it doesn't exist + await fs.ensureDir(targetDir); - // Create custom agent launcher + // Create custom agent launcher with flat naming: bmad_custom_agent_{name}.md const launcherContent = `--- description: "Use the ${agentName} custom agent" --- @@ -226,8 +95,9 @@ The agent will follow the persona and instructions from the main agent file. BMAD Custom agent `; - const fileName = `custom-${agentName.toLowerCase()}.md`; - const launcherPath = path.join(agentsDir, fileName); + // Use flat naming convention consistent with UnifiedInstaller + const fileName = `bmad_custom_agent_${agentName.toLowerCase()}.md`; + const launcherPath = path.join(targetDir, fileName); // Write the launcher file await fs.writeFile(launcherPath, launcherContent, 'utf8'); From f6dab0d0ff1adc1ea4b70a59276c0a9262dc81cb Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 10:55:02 -0600 Subject: [PATCH 03/11] Fix crush installer to use UnifiedInstaller - Replace individual generators with UnifiedInstaller class - Use NamingStyle.FLAT_COLON and TemplateType.CODEX - Remove manual counting logic, use counts from UnifiedInstaller - Keep cleanup() and installCustomAgentLauncher() as-is --- tools/cli/installers/lib/ide/crush.js | 67 +++++++++++++-------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/tools/cli/installers/lib/ide/crush.js b/tools/cli/installers/lib/ide/crush.js index b9312d67..1643b345 100644 --- a/tools/cli/installers/lib/ide/crush.js +++ b/tools/cli/installers/lib/ide/crush.js @@ -2,14 +2,14 @@ const path = require('node:path'); const fs = require('fs-extra'); 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'); /** * Crush IDE setup handler - * Creates commands in .crush/commands/ directory structure using flat colon naming + * + * Uses the UnifiedInstaller - all the complex artifact collection + * and writing logic is now centralized. */ class CrushSetup extends BaseIdeSetup { constructor() { @@ -31,47 +31,42 @@ class CrushSetup extends BaseIdeSetup { await this.cleanup(projectDir); // Create .crush/commands directory - const crushDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(crushDir, this.commandsDir); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); await this.ensureDir(commandsDir); - // Use underscore format: files written directly to commands dir (no bmad subfolder) - // Creates: .crush/commands/bmad_bmm_pm.md - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = 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); - - // Get ALL workflows using the new workflow command generator - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Write workflow-command artifacts using flat underscore naming - // Creates files like: bmad_bmm_correct-course.md - const workflowCount = await workflowGenerator.writeColonArtifacts(commandsDir, workflowArtifacts); - - // Generate task and tool commands using flat underscore naming - const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir); + // Use the unified installer + // Crush uses flat colon naming (bmad_bmm_pm.md) with no frontmatter (like Codex) + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: commandsDir, + namingStyle: NamingStyle.FLAT_COLON, + templateType: TemplateType.CODEX, + }, + options.selectedModules || [], + ); console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agent commands created`)); - console.log(chalk.dim(` - ${taskToolResult.tasks} task commands created`)); - console.log(chalk.dim(` - ${taskToolResult.tools} tool commands created`)); - console.log(chalk.dim(` - ${workflowCount} workflow commands created`)); + 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)}`)); console.log(chalk.dim('\n Commands can be accessed via Crush command palette')); return { success: true, - agents: agentCount, - tasks: taskToolResult.tasks || 0, - tools: taskToolResult.tools || 0, - workflows: workflowCount, + agents: counts.agents, + tasks: counts.tasks, + tools: counts.tools, + workflows: counts.workflows, }; } From c0adbc4e764998767cd7262236c53ea10f676641 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 10:55:03 -0600 Subject: [PATCH 04/11] Fix cline installer to use UnifiedInstaller - Replace individual generators with UnifiedInstaller class - Use NamingStyle.FLAT_DASH and TemplateType.CLINE - Remove collectClineArtifacts, flattenAndWriteArtifacts, flattenFilename methods - Reduce code by ~98 lines (36% reduction) - Keep cleanup(), installCustomAgentLauncher(), detect() as-is --- tools/cli/installers/lib/ide/cline.js | 148 +++++--------------------- 1 file changed, 25 insertions(+), 123 deletions(-) diff --git a/tools/cli/installers/lib/ide/cline.js b/tools/cli/installers/lib/ide/cline.js index 4c317048..156d3f7a 100644 --- a/tools/cli/installers/lib/ide/cline.js +++ b/tools/cli/installers/lib/ide/cline.js @@ -2,15 +2,14 @@ const path = require('node:path'); const fs = require('fs-extra'); 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 { getAgentsFromBmad, 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'); /** * Cline IDE setup handler - * Installs BMAD artifacts to .clinerules/workflows with flattened naming + * + * Uses UnifiedInstaller for all artifact installation. + * Installs BMAD artifacts to .clinerules/workflows with flattened naming. */ class ClineSetup extends BaseIdeSetup { constructor() { @@ -32,41 +31,45 @@ class ClineSetup extends BaseIdeSetup { const clineDir = path.join(projectDir, this.configDir); const workflowsDir = path.join(clineDir, this.workflowsDir); - await this.ensureDir(workflowsDir); + await fs.ensureDir(workflowsDir); // Clear old BMAD files await this.clearOldBmadFiles(workflowsDir); - // Collect all artifacts - const { artifacts, counts } = await this.collectClineArtifacts(projectDir, bmadDir, options); - - // Write flattened files - const written = await this.flattenAndWriteArtifacts(artifacts, workflowsDir); + // Use the unified installer - much simpler! + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: workflowsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.CLINE, + }, + options.selectedModules || [], + ); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${counts.agents} agents installed`)); console.log(chalk.dim(` - ${counts.tasks} tasks installed`)); console.log(chalk.dim(` - ${counts.workflows} workflow commands installed`)); - if (counts.workflowLaunchers > 0) { - console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers installed`)); + if (counts.tools > 0) { + console.log(chalk.dim(` - ${counts.tools} tools installed`)); } - console.log(chalk.dim(` - ${written} files written to ${path.relative(projectDir, workflowsDir)}`)); + console.log(chalk.dim(` - ${counts.total} files written to ${path.relative(projectDir, workflowsDir)}`)); // Usage instructions console.log(chalk.yellow('\n ⚠️ How to Use Cline Workflows')); console.log(chalk.cyan(' BMAD workflows are available as slash commands in Cline')); console.log(chalk.dim(' Usage:')); console.log(chalk.dim(' - Type / to see available commands')); - console.log(chalk.dim(' - All BMAD items start with "bmad_"')); - console.log(chalk.dim(' - Example: /bmad_bmm_pm')); + console.log(chalk.dim(' - All BMAD items start with "bmad-"')); + console.log(chalk.dim(' - Example: /bmad-bmm-pm')); return { success: true, - agents: counts.agents, - tasks: counts.tasks, - workflows: counts.workflows, - workflowLaunchers: counts.workflowLaunchers, - written, + ...counts, + destination: workflowsDir, }; } @@ -84,92 +87,6 @@ class ClineSetup extends BaseIdeSetup { return entries.some((entry) => entry.startsWith('bmad')); } - /** - * Collect all artifacts for Cline export - */ - async collectClineArtifacts(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); - - // Process agent launchers with project-specific paths - for (const agentArtifact of agentArtifacts) { - const content = agentArtifact.content; - - artifacts.push({ - type: 'agent', - module: agentArtifact.module, - sourcePath: agentArtifact.sourcePath, - relativePath: agentArtifact.relativePath, - content, - }); - } - - // Get tasks - 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, - path: task.path, - sourcePath: task.path, - relativePath: path.join(task.module, 'tasks', `${task.name}.md`), - content, - }); - } - - // Get workflows - 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, - }, - }; - } - - /** - * Flatten file path to bmad_module_type_name.md format - * Uses shared toDashPath utility - */ - flattenFilename(relativePath) { - return toDashPath(relativePath); - } - - /** - * Write all artifacts with flattened names - */ - 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; - } - /** * Clear old BMAD files from the workflows directory */ @@ -195,14 +112,6 @@ class ClineSetup extends BaseIdeSetup { } } - /** - * Read and process file with project-specific paths - */ - async readAndProcessWithProject(filePath, metadata, projectDir) { - const content = await fs.readFile(filePath, 'utf8'); - return super.processContent(content, metadata, projectDir); - } - /** * Cleanup Cline configuration */ @@ -261,13 +170,6 @@ The agent will follow the persona and instructions from the main agent file. type: 'custom-agent-launcher', }; } - - /** - * Utility: Ensure directory exists - */ - async ensureDir(dirPath) { - await fs.ensureDir(dirPath); - } } module.exports = { ClineSetup }; From c5d0fb55bab4aa62353f0f68b819614069ad6fed Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 10:55:05 -0600 Subject: [PATCH 05/11] Fix windsurf installer to use UnifiedInstaller with flat files - Replace manual artifact collection with UnifiedInstaller class - Remove nested folder structure (.windsurf/workflows/bmad/[module]/[type]/) - Now installs flat files to .windsurf/workflows/ (e.g., bmad-bmm-agent-pm.md) - Use NamingStyle.FLAT_DASH and TemplateType.WINDSURF - Add customTemplateFn for Windsurf-specific auto_execution_mode frontmatter - Simplify cleanup() and update installCustomAgentLauncher() for flat structure --- tools/cli/installers/lib/ide/windsurf.js | 277 +++++++++++------------ 1 file changed, 129 insertions(+), 148 deletions(-) diff --git a/tools/cli/installers/lib/ide/windsurf.js b/tools/cli/installers/lib/ide/windsurf.js index 92596db3..6463311e 100644 --- a/tools/cli/installers/lib/ide/windsurf.js +++ b/tools/cli/installers/lib/ide/windsurf.js @@ -1,16 +1,23 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); +const fs = require('fs-extra'); /** * Windsurf IDE setup handler + * + * Uses UnifiedInstaller for consistent artifact collection and writing. + * Windsurf-specific configuration: + * - Flat file naming (FLAT_DASH): bmad-bmm-agent-pm.md + * - Windsurf frontmatter with auto_execution_mode */ class WindsurfSetup extends BaseIdeSetup { constructor() { super('windsurf', 'Windsurf', true); // preferred IDE this.configDir = '.windsurf'; this.workflowsDir = 'workflows'; + this.unifiedInstaller = new UnifiedInstaller(this.bmadFolderName); } /** @@ -22,93 +29,34 @@ class WindsurfSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Create .windsurf/workflows/bmad directory structure + // Create .windsurf/workflows directory const windsurfDir = path.join(projectDir, this.configDir); const workflowsDir = path.join(windsurfDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); - await this.ensureDir(bmadWorkflowsDir); + await this.ensureDir(workflowsDir); // Clean up any existing BMAD workflows before reinstalling await this.cleanup(projectDir); - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); + // Use UnifiedInstaller with Windsurf-specific configuration + const counts = await this.unifiedInstaller.install(projectDir, bmadDir, { + targetDir: workflowsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.WINDSURF, + customTemplateFn: this.windsurfTemplate.bind(this), + }, options.selectedModules || []); - // Convert artifacts to agent format for module organization - const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name })); - - // Get tasks, tools, and workflows (standalone only) - const tasks = await this.getTasks(bmadDir, true); - const tools = await this.getTools(bmadDir, true); - const workflows = await this.getWorkflows(bmadDir, true); - - // Create directories for each module under bmad/ - const modules = new Set(); - for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module); - - for (const module of modules) { - await this.ensureDir(path.join(bmadWorkflowsDir, module)); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'agents')); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tasks')); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tools')); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'workflows')); - } - - // Process agent launchers as workflows with organized structure - let agentCount = 0; - for (const artifact of agentArtifacts) { - const processedContent = this.createWorkflowContent({ module: artifact.module, name: artifact.name }, artifact.content); - - // Organized path: bmad/module/agents/agent-name.md - const targetPath = path.join(bmadWorkflowsDir, artifact.module, 'agents', `${artifact.name}.md`); - await this.writeFile(targetPath, processedContent); - agentCount++; - } - - // Process tasks as workflows with organized structure - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const processedContent = this.createTaskWorkflowContent(task, content); - - // Organized path: bmad/module/tasks/task-name.md - const targetPath = path.join(bmadWorkflowsDir, task.module, 'tasks', `${task.name}.md`); - await this.writeFile(targetPath, processedContent); - taskCount++; - } - - // Process tools as workflows with organized structure - let toolCount = 0; - for (const tool of tools) { - const content = await this.readFile(tool.path); - const processedContent = this.createToolWorkflowContent(tool, content); - - // Organized path: bmad/module/tools/tool-name.md - const targetPath = path.join(bmadWorkflowsDir, tool.module, 'tools', `${tool.name}.md`); - await this.writeFile(targetPath, processedContent); - toolCount++; - } - - // Process workflows with organized structure - let workflowCount = 0; - for (const workflow of workflows) { - const content = await this.readFile(workflow.path); - const processedContent = this.createWorkflowWorkflowContent(workflow, content); - - // Organized path: bmad/module/workflows/workflow-name.md - const targetPath = path.join(bmadWorkflowsDir, workflow.module, 'workflows', `${workflow.name}.md`); - await this.writeFile(targetPath, processedContent); - workflowCount++; - } + // Post-process tasks and tools to add Windsurf auto_execution_mode + // UnifiedInstaller handles agents/workflows correctly, but tasks/tools + // need special handling for proper Windsurf frontmatter + await this.addWindsurfTaskToolFrontmatter(workflowsDir); console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed`)); - console.log(chalk.dim(` - ${taskCount} tasks installed`)); - console.log(chalk.dim(` - ${toolCount} tools installed`)); - console.log(chalk.dim(` - ${workflowCount} workflows installed`)); - console.log(chalk.dim(` - Organized in modules: ${[...modules].join(', ')}`)); + console.log(chalk.dim(` - ${counts.agents} agents installed`)); + console.log(chalk.dim(` - ${counts.tasks} tasks installed`)); + console.log(chalk.dim(` - ${counts.tools} tools installed`)); + console.log(chalk.dim(` - ${counts.workflows} workflows installed`)); + console.log(chalk.dim(` - Total: ${counts.total} items`)); console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`)); // Provide additional configuration hints @@ -122,88 +70,120 @@ class WindsurfSetup extends BaseIdeSetup { return { success: true, - agents: agentCount, - tasks: taskCount, - tools: toolCount, - workflows: workflowCount, + ...counts, }; } /** - * Create workflow content for an agent + * Windsurf-specific template function + * Adds proper Windsurf frontmatter with auto_execution_mode */ - createWorkflowContent(agent, content) { - // Strip existing frontmatter from launcher + windsurfTemplate(artifact, content, templateType) { + // Strip existing frontmatter const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: ${agent.name} -auto_execution_mode: 3 + // Determine auto_execution_mode based on type + let autoExecMode = '1'; // default for workflows + let description = artifact.name || artifact.displayName || 'workflow'; + + if (artifact.type === 'agent') { + autoExecMode = '3'; + description = artifact.name || 'agent'; + } else if (artifact.type === 'workflow') { + autoExecMode = '1'; + description = artifact.name || 'workflow'; + } + + return `--- +description: ${description} +auto_execution_mode: ${autoExecMode} +--- + +${contentWithoutFrontmatter}`; + } + + /** + * Add Windsurf auto_execution_mode to task and tool files + * These are generated by TaskToolCommandGenerator with basic YAML + * but need the Windsurf-specific auto_execution_mode field + */ + async addWindsurfTaskToolFrontmatter(workflowsDir) { + if (!(await fs.pathExists(workflowsDir))) { + return; + } + + const entries = await fs.readdir(workflowsDir, { withFileTypes: true }); + let updatedCount = 0; + + for (const entry of entries) { + if (!entry.name.startsWith('bmad-') || !entry.name.endsWith('.md')) { + continue; + } + + const filePath = path.join(workflowsDir, entry.name); + let content = await fs.readFile(filePath, 'utf8'); + + // Check if this is a task or tool file + // They have pattern: bmad-module-task-name.md or bmad-module-tool-name.md + const parts = entry.name.replace('bmad-', '').replace('.md', '').split('-'); + if (parts.length < 2) continue; + + const type = parts[parts.length - 2]; // second to last part should be 'task' or 'tool' + + if (type === 'task' || type === 'tool') { + // Check if auto_execution_mode is already present + if (content.includes('auto_execution_mode')) { + continue; + } + + // Extract existing description if present + const descMatch = content.match(/description: '(.+?)'/); + const description = descMatch ? descMatch[1] : entry.name.replace('.md', ''); + + // Strip existing frontmatter and add Windsurf-specific frontmatter + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); + + content = `--- +description: '${description}' +auto_execution_mode: 2 --- ${contentWithoutFrontmatter}`; - return workflowContent; + await fs.writeFile(filePath, content, 'utf8'); + updatedCount++; + } + } + + if (updatedCount > 0) { + console.log(chalk.dim(` Updated ${updatedCount} task/tool files with Windsurf frontmatter`)); + } } /** - * Create workflow content for a task - */ - createTaskWorkflowContent(task, content) { - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: task-${task.name} -auto_execution_mode: 2 ---- - -${content}`; - - return workflowContent; - } - - /** - * Create workflow content for a tool - */ - createToolWorkflowContent(tool, content) { - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: tool-${tool.name} -auto_execution_mode: 2 ---- - -${content}`; - - return workflowContent; - } - - /** - * Create workflow content for a workflow - */ - createWorkflowWorkflowContent(workflow, content) { - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: ${workflow.name} -auto_execution_mode: 1 ---- - -${content}`; - - return workflowContent; - } - - /** - * Cleanup Windsurf configuration - surgically remove only BMAD files + * Cleanup Windsurf configuration - remove only BMAD files */ async cleanup(projectDir) { - const fs = require('fs-extra'); - const bmadPath = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad'); + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - if (await fs.pathExists(bmadPath)) { - // Remove the entire bmad folder - this is our territory - await fs.remove(bmadPath); - console.log(chalk.dim(` Cleaned up existing BMAD workflows`)); + if (await fs.pathExists(workflowsDir)) { + // Remove all bmad* files from workflows directory + const entries = await fs.readdir(workflowsDir, { withFileTypes: true }); + let removedCount = 0; + + for (const entry of entries) { + if (entry.name.startsWith('bmad')) { + const entryPath = path.join(workflowsDir, entry.name); + await fs.remove(entryPath); + removedCount++; + } + } + + if (removedCount > 0) { + console.log(chalk.dim(` Cleaned up ${removedCount} existing BMAD workflow files`)); + } } } @@ -216,14 +196,13 @@ ${content}`; * @returns {Object|null} Info about created command */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const fs = require('fs-extra'); - const customAgentsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad', 'custom', 'agents'); + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); if (!(await this.exists(path.join(projectDir, this.configDir)))) { return null; // IDE not configured for this project } - await this.ensureDir(customAgentsDir); + await this.ensureDir(workflowsDir); 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. @@ -237,7 +216,7 @@ ${content}`; `; - // Windsurf uses workflow format with frontmatter + // Windsurf uses workflow format with frontmatter - flat naming const workflowContent = `--- description: ${metadata.title || agentName} auto_execution_mode: 3 @@ -245,12 +224,14 @@ auto_execution_mode: 3 ${launcherContent}`; - const launcherPath = path.join(customAgentsDir, `${agentName}.md`); + // Use flat naming: bmad-custom-agent-agentname.md + const flatName = `bmad-custom-agent-${agentName}.md`; + const launcherPath = path.join(workflowsDir, flatName); await fs.writeFile(launcherPath, workflowContent); return { path: launcherPath, - command: `bmad/custom/agents/${agentName}`, + command: flatName.replace('.md', ''), }; } } From 4cb5cc7dbc91535d0109d27a127b892a74904e4d Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 12:04:34 -0600 Subject: [PATCH 06/11] Fix gemini installer to use UnifiedInstaller with .toml support - Add fileExtension parameter support to path-utils (toColonPath, toDashPath) - Add TemplateType.GEMINI to unified-installer for TOML output format - Update task-tool-command-generator to support TOML format - Refactor gemini.js to use UnifiedInstaller with: - NamingStyle.FLAT_DASH for dash-separated filenames - TemplateType.GEMINI for TOML format (description + prompt fields) - fileExtension: '.toml' for Gemini CLI TOML format: description = "BMAD Agent: Title" prompt = """ Content here """ --- tools/cli/installers/lib/ide/gemini.js | 177 +++--------------- .../installers/lib/ide/shared/path-utils.js | 69 ++++++- .../ide/shared/task-tool-command-generator.js | 68 ++++--- .../lib/ide/shared/unified-installer.js | 67 +++++-- 4 files changed, 182 insertions(+), 199 deletions(-) diff --git a/tools/cli/installers/lib/ide/gemini.js b/tools/cli/installers/lib/ide/gemini.js index a1673573..c26bb05a 100644 --- a/tools/cli/installers/lib/ide/gemini.js +++ b/tools/cli/installers/lib/ide/gemini.js @@ -3,8 +3,7 @@ const fs = require('fs-extra'); const yaml = require('yaml'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); /** * Gemini CLI setup handler @@ -15,8 +14,6 @@ class GeminiSetup extends BaseIdeSetup { super('gemini', 'Gemini CLI', false); this.configDir = '.gemini'; this.commandsDir = 'commands'; - this.agentTemplatePath = path.join(__dirname, 'templates', 'gemini-agent-command.toml'); - this.taskTemplatePath = path.join(__dirname, 'templates', 'gemini-task-command.toml'); } /** @@ -62,169 +59,39 @@ class GeminiSetup extends BaseIdeSetup { await this.ensureDir(commandsDir); - // Clean up any existing BMAD files before reinstalling - await this.cleanup(projectDir); + // Use UnifiedInstaller for agents and workflows + const installer = new UnifiedInstaller(this.bmadFolderName); - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); + const config = { + targetDir: commandsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.GEMINI, + fileExtension: '.toml', + }; - // Get tasks and workflows (ALL workflows now generate commands) - const tasks = await this.getTasks(bmadDir); + const counts = await installer.install(projectDir, bmadDir, config, options.selectedModules || []); - // Get ALL workflows using the new workflow command generator - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Install agents as TOML files with bmad- prefix (flat structure) - let agentCount = 0; - for (const artifact of agentArtifacts) { - const tomlContent = await this.createAgentLauncherToml(artifact); - - // Flat structure: bmad-agent-{module}-{name}.toml - const tomlPath = path.join(commandsDir, `bmad-agent-${artifact.module}-${artifact.name}.toml`); - await this.writeFile(tomlPath, tomlContent); - agentCount++; - - console.log(chalk.green(` ✓ Added agent: /bmad_agents_${artifact.module}_${artifact.name}`)); - } - - // Install tasks as TOML files with bmad- prefix (flat structure) - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const tomlContent = await this.createTaskToml(task, content); - - // Flat structure: bmad-task-{module}-{name}.toml - const tomlPath = path.join(commandsDir, `bmad-task-${task.module}-${task.name}.toml`); - await this.writeFile(tomlPath, tomlContent); - taskCount++; - - console.log(chalk.green(` ✓ Added task: /bmad_tasks_${task.module}_${task.name}`)); - } - - // Install workflows as TOML files with bmad- prefix (flat structure) - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - // Create TOML wrapper around workflow command content - const tomlContent = await this.createWorkflowToml(artifact); - - // Flat structure: bmad-workflow-{module}-{name}.toml - const workflowName = path.basename(artifact.relativePath, '.md'); - const tomlPath = path.join(commandsDir, `bmad-workflow-${artifact.module}-${workflowName}.toml`); - await this.writeFile(tomlPath, tomlContent); - workflowCount++; - - console.log(chalk.green(` ✓ Added workflow: /bmad_workflows_${artifact.module}_${workflowName}`)); - } - } + // Generate activation names for display + const agentActivation = `/bmad_agents_{agent-name}`; + const workflowActivation = `/bmad_workflows_{workflow-name}`; + const taskActivation = `/bmad_tasks_{task-name}`; console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents configured`)); - console.log(chalk.dim(` - ${taskCount} tasks configured`)); - console.log(chalk.dim(` - ${workflowCount} workflows configured`)); + console.log(chalk.dim(` - ${counts.agents} agents configured`)); + console.log(chalk.dim(` - ${counts.workflows} workflows configured`)); + console.log(chalk.dim(` - ${counts.tasks} tasks configured`)); + console.log(chalk.dim(` - ${counts.tools} tools configured`)); console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); - console.log(chalk.dim(` - Agent activation: /bmad_agents_{agent-name}`)); - console.log(chalk.dim(` - Task activation: /bmad_tasks_{task-name}`)); - console.log(chalk.dim(` - Workflow activation: /bmad_workflows_{workflow-name}`)); + console.log(chalk.dim(` - Agent activation: ${agentActivation}`)); + console.log(chalk.dim(` - Workflow activation: ${workflowActivation}`)); + console.log(chalk.dim(` - Task activation: ${taskActivation}`)); return { success: true, - agents: agentCount, - tasks: taskCount, - workflows: workflowCount, + ...counts, }; } - /** - * Create agent launcher TOML content from artifact - */ - async createAgentLauncherToml(artifact) { - // Strip frontmatter from launcher content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim(); - - // Extract title from launcher frontmatter - const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); - - // Create TOML wrapper around launcher content (without frontmatter) - const description = `BMAD ${artifact.module.toUpperCase()} Agent: ${title}`; - - return `description = "${description}" -prompt = """ -${contentWithoutFrontmatter} -""" -`; - } - - /** - * Create agent TOML content using template - */ - async createAgentToml(agent, content) { - // Extract metadata - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); - - // Load template - const template = await fs.readFile(this.agentTemplatePath, 'utf8'); - - // Replace template variables - // Note: {user_name} and other {config_values} are left as-is for runtime substitution by Gemini - const tomlContent = template - .replaceAll('{{title}}', title) - .replaceAll('{_bmad}', '_bmad') - .replaceAll('{_bmad}', this.bmadFolderName) - .replaceAll('{{module}}', agent.module) - .replaceAll('{{name}}', agent.name); - - return tomlContent; - } - - /** - * Create task TOML content using template - */ - async createTaskToml(task, content) { - // Extract task name from XML if available - const nameMatch = content.match(/([^<]+)<\/name>/); - const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); - - // Load template - const template = await fs.readFile(this.taskTemplatePath, 'utf8'); - - // Replace template variables - const tomlContent = template - .replaceAll('{{taskName}}', taskName) - .replaceAll('{_bmad}', '_bmad') - .replaceAll('{_bmad}', this.bmadFolderName) - .replaceAll('{{module}}', task.module) - .replaceAll('{{filename}}', task.filename); - - return tomlContent; - } - - /** - * Create workflow TOML content from artifact - */ - async createWorkflowToml(artifact) { - // Extract description from artifact content - const descriptionMatch = artifact.content.match(/description:\s*"([^"]+)"/); - const description = descriptionMatch - ? descriptionMatch[1] - : `BMAD ${artifact.module.toUpperCase()} Workflow: ${path.basename(artifact.relativePath, '.md')}`; - - // Strip frontmatter from command content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim(); - - return `description = "${description}" -prompt = """ -${contentWithoutFrontmatter} -""" -`; - } - /** * Cleanup Gemini configuration - surgically remove only BMAD files */ diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index dc564774..6280f04d 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -5,6 +5,9 @@ * - Underscore format (bmad_module_name.md) - Windows-compatible universal format */ +// Default file extension for backward compatibility +const DEFAULT_FILE_EXTENSION = '.md'; + // Type segments - agents are included in naming, others are filtered out const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const AGENT_SEGMENT = 'agents'; @@ -18,15 +21,16 @@ const AGENT_SEGMENT = 'agents'; * @param {string} module - Module name (e.g., 'bmm', 'core') * @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools') * @param {string} name - Artifact name (e.g., 'pm', 'brainstorming') + * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot (e.g., '.md', '.toml') * @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_bmm_correct-course.md' */ -function toUnderscoreName(module, type, name) { +function toUnderscoreName(module, type, name, fileExtension = DEFAULT_FILE_EXTENSION) { const isAgent = type === AGENT_SEGMENT; // For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md' if (module === 'core') { - return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`; + return isAgent ? `bmad_agent_${name}${fileExtension}` : `bmad_${name}${fileExtension}`; } - return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`; + return isAgent ? `bmad_${module}_agent_${name}${fileExtension}` : `bmad_${module}_${name}${fileExtension}`; } /** @@ -36,10 +40,14 @@ function toUnderscoreName(module, type, name) { * Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix) * * @param {string} relativePath - Path like 'bmm/agents/pm.md' + * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot (e.g., '.md', '.toml') * @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_brainstorming.md' */ -function toUnderscorePath(relativePath) { - const withoutExt = relativePath.replace('.md', ''); +function toUnderscorePath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { + // Extract extension from relativePath to properly remove it + const extMatch = relativePath.match(/\.[^.]+$/); + const originalExt = extMatch ? extMatch[0] : ''; + const withoutExt = relativePath.replace(originalExt, ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; @@ -47,7 +55,7 @@ function toUnderscorePath(relativePath) { const name = parts.slice(2).join('_'); // Use toUnderscoreName for consistency - return toUnderscoreName(module, type, name); + return toUnderscoreName(module, type, name, fileExtension); } /** @@ -55,10 +63,11 @@ function toUnderscorePath(relativePath) { * Creates: 'bmad_custom_fred-commit-poet.md' * * @param {string} agentName - Custom agent name + * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot (e.g., '.md', '.toml') * @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md' */ -function customAgentUnderscoreName(agentName) { - return `bmad_custom_${agentName}.md`; +function customAgentUnderscoreName(agentName, fileExtension = DEFAULT_FILE_EXTENSION) { + return `bmad_custom_${agentName}${fileExtension}`; } /** @@ -134,9 +143,9 @@ function parseUnderscoreName(filename) { } // Backward compatibility aliases (deprecated) +// Note: These now use toDashPath and customAgentDashName which convert underscores to dashes const toColonName = toUnderscoreName; -const toColonPath = toUnderscorePath; -const toDashPath = toUnderscorePath; +const toDashName = toUnderscoreName; const customAgentColonName = customAgentUnderscoreName; const customAgentDashName = customAgentUnderscoreName; const isColonFormat = isUnderscoreFormat; @@ -144,7 +153,46 @@ const isDashFormat = isUnderscoreFormat; const parseColonName = parseUnderscoreName; const parseDashName = parseUnderscoreName; +/** + * Convert relative path to flat colon-separated name (for backward compatibility) + * This is actually the same as underscore format now (underscores in filenames) + * @param {string} relativePath - Path like 'bmm/agents/pm.md' + * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot + * @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' + */ +function toColonPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { + return toUnderscorePath(relativePath, fileExtension); +} + +/** + * Convert relative path to flat dash-separated name + * Converts: 'bmm/agents/pm.md' → 'bmad-bmm-agent-pm.md' + * Converts: 'bmm/workflows/correct-course' → 'bmad-bmm-correct-course.md' + * @param {string} relativePath - Path like 'bmm/agents/pm.md' + * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot + * @returns {string} Flat filename like 'bmad-bmm-agent-pm.md' + */ +function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { + // Extract extension from relativePath to properly remove it + const extMatch = relativePath.match(/\.[^.]+$/); + const originalExt = extMatch ? extMatch[0] : ''; + const withoutExt = relativePath.replace(originalExt, ''); + const parts = withoutExt.split(/[/\\]/); + + const module = parts[0]; + const type = parts[1]; + const name = parts.slice(2).join('-'); + + // Use dash naming style + const isAgent = type === AGENT_SEGMENT; + if (module === 'core') { + return isAgent ? `bmad-agent-${name}${fileExtension}` : `bmad-${name}${fileExtension}`; + } + return isAgent ? `bmad-${module}-agent-${name}${fileExtension}` : `bmad-${module}-${name}${fileExtension}`; +} + module.exports = { + DEFAULT_FILE_EXTENSION, toUnderscoreName, toUnderscorePath, customAgentUnderscoreName, @@ -153,6 +201,7 @@ module.exports = { // Backward compatibility aliases toColonName, toColonPath, + toDashName, toDashPath, customAgentColonName, customAgentDashName, 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 8483308b..5e35fdaf 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 @@ -16,8 +16,11 @@ class TaskToolCommandGenerator { /** * Generate command content for a task or tool + * @param {Object} item - Task or tool item from manifest + * @param {string} type - 'task' or 'tool' + * @param {string} [format='yaml'] - Output format: 'yaml' or 'toml' */ - generateCommandContent(item, type) { + generateCommandContent(item, type, format = 'yaml') { const description = item.description || `Execute ${item.displayName || item.name}`; // Convert path to use {project-root} placeholder @@ -26,16 +29,29 @@ class TaskToolCommandGenerator { itemPath = `{project-root}/${itemPath}`; } - return `--- -description: '${description.replaceAll("'", "''")}' ---- - -# ${item.displayName || item.name} + const content = `# ${item.displayName || item.name} LOAD and execute the ${type} at: ${itemPath} Follow all instructions in the ${type} file exactly as written. `; + + if (format === 'toml') { + // Escape any triple quotes in content + const escapedContent = content.replace(/"""/g, '\\"\\"\\"'); + return `description = "${description}" +prompt = """ +${escapedContent} +""" +`; + } + + // Default YAML format + return `--- +description: '${description.replaceAll("'", "''")}' +--- + +${content}`; } /** @@ -91,9 +107,10 @@ Follow all instructions in the ${type} file exactly as written. * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory * @param {string} baseCommandsDir - Base commands directory for the IDE + * @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml') * @returns {Object} Generation results */ - async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir) { + async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir, fileExtension = '.md') { const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); @@ -101,16 +118,18 @@ Follow all instructions in the ${type} file exactly as written. const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; + // Determine format based on file extension + const format = fileExtension === '.toml' ? 'toml' : 'yaml'; let generatedCount = 0; // DEBUG: Log parameters - console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}`); + console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}, format=${format}`); // 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 commandContent = this.generateCommandContent(task, 'task', format); + // Use underscore format: bmad_bmm_name. + const flatName = toColonName(task.module, 'tasks', task.name, fileExtension); const commandPath = path.join(baseCommandsDir, flatName); console.log(`[DEBUG generateColonTaskToolCommands] Writing task ${task.name} to: ${commandPath}`); await fs.ensureDir(path.dirname(commandPath)); @@ -120,9 +139,9 @@ 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 - const flatName = toColonName(tool.module, 'tools', tool.name); + const commandContent = this.generateCommandContent(tool, 'tool', format); + // Use underscore format: bmad_bmm_name. + const flatName = toColonName(tool.module, 'tools', tool.name, fileExtension); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); @@ -137,15 +156,16 @@ Follow all instructions in the ${type} file exactly as written. } /** - * Generate task and tool commands using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_bmad-help.md + * Generate task and tool commands using dash format + * Creates flat files like: bmad-bmm-bmad-help.md * * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory * @param {string} baseCommandsDir - Base commands directory for the IDE + * @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml') * @returns {Object} Generation results */ - async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir) { + async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir, fileExtension = '.md') { const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); @@ -153,13 +173,15 @@ Follow all instructions in the ${type} file exactly as written. const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; + // Determine format based on file extension + const format = fileExtension === '.toml' ? 'toml' : 'yaml'; let generatedCount = 0; // Generate command files for tasks for (const task of standaloneTasks) { - const commandContent = this.generateCommandContent(task, 'task'); - // Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath) - const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); + const commandContent = this.generateCommandContent(task, 'task', format); + // Use dash format: bmad-bmm-task-name. + const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`, fileExtension); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); @@ -168,9 +190,9 @@ 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 (toDashPath aliases toColonPath) - const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); + const commandContent = this.generateCommandContent(tool, 'tool', format); + // Use dash format: bmad-bmm-tool-name. + const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`, fileExtension); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); await fs.writeFile(commandPath, commandContent); diff --git a/tools/cli/installers/lib/ide/shared/unified-installer.js b/tools/cli/installers/lib/ide/shared/unified-installer.js index 98097320..75633460 100644 --- a/tools/cli/installers/lib/ide/shared/unified-installer.js +++ b/tools/cli/installers/lib/ide/shared/unified-installer.js @@ -39,6 +39,7 @@ const TemplateType = { CLINE: 'cline', // No frontmatter, direct content WINDSURF: 'windsurf', // YAML with auto_execution_mode AUGMENT: 'augment', // YAML frontmatter + GEMINI: 'gemini', // TOML frontmatter with description/prompt }; /** @@ -47,6 +48,7 @@ const TemplateType = { * @property {string} targetDir - Full path to target directory * @property {NamingStyle} namingStyle - How to name files * @property {TemplateType} templateType - What template format to use + * @property {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml') * @property {boolean} includeNestedStructure - For NESTED style, create subdirectories * @property {Function} [customTemplateFn] - Optional custom template function */ @@ -73,12 +75,13 @@ class UnifiedInstaller { targetDir, namingStyle = NamingStyle.FLAT_COLON, templateType = TemplateType.CLAUDE, + fileExtension = '.md', includeNestedStructure = false, customTemplateFn = null, } = config; // Clean up any existing BMAD files in target directory - await this.cleanupBmadFiles(targetDir); + await this.cleanupBmadFiles(targetDir, fileExtension); // Ensure target directory exists await fs.ensureDir(targetDir); @@ -95,7 +98,7 @@ class UnifiedInstaller { // 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'); + counts.agents = await this.writeArtifacts(agentArtifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, 'agent'); // 2. Install Workflows (filter out README artifacts) const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); @@ -109,6 +112,7 @@ class UnifiedInstaller { targetDir, namingStyle, templateType, + fileExtension, customTemplateFn, 'workflow', ); @@ -121,8 +125,8 @@ class UnifiedInstaller { // 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); + ? await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension) + : await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension); counts.tasks = taskToolResult.tasks || 0; counts.tools = taskToolResult.tools || 0; @@ -134,8 +138,10 @@ class UnifiedInstaller { /** * Clean up any existing BMAD files in target directory + * @param {string} targetDir - Target directory to clean + * @param {string} [fileExtension='.md'] - File extension to match */ - async cleanupBmadFiles(targetDir) { + async cleanupBmadFiles(targetDir, fileExtension = '.md') { if (!(await fs.pathExists(targetDir))) { return; } @@ -144,7 +150,8 @@ class UnifiedInstaller { const entries = await fs.readdir(targetDir, { withFileTypes: true }); for (const entry of entries) { - if (entry.name.startsWith('bmad')) { + // Only remove files with the matching extension + if (entry.name.startsWith('bmad') && entry.name.endsWith(fileExtension)) { const entryPath = path.join(targetDir, entry.name); await fs.remove(entryPath); } @@ -153,9 +160,17 @@ class UnifiedInstaller { /** * Write artifacts with specified naming style and template + * @param {Array} artifacts - Artifacts to write + * @param {string} targetDir - Target directory + * @param {NamingStyle} namingStyle - Naming style to use + * @param {TemplateType} templateType - Template type to use + * @param {string} fileExtension - File extension including dot + * @param {Function} customTemplateFn - Optional custom template function + * @param {string} artifactType - Type of artifact for logging + * @returns {Promise} Number of artifacts written */ - async writeArtifacts(artifacts, targetDir, namingStyle, templateType, customTemplateFn, artifactType) { - console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}`); + async writeArtifacts(artifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, artifactType) { + console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}, fileExtension=${fileExtension}`); let written = 0; for (const artifact of artifacts) { @@ -165,14 +180,14 @@ class UnifiedInstaller { console.log(`[DEBUG] writeArtifacts processing: relativePath=${artifact.relativePath}, name=${artifact.name}`); if (namingStyle === NamingStyle.FLAT_COLON) { - const flatName = toColonPath(artifact.relativePath); + const flatName = toColonPath(artifact.relativePath, fileExtension); targetPath = path.join(targetDir, flatName); } else if (namingStyle === NamingStyle.FLAT_DASH) { - const flatName = toDashPath(artifact.relativePath); + const flatName = toDashPath(artifact.relativePath, fileExtension); targetPath = path.join(targetDir, flatName); } else { // Fallback: treat as flat even if NESTED specified - const flatName = toColonPath(artifact.relativePath); + const flatName = toColonPath(artifact.relativePath, fileExtension); targetPath = path.join(targetDir, flatName); } @@ -218,6 +233,11 @@ class UnifiedInstaller { return this.addAugmentFrontmatter(artifact, content); } + case TemplateType.GEMINI: { + // Add Gemini TOML frontmatter + return this.addGeminiFrontmatter(artifact, content); + } + default: { return content; } @@ -269,6 +289,31 @@ description: ${name} return content; } + /** + * Add Gemini TOML frontmatter + * Converts content to TOML format with description and prompt fields + */ + addGeminiFrontmatter(artifact, content) { + // Remove existing YAML frontmatter if present + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = content.replace(frontmatterRegex, '').trim(); + + // Extract description from artifact or content + let description = artifact.name || artifact.displayName || 'BMAD Command'; + if (artifact.module) { + description = `BMAD ${artifact.module.toUpperCase()} ${artifact.type || 'Command'}: ${description}`; + } + + // Escape any triple quotes in content + const escapedContent = contentWithoutFrontmatter.replace(/"""/g, '\\"\\"\\"'); + + return `description = "${description}" +prompt = """ +${escapedContent} +""" +`; + } + /** * Get tasks from manifest CSV */ From 5aef6379b9eda5317506ebfe547a17249b94698a Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 12:12:06 -0600 Subject: [PATCH 07/11] Fix github-copilot installer to use UnifiedInstaller for prompts - Add .github/prompts directory alongside .github/agents - Use UnifiedInstaller with TemplateType.COPILOT for prompts/workflows/tasks/tools - Fix typo: bmd-custom- -> bmad- prefix for agents - Update cleanup to handle both directories - Format fixes for auggie.js and windsurf.js --- tools/cli/installers/lib/ide/auggie.js | 17 +++-- .../cli/installers/lib/ide/github-copilot.js | 65 +++++++++++++++---- .../lib/ide/shared/unified-installer.js | 60 ++++++++++++++++- tools/cli/installers/lib/ide/windsurf.js | 19 ++++-- 4 files changed, 134 insertions(+), 27 deletions(-) diff --git a/tools/cli/installers/lib/ide/auggie.js b/tools/cli/installers/lib/ide/auggie.js index 2a65e57d..5a4170b5 100644 --- a/tools/cli/installers/lib/ide/auggie.js +++ b/tools/cli/installers/lib/ide/auggie.js @@ -28,12 +28,17 @@ class AuggieSetup extends BaseIdeSetup { const targetDir = path.join(projectDir, '.augment', 'commands'); // Install using UnifiedInstaller - const counts = await this.installer.install(projectDir, bmadDir, { - targetDir, - namingStyle: NamingStyle.FLAT_COLON, - templateType: TemplateType.AUGMENT, - includeNestedStructure: false, - }, options.selectedModules || []); + const counts = await this.installer.install( + projectDir, + bmadDir, + { + targetDir, + namingStyle: NamingStyle.FLAT_COLON, + templateType: TemplateType.AUGMENT, + includeNestedStructure: false, + }, + options.selectedModules || [], + ); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${counts.agents} agents installed`)); diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js index a7c6c925..3c504701 100644 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ b/tools/cli/installers/lib/ide/github-copilot.js @@ -2,6 +2,7 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); const prompts = require('../../../lib/prompts'); /** @@ -13,6 +14,7 @@ class GitHubCopilotSetup extends BaseIdeSetup { super('github-copilot', 'GitHub Copilot', true); // preferred IDE this.configDir = '.github'; this.agentsDir = 'agents'; + this.promptsDir = 'prompts'; this.vscodeDir = '.vscode'; } @@ -94,41 +96,63 @@ class GitHubCopilotSetup extends BaseIdeSetup { const config = options.preCollectedConfig || {}; await this.configureVsCodeSettings(projectDir, { ...options, ...config }); - // Create .github/agents directory + // Create .github/agents and .github/prompts directories const githubDir = path.join(projectDir, this.configDir); const agentsDir = path.join(githubDir, this.agentsDir); + const promptsDir = path.join(githubDir, this.promptsDir); await this.ensureDir(agentsDir); + await this.ensureDir(promptsDir); // Clean up any existing BMAD files before reinstalling await this.cleanup(projectDir); - // Generate agent launchers + // 1. Generate agent launchers (custom .agent.md format - not using UnifiedInstaller) const agentGen = new AgentCommandGenerator(this.bmadFolderName); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - // Create agent files with bmd- prefix + // Create agent files with bmad- prefix let agentCount = 0; for (const artifact of agentArtifacts) { const content = artifact.content; const agentContent = await this.createAgentContent({ module: artifact.module, name: artifact.name }, content); - // Use bmd- prefix: bmd-custom-{module}-{name}.agent.md - const targetPath = path.join(agentsDir, `bmd-custom-${artifact.module}-${artifact.name}.agent.md`); + // Use bmad- prefix: bmad-{module}-{name}.agent.md + const targetPath = path.join(agentsDir, `bmad-${artifact.module}-${artifact.name}.agent.md`); await this.writeFile(targetPath, agentContent); agentCount++; - console.log(chalk.green(` ✓ Created agent: bmd-custom-${artifact.module}-${artifact.name}`)); + console.log(chalk.green(` ✓ Created agent: bmad-${artifact.module}-${artifact.name}`)); } + // 2. Install prompts using UnifiedInstaller + const installer = new UnifiedInstaller(this.bmadFolderName); + const promptCounts = await installer.install( + projectDir, + bmadDir, + { + targetDir: promptsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.COPILOT, + }, + options.selectedModules || [], + ); + console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents created`)); + console.log( + chalk.dim( + ` - ${promptCounts.agents} prompts, ${promptCounts.workflows} workflows, ${promptCounts.tasks + promptCounts.tools} tasks/tools`, + ), + ); console.log(chalk.dim(` - Agents directory: ${path.relative(projectDir, agentsDir)}`)); + console.log(chalk.dim(` - Prompts directory: ${path.relative(projectDir, promptsDir)}`)); console.log(chalk.dim(` - VS Code settings configured`)); - console.log(chalk.dim('\n Agents available in VS Code Chat view')); + console.log(chalk.dim('\n Agents and prompts available in VS Code Chat view')); return { success: true, agents: agentCount, + prompts: promptCounts.total, settings: true, }; } @@ -286,14 +310,15 @@ ${cleanContent} } } - // Clean up new agents directory + // Clean up agents directory const agentsDir = path.join(projectDir, this.configDir, this.agentsDir); if (await fs.pathExists(agentsDir)) { const files = await fs.readdir(agentsDir); let removed = 0; for (const file of files) { - if (file.startsWith('bmd-') && file.endsWith('.agent.md')) { + // Remove old bmd-* files (typo fix) and current bmad-* files + if ((file.startsWith('bmd-') || file.startsWith('bmad-')) && file.endsWith('.agent.md')) { await fs.remove(path.join(agentsDir, file)); removed++; } @@ -303,6 +328,24 @@ ${cleanContent} console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`)); } } + + // Clean up prompts directory + const promptsDir = path.join(projectDir, this.configDir, this.promptsDir); + if (await fs.pathExists(promptsDir)) { + const files = await fs.readdir(promptsDir); + let removed = 0; + + for (const file of files) { + if (file.startsWith('bmad-') && file.endsWith('.md')) { + await fs.remove(path.join(promptsDir, file)); + removed++; + } + } + + if (removed > 0) { + console.log(chalk.dim(` Cleaned up ${removed} existing BMAD prompts`)); + } + } } /** @@ -370,12 +413,12 @@ tools: ${JSON.stringify(copilotTools)} ${launcherContent} `; - const agentFilePath = path.join(agentsDir, `bmd-custom-${agentName}.agent.md`); + const agentFilePath = path.join(agentsDir, `bmad-${agentName}.agent.md`); await this.writeFile(agentFilePath, agentContent); return { path: agentFilePath, - command: `bmd-custom-${agentName}`, + command: `bmad-${agentName}`, }; } } diff --git a/tools/cli/installers/lib/ide/shared/unified-installer.js b/tools/cli/installers/lib/ide/shared/unified-installer.js index 75633460..ef93dcd3 100644 --- a/tools/cli/installers/lib/ide/shared/unified-installer.js +++ b/tools/cli/installers/lib/ide/shared/unified-installer.js @@ -40,6 +40,7 @@ const TemplateType = { WINDSURF: 'windsurf', // YAML with auto_execution_mode AUGMENT: 'augment', // YAML frontmatter GEMINI: 'gemini', // TOML frontmatter with description/prompt + COPILOT: 'copilot', // YAML with tools array for GitHub Copilot }; /** @@ -98,7 +99,15 @@ class UnifiedInstaller { // 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, fileExtension, customTemplateFn, 'agent'); + counts.agents = await this.writeArtifacts( + agentArtifacts, + targetDir, + namingStyle, + templateType, + fileExtension, + customTemplateFn, + 'agent', + ); // 2. Install Workflows (filter out README artifacts) const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); @@ -170,7 +179,9 @@ class UnifiedInstaller { * @returns {Promise} Number of artifacts written */ async writeArtifacts(artifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, artifactType) { - console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}, fileExtension=${fileExtension}`); + console.log( + `[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}, fileExtension=${fileExtension}`, + ); let written = 0; for (const artifact of artifacts) { @@ -238,6 +249,11 @@ class UnifiedInstaller { return this.addGeminiFrontmatter(artifact, content); } + case TemplateType.COPILOT: { + // Add Copilot frontmatter with tools array + return this.addCopilotFrontmatter(artifact, content); + } + default: { return content; } @@ -305,7 +321,7 @@ description: ${name} } // Escape any triple quotes in content - const escapedContent = contentWithoutFrontmatter.replace(/"""/g, '\\"\\"\\"'); + const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`); return `description = "${description}" prompt = """ @@ -314,6 +330,44 @@ ${escapedContent} `; } + /** + * Add GitHub Copilot frontmatter with tools array + */ + addCopilotFrontmatter(artifact, content) { + // Remove existing frontmatter if present + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); + + // GitHub Copilot tools array (as specified) + const tools = [ + 'changes', + 'edit', + 'fetch', + 'githubRepo', + 'problems', + 'runCommands', + 'runTasks', + 'runTests', + 'search', + 'runSubagent', + 'testFailure', + 'todos', + 'usages', + ]; + + const name = artifact.name || artifact.displayName || 'prompt'; + const description = `Activates the ${name} ${artifact.type || 'workflow'}.`; + + const frontmatter = `--- +description: "${description}" +tools: ${JSON.stringify(tools)} +--- + +`; + + return frontmatter + contentWithoutFrontmatter; + } + /** * Get tasks from manifest CSV */ diff --git a/tools/cli/installers/lib/ide/windsurf.js b/tools/cli/installers/lib/ide/windsurf.js index 6463311e..2be6e189 100644 --- a/tools/cli/installers/lib/ide/windsurf.js +++ b/tools/cli/installers/lib/ide/windsurf.js @@ -39,12 +39,17 @@ class WindsurfSetup extends BaseIdeSetup { await this.cleanup(projectDir); // Use UnifiedInstaller with Windsurf-specific configuration - const counts = await this.unifiedInstaller.install(projectDir, bmadDir, { - targetDir: workflowsDir, - namingStyle: NamingStyle.FLAT_DASH, - templateType: TemplateType.WINDSURF, - customTemplateFn: this.windsurfTemplate.bind(this), - }, options.selectedModules || []); + const counts = await this.unifiedInstaller.install( + projectDir, + bmadDir, + { + targetDir: workflowsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.WINDSURF, + customTemplateFn: this.windsurfTemplate.bind(this), + }, + options.selectedModules || [], + ); // Post-process tasks and tools to add Windsurf auto_execution_mode // UnifiedInstaller handles agents/workflows correctly, but tasks/tools @@ -129,7 +134,7 @@ ${contentWithoutFrontmatter}`; const parts = entry.name.replace('bmad-', '').replace('.md', '').split('-'); if (parts.length < 2) continue; - const type = parts[parts.length - 2]; // second to last part should be 'task' or 'tool' + const type = parts.at(-2); // second to last part should be 'task' or 'tool' if (type === 'task' || type === 'tool') { // Check if auto_execution_mode is already present From b102694c642131c9b253e03322768fc088028cd4 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 12:15:57 -0600 Subject: [PATCH 08/11] Fix: use String.raw for escaped triple quotes in TOML --- .../installers/lib/ide/shared/task-tool-command-generator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5e35fdaf..726356ee 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 @@ -38,7 +38,7 @@ Follow all instructions in the ${type} file exactly as written. if (format === 'toml') { // Escape any triple quotes in content - const escapedContent = content.replace(/"""/g, '\\"\\"\\"'); + const escapedContent = content.replaceAll('"""', String.raw`\"\"\"`); return `description = "${description}" prompt = """ ${escapedContent} From b4f230f565d07edd1b4927b4b01e1e8d07c74d83 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 24 Jan 2026 23:05:43 -0600 Subject: [PATCH 09/11] installer standardization --- .../lib/ide/STANDARDIZATION_PLAN.md | 208 ------------ tools/cli/installers/lib/ide/antigravity.js | 33 +- tools/cli/installers/lib/ide/iflow.js | 79 ++--- tools/cli/installers/lib/ide/qwen.js | 275 ++++------------ tools/cli/installers/lib/ide/rovo-dev.js | 311 ++++++------------ .../lib/ide/shared/agent-command-generator.js | 16 +- .../installers/lib/ide/shared/path-utils.js | 16 +- .../lib/ide/shared/unified-installer.js | 9 +- .../ide/templates/agent-command-template.md | 2 +- .../lib/ide/templates/workflow-commander.md | 1 + 10 files changed, 243 insertions(+), 707 deletions(-) delete mode 100644 tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md diff --git a/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md b/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md deleted file mode 100644 index f7116cb5..00000000 --- a/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md +++ /dev/null @@ -1,208 +0,0 @@ -# IDE Installer Standardization Plan - -## Overview - -Standardize IDE installers to use **flat file naming** with **underscores** (Windows-compatible) and centralize duplicated code in shared utilities. - -**Key Rule: All IDEs use underscore format for Windows compatibility (colons don't work on Windows).** - -## Current State Analysis - -### File Structure Patterns - -| IDE | Current Pattern | Path Format | -|-----|-----------------|-------------| -| **claude-code** | Hierarchical | `.claude/commands/bmad/{module}/agents/{name}.md` | -| **cursor** | Hierarchical | `.cursor/commands/bmad/{module}/agents/{name}.md` | -| **crush** | Hierarchical | `.crush/commands/bmad/{module}/agents/{name}.md` | -| **antigravity** | Flattened (underscores) | `.agent/workflows/bmad_module_agents_name.md` | -| **codex** | Flattened (underscores) | `~/.codex/prompts/bmad_module_agents_name.md` | -| **cline** | Flattened (underscores) | `.clinerules/workflows/bmad_module_type_name.md` | -| **roo** | Flattened (underscores) | `.roo/commands/bmad_module_agent_name.md` | -| **auggie** | Hybrid | `.augment/commands/bmad/agents/{module}-{name}.md` | -| **iflow** | Hybrid | `.iflow/commands/bmad/agents/{module}-{name}.md` | -| **trae** | Different (rules) | `.trae/rules/bmad-agent-{module}-{name}.md` | -| **github-copilot** | Different (agents) | `.github/agents/bmd-custom-{module}-{name}.agent.md` | - -### Shared Generators (in `/shared`) - -1. `agent-command-generator.js` - generates agent launchers -2. `task-tool-command-generator.js` - generates task/tool commands -3. `workflow-command-generator.js` - generates workflow commands - -All currently create artifacts with **nested relative paths** like `{module}/agents/{name}.md` - -### Code Duplication Issues - -1. **Flattening logic** duplicated in multiple IDEs -2. **Agent launcher content creation** duplicated -3. **Path transformation** duplicated - -## Target Standardization - -### For All IDEs (underscore format - Windows-compatible) - -**IDEs affected:** claude-code, cursor, crush, antigravity, codex, cline, roo - -``` -Format: bmad_{module}_{type}_{name}.md - -Examples: -- Agent: bmad_bmm_agents_pm.md -- Agent: bmad_core_agents_dev.md -- Workflow: bmad_bmm_workflows_correct-course.md -- Task: bmad_bmm_tasks_bmad-help.md -- Tool: bmad_core_tools_code-review.md -- Custom: bmad_custom_agents_fred-commit-poet.md -``` - -**Note:** Type segments (agents, workflows, tasks, tools) are filtered out from names: -- `bmm/agents/pm.md` → `bmad_bmm_pm.md` (not `bmad_bmm_agents_pm.md`) - -### For Hybrid IDEs (keep as-is) - -**IDEs affected:** auggie, iflow - -These use `{module}-{name}.md` format within subdirectories - keep as-is. - -### Skip (drastically different) - -**IDEs affected:** trae, github-copilot - -## Implementation Plan - -### Phase 1: Create Shared Utility - -**File:** `shared/path-utils.js` - -```javascript -/** - * Convert hierarchical path to flat underscore-separated name (Windows-compatible) - * @param {string} module - Module name (e.g., 'bmm', 'core') - * @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools') - filtered out - * @param {string} name - Artifact name (e.g., 'pm', 'correct-course') - * @returns {string} Flat filename like 'bmad_bmm_pm.md' - */ -function toUnderscoreName(module, type, name) { - return `bmad_${module}_${name}.md`; -} - -/** - * Convert relative path to flat underscore-separated name (Windows-compatible) - * @param {string} relativePath - Path like 'bmm/agents/pm.md' - * @returns {string} Flat filename like 'bmad_bmm_pm.md' - */ -function toUnderscorePath(relativePath) { - const withoutExt = relativePath.replace('.md', ''); - const parts = withoutExt.split(/[\/\\]/); - // Filter out type segments (agents, workflows, tasks, tools) - const filtered = parts.filter((p) => !TYPE_SEGMENTS.includes(p)); - return `bmad_${filtered.join('_')}.md`; -} - -/** - * Create custom agent underscore name - * @param {string} agentName - Custom agent name - * @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md' - */ -function customAgentUnderscoreName(agentName) { - return `bmad_custom_${agentName}.md`; -} - -// Backward compatibility aliases -const toColonName = toUnderscoreName; -const toColonPath = toUnderscorePath; -const toDashPath = toUnderscorePath; -const customAgentColonName = customAgentUnderscoreName; -const customAgentDashName = customAgentUnderscoreName; - -module.exports = { - toUnderscoreName, - toUnderscorePath, - customAgentUnderscoreName, - // Backward compatibility - toColonName, - toColonPath, - toDashPath, - customAgentColonName, - customAgentDashName, -}; -``` - -### Phase 2: Update Shared Generators - -**Files to modify:** -- `shared/agent-command-generator.js` -- `shared/task-tool-command-generator.js` -- `shared/workflow-command-generator.js` - -**Changes:** -1. Import path utilities -2. Change `relativePath` to use flat format -3. Add method `writeColonArtifacts()` for folder-based IDEs (uses underscore) -4. Add method `writeDashArtifacts()` for flat IDEs (uses underscore) - -### Phase 3: Update All IDEs - -**Files to modify:** -- `claude-code.js` -- `cursor.js` -- `crush.js` -- `antigravity.js` -- `codex.js` -- `cline.js` -- `roo.js` - -**Changes:** -1. Import utilities from path-utils -2. Change from hierarchical to flat underscore naming -3. Update cleanup to handle flat structure (`startsWith('bmad')`) - -### Phase 4: Update Base Class - -**File:** `_base-ide.js` - -**Changes:** -1. Mark `flattenFilename()` as `@deprecated` -2. Add comment pointing to new path-utils - -## Migration Checklist - -### New Files -- [x] Create `shared/path-utils.js` - -### All IDEs (convert to underscore format) -- [x] Update `shared/agent-command-generator.js` - update for underscore -- [x] Update `shared/task-tool-command-generator.js` - update for underscore -- [x] Update `shared/workflow-command-generator.js` - update for underscore -- [x] Update `claude-code.js` - convert to underscore format -- [x] Update `cursor.js` - convert to underscore format -- [x] Update `crush.js` - convert to underscore format -- [ ] Update `antigravity.js` - use underscore format -- [ ] Update `codex.js` - use underscore format -- [ ] Update `cline.js` - use underscore format -- [ ] Update `roo.js` - use underscore format - -### CSV Command Files -- [x] Update `src/core/module-help.csv` - change colons to underscores -- [x] Update `src/bmm/module-help.csv` - change colons to underscores - -### Base Class -- [ ] Update `_base-ide.js` - add deprecation notice - -### Testing -- [ ] Test claude-code installation -- [ ] Test cursor installation -- [ ] Test crush installation -- [ ] Test antigravity installation -- [ ] Test codex installation -- [ ] Test cline installation -- [ ] Test roo installation - -## Notes - -1. **Filter type segments**: agents, workflows, tasks, tools are filtered out from flat names -2. **Underscore format**: Universal underscore format for Windows compatibility -3. **Custom agents**: Follow the same pattern as regular agents -4. **Backward compatibility**: Old function names kept as aliases -5. **Cleanup**: Will remove old `bmad:` format files on next install diff --git a/tools/cli/installers/lib/ide/antigravity.js b/tools/cli/installers/lib/ide/antigravity.js index 73464f0d..4e472c1e 100644 --- a/tools/cli/installers/lib/ide/antigravity.js +++ b/tools/cli/installers/lib/ide/antigravity.js @@ -91,10 +91,13 @@ class AntigravitySetup extends BaseIdeSetup { * @param {string} projectDir - Project directory */ async cleanup(projectDir) { - const bmadWorkflowsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad'); + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - if (await fs.pathExists(bmadWorkflowsDir)) { - await fs.remove(bmadWorkflowsDir); + if (await fs.pathExists(workflowsDir)) { + const bmadFiles = (await fs.readdir(workflowsDir)).filter((f) => f.startsWith('bmad')); + for (const f of bmadFiles) { + await fs.remove(path.join(workflowsDir, f)); + } console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`)); } } @@ -115,11 +118,9 @@ class AntigravitySetup extends BaseIdeSetup { await this.cleanup(projectDir); // Create .agent/workflows directory structure - const agentDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(agentDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - await this.ensureDir(bmadWorkflowsDir); + await this.ensureDir(workflowsDir); // Generate agent launchers using AgentCommandGenerator // This creates small launcher files that reference the actual agents in _bmad/ @@ -129,7 +130,7 @@ class AntigravitySetup extends BaseIdeSetup { // Write agent launcher files with FLATTENED naming using shared utility // Antigravity ignores directory structure, so we flatten to: bmad_module_name.md // This creates slash commands like /bmad_bmm_dev instead of /dev - const agentCount = await agentGen.writeDashArtifacts(bmadWorkflowsDir, agentArtifacts); + const agentCount = await agentGen.writeDashArtifacts(workflowsDir, agentArtifacts); // Process Antigravity specific injections for installed modules // Use pre-collected configuration if available, or skip if already configured @@ -148,12 +149,12 @@ class AntigravitySetup extends BaseIdeSetup { const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); // Write workflow-command artifacts with FLATTENED naming using shared utility - const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts); + const workflowCommandCount = await workflowGen.writeDashArtifacts(workflowsDir, workflowArtifacts); // 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.generateDashTaskToolCommands(projectDir, bmadDir, bmadWorkflowsDir); + const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, workflowsDir); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed`)); @@ -167,7 +168,7 @@ class AntigravitySetup extends BaseIdeSetup { ), ); } - console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, bmadWorkflowsDir)}`)); + console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`)); console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad_module_agents_name)`)); return { @@ -430,12 +431,10 @@ class AntigravitySetup extends BaseIdeSetup { * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - // Create .agent/workflows/bmad directory structure (same as regular agents) - const agentDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(agentDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); + // Create .agent/workflows directory structure + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - await fs.ensureDir(bmadWorkflowsDir); + await fs.ensureDir(workflowsDir); // Create custom agent launcher with same pattern as regular agents const launcherContent = `name: '${agentName}' @@ -458,7 +457,7 @@ usage: | // Use underscore format: bmad_custom_fred-commit-poet.md const fileName = customAgentDashName(agentName); - const launcherPath = path.join(bmadWorkflowsDir, fileName); + const launcherPath = path.join(workflowsDir, fileName); // Write the launcher file await fs.writeFile(launcherPath, launcherContent, 'utf8'); diff --git a/tools/cli/installers/lib/ide/iflow.js b/tools/cli/installers/lib/ide/iflow.js index bbe6d470..0133877d 100644 --- a/tools/cli/installers/lib/ide/iflow.js +++ b/tools/cli/installers/lib/ide/iflow.js @@ -25,30 +25,21 @@ class IFlowSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Create .iflow/commands/bmad directory structure - const iflowDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad'); - const agentsDir = path.join(commandsDir, 'agents'); - const tasksDir = path.join(commandsDir, 'tasks'); - const workflowsDir = path.join(commandsDir, 'workflows'); + // Clean up old BMAD installation first + await this.cleanup(projectDir); - await this.ensureDir(agentsDir); - await this.ensureDir(tasksDir); - await this.ensureDir(workflowsDir); + // Create .iflow/commands directory structure (flat files, no bmad subfolder) + const iflowDir = path.join(projectDir, this.configDir); + const commandsDir = path.join(iflowDir, this.commandsDir); + + await this.ensureDir(commandsDir); // Generate agent launchers const agentGen = new AgentCommandGenerator(this.bmadFolderName); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - // Setup agents as commands - let agentCount = 0; - for (const artifact of agentArtifacts) { - const commandContent = await this.createAgentCommand(artifact); - - const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`); - await this.writeFile(targetPath, commandContent); - agentCount++; - } + // Setup agents as commands (flat files with dash naming) + const agentCount = await agentGen.writeDashArtifacts(commandsDir, agentArtifacts); // Get tasks and workflows (ALL workflows now generate commands) const tasks = await this.getTasks(bmadDir); @@ -57,26 +48,11 @@ class IFlowSetup extends BaseIdeSetup { const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - // Setup tasks as commands - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const commandContent = this.createTaskCommand(task, content); + // Setup workflows as commands (flat files with dash naming) + const workflowCount = await workflowGenerator.writeDashArtifacts(commandsDir, workflowArtifacts); - const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - taskCount++; - } - - // Setup workflows as commands (already generated) - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - const targetPath = path.join(workflowsDir, `${artifact.module}-${path.basename(artifact.relativePath, '.md')}.md`); - await this.writeFile(targetPath, artifact.content); - workflowCount++; - } - } + // TODO: tasks not yet implemented with flat naming + const taskCount = 0; console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agent commands created`)); @@ -132,11 +108,20 @@ Part of the BMAD ${task.module.toUpperCase()} module. * Cleanup iFlow configuration */ async cleanup(projectDir) { - const fs = require('fs-extra'); - const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad'); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); + const bmadFolder = path.join(commandsDir, 'bmad'); - if (await fs.pathExists(bmadCommandsDir)) { - await fs.remove(bmadCommandsDir); + // Remove old bmad subfolder if it exists + if (await fs.pathExists(bmadFolder)) { + await fs.remove(bmadFolder); + } + + // Also remove any bmad* files at commands root + if (await fs.pathExists(commandsDir)) { + const bmadFiles = (await fs.readdir(commandsDir)).filter((f) => f.startsWith('bmad')); + for (const f of bmadFiles) { + await fs.remove(path.join(commandsDir, f)); + } console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`)); } } @@ -150,11 +135,10 @@ Part of the BMAD ${task.module.toUpperCase()} module. * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const iflowDir = path.join(projectDir, this.configDir); - const bmadCommandsDir = path.join(iflowDir, this.commandsDir, 'bmad'); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - // Create .iflow/commands/bmad directory if it doesn't exist - await fs.ensureDir(bmadCommandsDir); + // Create .iflow/commands directory if it doesn't exist + await fs.ensureDir(commandsDir); // Create custom agent launcher const launcherContent = `# ${agentName} Custom Agent @@ -173,8 +157,9 @@ The agent will follow the persona and instructions from the main agent file. *Generated by BMAD Method*`; - const fileName = `custom-${agentName.toLowerCase()}.md`; - const launcherPath = path.join(bmadCommandsDir, fileName); + const { customAgentDashName } = require('./shared/path-utils'); + const fileName = customAgentDashName(agentName); + const launcherPath = path.join(commandsDir, fileName); // Write the launcher file await fs.writeFile(launcherPath, launcherContent, 'utf8'); diff --git a/tools/cli/installers/lib/ide/qwen.js b/tools/cli/installers/lib/ide/qwen.js index 7ac72f09..fab6ee00 100644 --- a/tools/cli/installers/lib/ide/qwen.js +++ b/tools/cli/installers/lib/ide/qwen.js @@ -2,19 +2,17 @@ const path = require('node:path'); const fs = require('fs-extra'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); -const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); /** * Qwen Code setup handler - * Creates TOML command files in .qwen/commands/BMad/ + * Creates TOML command files in .qwen/commands/ */ class QwenSetup extends BaseIdeSetup { constructor() { super('qwen', 'Qwen Code'); this.configDir = '.qwen'; this.commandsDir = 'commands'; - this.bmadDir = 'bmad'; } /** @@ -26,118 +24,43 @@ class QwenSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Create .qwen/commands/BMad directory structure + // Create .qwen/commands directory (flat structure, no bmad subfolder) const qwenDir = path.join(projectDir, this.configDir); const commandsDir = path.join(qwenDir, this.commandsDir); - const bmadCommandsDir = path.join(commandsDir, this.bmadDir); - await this.ensureDir(bmadCommandsDir); + await this.ensureDir(commandsDir); // Update existing settings.json if present await this.updateSettings(qwenDir); - // Clean up old configuration if exists + // Clean up old configuration await this.cleanupOldConfig(qwenDir); + await this.cleanup(projectDir); - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Get tasks, tools, and workflows (standalone only for tools/workflows) - const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); - const tools = await this.getTools(bmadDir, true); - const workflows = await this.getWorkflows(bmadDir, true); - - // Create directories for each module (including standalone) - const modules = new Set(); - for (const item of [...agentArtifacts, ...tasks, ...tools, ...workflows]) modules.add(item.module); - - for (const module of modules) { - await this.ensureDir(path.join(bmadCommandsDir, module)); - await this.ensureDir(path.join(bmadCommandsDir, module, 'agents')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'tools')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'workflows')); - } - - // Create TOML files for each agent launcher - let agentCount = 0; - for (const artifact of agentArtifacts) { - // Convert markdown launcher content to TOML format - const tomlContent = this.processAgentLauncherContent(artifact.content, { - module: artifact.module, - name: artifact.name, - }); - - const targetPath = path.join(bmadCommandsDir, artifact.module, 'agents', `${artifact.name}.toml`); - - await this.writeFile(targetPath, tomlContent); - - agentCount++; - console.log(chalk.green(` ✓ Added agent: /bmad_${artifact.module}_agents_${artifact.name}`)); - } - - // Create TOML files for each task - let taskCount = 0; - for (const task of tasks) { - const content = await this.readAndProcess(task.path, { - module: task.module, - name: task.name, - }); - - const targetPath = path.join(bmadCommandsDir, task.module, 'tasks', `${task.name}.toml`); - - await this.writeFile(targetPath, content); - - taskCount++; - console.log(chalk.green(` ✓ Added task: /bmad_${task.module}_tasks_${task.name}`)); - } - - // Create TOML files for each tool - let toolCount = 0; - for (const tool of tools) { - const content = await this.readAndProcess(tool.path, { - module: tool.module, - name: tool.name, - }); - - const targetPath = path.join(bmadCommandsDir, tool.module, 'tools', `${tool.name}.toml`); - - await this.writeFile(targetPath, content); - - toolCount++; - console.log(chalk.green(` ✓ Added tool: /bmad_${tool.module}_tools_${tool.name}`)); - } - - // Create TOML files for each workflow - let workflowCount = 0; - for (const workflow of workflows) { - const content = await this.readAndProcess(workflow.path, { - module: workflow.module, - name: workflow.name, - }); - - const targetPath = path.join(bmadCommandsDir, workflow.module, 'workflows', `${workflow.name}.toml`); - - await this.writeFile(targetPath, content); - - workflowCount++; - console.log(chalk.green(` ✓ Added workflow: /bmad_${workflow.module}_workflows_${workflow.name}`)); - } + // Use the unified installer with QWEN template for TOML format + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: commandsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.QWEN, + fileExtension: '.toml', + }, + options.selectedModules || [], + ); console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents configured`)); - console.log(chalk.dim(` - ${taskCount} tasks configured`)); - console.log(chalk.dim(` - ${toolCount} tools configured`)); - console.log(chalk.dim(` - ${workflowCount} workflows configured`)); - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`)); + console.log(chalk.dim(` - ${counts.agents} agents configured`)); + console.log(chalk.dim(` - ${counts.tasks} tasks configured`)); + console.log(chalk.dim(` - ${counts.tools} tools configured`)); + console.log(chalk.dim(` - ${counts.workflows} workflows configured`)); + console.log(chalk.dim(` - ${counts.total} TOML files written to ${path.relative(projectDir, commandsDir)}`)); return { success: true, - agents: agentCount, - tasks: taskCount, - tools: toolCount, - workflows: workflowCount, + ...counts, }; } @@ -145,7 +68,6 @@ class QwenSetup extends BaseIdeSetup { * Update settings.json to remove old agent references */ async updateSettings(qwenDir) { - const fs = require('fs-extra'); const settingsPath = path.join(qwenDir, 'settings.json'); if (await fs.pathExists(settingsPath)) { @@ -180,7 +102,6 @@ class QwenSetup extends BaseIdeSetup { * Clean up old configuration directories */ async cleanupOldConfig(qwenDir) { - const fs = require('fs-extra'); const agentsDir = path.join(qwenDir, 'agents'); const bmadMethodDir = path.join(qwenDir, 'bmad-method'); const bmadDir = path.join(qwenDir, 'bmadDir'); @@ -201,117 +122,39 @@ class QwenSetup extends BaseIdeSetup { } } - /** - * Read and process file content - */ - async readAndProcess(filePath, metadata) { - const fs = require('fs-extra'); - const content = await fs.readFile(filePath, 'utf8'); - return this.processContent(content, metadata); - } - - /** - * Process agent launcher content and convert to TOML format - * @param {string} launcherContent - Launcher markdown content - * @param {Object} metadata - File metadata - * @returns {string} TOML formatted content - */ - processAgentLauncherContent(launcherContent, metadata = {}) { - // Strip frontmatter from launcher content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, ''); - - // Extract title for TOML description - const titleMatch = launcherContent.match(/description:\s*"([^"]+)"/); - const title = titleMatch ? titleMatch[1] : metadata.name; - - // Create TOML with launcher content (without frontmatter) - return `description = "BMAD ${metadata.module.toUpperCase()} Agent: ${title}" -prompt = """ -${contentWithoutFrontmatter.trim()} -""" -`; - } - - /** - * Override processContent to add TOML metadata header for Qwen - * @param {string} content - File content - * @param {Object} metadata - File metadata - * @returns {string} Processed content with Qwen template - */ - processContent(content, metadata = {}) { - // First apply base processing (includes activation injection for agents) - let prompt = super.processContent(content, metadata); - - // Determine the type and description based on content - const isAgent = content.includes(' word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - /** * Cleanup Qwen configuration */ async cleanup(projectDir) { - const fs = require('fs-extra'); - const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, this.bmadDir); - const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method'); - const oldBMadDir = path.join(projectDir, this.configDir, 'BMad'); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); + if (await fs.pathExists(commandsDir)) { + // Remove any bmad* files from the commands directory + const entries = await fs.readdir(commandsDir); + for (const entry of entries) { + if (entry.startsWith('bmad')) { + await fs.remove(path.join(commandsDir, entry)); + } + } + } + + // Also remove legacy bmad subfolder if it exists + const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad'); if (await fs.pathExists(bmadCommandsDir)) { await fs.remove(bmadCommandsDir); - console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`)); + console.log(chalk.dim(` Cleaned up existing BMAD configuration from Qwen Code`)); } + const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method'); if (await fs.pathExists(oldBmadMethodDir)) { await fs.remove(oldBmadMethodDir); - console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`)); + console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`)); } + const oldBMadDir = path.join(projectDir, this.configDir, 'BMad'); if (await fs.pathExists(oldBMadDir)) { await fs.remove(oldBMadDir); - console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`)); + console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`)); } } @@ -324,14 +167,12 @@ ${prompt} * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const qwenDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(qwenDir, this.commandsDir); - const bmadCommandsDir = path.join(commandsDir, this.bmadDir); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - // Create .qwen/commands/BMad directory if it doesn't exist - await fs.ensureDir(bmadCommandsDir); + // Create .qwen/commands directory if it doesn't exist + await fs.ensureDir(commandsDir); - // Create custom agent launcher in TOML format (same pattern as regular agents) + // Create custom agent launcher content const launcherContent = `# ${agentName} Custom Agent **⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! @@ -348,14 +189,20 @@ The agent will follow the persona and instructions from the main agent file. *Generated by BMAD Method*`; - // Use Qwen's TOML conversion method - const tomlContent = this.processAgentLauncherContent(launcherContent, { - name: agentName, - module: 'custom', - }); + // Convert to TOML format using the same method as UnifiedInstaller + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '').trim(); + const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`); - const fileName = `custom-${agentName.toLowerCase()}.toml`; - const launcherPath = path.join(bmadCommandsDir, fileName); + const tomlContent = `description = "BMAD Custom Agent: ${agentName}" +prompt = """ +${escapedContent} +""" +`; + + // Use flat naming: bmad-custom-agent-agentname.toml + const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.toml`; + const launcherPath = path.join(commandsDir, fileName); // Write the launcher file await fs.writeFile(launcherPath, tomlContent, 'utf8'); @@ -363,7 +210,7 @@ The agent will follow the persona and instructions from the main agent file. return { ide: 'qwen', path: path.relative(projectDir, launcherPath), - command: agentName, + command: fileName.replace('.toml', ''), type: 'custom-agent-launcher', }; } diff --git a/tools/cli/installers/lib/ide/rovo-dev.js b/tools/cli/installers/lib/ide/rovo-dev.js index d329e1ad..1151a2d5 100644 --- a/tools/cli/installers/lib/ide/rovo-dev.js +++ b/tools/cli/installers/lib/ide/rovo-dev.js @@ -2,69 +2,19 @@ const path = require('node:path'); const fs = require('fs-extra'); const chalk = require('chalk'); const { BaseIdeSetup } = require('./_base-ide'); -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'); /** * Rovo Dev IDE setup handler * - * Installs BMAD agents as Rovo Dev subagents in .rovodev/subagents/ - * Installs workflows and tasks/tools as reference guides in .rovodev/ - * Rovo Dev automatically discovers agents and integrates with BMAD like other IDEs + * Uses UnifiedInstaller for all artifact installation with flat file structure. + * All BMAD artifacts are installed to .rovodev/workflows/ as flat files. */ class RovoDevSetup extends BaseIdeSetup { constructor() { super('rovo-dev', 'Atlassian Rovo Dev', false); this.configDir = '.rovodev'; - this.subagentsDir = 'subagents'; this.workflowsDir = 'workflows'; - this.referencesDir = 'references'; - } - - /** - * Cleanup old BMAD installation before reinstalling - * @param {string} projectDir - Project directory - */ - async cleanup(projectDir) { - const rovoDevDir = path.join(projectDir, this.configDir); - - if (!(await fs.pathExists(rovoDevDir))) { - return; - } - - // Clean BMAD agents from subagents directory - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); - if (await fs.pathExists(subagentsDir)) { - const entries = await fs.readdir(subagentsDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(subagentsDir, file)); - } - } - - // Clean BMAD workflows from workflows directory - const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - if (await fs.pathExists(workflowsDir)) { - const entries = await fs.readdir(workflowsDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(workflowsDir, file)); - } - } - - // Clean BMAD tasks/tools from references directory - const referencesDir = path.join(rovoDevDir, this.referencesDir); - if (await fs.pathExists(referencesDir)) { - const entries = await fs.readdir(referencesDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(referencesDir, file)); - } - } } /** @@ -81,155 +31,76 @@ class RovoDevSetup extends BaseIdeSetup { // Create .rovodev directory structure const rovoDevDir = path.join(projectDir, this.configDir); - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - const referencesDir = path.join(rovoDevDir, this.referencesDir); - await this.ensureDir(subagentsDir); await this.ensureDir(workflowsDir); - await this.ensureDir(referencesDir); - // Generate and install agents - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); + // Use the unified installer - all artifacts go to workflows folder as flat files + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: workflowsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.CLAUDE, + }, + options.selectedModules || [], + ); - let agentCount = 0; - for (const artifact of agentArtifacts) { - const subagentFilename = `bmad-${artifact.module}-${artifact.name}.md`; - const targetPath = path.join(subagentsDir, subagentFilename); - const subagentContent = this.convertToRovoDevSubagent(artifact.content, artifact.name, artifact.module); - await this.writeFile(targetPath, subagentContent); - agentCount++; - } - - // Generate and install workflows - const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGen.collectWorkflowArtifacts(bmadDir); - - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - const workflowFilename = path.basename(artifact.relativePath); - const targetPath = path.join(workflowsDir, workflowFilename); - await this.writeFile(targetPath, artifact.content); - workflowCount++; - } - } - - // Generate and install tasks and tools - const taskToolGen = new TaskToolCommandGenerator(); - const { tasks: taskCount, tools: toolCount } = await this.generateTaskToolReferences(bmadDir, referencesDir, taskToolGen); - - // Summary output console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed to .rovodev/subagents/`)); - if (workflowCount > 0) { - console.log(chalk.dim(` - ${workflowCount} workflows installed to .rovodev/workflows/`)); + console.log(chalk.dim(` - ${counts.agents} agents installed`)); + if (counts.workflows > 0) { + console.log(chalk.dim(` - ${counts.workflows} workflows installed`)); } - if (taskCount + toolCount > 0) { - console.log( - chalk.dim(` - ${taskCount + toolCount} tasks/tools installed to .rovodev/references/ (${taskCount} tasks, ${toolCount} tools)`), - ); + if (counts.tasks + counts.tools > 0) { + console.log(chalk.dim(` - ${counts.tasks + counts.tools} tasks/tools installed (${counts.tasks} tasks, ${counts.tools} tools)`)); } - console.log(chalk.yellow(`\n Note: Agents are automatically discovered by Rovo Dev`)); - console.log(chalk.dim(` - Access agents by typing @ in Rovo Dev to see available options`)); - console.log(chalk.dim(` - Workflows and references are available in .rovodev/ directory`)); + console.log(chalk.dim(` - ${counts.total} files written to ${path.relative(projectDir, workflowsDir)}`)); + console.log(chalk.yellow(`\n Note: All BMAD items are available in .rovodev/workflows/`)); + console.log(chalk.dim(` - Access items by typing @ in Rovo Dev to see available files`)); return { success: true, - agents: agentCount, - workflows: workflowCount, - tasks: taskCount, - tools: toolCount, + ...counts, }; } /** - * Generate task and tool reference guides - * @param {string} bmadDir - BMAD directory - * @param {string} referencesDir - References directory - * @param {TaskToolCommandGenerator} taskToolGen - Generator instance + * Cleanup old BMAD installation before reinstalling + * @param {string} projectDir - Project directory */ - async generateTaskToolReferences(bmadDir, referencesDir, taskToolGen) { - const tasks = await taskToolGen.loadTaskManifest(bmadDir); - const tools = await taskToolGen.loadToolManifest(bmadDir); + async cleanup(projectDir) { + const rovoDevDir = path.join(projectDir, this.configDir); - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - - let taskCount = 0; - for (const task of standaloneTasks) { - const commandContent = taskToolGen.generateCommandContent(task, 'task'); - const targetPath = path.join(referencesDir, `bmad-task-${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - taskCount++; + if (!(await fs.pathExists(rovoDevDir))) { + return; } - let toolCount = 0; - for (const tool of standaloneTools) { - const commandContent = taskToolGen.generateCommandContent(tool, 'tool'); - const targetPath = path.join(referencesDir, `bmad-tool-${tool.module}-${tool.name}.md`); - await this.writeFile(targetPath, commandContent); - toolCount++; + // Clean BMAD files from workflows directory + const workflowsDir = path.join(rovoDevDir, this.workflowsDir); + if (await fs.pathExists(workflowsDir)) { + const entries = await fs.readdir(workflowsDir); + const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); + + for (const file of bmadFiles) { + await fs.remove(path.join(workflowsDir, file)); + } } - return { tasks: taskCount, tools: toolCount }; - } - - /** - * Convert BMAD agent launcher to Rovo Dev subagent format - * - * Rovo Dev subagents use Markdown files with YAML frontmatter containing: - * - name: Unique identifier for the subagent - * - description: One-line description of the subagent's purpose - * - tools: Array of tools the subagent can use (optional) - * - model: Specific model for this subagent (optional) - * - load_memory: Whether to load memory files (optional, defaults to true) - * - * @param {string} launcherContent - Original agent launcher content - * @param {string} agentName - Name of the agent - * @param {string} moduleName - Name of the module - * @returns {string} Rovo Dev subagent-formatted content - */ - convertToRovoDevSubagent(launcherContent, agentName, moduleName) { - // Extract metadata from the launcher XML - const titleMatch = launcherContent.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName); - - const descriptionMatch = launcherContent.match(/description="([^"]+)"/); - const description = descriptionMatch ? descriptionMatch[1] : `BMAD agent: ${title}`; - - const roleDefinitionMatch = launcherContent.match(/roleDefinition="([^"]+)"/); - const roleDefinition = roleDefinitionMatch ? roleDefinitionMatch[1] : `You are a specialized agent for ${title.toLowerCase()} tasks.`; - - // Extract the main system prompt from the launcher (content after closing tags) - let systemPrompt = roleDefinition; - - // Try to extract additional instructions from the launcher content - const instructionsMatch = launcherContent.match(/([\s\S]*?)<\/instructions>/); - if (instructionsMatch) { - systemPrompt += '\n\n' + instructionsMatch[1].trim(); + // Remove legacy subagents directory + const subagentsDir = path.join(rovoDevDir, 'subagents'); + if (await fs.pathExists(subagentsDir)) { + await fs.remove(subagentsDir); + console.log(chalk.dim(` Removed legacy subagents directory`)); } - // Build YAML frontmatter for Rovo Dev subagent - const frontmatter = { - name: `bmad-${moduleName}-${agentName}`, - description: description, - // Note: tools and model can be added by users in their .rovodev/subagents/*.md files - // We don't enforce specific tools since BMAD agents are flexible - }; - - // Create YAML frontmatter string with proper quoting for special characters - let yamlContent = '---\n'; - yamlContent += `name: ${frontmatter.name}\n`; - // Quote description to handle colons and other special characters in YAML - yamlContent += `description: "${frontmatter.description.replaceAll('"', String.raw`\"`)}"\n`; - yamlContent += '---\n'; - - // Combine frontmatter with system prompt - const subagentContent = yamlContent + systemPrompt; - - return subagentContent; + // Remove legacy references directory + const referencesDir = path.join(rovoDevDir, 'references'); + if (await fs.pathExists(referencesDir)) { + await fs.remove(referencesDir); + console.log(chalk.dim(` Removed legacy references directory`)); + } } /** @@ -244,20 +115,7 @@ class RovoDevSetup extends BaseIdeSetup { return false; } - // Check for BMAD agents in subagents directory - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); - if (await fs.pathExists(subagentsDir)) { - try { - const entries = await fs.readdir(subagentsDir); - if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) { - return true; - } - } catch { - // Continue checking other directories - } - } - - // Check for BMAD workflows in workflows directory + // Check for BMAD files in workflows directory const workflowsDir = path.join(rovoDevDir, this.workflowsDir); if (await fs.pathExists(workflowsDir)) { try { @@ -266,25 +124,64 @@ class RovoDevSetup extends BaseIdeSetup { return true; } } catch { - // Continue checking other directories - } - } - - // Check for BMAD tasks/tools in references directory - const referencesDir = path.join(rovoDevDir, this.referencesDir); - if (await fs.pathExists(referencesDir)) { - try { - const entries = await fs.readdir(referencesDir); - if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) { - return true; - } - } catch { - // Continue + // Continue checking } } return false; } + + /** + * Install a custom agent launcher for Rovo Dev + * @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} Installation result + */ + async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); + + if (!(await this.exists(path.join(projectDir, this.configDir)))) { + return null; + } + + await this.ensureDir(workflowsDir); + + const launcherContent = `--- +name: ${agentName} +description: Custom BMAD agent: ${agentName} +--- + +# ${agentName} Custom Agent + +**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! + +This is a launcher for the custom BMAD agent "${agentName}". + +## Usage +1. First run: \`${agentPath}\` to load the complete agent +2. Then use this workflow as ${agentName} + +The agent will follow the persona and instructions from the main agent file. + +--- + +*Generated by BMAD Method*`; + + // Use flat naming: bmad-custom-agent-agentname.md + const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.md`; + const launcherPath = path.join(workflowsDir, fileName); + + await fs.writeFile(launcherPath, launcherContent, 'utf8'); + + return { + ide: 'rovo-dev', + path: path.relative(projectDir, launcherPath), + command: fileName.replace('.md', ''), + type: 'custom-agent-launcher', + }; + } } module.exports = { RovoDevSetup }; diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 29319af8..1d2b5df8 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -1,6 +1,5 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); /** @@ -65,9 +64,8 @@ class AgentCommandGenerator { .replaceAll('{{name}}', agent.name) .replaceAll('{{module}}', agent.module) .replaceAll('{{path}}', agentPathInModule) - .replaceAll('{{description}}', agent.description || `${agent.name} agent`) - .replaceAll('_bmad', this.bmadFolderName) - .replaceAll('_bmad', '_bmad'); + .replaceAll('{{relativePath}}', path.join(agent.module, 'agents', agentPathInModule)) + .replaceAll('{{description}}', agent.description || `${agent.name} agent`); } /** @@ -109,7 +107,7 @@ class AgentCommandGenerator { // Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md const flatName = toColonPath(artifact.relativePath); const launcherPath = path.join(baseCommandsDir, flatName); - await fs.ensureDir(path.dirname(launcherPath)); + await fs.ensureDir(baseCommandsDir); await fs.writeFile(launcherPath, artifact.content); writtenCount++; } @@ -119,8 +117,8 @@ class AgentCommandGenerator { } /** - * Write agent launcher artifacts using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_pm.md + * Write agent launcher artifacts using dash format + * Creates flat files like: bmad-bmm-agent-pm.md * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Agent launcher artifacts @@ -131,10 +129,10 @@ class AgentCommandGenerator { for (const artifact of artifacts) { if (artifact.type === 'agent-launcher') { - // Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md + // Convert relativePath to dash format: bmm/agents/pm.md → bmad-bmm-agent-pm.md const flatName = toDashPath(artifact.relativePath); const launcherPath = path.join(baseCommandsDir, flatName); - await fs.ensureDir(path.dirname(launcherPath)); + await fs.ensureDir(baseCommandsDir); await fs.writeFile(launcherPath, artifact.content); writtenCount++; } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index 6280f04d..d020f3d3 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -37,6 +37,7 @@ function toUnderscoreName(module, type, name, fileExtension = DEFAULT_FILE_EXTEN * Convert relative path to flat underscore-separated name * Converts: 'bmm/agents/pm.md' → 'bmad_bmm_agent_pm.md' * Converts: 'bmm/workflows/correct-course.md' → 'bmad_bmm_correct-course.md' + * Converts: 'bmad_bmb/agents/agent-builder.md' → 'bmad_bmb_agent_agent-builder.md' (bmad prefix already in module) * Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix) * * @param {string} relativePath - Path like 'bmm/agents/pm.md' @@ -54,8 +55,14 @@ function toUnderscorePath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) const type = parts[1]; const name = parts.slice(2).join('_'); - // Use toUnderscoreName for consistency - return toUnderscoreName(module, type, name, fileExtension); + const isAgent = type === AGENT_SEGMENT; + // For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md' + if (module === 'core') { + return isAgent ? `bmad_agent_${name}${fileExtension}` : `bmad_${name}${fileExtension}`; + } + // If module already starts with 'bmad_', don't add another prefix + const prefix = module.startsWith('bmad_') ? '' : 'bmad_'; + return isAgent ? `${prefix}${module}_agent_${name}${fileExtension}` : `${prefix}${module}_${name}${fileExtension}`; } /** @@ -168,6 +175,7 @@ function toColonPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { * Convert relative path to flat dash-separated name * Converts: 'bmm/agents/pm.md' → 'bmad-bmm-agent-pm.md' * Converts: 'bmm/workflows/correct-course' → 'bmad-bmm-correct-course.md' + * Converts: 'bmad-bmb/agents/agent-builder.md' → 'bmad-bmb-agent-agent-builder.md' (bmad prefix already in module) * @param {string} relativePath - Path like 'bmm/agents/pm.md' * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot * @returns {string} Flat filename like 'bmad-bmm-agent-pm.md' @@ -188,7 +196,9 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { if (module === 'core') { return isAgent ? `bmad-agent-${name}${fileExtension}` : `bmad-${name}${fileExtension}`; } - return isAgent ? `bmad-${module}-agent-${name}${fileExtension}` : `bmad-${module}-${name}${fileExtension}`; + // If module already starts with 'bmad-', don't add another prefix + const prefix = module.startsWith('bmad-') ? '' : 'bmad-'; + return isAgent ? `${prefix}${module}-agent-${name}${fileExtension}` : `${prefix}${module}-${name}${fileExtension}`; } module.exports = { diff --git a/tools/cli/installers/lib/ide/shared/unified-installer.js b/tools/cli/installers/lib/ide/shared/unified-installer.js index ef93dcd3..fec04944 100644 --- a/tools/cli/installers/lib/ide/shared/unified-installer.js +++ b/tools/cli/installers/lib/ide/shared/unified-installer.js @@ -40,6 +40,7 @@ const TemplateType = { WINDSURF: 'windsurf', // YAML with auto_execution_mode AUGMENT: 'augment', // YAML frontmatter GEMINI: 'gemini', // TOML frontmatter with description/prompt + QWEN: 'qwen', // TOML frontmatter with description/prompt (same as Gemini) COPILOT: 'copilot', // YAML with tools array for GitHub Copilot }; @@ -209,7 +210,8 @@ class UnifiedInstaller { content = this.applyTemplate(artifact, content, templateType); } - await fs.ensureDir(path.dirname(targetPath)); + // For flat files, just ensure targetDir exists (no nested dirs needed) + await fs.ensureDir(targetDir); await fs.writeFile(targetPath, content, 'utf8'); written++; } @@ -254,6 +256,11 @@ class UnifiedInstaller { return this.addCopilotFrontmatter(artifact, content); } + case TemplateType.QWEN: { + // Add Qwen TOML frontmatter (same as Gemini) + return this.addGeminiFrontmatter(artifact, content); + } + default: { return content; } diff --git a/tools/cli/installers/lib/ide/templates/agent-command-template.md b/tools/cli/installers/lib/ide/templates/agent-command-template.md index 89713631..23974f0d 100644 --- a/tools/cli/installers/lib/ide/templates/agent-command-template.md +++ b/tools/cli/installers/lib/ide/templates/agent-command-template.md @@ -6,7 +6,7 @@ description: '{{description}}' You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. -1. LOAD the FULL agent file from @_bmad/{{module}}/agents/{{path}} +1. LOAD the FULL agent file from @_bmad/{{relativePath}} 2. READ its entire contents - this contains the complete agent persona, menu, and instructions 3. Execute ALL activation steps exactly as written in the agent file 4. Follow the agent's persona and menu system precisely diff --git a/tools/cli/installers/lib/ide/templates/workflow-commander.md b/tools/cli/installers/lib/ide/templates/workflow-commander.md index 3645c1a2..7355bc73 100644 --- a/tools/cli/installers/lib/ide/templates/workflow-commander.md +++ b/tools/cli/installers/lib/ide/templates/workflow-commander.md @@ -1,4 +1,5 @@ --- +name: '{{name}}' description: '{{description}}' --- From 85339708e6428cddd96147a1b64b2b8417b8da29 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sun, 25 Jan 2026 03:42:14 -0600 Subject: [PATCH 10/11] normalize commands --- .../cli/installers/lib/ide/_config-driven.js | 446 ++++++++++++++ tools/cli/installers/lib/ide/antigravity.js | 474 --------------- tools/cli/installers/lib/ide/auggie.js | 119 ---- tools/cli/installers/lib/ide/claude-code.js | 401 ------------- tools/cli/installers/lib/ide/cline.js | 175 ------ tools/cli/installers/lib/ide/codex.js | 293 ++++++---- tools/cli/installers/lib/ide/crush.js | 144 ----- tools/cli/installers/lib/ide/cursor.js | 131 ----- tools/cli/installers/lib/ide/gemini.js | 168 ------ .../cli/installers/lib/ide/github-copilot.js | 426 -------------- tools/cli/installers/lib/ide/iflow.js | 176 ------ tools/cli/installers/lib/ide/kilo.js | 26 +- tools/cli/installers/lib/ide/manager.js | 114 +++- tools/cli/installers/lib/ide/opencode.js | 257 -------- .../installers/lib/ide}/platform-codes.yaml | 237 +++++--- tools/cli/installers/lib/ide/qwen.js | 219 ------- tools/cli/installers/lib/ide/roo.js | 273 --------- tools/cli/installers/lib/ide/rovo-dev.js | 187 ------ .../lib/ide/shared/agent-command-generator.js | 4 +- .../lib/ide/shared/bmad-artifacts.js | 36 +- .../installers/lib/ide/shared/path-utils.js | 92 +++ .../ide/shared/task-tool-command-generator.js | 199 +++++-- .../lib/ide/shared/unified-installer.js | 548 ++++++++---------- .../ide/shared/workflow-command-generator.js | 3 + .../templates/codex-agent-command-template.md | 15 + .../templates/codex-custom-agent-template.md | 8 + .../ide/templates/frontmatter/common-toml.md | 4 + .../ide/templates/frontmatter/common-yaml.md | 4 + .../templates/frontmatter/copilot-agent.md | 7 + .../lib/ide/templates/frontmatter/copilot.md | 4 + .../templates/frontmatter/opencode-agent.md | 5 + .../lib/ide/templates/frontmatter/opencode.md | 4 + .../lib/ide/templates/frontmatter/roo.md | 4 + .../lib/ide/templates/frontmatter/trae.md | 4 + .../lib/ide/templates/frontmatter/windsurf.md | 4 + tools/cli/installers/lib/ide/trae.js | 313 ---------- tools/cli/installers/lib/ide/windsurf.js | 244 -------- tools/cli/lib/platform-codes.js | 2 +- tools/cli/lib/ui.js | 4 +- 39 files changed, 1477 insertions(+), 4297 deletions(-) create mode 100644 tools/cli/installers/lib/ide/_config-driven.js delete mode 100644 tools/cli/installers/lib/ide/antigravity.js delete mode 100644 tools/cli/installers/lib/ide/auggie.js delete mode 100644 tools/cli/installers/lib/ide/claude-code.js delete mode 100644 tools/cli/installers/lib/ide/cline.js delete mode 100644 tools/cli/installers/lib/ide/crush.js delete mode 100644 tools/cli/installers/lib/ide/cursor.js delete mode 100644 tools/cli/installers/lib/ide/gemini.js delete mode 100644 tools/cli/installers/lib/ide/github-copilot.js delete mode 100644 tools/cli/installers/lib/ide/iflow.js delete mode 100644 tools/cli/installers/lib/ide/opencode.js rename tools/{ => cli/installers/lib/ide}/platform-codes.yaml (52%) delete mode 100644 tools/cli/installers/lib/ide/qwen.js delete mode 100644 tools/cli/installers/lib/ide/roo.js delete mode 100644 tools/cli/installers/lib/ide/rovo-dev.js create mode 100644 tools/cli/installers/lib/ide/templates/codex-agent-command-template.md create mode 100644 tools/cli/installers/lib/ide/templates/codex-custom-agent-template.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/common-toml.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/common-yaml.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/copilot-agent.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/copilot.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/opencode-agent.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/opencode.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/roo.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/trae.md create mode 100644 tools/cli/installers/lib/ide/templates/frontmatter/windsurf.md delete mode 100644 tools/cli/installers/lib/ide/trae.js delete mode 100644 tools/cli/installers/lib/ide/windsurf.js diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js new file mode 100644 index 00000000..f0a0979f --- /dev/null +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -0,0 +1,446 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const chalk = require('chalk'); +const yaml = require('yaml'); +const { BaseIdeSetup } = require('./_base-ide'); +const { UnifiedInstaller } = require('./shared/unified-installer'); +const { toSuffixBasedName, getArtifactSuffix, customAgentSuffixName } = require('./shared/path-utils'); + +/** + * Load platform codes configuration from platform-codes.yaml + * @returns {Object} Platform configuration object + */ +async function loadPlatformCodes() { + const platformCodesPath = path.join(__dirname, 'platform-codes.yaml'); + + if (!(await fs.pathExists(platformCodesPath))) { + console.warn(chalk.yellow('Warning: platform-codes.yaml not found')); + return { platforms: {} }; + } + + const content = await fs.readFile(platformCodesPath, 'utf8'); + const config = yaml.parse(content); + return config; +} + +/** + * Config-driven IDE setup handler + * + * Reads installer configuration from platform-codes.yaml and uses + * UnifiedInstaller to perform the actual installation. + * + * This eliminates the need for separate installer files for most IDEs. + */ +class ConfigDrivenIdeSetup extends BaseIdeSetup { + /** + * @param {string} platformCode - Platform code (e.g., 'claude-code', 'cursor') + * @param {Object} platformConfig - Platform configuration from platform-codes.yaml + */ + constructor(platformCode, platformConfig) { + super(platformCode, platformConfig.name, platformConfig.preferred); + this.platformConfig = platformConfig; + this.installerConfig = platformConfig.installer || null; + } + + /** + * Setup IDE configuration using config-driven approach + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + * @returns {Promise} Setup result + */ + async setup(projectDir, bmadDir, options = {}) { + console.log(chalk.cyan(`Setting up ${this.name}...`)); + + if (!this.installerConfig) { + console.warn(chalk.yellow(`No installer configuration found for ${this.name}`)); + return { success: false, reason: 'no-config' }; + } + + // Handle multi-target installations (like github-copilot, opencode) + if (this.installerConfig.targets) { + return this.installToMultipleTargets(projectDir, bmadDir, this.installerConfig.targets, options); + } + + // Handle single-target installations + if (this.installerConfig.target_dir) { + return this.installToTarget(projectDir, bmadDir, this.installerConfig, options); + } + + console.warn(chalk.yellow(`Invalid installer configuration for ${this.name}`)); + return { success: false, reason: 'invalid-config' }; + } + + /** + * Install artifacts to a single target directory + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} targetConfig - Target configuration + * @param {Object} options - Setup options + * @returns {Promise} Setup result + */ + async installToTarget(projectDir, bmadDir, targetConfig, options) { + const targetDir = path.join(projectDir, targetConfig.dir || targetConfig.target_dir); + + // Clean up old BMAD installation first + await this.cleanupTarget(targetDir, targetConfig.file_extension || '.md'); + + // Ensure target directory exists + await this.ensureDir(targetDir); + + // Get frontmatter template from config (defaults to common-yaml.md) + const frontmatterTemplate = targetConfig.frontmatter_template || 'common-yaml.md'; + + // Use the unified installer + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir, + namingStyle: 'suffix-based', + frontmatterTemplate, + fileExtension: targetConfig.file_extension || '.md', + skipExisting: targetConfig.skip_existing || false, + artifactTypes: targetConfig.artifact_types, + }, + 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(` - Target directory: ${path.relative(projectDir, targetDir)}`)); + + return { + success: true, + agents: counts.agents, + tasks: counts.tasks, + tools: counts.tools, + workflows: counts.workflows, + }; + } + + /** + * Install artifacts to multiple target directories + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Array} targets - Array of target configurations + * @param {Object} options - Setup options + * @returns {Promise} Setup result + */ + async installToMultipleTargets(projectDir, bmadDir, targets, options) { + const totalCounts = { + agents: 0, + workflows: 0, + tasks: 0, + tools: 0, + total: 0, + }; + + const targetNames = []; + + for (const targetConfig of targets) { + const targetDir = path.join(projectDir, targetConfig.dir); + + // Clean up old BMAD installation first + await this.cleanupTarget(targetDir, targetConfig.file_extension || '.md'); + + // Ensure target directory exists + await this.ensureDir(targetDir); + + // Get frontmatter template from config (defaults to common-yaml.md) + const frontmatterTemplate = targetConfig.frontmatter_template || 'common-yaml.md'; + + // Use the unified installer for this target + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir, + namingStyle: 'suffix-based', + frontmatterTemplate, + fileExtension: targetConfig.file_extension || '.md', + skipExisting: targetConfig.skip_existing || false, + artifactTypes: targetConfig.artifact_types, + }, + options.selectedModules || [], + ); + + // Accumulate counts + totalCounts.agents += counts.agents; + totalCounts.workflows += counts.workflows; + totalCounts.tasks += counts.tasks; + totalCounts.tools += counts.tools; + + targetNames.push(path.relative(projectDir, targetDir)); + } + + totalCounts.total = totalCounts.agents + totalCounts.workflows + totalCounts.tasks + totalCounts.tools; + + console.log(chalk.green(`✓ ${this.name} configured:`)); + console.log(chalk.dim(` - ${totalCounts.agents} agents installed`)); + if (totalCounts.workflows > 0) { + console.log(chalk.dim(` - ${totalCounts.workflows} workflow commands generated`)); + } + if (totalCounts.tasks + totalCounts.tools > 0) { + console.log( + chalk.dim( + ` - ${totalCounts.tasks + totalCounts.tools} task/tool commands generated (${totalCounts.tasks} tasks, ${totalCounts.tools} tools)`, + ), + ); + } + console.log(chalk.dim(` - Target directories: ${targetNames.join(', ')}`)); + + // Handle VS Code settings if needed (for github-copilot) + if (this.installerConfig.has_vscode_settings) { + await this.configureVsCodeSettings(projectDir, options); + } + + return { + success: true, + ...totalCounts, + }; + } + + /** + * Configure VS Code settings for GitHub Copilot + * @param {string} projectDir - Project directory + * @param {Object} options - Setup options + */ + async configureVsCodeSettings(projectDir, options) { + const vscodeDir = path.join(projectDir, '.vscode'); + const settingsPath = path.join(vscodeDir, 'settings.json'); + + await this.ensureDir(vscodeDir); + + // Read existing settings + let existingSettings = {}; + if (await fs.pathExists(settingsPath)) { + try { + const content = await fs.readFile(settingsPath, 'utf8'); + existingSettings = JSON.parse(content); + } catch { + console.warn(chalk.yellow(' Could not parse settings.json, creating new')); + } + } + + // BMAD VS Code settings + const bmadSettings = { + 'chat.agent.enabled': true, + 'chat.agent.maxRequests': 15, + 'github.copilot.chat.agent.runTasks': true, + 'chat.mcp.discovery.enabled': true, + 'github.copilot.chat.agent.autoFix': true, + 'chat.tools.autoApprove': false, + }; + + // Merge settings (existing take precedence) + const mergedSettings = { ...bmadSettings, ...existingSettings }; + + // Write settings + await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2)); + console.log(chalk.dim(` - VS Code settings configured`)); + } + + /** + * Clean up a specific target directory + * @param {string} targetDir - Target directory to clean + * @param {string} [fileExtension='.md'] - File extension to match + */ + async cleanupTarget(targetDir, fileExtension = '.md') { + if (!(await fs.pathExists(targetDir))) { + return; + } + + const entries = await fs.readdir(targetDir); + let removed = 0; + + for (const entry of entries) { + // Remove bmad* files with the matching extension + if (entry.startsWith('bmad') && entry.endsWith(fileExtension)) { + await fs.remove(path.join(targetDir, entry)); + removed++; + } + } + + if (removed > 0) { + console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`)); + } + } + + /** + * Cleanup IDE configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + if (!this.installerConfig) { + return; + } + + // Handle multi-target cleanup + if (this.installerConfig.targets) { + for (const targetConfig of this.installerConfig.targets) { + const targetDir = path.join(projectDir, targetConfig.dir); + await this.cleanupTarget(targetDir, targetConfig.file_extension || '.md'); + } + return; + } + + // Handle single-target cleanup + if (this.installerConfig.target_dir) { + const targetDir = path.join(projectDir, this.installerConfig.target_dir); + await this.cleanupTarget(targetDir, this.installerConfig.file_extension || '.md'); + } + } + + /** + * Install a custom agent launcher for this IDE + * @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) { + if (!this.installerConfig) { + return null; + } + + // Determine target directory for agents + let targetDir; + let fileExtension = '.md'; + let frontmatterTemplate = 'common-yaml.md'; + + if (this.installerConfig.targets) { + // For multi-target IDEs like github-copilot, find the agents target + const agentsTarget = this.installerConfig.targets.find((t) => t.artifact_types && t.artifact_types.includes('agents')); + if (!agentsTarget) { + return null; // No agents target found + } + targetDir = path.join(projectDir, agentsTarget.dir); + fileExtension = agentsTarget.file_extension || '.md'; + frontmatterTemplate = agentsTarget.frontmatter_template || 'common-yaml.md'; + } else if (this.installerConfig.target_dir) { + targetDir = path.join(projectDir, this.installerConfig.target_dir); + fileExtension = this.installerConfig.file_extension || '.md'; + frontmatterTemplate = this.installerConfig.frontmatter_template || 'common-yaml.md'; + } else { + return null; + } + + if (!(await this.exists(targetDir))) { + return null; + } + + await this.ensureDir(targetDir); + + // Create launcher content using frontmatter template + const launcherContent = await this.createLauncherContent(agentName, agentPath, metadata, frontmatterTemplate); + + // Use suffix-based naming for custom agents + const fileName = customAgentSuffixName(agentName, fileExtension); + const launcherPath = path.join(targetDir, fileName); + await this.writeFile(launcherPath, launcherContent); + + return { + path: launcherPath, + command: fileName.replace(fileExtension, ''), + }; + } + + /** + * Create launcher content using frontmatter template + * @param {string} agentName - Agent name + * @param {string} agentPath - Path to agent file + * @param {Object} metadata - Agent metadata + * @param {string} frontmatterTemplate - Template filename + * @returns {Promise} Launcher content + */ + async createLauncherContent(agentName, agentPath, metadata, frontmatterTemplate) { + const title = metadata.title || this.formatTitle(agentName); + + // Base activation content + const activationContent = `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} +2. READ its entire contents - this contains the complete agent persona, menu, and instructions +3. FOLLOW every step in the section precisely +4. DISPLAY the welcome/greeting as instructed +5. PRESENT the numbered menu +6. WAIT for user input before proceeding + +`; + + // Load frontmatter template + const { UnifiedInstaller } = require('./shared/unified-installer'); + const installer = new UnifiedInstaller(this.bmadFolderName); + const templateContent = await installer.loadFrontmatterTemplate(frontmatterTemplate); + + if (!templateContent) { + // Fallback to basic YAML + return `--- +name: '${agentName}' +description: '${title} agent' +--- + +${activationContent}`; + } + + // Apply template variables + const variables = { + name: agentName, + title, + displayName: agentName, + description: `Activates the ${title} agent persona.`, + icon: '🤖', + content: activationContent, + tools: JSON.stringify([ + 'changes', + 'edit', + 'fetch', + 'githubRepo', + 'problems', + 'runCommands', + 'runTasks', + 'runTests', + 'search', + 'runSubagent', + 'testFailure', + 'todos', + 'usages', + ]), + }; + + let result = templateContent; + for (const [key, value] of Object.entries(variables)) { + result = result.replaceAll(`{{${key}}}`, value); + } + + // Handle TOML templates specially + if (frontmatterTemplate.includes('toml')) { + const escapedContent = activationContent.replaceAll('"""', String.raw`\"\"\"`); + result = result.replace( + /prompt = """/, + `prompt = """\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\n${escapedContent}`, + ); + return result; + } + + return result + activationContent; + } +} + +module.exports = { + ConfigDrivenIdeSetup, + loadPlatformCodes, +}; diff --git a/tools/cli/installers/lib/ide/antigravity.js b/tools/cli/installers/lib/ide/antigravity.js deleted file mode 100644 index 4e472c1e..00000000 --- a/tools/cli/installers/lib/ide/antigravity.js +++ /dev/null @@ -1,474 +0,0 @@ -const path = require('node:path'); -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 { - loadModuleInjectionConfig, - shouldApplyInjection, - filterAgentInstructions, - resolveSubagentFiles, -} = require('./shared/module-injections'); -const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts'); -const { toDashPath, customAgentDashName } = require('./shared/path-utils'); -const prompts = require('../../../lib/prompts'); - -/** - * Google Antigravity IDE setup handler - * - * Uses .agent/workflows/ directory for slash commands - */ -class AntigravitySetup extends BaseIdeSetup { - constructor() { - super('antigravity', 'Google Antigravity', true); - this.configDir = '.agent'; - this.workflowsDir = 'workflows'; - } - - /** - * Prompt for subagent installation location - * @returns {Promise} Selected location ('project' or 'user') - */ - async _promptInstallLocation() { - return prompts.select({ - message: 'Where would you like to install Antigravity subagents?', - choices: [ - { name: 'Project level (.agent/agents/)', value: 'project' }, - { name: 'User level (~/.agent/agents/)', value: 'user' }, - ], - default: 'project', - }); - } - - /** - * 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 Antigravity sub-module injection config in SOURCE directory - // const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'antigravity', '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 workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - - if (await fs.pathExists(workflowsDir)) { - const bmadFiles = (await fs.readdir(workflowsDir)).filter((f) => f.startsWith('bmad')); - for (const f of bmadFiles) { - await fs.remove(path.join(workflowsDir, f)); - } - console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`)); - } - } - - /** - * Setup Antigravity IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - // Store project directory for use in processContent - this.projectDir = projectDir; - - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Clean up old BMAD installation first - await this.cleanup(projectDir); - - // Create .agent/workflows directory structure - const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - - await this.ensureDir(workflowsDir); - - // 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 with FLATTENED naming using shared utility - // Antigravity ignores directory structure, so we flatten to: bmad_module_name.md - // This creates slash commands like /bmad_bmm_dev instead of /dev - const agentCount = await agentGen.writeDashArtifacts(workflowsDir, agentArtifacts); - - // Process Antigravity 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); - } else { - await this.processModuleInjections(projectDir, bmadDir, options); - } - - // 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 with FLATTENED naming using shared utility - const workflowCommandCount = await workflowGen.writeDashArtifacts(workflowsDir, workflowArtifacts); - - // 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.generateDashTaskToolCommands(projectDir, bmadDir, workflowsDir); - - 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(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`)); - console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad_module_agents_name)`)); - - return { - success: true, - agents: agentCount, - }; - } - - /** - * Read and process file content - */ - async readAndProcess(filePath, metadata) { - const content = await fs.readFile(filePath, 'utf8'); - return this.processContent(content, metadata); - } - - /** - * 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); - } - - /** - * Get agents from source modules (not installed location) - */ - 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'); - - if (await fs.pathExists(agentsPath)) { - const moduleAgents = await getAgentsFromDir(agentsPath, moduleName); - agents.push(...moduleAgents); - } - } - - return agents; - } - - /** - * 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, - handler: 'antigravity', - subagentChoices, - installLocation, - interactive: false, - }); - } - - /** - * Process Antigravity specific injections for installed modules - * Looks for injections.yaml in each module's antigravity 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, - handler: 'antigravity', - subagentChoices, - installLocation, - interactive: true, - }); - - if (updatedChoices) { - subagentChoices = updatedChoices; - } - if (updatedLocation) { - installLocation = updatedLocation; - } - } - - async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) { - let choices = subagentChoices; - let location = installLocation; - - for (const moduleName of modules) { - const configData = await loadModuleInjectionConfig(handler, moduleName); - - if (!configData) { - continue; - } - - const { config, handlerBaseDir } = configData; - - if (interactive) { - console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler} features...`)); - } - - // if (interactive && config.subagents && !choices) { - // choices = await this.promptSubagentInstallation(config.subagents); - - // if (choices.install !== 'none') { - // location = await this._promptInstallLocation(); - // } - // } - - if (config.injections && choices && choices.install !== 'none') { - for (const injection of config.injections) { - if (shouldApplyInjection(injection, choices)) { - await this.injectContent(projectDir, injection, choices); - } - } - } - - if (config.subagents && choices && choices.install !== 'none') { - await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project'); - } - } - - return { subagentChoices: choices, installLocation: location }; - } - - /** - * 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 Antigravity subagents for enhanced functionality?', - choices: [ - { name: 'Yes, install all subagents', value: 'all' }, - { name: 'Yes, let me choose specific subagents', value: 'selective' }, - { name: 'No, skip subagent installation', value: 'none' }, - ], - default: 'all', - }); - - if (install === 'selective') { - // Show list of available subagents with descriptions - const subagentInfo = { - 'market-researcher.md': 'Market research and competitive analysis', - 'requirements-analyst.md': 'Requirements extraction and validation', - 'technical-evaluator.md': 'Technology stack evaluation', - 'epic-optimizer.md': 'Epic and story breakdown optimization', - 'document-reviewer.md': 'Document quality review', - }; - - const selected = await prompts.multiselect({ - message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, - choices: subagentConfig.files.map((file) => ({ - name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, - value: file, - checked: true, - })), - }); - - return { install: 'selective', selected }; - } - - return { install }; - } - - /** - * Inject content at specified point in file - */ - async injectContent(projectDir, injection, subagentChoices = null) { - const targetPath = path.join(projectDir, injection.file); - - if (await this.exists(targetPath)) { - let content = await fs.readFile(targetPath, 'utf8'); - const marker = ``; - - 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); - } - - content = content.replace(marker, injectionContent); - await fs.writeFile(targetPath, content); - console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`)); - } - } - } - - /** - * Copy selected subagents to appropriate Antigravity agents directory - */ - 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(), '.agent', 'agents'); - console.log(chalk.dim(` Installing subagents globally to: ~/.agent/agents/`)); - } else { - targetDir = path.join(projectDir, '.agent', 'agents'); - console.log(chalk.dim(` Installing subagents to project: .agent/agents/`)); - } - - // Ensure target directory exists - await this.ensureDir(targetDir); - - const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); - - let copiedCount = 0; - for (const resolved of resolvedFiles) { - try { - const sourcePath = resolved.absolutePath; - - const subFolder = path.dirname(resolved.relativePath); - let targetPath; - if (subFolder && subFolder !== '.') { - const targetSubDir = path.join(targetDir, subFolder); - await this.ensureDir(targetSubDir); - targetPath = path.join(targetSubDir, path.basename(resolved.file)); - } else { - targetPath = path.join(targetDir, path.basename(resolved.file)); - } - - await fs.copyFile(sourcePath, targetPath); - console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`)); - copiedCount++; - } catch (error) { - console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`)); - } - } - - if (copiedCount > 0) { - console.log(chalk.dim(` Total subagents installed: ${copiedCount}`)); - } - } - - /** - * Install a custom agent launcher for Antigravity - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - // Create .agent/workflows directory structure - const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - - await fs.ensureDir(workflowsDir); - - // Create custom agent launcher with same pattern as regular agents - const launcherContent = `name: '${agentName}' -description: '${agentName} agent' -usage: | - Custom BMAD agent: ${agentName} - - Launch with: /${agentName} - - 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} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. EXECUTE as ${agentName} with full persona adoption - - ---- - -⚠️ **IMPORTANT**: Run @${agentPath} to load the complete agent before using this launcher!`; - - // Use underscore format: bmad_custom_fred-commit-poet.md - const fileName = customAgentDashName(agentName); - const launcherPath = path.join(workflowsDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'antigravity', - path: path.relative(projectDir, launcherPath), - command: `/${fileName.replace('.md', '')}`, - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { AntigravitySetup }; diff --git a/tools/cli/installers/lib/ide/auggie.js b/tools/cli/installers/lib/ide/auggie.js deleted file mode 100644 index 5a4170b5..00000000 --- a/tools/cli/installers/lib/ide/auggie.js +++ /dev/null @@ -1,119 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); - -/** - * Auggie CLI setup handler - * Installs to project directory (.augment/commands) - */ -class AuggieSetup extends BaseIdeSetup { - constructor() { - super('auggie', 'Auggie CLI'); - this.detectionPaths = ['.augment']; - this.installer = new UnifiedInstaller(this.bmadFolderName); - } - - /** - * Setup Auggie CLI configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Use flat file structure in .augment/commands/ - const targetDir = path.join(projectDir, '.augment', 'commands'); - - // Install using UnifiedInstaller - const counts = await this.installer.install( - projectDir, - bmadDir, - { - targetDir, - namingStyle: NamingStyle.FLAT_COLON, - templateType: TemplateType.AUGMENT, - includeNestedStructure: false, - }, - options.selectedModules || [], - ); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${counts.agents} agents installed`)); - console.log(chalk.dim(` - ${counts.tasks} tasks installed`)); - console.log(chalk.dim(` - ${counts.tools} tools installed`)); - console.log(chalk.dim(` - ${counts.workflows} workflows installed`)); - console.log(chalk.dim(` - Location: ${path.relative(projectDir, targetDir)}`)); - console.log(chalk.yellow(`\n 💡 Tip: Add 'model: gpt-4o' to command frontmatter to specify AI model`)); - - return { - success: true, - ...counts, - }; - } - - /** - * Cleanup Auggie configuration - * Removes bmad* files from .augment/commands/ - */ - async cleanup(projectDir) { - const targetDir = path.join(projectDir, '.augment', 'commands'); - await this.installer.cleanupBmadFiles(targetDir); - console.log(chalk.dim(` Removed old BMAD commands`)); - } - - /** - * Install a custom agent launcher for Auggie - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - // Auggie uses .augment/commands directory with flat structure - const targetDir = path.join(projectDir, '.augment', 'commands'); - - // Create .augment/commands directory if it doesn't exist - await fs.ensureDir(targetDir); - - // Create custom agent launcher with flat naming: bmad_custom_agent_{name}.md - const launcherContent = `--- -description: "Use the ${agentName} custom agent" ---- - -# ${agentName} Custom Agent - -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this command to activate ${agentName} - -The agent will follow the persona and instructions from the main agent file. - -## Module -BMAD Custom agent -`; - - // Use flat naming convention consistent with UnifiedInstaller - const fileName = `bmad_custom_agent_${agentName.toLowerCase()}.md`; - const launcherPath = path.join(targetDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'auggie', - path: path.relative(projectDir, launcherPath), - command: agentName, - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { AuggieSetup }; diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js deleted file mode 100644 index 0ddff285..00000000 --- a/tools/cli/installers/lib/ide/claude-code.js +++ /dev/null @@ -1,401 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); -const { - loadModuleInjectionConfig, - shouldApplyInjection, - filterAgentInstructions, - resolveSubagentFiles, -} = require('./shared/module-injections'); -const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts'); -const { customAgentColonName } = require('./shared/path-utils'); -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); - this.configDir = '.claude'; - this.commandsDir = 'commands'; - this.agentsDir = 'agents'; - } - - /** - * Prompt for subagent installation location - */ - async promptInstallLocation() { - return prompts.select({ - message: 'Where would you like to install Claude Code subagents?', - choices: [ - { name: 'Project level (.claude/agents/)', value: 'project' }, - { name: 'User level (~/.claude/agents/)', value: 'user' }, - ], - default: 'project', - }); - } - - /** - * Cleanup old BMAD installation before reinstalling - */ - async cleanup(projectDir) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - // 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; - for (const entry of entries) { - if (entry.startsWith('bmad')) { - await fs.remove(path.join(commandsDir, entry)); - removedCount++; - } - } - } - } - - /** - * Setup Claude Code IDE configuration - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(`[DEBUG CLAUDE-CODE] setup called! projectDir=${projectDir}`); - this.projectDir = projectDir; - - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - await this.cleanup(projectDir); - - const claudeDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(claudeDir, this.commandsDir); - await this.ensureDir(commandsDir); - - // 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 - if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) { - await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {}); - } else if (options.preCollectedConfig) { - await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig); - } else { - await this.processModuleInjections(projectDir, bmadDir, options); - } - - 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, - }; - } - - /** - * Read and process file content - */ - async readAndProcess(filePath, metadata) { - const content = await fs.readFile(filePath, 'utf8'); - return this.processContent(content, metadata); - } - - /** - * Override processContent to keep {project-root} placeholder - */ - processContent(content, metadata = {}) { - return super.processContent(content, metadata); - } - - /** - * Get agents from source modules (not installed location) - */ - async getAgentsFromSource(sourceDir, selectedModules) { - const 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); - } - - for (const moduleName of selectedModules) { - const modulePath = path.join(sourceDir, moduleName); - const agentsPath = path.join(modulePath, 'agents'); - - if (await fs.pathExists(agentsPath)) { - const moduleAgents = await getAgentsFromDir(agentsPath, moduleName); - agents.push(...moduleAgents); - } - } - - return agents; - } - - /** - * Process module injections with pre-collected configuration - */ - async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) { - const modules = options.selectedModules || []; - const { subagentChoices, installLocation } = preCollectedConfig; - - await this.processModuleInjectionsInternal({ - projectDir, - modules, - handler: 'claude-code', - subagentChoices, - installLocation, - interactive: false, - }); - } - - /** - * Process Claude Code specific injections for installed modules - */ - async processModuleInjections(projectDir, bmadDir, options) { - const modules = options.selectedModules || []; - let subagentChoices = null; - let installLocation = null; - - const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ - projectDir, - modules, - handler: 'claude-code', - subagentChoices, - installLocation, - interactive: true, - }); - - if (updatedChoices) { - subagentChoices = updatedChoices; - } - if (updatedLocation) { - installLocation = updatedLocation; - } - } - - 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; - - for (const moduleName of modules) { - const configData = await loadModuleInjectionConfig(handler, moduleName); - - if (!configData) { - continue; - } - - const { config, handlerBaseDir } = configData; - - if (interactive) { - console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler.replace('-', ' ')} features...`)); - } - - if (interactive && config.subagents && !choices) { - // choices = await this.promptSubagentInstallation(config.subagents); - // if (choices.install !== 'none') { - // location = await this.promptInstallLocation(); - // } - } - - if (config.injections && choices && choices.install !== 'none') { - for (const injection of config.injections) { - if (shouldApplyInjection(injection, choices)) { - await this.injectContent(projectDir, injection, choices); - } - } - } - - if (config.subagents && choices && choices.install !== 'none') { - await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project'); - } - } - - return { subagentChoices: choices, installLocation: location }; - } - - /** - * Prompt user for subagent installation preferences - */ - async promptSubagentInstallation(subagentConfig) { - const install = await prompts.select({ - message: 'Would you like to install Claude Code subagents for enhanced functionality?', - choices: [ - { name: 'Yes, install all subagents', value: 'all' }, - { name: 'Yes, let me choose specific subagents', value: 'selective' }, - { name: 'No, skip subagent installation', value: 'none' }, - ], - default: 'all', - }); - - if (install === 'selective') { - const subagentInfo = { - 'market-researcher.md': 'Market research and competitive analysis', - 'requirements-analyst.md': 'Requirements extraction and validation', - 'technical-evaluator.md': 'Technology stack evaluation', - 'epic-optimizer.md': 'Epic and story breakdown optimization', - 'document-reviewer.md': 'Document quality review', - }; - - const selected = await prompts.multiselect({ - message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, - options: subagentConfig.files.map((file) => ({ - label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, - value: file, - })), - initialValues: subagentConfig.files, - }); - - return { install: 'selective', selected }; - } - - return { install }; - } - - /** - * Inject content at specified point in file - */ - async injectContent(projectDir, injection, subagentChoices = null) { - const targetPath = path.join(projectDir, injection.file); - - if (await this.exists(targetPath)) { - let content = await fs.readFile(targetPath, 'utf8'); - const marker = ``; - - if (content.includes(marker)) { - let injectionContent = injection.content; - - if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') { - injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected); - } - - content = content.replace(marker, injectionContent); - await fs.writeFile(targetPath, content); - console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`)); - } - } - } - - /** - * Copy selected subagents to appropriate Claude agents directory - */ - async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) { - const os = require('node:os'); - - let targetDir; - if (location === 'user') { - targetDir = path.join(os.homedir(), '.claude', 'agents'); - console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`)); - } else { - targetDir = path.join(projectDir, '.claude', 'agents'); - console.log(chalk.dim(` Installing subagents to project: .claude/agents/`)); - } - - await this.ensureDir(targetDir); - - const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); - - let copiedCount = 0; - for (const resolved of resolvedFiles) { - try { - const sourcePath = resolved.absolutePath; - - const subFolder = path.dirname(resolved.relativePath); - let targetPath; - if (subFolder && subFolder !== '.') { - const targetSubDir = path.join(targetDir, subFolder); - await this.ensureDir(targetSubDir); - targetPath = path.join(targetSubDir, path.basename(resolved.file)); - } else { - targetPath = path.join(targetDir, path.basename(resolved.file)); - } - - await fs.copyFile(sourcePath, targetPath); - console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`)); - copiedCount++; - } catch (error) { - console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`)); - } - } - - if (copiedCount > 0) { - console.log(chalk.dim(` Total subagents installed: ${copiedCount}`)); - } - } - - /** - * Install a custom agent launcher for Claude Code - */ - 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; - } - - await this.ensureDir(commandsDir); - - 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} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. FOLLOW every step in the section precisely -4. DISPLAY the welcome/greeting as instructed -5. PRESENT the numbered menu -6. WAIT for user input before proceeding - -`; - - const launcherName = customAgentColonName(agentName); - const launcherPath = path.join(commandsDir, launcherName); - await this.writeFile(launcherPath, launcherContent); - - return { - path: launcherPath, - command: `/${launcherName.replace('.md', '')}`, - }; - } -} - -module.exports = { ClaudeCodeSetup }; diff --git a/tools/cli/installers/lib/ide/cline.js b/tools/cli/installers/lib/ide/cline.js deleted file mode 100644 index 156d3f7a..00000000 --- a/tools/cli/installers/lib/ide/cline.js +++ /dev/null @@ -1,175 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const chalk = require('chalk'); -const { BaseIdeSetup } = require('./_base-ide'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); -const { customAgentDashName } = require('./shared/path-utils'); - -/** - * Cline IDE setup handler - * - * Uses UnifiedInstaller for all artifact installation. - * Installs BMAD artifacts to .clinerules/workflows with flattened naming. - */ -class ClineSetup extends BaseIdeSetup { - constructor() { - super('cline', 'Cline', false); - this.configDir = '.clinerules'; - this.workflowsDir = 'workflows'; - } - - /** - * Setup Cline IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Create .clinerules/workflows directory - const clineDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(clineDir, this.workflowsDir); - - await fs.ensureDir(workflowsDir); - - // Clear old BMAD files - await this.clearOldBmadFiles(workflowsDir); - - // Use the unified installer - much simpler! - const installer = new UnifiedInstaller(this.bmadFolderName); - const counts = await installer.install( - projectDir, - bmadDir, - { - targetDir: workflowsDir, - namingStyle: NamingStyle.FLAT_DASH, - templateType: TemplateType.CLINE, - }, - options.selectedModules || [], - ); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${counts.agents} agents installed`)); - console.log(chalk.dim(` - ${counts.tasks} tasks installed`)); - console.log(chalk.dim(` - ${counts.workflows} workflow commands installed`)); - if (counts.tools > 0) { - console.log(chalk.dim(` - ${counts.tools} tools installed`)); - } - console.log(chalk.dim(` - ${counts.total} files written to ${path.relative(projectDir, workflowsDir)}`)); - - // Usage instructions - console.log(chalk.yellow('\n ⚠️ How to Use Cline Workflows')); - console.log(chalk.cyan(' BMAD workflows are available as slash commands in Cline')); - console.log(chalk.dim(' Usage:')); - console.log(chalk.dim(' - Type / to see available commands')); - console.log(chalk.dim(' - All BMAD items start with "bmad-"')); - console.log(chalk.dim(' - Example: /bmad-bmm-pm')); - - return { - success: true, - ...counts, - destination: workflowsDir, - }; - } - - /** - * Detect Cline installation by checking for .clinerules/workflows directory - */ - async detect(projectDir) { - const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - - if (!(await fs.pathExists(workflowsDir))) { - return false; - } - - const entries = await fs.readdir(workflowsDir); - return entries.some((entry) => entry.startsWith('bmad')); - } - - /** - * Clear old BMAD files from the workflows directory - */ - async clearOldBmadFiles(destDir) { - if (!(await fs.pathExists(destDir))) { - return; - } - - const entries = await fs.readdir(destDir); - - for (const entry of entries) { - if (!entry.startsWith('bmad')) { - continue; - } - - const entryPath = path.join(destDir, entry); - const stat = await fs.stat(entryPath); - if (stat.isFile()) { - await fs.remove(entryPath); - } else if (stat.isDirectory()) { - await fs.remove(entryPath); - } - } - } - - /** - * Cleanup Cline configuration - */ - async cleanup(projectDir) { - const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - await this.clearOldBmadFiles(workflowsDir); - console.log(chalk.dim(`Removed ${this.name} BMAD configuration`)); - } - - /** - * Install a custom agent launcher for Cline - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const clineDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(clineDir, this.workflowsDir); - - // Create .clinerules/workflows directory if it doesn't exist - await fs.ensureDir(workflowsDir); - - // Create custom agent launcher workflow - const launcherContent = `name: ${agentName} -description: Custom BMAD agent: ${agentName} - -# ${agentName} Custom Agent - -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this workflow as ${agentName} - -The agent will follow the persona and instructions from the main agent file. - ---- - -*Generated by BMAD Method*`; - - // Use underscore format: bmad_custom_fred-commit-poet.md - const fileName = customAgentDashName(agentName); - const launcherPath = path.join(workflowsDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'cline', - path: path.relative(projectDir, launcherPath), - command: fileName.replace('.md', ''), - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { ClineSetup }; diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index ea3870ab..3040d056 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -2,25 +2,85 @@ const path = require('node:path'); const fs = require('fs-extra'); const os = require('node:os'); const chalk = require('chalk'); -const { BaseIdeSetup } = require('./_base-ide'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); -const { customAgentDashName } = require('./shared/path-utils'); +const { ConfigDrivenIdeSetup } = require('./_config-driven'); +const { getSourcePath } = require('../../../lib/project-root'); const prompts = require('../../../lib/prompts'); /** * Codex setup handler (CLI mode) * - * Uses UnifiedInstaller for all artifact installation. + * Extends config-driven setup with Codex-specific features: + * - Install location choice (global vs project-specific) + * - Configuration prompts + * - Detailed setup instructions */ -class CodexSetup extends BaseIdeSetup { +class CodexSetup extends ConfigDrivenIdeSetup { constructor() { - super('codex', 'Codex', true); + // Initialize with codex platform config + const platformConfig = { + name: 'Codex', + preferred: false, + installer: { + target_dir: '.codex/prompts', + frontmatter_template: 'none', // Codex uses no frontmatter + }, + }; + super('codex', platformConfig); } /** - * Collect configuration choices before installation + * Get the Codex agent command activation header from central template + * @returns {string} The activation header text */ - async collectConfiguration(options = {}) { + async getAgentCommandHeader() { + const headerPath = getSourcePath('tools/cli/installers/lib/ide/templates', 'codex-agent-command-template.md'); + return await fs.readFile(headerPath, 'utf8'); + } + + /** + * Override setup to add install location choice and instructions + */ + async setup(projectDir, bmadDir, options = {}) { + console.log(chalk.cyan(`Setting up ${this.name}...`)); + + // Collect install location choice + const installLocation = options.preCollectedConfig?.installLocation || (await this.collectInstallLocation()); + + // Determine destination directory + const destDir = this.getCodexPromptDir(projectDir, installLocation); + await fs.ensureDir(destDir); + await this.clearOldBmadFiles(destDir); + + // Use unified installer with custom destination + const { UnifiedInstaller, NamingStyle } = require('./shared/unified-installer'); + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: destDir, + namingStyle: NamingStyle.FLAT_DASH, + frontmatterTemplate: 'none', // Codex uses no frontmatter + }, + options.selectedModules || [], + ); + + // Show results and instructions + this.printResults(counts, destDir, installLocation); + + return { + success: true, + mode: 'cli', + ...counts, + destination: destDir, + installLocation, + }; + } + + /** + * Collect install location choice from user + */ + async collectInstallLocation() { let confirmed = false; let installLocation = 'global'; @@ -29,11 +89,11 @@ class CodexSetup extends BaseIdeSetup { message: 'Where would you like to install Codex CLI prompts?', choices: [ { - name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)', + name: 'Global - Simple for single project (~/codex/prompts, references THIS project only)', value: 'global', }, { - name: `Project-specific - Recommended for real work (requires CODEX_HOME=${path.sep}.codex)`, + name: `Project-specific - Recommended for real work (requires CODEX_HOME=/.codex)`, value: 'project', }, ], @@ -61,80 +121,8 @@ class CodexSetup extends BaseIdeSetup { } /** - * Setup Codex configuration + * Get Codex prompts directory based on location choice */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - const installLocation = options.preCollectedConfig?.installLocation || 'global'; - const destDir = this.getCodexPromptDir(projectDir, installLocation); - - await fs.ensureDir(destDir); - await this.clearOldBmadFiles(destDir); - - // 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 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(` - ${counts.total} Codex prompt files written`)); - console.log(chalk.dim(` - Destination: ${destDir}`)); - - return { - success: true, - mode: 'cli', - ...counts, - destination: destDir, - installLocation, - }; - } - - /** - * Detect Codex installation by checking for BMAD prompt exports - */ - async detect(projectDir) { - const globalDir = this.getCodexPromptDir(null, 'global'); - const projectDir_local = projectDir || process.cwd(); - const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project'); - - // Check global location - if (await fs.pathExists(globalDir)) { - const entries = await fs.readdir(globalDir); - if (entries.some((entry) => entry.startsWith('bmad'))) { - return true; - } - } - - // Check project-specific location - if (await fs.pathExists(projectSpecificDir)) { - const entries = await fs.readdir(projectSpecificDir); - if (entries.some((entry) => entry.startsWith('bmad'))) { - return true; - } - } - - return false; - } - getCodexPromptDir(projectDir = null, location = 'global') { if (location === 'project' && projectDir) { return path.join(projectDir, '.codex', 'prompts'); @@ -142,25 +130,28 @@ class CodexSetup extends BaseIdeSetup { return path.join(os.homedir(), '.codex', 'prompts'); } - async clearOldBmadFiles(destDir) { - if (!(await fs.pathExists(destDir))) { - return; + /** + * Print results and instructions + */ + printResults(counts, destDir, installLocation) { + console.log(chalk.green(`✓ Codex configured:`)); + console.log(chalk.dim(` - Mode: CLI`)); + console.log(chalk.dim(` - Location: ${installLocation}`)); + 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 (${counts.tasks} tasks, ${counts.tools} tools)`)); + } + console.log(chalk.dim(` - ${counts.total} files written`)); + console.log(chalk.dim(` - Destination: ${destDir}`)); - const entries = await fs.readdir(destDir); - - for (const entry of entries) { - if (!entry.startsWith('bmad')) { - continue; - } - - const entryPath = path.join(destDir, entry); - const stat = await fs.stat(entryPath); - if (stat.isFile()) { - await fs.remove(entryPath); - } else if (stat.isDirectory()) { - await fs.remove(entryPath); - } + // Show setup instructions if project-specific + if (installLocation === 'project') { + console.log(''); + console.log(chalk.yellow(' Next steps:')); + console.log(chalk.dim(this.getProjectSpecificNextSteps())); } } @@ -226,20 +217,73 @@ class CodexSetup extends BaseIdeSetup { chalk.dim(' After adding, run: source ~/.bashrc (or source ~/.zshrc)'), chalk.dim(' (The $PWD uses your current working directory)'), ]; - const closingLines = [ - '', - chalk.dim(' This tells Codex CLI to use prompts from this project instead of ~/.codex'), - '', - chalk.bold.cyan('═'.repeat(70)), - '', - ]; - const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines]; - return lines.join('\n'); + return [...commonLines, ...(isWindows ? windowsLines : unixLines)].join('\n'); } /** - * Cleanup Codex configuration + * Get next steps for project-specific installation + */ + getProjectSpecificNextSteps() { + const isWindows = os.platform() === 'win32'; + if (isWindows) { + return `Create codex.cmd in project root with:\n set CODEX_HOME=%~dp0.codex\n codex %*`; + } + return `Add to ~/.bashrc or ~/.zshrc:\n alias codex='CODEX_HOME="$PWD/.codex" codex'`; + } + + /** + * Clear old BMAD files from destination + */ + async clearOldBmadFiles(destDir) { + if (!(await fs.pathExists(destDir))) { + return; + } + + const entries = await fs.readdir(destDir); + for (const entry of entries) { + if (!entry.startsWith('bmad')) { + continue; + } + const entryPath = path.join(destDir, entry); + const stat = await fs.stat(entryPath); + if (stat.isFile()) { + await fs.remove(entryPath); + } else if (stat.isDirectory()) { + await fs.remove(entryPath); + } + } + } + + /** + * Detect Codex installation (checks both global and project locations) + */ + async detect(projectDir) { + const globalDir = this.getCodexPromptDir(null, 'global'); + const projectDir_local = projectDir || process.cwd(); + const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project'); + + // Check global location + if (await fs.pathExists(globalDir)) { + const entries = await fs.readdir(globalDir); + if (entries.some((entry) => entry.startsWith('bmad'))) { + return true; + } + } + + // Check project-specific location + if (await fs.pathExists(projectSpecificDir)) { + const entries = await fs.readdir(projectSpecificDir); + if (entries.some((entry) => entry.startsWith('bmad'))) { + return true; + } + } + + return false; + } + + /** + * Cleanup Codex configuration (both global and project-specific) */ async cleanup(projectDir = null) { const globalDir = this.getCodexPromptDir(null, 'global'); @@ -258,26 +302,25 @@ class CodexSetup extends BaseIdeSetup { const destDir = this.getCodexPromptDir(projectDir, 'project'); await fs.ensureDir(destDir); - const launcherContent = `--- -name: '${agentName}' -description: '${agentName} agent' ---- + // Load the custom agent launcher template + const templatePath = getSourcePath('tools/cli/installers/lib/ide/templates', 'codex-custom-agent-template.md'); + let templateContent = await fs.readFile(templatePath, 'utf8'); -You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. + // Get activation header + const activationHeader = await this.getAgentCommandHeader(); - -1. LOAD the FULL agent file from @${agentPath} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. FOLLOW every step in the section precisely -4. DISPLAY the welcome/greeting as instructed -5. PRESENT the numbered menu -6. WAIT for user input before proceeding - -`; + // Replace placeholders + const relativePath = `_bmad/${agentPath}`; + templateContent = templateContent + .replaceAll('{{name}}', agentName) + .replaceAll('{{description}}', `${agentName} agent`) + .replaceAll('{{activationHeader}}', activationHeader) + .replaceAll('{{relativePath}}', relativePath); + const { customAgentDashName } = require('./shared/path-utils'); const fileName = customAgentDashName(agentName); const launcherPath = path.join(destDir, fileName); - await fs.writeFile(launcherPath, launcherContent, 'utf8'); + await fs.writeFile(launcherPath, templateContent, 'utf8'); return { path: path.relative(projectDir, launcherPath), diff --git a/tools/cli/installers/lib/ide/crush.js b/tools/cli/installers/lib/ide/crush.js deleted file mode 100644 index 1643b345..00000000 --- a/tools/cli/installers/lib/ide/crush.js +++ /dev/null @@ -1,144 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); -const { customAgentColonName } = require('./shared/path-utils'); - -/** - * Crush IDE setup handler - * - * Uses the UnifiedInstaller - all the complex artifact collection - * and writing logic is now centralized. - */ -class CrushSetup extends BaseIdeSetup { - constructor() { - super('crush', 'Crush'); - this.configDir = '.crush'; - this.commandsDir = 'commands'; - } - - /** - * Setup Crush IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Clean up old BMAD installation first - await this.cleanup(projectDir); - - // Create .crush/commands directory - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - await this.ensureDir(commandsDir); - - // Use the unified installer - // Crush uses flat colon naming (bmad_bmm_pm.md) with no frontmatter (like Codex) - const installer = new UnifiedInstaller(this.bmadFolderName); - const counts = await installer.install( - projectDir, - bmadDir, - { - targetDir: commandsDir, - namingStyle: NamingStyle.FLAT_COLON, - templateType: TemplateType.CODEX, - }, - 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)}`)); - console.log(chalk.dim('\n Commands can be accessed via Crush command palette')); - - return { - success: true, - agents: counts.agents, - tasks: counts.tasks, - tools: counts.tools, - workflows: counts.workflows, - }; - } - - /** - * Cleanup Crush configuration - */ - 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) - if (await fs.pathExists(commandsDir)) { - const entries = await fs.readdir(commandsDir); - for (const entry of entries) { - if (entry.startsWith('bmad')) { - await fs.remove(path.join(commandsDir, entry)); - } - } - } - // 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 BMAD commands from Crush`)); - } - } - - /** - * Install a custom agent launcher for Crush - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - // Create .crush/commands directory if it doesn't exist - await fs.ensureDir(commandsDir); - - // Create custom agent launcher - const launcherContent = `# ${agentName} Custom Agent - -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this command to activate ${agentName} - -The agent will follow the persona and instructions from the main agent file. - ---- - -*Generated by BMAD Method*`; - - // 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); - - // Write the launcher file - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'crush', - path: path.relative(projectDir, launcherPath), - command: launcherName.replace('.md', ''), - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { CrushSetup }; diff --git a/tools/cli/installers/lib/ide/cursor.js b/tools/cli/installers/lib/ide/cursor.js deleted file mode 100644 index 72e43ec1..00000000 --- a/tools/cli/installers/lib/ide/cursor.js +++ /dev/null @@ -1,131 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -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); - this.configDir = '.cursor'; - this.rulesDir = 'rules'; - this.commandsDir = 'commands'; - } - - /** - * 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); - - if (await fs.pathExists(commandsDir)) { - const entries = await fs.readdir(commandsDir); - for (const entry of entries) { - if (entry.startsWith('bmad')) { - await fs.remove(path.join(commandsDir, entry)); - } - } - } - // 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}`)); - } - } - - /** - * Install a custom agent launcher for Cursor - */ - 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; - } - - await this.ensureDir(commandsDir); - - 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} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. FOLLOW every step in the section precisely -4. DISPLAY the welcome/greeting as instructed -5. PRESENT the numbered menu -6. WAIT for user input before proceeding - -`; - - const launcherName = customAgentColonName(agentName); - const launcherPath = path.join(commandsDir, launcherName); - await this.writeFile(launcherPath, launcherContent); - - return { - path: launcherPath, - command: `/${launcherName.replace('.md', '')}`, - }; - } -} - -module.exports = { CursorSetup }; diff --git a/tools/cli/installers/lib/ide/gemini.js b/tools/cli/installers/lib/ide/gemini.js deleted file mode 100644 index c26bb05a..00000000 --- a/tools/cli/installers/lib/ide/gemini.js +++ /dev/null @@ -1,168 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const yaml = require('yaml'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); - -/** - * Gemini CLI setup handler - * Creates TOML files in .gemini/commands/ structure - */ -class GeminiSetup extends BaseIdeSetup { - constructor() { - super('gemini', 'Gemini CLI', false); - this.configDir = '.gemini'; - this.commandsDir = 'commands'; - } - - /** - * Load config values from bmad installation - * @param {string} bmadDir - BMAD installation directory - * @returns {Object} Config values - */ - async loadConfigValues(bmadDir) { - const configValues = { - user_name: 'User', // Default fallback - }; - - // Try to load core config.yaml - const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml'); - if (await fs.pathExists(coreConfigPath)) { - try { - const configContent = await fs.readFile(coreConfigPath, 'utf8'); - const config = yaml.parse(configContent); - - if (config.user_name) { - configValues.user_name = config.user_name; - } - } catch (error) { - console.warn(chalk.yellow(` Warning: Could not load config values: ${error.message}`)); - } - } - - return configValues; - } - - /** - * Setup Gemini CLI configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Create .gemini/commands directory (flat structure with bmad- prefix) - const geminiDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(geminiDir, this.commandsDir); - - await this.ensureDir(commandsDir); - - // Use UnifiedInstaller for agents and workflows - const installer = new UnifiedInstaller(this.bmadFolderName); - - const config = { - targetDir: commandsDir, - namingStyle: NamingStyle.FLAT_DASH, - templateType: TemplateType.GEMINI, - fileExtension: '.toml', - }; - - const counts = await installer.install(projectDir, bmadDir, config, options.selectedModules || []); - - // Generate activation names for display - const agentActivation = `/bmad_agents_{agent-name}`; - const workflowActivation = `/bmad_workflows_{workflow-name}`; - const taskActivation = `/bmad_tasks_{task-name}`; - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${counts.agents} agents configured`)); - console.log(chalk.dim(` - ${counts.workflows} workflows configured`)); - console.log(chalk.dim(` - ${counts.tasks} tasks configured`)); - console.log(chalk.dim(` - ${counts.tools} tools configured`)); - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); - console.log(chalk.dim(` - Agent activation: ${agentActivation}`)); - console.log(chalk.dim(` - Workflow activation: ${workflowActivation}`)); - console.log(chalk.dim(` - Task activation: ${taskActivation}`)); - - return { - success: true, - ...counts, - }; - } - - /** - * Cleanup Gemini configuration - surgically remove only BMAD files - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - if (await fs.pathExists(commandsDir)) { - // Remove any bmad* files (cleans up old bmad- and bmad: formats) - const files = await fs.readdir(commandsDir); - let removed = 0; - - for (const file of files) { - if (file.startsWith('bmad') && file.endsWith('.toml')) { - await fs.remove(path.join(commandsDir, file)); - removed++; - } - } - - if (removed > 0) { - console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`)); - } - } - } - - /** - * Install a custom agent launcher for Gemini - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const geminiDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(geminiDir, this.commandsDir); - - // Create .gemini/commands directory if it doesn't exist - await fs.ensureDir(commandsDir); - - // Create custom agent launcher in TOML format - const launcherContent = `description = "Custom BMAD Agent: ${agentName}" -prompt = """ -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this command to activate ${agentName} - -The agent will follow the persona and instructions from the main agent file. - ---- - -*Generated by BMAD Method* -"""`; - - const fileName = `bmad-custom-${agentName.toLowerCase()}.toml`; - const launcherPath = path.join(commandsDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'gemini', - path: path.relative(projectDir, launcherPath), - command: agentName, - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { GeminiSetup }; diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js deleted file mode 100644 index 3c504701..00000000 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ /dev/null @@ -1,426 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); -const prompts = require('../../../lib/prompts'); - -/** - * GitHub Copilot setup handler - * Creates agents in .github/agents/ and configures VS Code settings - */ -class GitHubCopilotSetup extends BaseIdeSetup { - constructor() { - super('github-copilot', 'GitHub Copilot', true); // preferred IDE - this.configDir = '.github'; - this.agentsDir = 'agents'; - this.promptsDir = 'prompts'; - this.vscodeDir = '.vscode'; - } - - /** - * Collect configuration choices before installation - * @param {Object} options - Configuration options - * @returns {Object} Collected configuration - */ - async collectConfiguration(options = {}) { - const config = {}; - - console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration')); - console.log(chalk.dim(' GitHub Copilot works best with specific settings\n')); - - config.vsCodeConfig = await prompts.select({ - message: 'How would you like to configure VS Code settings?', - choices: [ - { name: 'Use recommended defaults (fastest)', value: 'defaults' }, - { name: 'Configure each setting manually', value: 'manual' }, - { name: 'Skip settings configuration', value: 'skip' }, - ], - default: 'defaults', - }); - - if (config.vsCodeConfig === 'manual') { - config.manualSettings = await prompts.prompt([ - { - type: 'input', - name: 'maxRequests', - message: 'Maximum requests per session (1-50)?', - default: '15', - validate: (input) => { - const num = parseInt(input, 10); - if (isNaN(num)) return 'Enter a valid number 1-50'; - if (num < 1 || num > 50) return 'Enter a number between 1-50'; - return true; - }, - }, - { - type: 'confirm', - name: 'runTasks', - message: 'Allow running workspace tasks?', - default: true, - }, - { - type: 'confirm', - name: 'mcpDiscovery', - message: 'Enable MCP server discovery?', - default: true, - }, - { - type: 'confirm', - name: 'autoFix', - message: 'Enable automatic error fixing?', - default: true, - }, - { - type: 'confirm', - name: 'autoApprove', - message: 'Auto-approve tools (less secure)?', - default: false, - }, - ]); - } - - return config; - } - - /** - * Setup GitHub Copilot configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Configure VS Code settings using pre-collected config if available - const config = options.preCollectedConfig || {}; - await this.configureVsCodeSettings(projectDir, { ...options, ...config }); - - // Create .github/agents and .github/prompts directories - const githubDir = path.join(projectDir, this.configDir); - const agentsDir = path.join(githubDir, this.agentsDir); - const promptsDir = path.join(githubDir, this.promptsDir); - await this.ensureDir(agentsDir); - await this.ensureDir(promptsDir); - - // Clean up any existing BMAD files before reinstalling - await this.cleanup(projectDir); - - // 1. Generate agent launchers (custom .agent.md format - not using UnifiedInstaller) - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Create agent files with bmad- prefix - let agentCount = 0; - for (const artifact of agentArtifacts) { - const content = artifact.content; - const agentContent = await this.createAgentContent({ module: artifact.module, name: artifact.name }, content); - - // Use bmad- prefix: bmad-{module}-{name}.agent.md - const targetPath = path.join(agentsDir, `bmad-${artifact.module}-${artifact.name}.agent.md`); - await this.writeFile(targetPath, agentContent); - agentCount++; - - console.log(chalk.green(` ✓ Created agent: bmad-${artifact.module}-${artifact.name}`)); - } - - // 2. Install prompts using UnifiedInstaller - const installer = new UnifiedInstaller(this.bmadFolderName); - const promptCounts = await installer.install( - projectDir, - bmadDir, - { - targetDir: promptsDir, - namingStyle: NamingStyle.FLAT_DASH, - templateType: TemplateType.COPILOT, - }, - options.selectedModules || [], - ); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents created`)); - console.log( - chalk.dim( - ` - ${promptCounts.agents} prompts, ${promptCounts.workflows} workflows, ${promptCounts.tasks + promptCounts.tools} tasks/tools`, - ), - ); - console.log(chalk.dim(` - Agents directory: ${path.relative(projectDir, agentsDir)}`)); - console.log(chalk.dim(` - Prompts directory: ${path.relative(projectDir, promptsDir)}`)); - console.log(chalk.dim(` - VS Code settings configured`)); - console.log(chalk.dim('\n Agents and prompts available in VS Code Chat view')); - - return { - success: true, - agents: agentCount, - prompts: promptCounts.total, - settings: true, - }; - } - - /** - * Configure VS Code settings for GitHub Copilot - */ - async configureVsCodeSettings(projectDir, options) { - const fs = require('fs-extra'); - const vscodeDir = path.join(projectDir, this.vscodeDir); - const settingsPath = path.join(vscodeDir, 'settings.json'); - - await this.ensureDir(vscodeDir); - - // Read existing settings - let existingSettings = {}; - if (await fs.pathExists(settingsPath)) { - try { - const content = await fs.readFile(settingsPath, 'utf8'); - existingSettings = JSON.parse(content); - console.log(chalk.yellow(' Found existing .vscode/settings.json')); - } catch { - console.warn(chalk.yellow(' Could not parse settings.json, creating new')); - } - } - - // Use pre-collected configuration or skip if not available - let configChoice = options.vsCodeConfig; - if (!configChoice) { - // If no pre-collected config, skip configuration - console.log(chalk.yellow(' ⚠ No configuration collected, skipping VS Code settings')); - return; - } - - if (configChoice === 'skip') { - console.log(chalk.yellow(' ⚠ Skipping VS Code settings')); - return; - } - - let bmadSettings = {}; - - if (configChoice === 'defaults') { - bmadSettings = { - 'chat.agent.enabled': true, - 'chat.agent.maxRequests': 15, - 'github.copilot.chat.agent.runTasks': true, - 'chat.mcp.discovery.enabled': true, - 'github.copilot.chat.agent.autoFix': true, - 'chat.tools.autoApprove': false, - }; - console.log(chalk.green(' ✓ Using recommended defaults')); - } else { - // Manual configuration - use pre-collected settings - const manual = options.manualSettings || {}; - - const maxRequests = parseInt(manual.maxRequests || '15', 10); - bmadSettings = { - 'chat.agent.enabled': true, - 'chat.agent.maxRequests': isNaN(maxRequests) ? 15 : maxRequests, - 'github.copilot.chat.agent.runTasks': manual.runTasks === undefined ? true : manual.runTasks, - 'chat.mcp.discovery.enabled': manual.mcpDiscovery === undefined ? true : manual.mcpDiscovery, - 'github.copilot.chat.agent.autoFix': manual.autoFix === undefined ? true : manual.autoFix, - 'chat.tools.autoApprove': manual.autoApprove || false, - }; - } - - // Merge settings (existing take precedence) - const mergedSettings = { ...bmadSettings, ...existingSettings }; - - // Write settings - await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2)); - console.log(chalk.green(' ✓ VS Code settings configured')); - } - - /** - * Create agent content - */ - async createAgentContent(agent, content) { - // Extract metadata from launcher frontmatter if present - const descMatch = content.match(/description:\s*"([^"]+)"/); - const title = descMatch ? descMatch[1] : this.formatTitle(agent.name); - - const description = `Activates the ${title} agent persona.`; - - // Strip any existing frontmatter from the content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - let cleanContent = content; - if (frontmatterRegex.test(content)) { - cleanContent = content.replace(frontmatterRegex, '').trim(); - } - - // Available GitHub Copilot tools (November 2025 - Official VS Code Documentation) - // Reference: https://code.visualstudio.com/docs/copilot/reference/copilot-vscode-features#_chat-tools - const tools = [ - 'changes', // List of source control changes - 'edit', // Edit files in your workspace including: createFile, createDirectory, editNotebook, newJupyterNotebook and editFiles - 'fetch', // Fetch content from web page - 'githubRepo', // Perform code search in GitHub repo - 'problems', // Add workspace issues from Problems panel - 'runCommands', // Runs commands in the terminal including: getTerminalOutput, terminalSelection, terminalLastCommand and runInTerminal - 'runTasks', // Runs tasks and gets their output for your workspace - 'runTests', // Run unit tests in workspace - 'search', // Search and read files in your workspace, including:fileSearch, textSearch, listDirectory, readFile, codebase and searchResults - 'runSubagent', // Runs a task within an isolated subagent context. Enables efficient organization of tasks and context window management. - 'testFailure', // Get unit test failure information - 'todos', // Tool for managing and tracking todo items for task planning - 'usages', // Find references and navigate definitions - ]; - - let agentContent = `--- -description: "${description.replaceAll('"', String.raw`\"`)}" -tools: ${JSON.stringify(tools)} ---- - -# ${title} Agent - -${cleanContent} - -`; - - return agentContent; - } - - /** - * Format name as title - */ - formatTitle(name) { - return name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - /** - * Cleanup GitHub Copilot configuration - surgically remove only BMAD files - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - - // Clean up old chatmodes directory - const chatmodesDir = path.join(projectDir, this.configDir, 'chatmodes'); - if (await fs.pathExists(chatmodesDir)) { - const files = await fs.readdir(chatmodesDir); - let removed = 0; - - for (const file of files) { - if (file.startsWith('bmad') && file.endsWith('.chatmode.md')) { - await fs.remove(path.join(chatmodesDir, file)); - removed++; - } - } - - if (removed > 0) { - console.log(chalk.dim(` Cleaned up ${removed} old BMAD chat modes`)); - } - } - - // Clean up agents directory - const agentsDir = path.join(projectDir, this.configDir, this.agentsDir); - if (await fs.pathExists(agentsDir)) { - const files = await fs.readdir(agentsDir); - let removed = 0; - - for (const file of files) { - // Remove old bmd-* files (typo fix) and current bmad-* files - if ((file.startsWith('bmd-') || file.startsWith('bmad-')) && file.endsWith('.agent.md')) { - await fs.remove(path.join(agentsDir, file)); - removed++; - } - } - - if (removed > 0) { - console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`)); - } - } - - // Clean up prompts directory - const promptsDir = path.join(projectDir, this.configDir, this.promptsDir); - if (await fs.pathExists(promptsDir)) { - const files = await fs.readdir(promptsDir); - let removed = 0; - - for (const file of files) { - if (file.startsWith('bmad-') && file.endsWith('.md')) { - await fs.remove(path.join(promptsDir, file)); - removed++; - } - } - - if (removed > 0) { - console.log(chalk.dim(` Cleaned up ${removed} existing BMAD prompts`)); - } - } - } - - /** - * Install a custom agent launcher for GitHub Copilot - * @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 agentsDir = path.join(projectDir, this.configDir, this.agentsDir); - - if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project - } - - await this.ensureDir(agentsDir); - - 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. - - -1. LOAD the FULL agent file from @${agentPath} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. FOLLOW every step in the section precisely -4. DISPLAY the welcome/greeting as instructed -5. PRESENT the numbered menu -6. WAIT for user input before proceeding - -`; - - // GitHub Copilot needs specific tools in frontmatter - const copilotTools = [ - 'changes', - 'codebase', - 'createDirectory', - 'createFile', - 'editFiles', - 'fetch', - 'fileSearch', - 'githubRepo', - 'listDirectory', - 'problems', - 'readFile', - 'runInTerminal', - 'runTask', - 'runTests', - 'runVscodeCommand', - 'search', - 'searchResults', - 'terminalLastCommand', - 'terminalSelection', - 'testFailure', - 'textSearch', - 'usages', - ]; - - const agentContent = `--- -description: "Activates the ${metadata.title || agentName} agent persona." -tools: ${JSON.stringify(copilotTools)} ---- - -# ${metadata.title || agentName} Agent - -${launcherContent} -`; - - const agentFilePath = path.join(agentsDir, `bmad-${agentName}.agent.md`); - await this.writeFile(agentFilePath, agentContent); - - return { - path: agentFilePath, - command: `bmad-${agentName}`, - }; - } -} - -module.exports = { GitHubCopilotSetup }; diff --git a/tools/cli/installers/lib/ide/iflow.js b/tools/cli/installers/lib/ide/iflow.js deleted file mode 100644 index 0133877d..00000000 --- a/tools/cli/installers/lib/ide/iflow.js +++ /dev/null @@ -1,176 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); - -/** - * iFlow CLI setup handler - * Creates commands in .iflow/commands/ directory structure - */ -class IFlowSetup extends BaseIdeSetup { - constructor() { - super('iflow', 'iFlow CLI'); - this.configDir = '.iflow'; - this.commandsDir = 'commands'; - } - - /** - * Setup iFlow CLI configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Clean up old BMAD installation first - await this.cleanup(projectDir); - - // Create .iflow/commands directory structure (flat files, no bmad subfolder) - const iflowDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(iflowDir, this.commandsDir); - - await this.ensureDir(commandsDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Setup agents as commands (flat files with dash naming) - const agentCount = await agentGen.writeDashArtifacts(commandsDir, agentArtifacts); - - // Get tasks and workflows (ALL workflows now generate commands) - const tasks = await this.getTasks(bmadDir); - - // Get ALL workflows using the new workflow command generator - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Setup workflows as commands (flat files with dash naming) - const workflowCount = await workflowGenerator.writeDashArtifacts(commandsDir, workflowArtifacts); - - // TODO: tasks not yet implemented with flat naming - const taskCount = 0; - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agent commands created`)); - console.log(chalk.dim(` - ${taskCount} task commands created`)); - console.log(chalk.dim(` - ${workflowCount} workflow commands created`)); - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); - - return { - success: true, - agents: agentCount, - tasks: taskCount, - workflows: workflowCount, - }; - } - - /** - * Create agent command content - */ - async createAgentCommand(artifact) { - // The launcher content is already complete - just return it as-is - return artifact.content; - } - - /** - * Create task command content - */ - createTaskCommand(task, content) { - // Extract task name - const nameMatch = content.match(/([^<]+)<\/name>/); - const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); - - let commandContent = `# /task-${task.name} Command - -When this command is used, execute the following task: - -## ${taskName} Task - -${content} - -## Usage - -This command executes the ${taskName} task from the BMAD ${task.module.toUpperCase()} module. - -## Module - -Part of the BMAD ${task.module.toUpperCase()} module. -`; - - return commandContent; - } - - /** - * Cleanup iFlow configuration - */ - async cleanup(projectDir) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - const bmadFolder = path.join(commandsDir, 'bmad'); - - // Remove old bmad subfolder if it exists - if (await fs.pathExists(bmadFolder)) { - await fs.remove(bmadFolder); - } - - // Also remove any bmad* files at commands root - if (await fs.pathExists(commandsDir)) { - const bmadFiles = (await fs.readdir(commandsDir)).filter((f) => f.startsWith('bmad')); - for (const f of bmadFiles) { - await fs.remove(path.join(commandsDir, f)); - } - console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`)); - } - } - - /** - * Install a custom agent launcher for iFlow - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - // Create .iflow/commands directory if it doesn't exist - await fs.ensureDir(commandsDir); - - // Create custom agent launcher - const launcherContent = `# ${agentName} Custom Agent - -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this command to activate ${agentName} - -The agent will follow the persona and instructions from the main agent file. - ---- - -*Generated by BMAD Method*`; - - const { customAgentDashName } = require('./shared/path-utils'); - const fileName = customAgentDashName(agentName); - const launcherPath = path.join(commandsDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'iflow', - path: path.relative(projectDir, launcherPath), - command: agentName, - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { IFlowSetup }; diff --git a/tools/cli/installers/lib/ide/kilo.js b/tools/cli/installers/lib/ide/kilo.js index 45e38021..1f6cf82f 100644 --- a/tools/cli/installers/lib/ide/kilo.js +++ b/tools/cli/installers/lib/ide/kilo.js @@ -115,18 +115,20 @@ class KiloSetup extends BaseIdeSetup { // Build mode entry (KiloCode uses same schema as Roo) const slug = `bmad-${artifact.module}-${artifact.name}`; - let modeEntry = ` - slug: ${slug}\n`; - modeEntry += ` name: '${icon} ${title}'\n`; - modeEntry += ` roleDefinition: ${roleDefinition}\n`; - modeEntry += ` whenToUse: ${whenToUse}\n`; - modeEntry += ` customInstructions: |\n`; - modeEntry += ` ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`; - modeEntry += ` groups:\n`; - modeEntry += ` - read\n`; - modeEntry += ` - edit\n`; - modeEntry += ` - browser\n`; - modeEntry += ` - command\n`; - modeEntry += ` - mcp\n`; + const modeEntry = ` - slug: ${slug} + name: '${icon} ${title}' + roleDefinition: ${roleDefinition} + whenToUse: ${whenToUse} + customInstructions: | + ${activationHeader.trim()} + Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode + groups: + - read + - edit + - browser + - command + - mcp +`; return modeEntry; } diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 97462746..b31f571e 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -1,16 +1,36 @@ const fs = require('fs-extra'); const path = require('node:path'); const chalk = require('chalk'); +const yaml = require('yaml'); +const { ConfigDrivenIdeSetup, loadPlatformCodes } = require('./_config-driven'); /** * IDE Manager - handles IDE-specific setup * Dynamically discovers and loads IDE handlers + * + * NEW: Loads config-driven handlers from platform-codes.yaml + * Custom installer files (like kilo.js, kiro-cli.js) are still supported + * for IDEs with truly unique requirements. */ class IdeManager { constructor() { this.handlers = new Map(); - this.loadHandlers(); + this.platformConfig = null; this.bmadFolderName = 'bmad'; // Default, can be overridden + this._initialized = false; + // Load custom handlers synchronously + this.loadCustomInstallerFiles(__dirname); + } + + /** + * Ensure handlers are initialized (loads config-driven handlers) + * Call this before using handlers if needed + */ + async ensureInitialized() { + if (!this._initialized) { + await this.loadConfigDrivenHandlers(); + this._initialized = true; + } } /** @@ -28,15 +48,28 @@ class IdeManager { } /** - * Dynamically load all IDE handlers from directory + * Dynamically load all IDE handlers + * + * Loading order: + * 1. Load custom installer files (kilo.js, kiro-cli.js) for IDEs with unique requirements + * 2. Load config-driven handlers from platform-codes.yaml for all other IDEs + * @deprecated Use ensureInitialized() instead */ - loadHandlers() { - const ideDir = __dirname; + async loadHandlers() { + await this.ensureInitialized(); + } + /** + * Load custom installer files (for IDEs with truly unique requirements) + * Synchronous version for constructor + * @param {string} ideDir - IDE handlers directory + */ + loadCustomInstallerFiles(ideDir) { try { // Get all JS files in the IDE directory const files = fs.readdirSync(ideDir).filter((file) => { - // Skip base class, manager, utility files (starting with _), and helper modules + // Skip base class, manager, config-driven, utility files (starting with _) + // Also skip shared directory and generator files return ( file.endsWith('.js') && !file.startsWith('_') && @@ -74,15 +107,64 @@ class IdeManager { } } } catch (error) { - console.error(chalk.red('Failed to load IDE handlers:'), error.message); + console.error(chalk.red('Failed to load custom IDE handlers:'), error.message); + } + } + + /** + * Load config-driven handlers from platform-codes.yaml + * Async version called by ensureInitialized() + */ + async loadConfigDrivenHandlers() { + try { + // Load platform-codes.yaml configuration + this.platformConfig = await loadPlatformCodes(); + + // Create config-driven handlers for platforms with installer config + if (this.platformConfig.platforms) { + for (const [platformCode, platformInfo] of Object.entries(this.platformConfig.platforms)) { + // Skip if custom handler already exists + if (this.handlers.has(platformCode)) { + continue; + } + + // Skip if no installer config + if (!platformInfo.installer) { + continue; + } + + try { + const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); + handler.setBmadFolderName(this.bmadFolderName); + this.handlers.set(platformCode, handler); + } catch (error) { + console.warn(chalk.yellow(` Warning: Could not create config-driven handler for ${platformCode}: ${error.message}`)); + } + } + } + + // Log summary + const customCount = [...this.handlers.entries()].filter(([key]) => { + const handler = this.handlers.get(key); + return handler && !(handler instanceof ConfigDrivenIdeSetup); + }).length; + const configCount = [...this.handlers.entries()].filter(([key]) => { + const handler = this.handlers.get(key); + return handler && handler instanceof ConfigDrivenIdeSetup; + }).length; + console.log(chalk.dim(` Loaded ${customCount} custom handlers, ${configCount} config-driven handlers`)); + } catch (error) { + console.error(chalk.red('Failed to load config-driven handlers:'), error.message); } } /** * Get all available IDEs with their metadata - * @returns {Array} Array of IDE information objects + * @returns {Promise} Array of IDE information objects */ - getAvailableIdes() { + async getAvailableIdes() { + await this.ensureInitialized(); + const ides = []; for (const [key, handler] of this.handlers) { @@ -113,18 +195,20 @@ class IdeManager { /** * Get preferred IDEs - * @returns {Array} Array of preferred IDE information + * @returns {Promise} Array of preferred IDE information */ - getPreferredIdes() { - return this.getAvailableIdes().filter((ide) => ide.preferred); + async getPreferredIdes() { + const ides = await this.getAvailableIdes(); + return ides.filter((ide) => ide.preferred); } /** * Get non-preferred IDEs - * @returns {Array} Array of non-preferred IDE information + * @returns {Promise} Array of non-preferred IDE information */ - getOtherIdes() { - return this.getAvailableIdes().filter((ide) => !ide.preferred); + async getOtherIdes() { + const ides = await this.getAvailableIdes(); + return ides.filter((ide) => !ide.preferred); } /** @@ -135,6 +219,8 @@ class IdeManager { * @param {Object} options - Setup options */ async setup(ideName, projectDir, bmadDir, options = {}) { + await this.ensureInitialized(); + const handler = this.handlers.get(ideName.toLowerCase()); if (!handler) { diff --git a/tools/cli/installers/lib/ide/opencode.js b/tools/cli/installers/lib/ide/opencode.js deleted file mode 100644 index 3ca6aeb4..00000000 --- a/tools/cli/installers/lib/ide/opencode.js +++ /dev/null @@ -1,257 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const os = require('node:os'); -const chalk = require('chalk'); -const yaml = require('yaml'); -const { BaseIdeSetup } = require('./_base-ide'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); - -/** - * OpenCode IDE setup handler - */ -class OpenCodeSetup extends BaseIdeSetup { - constructor() { - super('opencode', 'OpenCode', true); // Mark as preferred/recommended - this.configDir = '.opencode'; - this.commandsDir = 'command'; - this.agentsDir = 'agent'; - } - - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - const baseDir = path.join(projectDir, this.configDir); - const commandsBaseDir = path.join(baseDir, this.commandsDir); - const agentsBaseDir = path.join(baseDir, this.agentsDir); - - await this.ensureDir(commandsBaseDir); - await this.ensureDir(agentsBaseDir); - - // Clean up any existing BMAD files before reinstalling - await this.cleanup(projectDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Install primary agents with flat naming: bmad-agent-{module}-{name}.md - // OpenCode agents go in the agent folder (not command folder) - let agentCount = 0; - for (const artifact of agentArtifacts) { - const agentContent = artifact.content; - // Flat structure in agent folder: bmad-agent-{module}-{name}.md - const targetPath = path.join(agentsBaseDir, `bmad-agent-${artifact.module}-${artifact.name}.md`); - await this.writeFile(targetPath, agentContent); - agentCount++; - } - - // Install workflow commands with flat naming: bmad-{module}-{workflow-name} - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - let workflowCommandCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - const commandContent = artifact.content; - // Flat structure: bmad-{module}-{workflow-name}.md - // artifact.relativePath is like: bmm/workflows/plan-project.md - const workflowName = path.basename(artifact.relativePath, '.md'); - const targetPath = path.join(commandsBaseDir, `bmad-${artifact.module}-${workflowName}.md`); - await this.writeFile(targetPath, commandContent); - workflowCommandCount++; - } - // Skip workflow launcher READMEs as they're not needed in flat structure - } - - // Install task and tool commands with flat naming - const { tasks, tools } = await this.generateFlatTaskToolCommands(bmadDir, commandsBaseDir); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed to .opencode/agent/`)); - if (workflowCommandCount > 0) { - console.log(chalk.dim(` - ${workflowCommandCount} workflows installed to .opencode/command/`)); - } - if (tasks + tools > 0) { - console.log(chalk.dim(` - ${tasks + tools} tasks/tools installed to .opencode/command/ (${tasks} tasks, ${tools} tools)`)); - } - - return { - success: true, - agents: agentCount, - workflows: workflowCommandCount, - workflowCounts, - }; - } - - /** - * Generate flat task and tool commands for OpenCode - * OpenCode doesn't support nested command directories - */ - async generateFlatTaskToolCommands(bmadDir, commandsBaseDir) { - const taskToolGen = new TaskToolCommandGenerator(); - const tasks = await taskToolGen.loadTaskManifest(bmadDir); - const tools = await taskToolGen.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) : []; - - // Generate command files for tasks with flat naming: bmad-task-{module}-{name}.md - for (const task of standaloneTasks) { - const commandContent = taskToolGen.generateCommandContent(task, 'task'); - const targetPath = path.join(commandsBaseDir, `bmad-task-${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - } - - // Generate command files for tools with flat naming: bmad-tool-{module}-{name}.md - for (const tool of standaloneTools) { - const commandContent = taskToolGen.generateCommandContent(tool, 'tool'); - const targetPath = path.join(commandsBaseDir, `bmad-tool-${tool.module}-${tool.name}.md`); - await this.writeFile(targetPath, commandContent); - } - - return { - tasks: standaloneTasks.length, - tools: standaloneTools.length, - }; - } - - async readAndProcess(filePath, metadata) { - const content = await fs.readFile(filePath, 'utf8'); - return this.processContent(content, metadata); - } - - async createAgentContent(content, metadata) { - const { frontmatter = {}, body } = this.parseFrontmatter(content); - - frontmatter.description = - frontmatter.description && String(frontmatter.description).trim().length > 0 - ? frontmatter.description - : `BMAD ${metadata.module} agent: ${metadata.name}`; - - // OpenCode agents use: 'primary' mode for main agents - frontmatter.mode = 'primary'; - - const frontmatterString = this.stringifyFrontmatter(frontmatter); - - // Get the activation header from central template - const activationHeader = await this.getAgentCommandHeader(); - - return `${frontmatterString}\n\n${activationHeader}\n\n${body}`; - } - - parseFrontmatter(content) { - const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/); - if (!match) { - return { data: {}, body: content }; - } - - const body = content.slice(match[0].length); - - let frontmatter = {}; - try { - frontmatter = yaml.parse(match[1]) || {}; - } catch { - frontmatter = {}; - } - - return { frontmatter, body }; - } - - stringifyFrontmatter(frontmatter) { - const yamlText = yaml - .dump(frontmatter, { - indent: 2, - lineWidth: -1, - noRefs: true, - sortKeys: false, - }) - .trimEnd(); - - return `---\n${yamlText}\n---`; - } - - /** - * Cleanup OpenCode configuration - surgically remove only BMAD files - */ - async cleanup(projectDir) { - const agentsDir = path.join(projectDir, this.configDir, this.agentsDir); - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - let removed = 0; - - // Clean up agent folder - if (await fs.pathExists(agentsDir)) { - const files = await fs.readdir(agentsDir); - for (const file of files) { - if (file.startsWith('bmad') && file.endsWith('.md')) { - await fs.remove(path.join(agentsDir, file)); - removed++; - } - } - } - - // Clean up command folder - if (await fs.pathExists(commandsDir)) { - const files = await fs.readdir(commandsDir); - for (const file of files) { - if (file.startsWith('bmad') && file.endsWith('.md')) { - await fs.remove(path.join(commandsDir, file)); - removed++; - } - } - } - - if (removed > 0) { - console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`)); - } - } - - /** - * Install a custom agent launcher for OpenCode - * @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 agentsDir = path.join(projectDir, this.configDir, this.agentsDir); - - if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project - } - - await this.ensureDir(agentsDir); - - const launcherContent = `--- -name: '${agentName}' -description: '${metadata.title || agentName} agent' -mode: 'primary' ---- - -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} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. FOLLOW every step in the section precisely -4. DISPLAY the welcome/greeting as instructed -5. PRESENT the numbered menu -6. WAIT for user input before proceeding - -`; - - // OpenCode uses flat naming: bmad-agent-custom-{name}.md - const launcherPath = path.join(agentsDir, `bmad-agent-custom-${agentName}.md`); - await this.writeFile(launcherPath, launcherContent); - - return { - path: launcherPath, - command: `bmad-agent-custom-${agentName}`, - }; - } -} - -module.exports = { OpenCodeSetup }; diff --git a/tools/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml similarity index 52% rename from tools/platform-codes.yaml rename to tools/cli/installers/lib/ide/platform-codes.yaml index 04c4a45f..61f5b854 100644 --- a/tools/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -5,127 +5,177 @@ # the installation system to identify different platforms (IDEs, tools, etc.) # # Format: -# code: Platform identifier used internally +# code: Platform identifier used internally (key) # name: Display name shown to users # preferred: Whether this platform is shown as a recommended option on install -# category: Type of platform (ide, tool, service, etc.) +# category: Type of platform (ide, cli, tool, service, etc.) +# installer: Installation configuration (optional) +# frontmatter_template: Path to frontmatter template file (relative to templates/frontmatter/) +# If not specified, uses 'common-yaml.md' default platforms: - # Recommended Platforms - claude-code: - name: "Claude Code" - preferred: true - category: cli - description: "Anthropic's official CLI for Claude" - - windsurf: - name: "Windsurf" - preferred: true - category: ide - description: "AI-powered IDE with cascade flows" - - cursor: - name: "Cursor" - preferred: true - category: ide - description: "AI-first code editor" - - # Other IDEs and Tools - cline: - name: "Cline" + antigravity: + name: "Google Antigravity" preferred: false category: ide - description: "AI coding assistant" - - opencode: - name: "OpenCode" - preferred: false - category: ide - description: "OpenCode terminal coding assistant" + description: "Google's AI development environment" + installer: + target_dir: .antigravity/commands + frontmatter_template: common-yaml.md auggie: name: "Auggie" preferred: false category: cli description: "AI development tool" + installer: + target_dir: .augment/commands + frontmatter_template: common-yaml.md - roo: - name: "Roo Cline" + cline: + name: "Cline" preferred: false category: ide - description: "Enhanced Cline fork" + description: "AI coding assistant" + installer: + target_dir: .cline/commands + frontmatter_template: none # No frontmatter, content as-is - rovo: - name: "Rovo" - preferred: false - category: ide - description: "Atlassian's AI coding assistant" - - rovo-dev: - name: "Rovo Dev" - preferred: false - category: ide - description: "Atlassian's Rovo development environment" - - kiro-cli: - name: "Kiro CLI" - preferred: false + claude-code: + name: "Claude Code" + preferred: true category: cli - description: "Kiro command-line interface" - - github-copilot: - name: "GitHub Copilot" - preferred: false - category: ide - description: "GitHub's AI pair programmer" - - codex: - name: "Codex" - preferred: false - category: cli - description: "OpenAI Codex integration" - - qwen: - name: "QwenCoder" - preferred: false - category: ide - description: "Qwen AI coding assistant" - - gemini: - name: "Gemini CLI" - preferred: false - category: cli - description: "Google's CLI for Gemini" - - iflow: - name: "iFlow" - preferred: false - category: ide - description: "AI workflow automation" - - kilo: - name: "KiloCoder" - preferred: false - category: ide - description: "AI coding platform" + description: "Anthropic's official CLI for Claude" + installer: + target_dir: .claude/commands + frontmatter_template: common-yaml.md crush: name: "Crush" preferred: false category: ide description: "AI development assistant" + installer: + target_dir: .crush/commands + frontmatter_template: common-yaml.md - antigravity: - name: "Google Antigravity" + cursor: + name: "Cursor" + preferred: true + category: ide + description: "AI-first code editor" + installer: + target_dir: .cursor/commands + frontmatter_template: common-yaml.md + + gemini: + name: "Gemini CLI" + preferred: false + category: cli + description: "Google's CLI for Gemini" + installer: + target_dir: .gemini/commands + file_extension: .toml + frontmatter_template: common-toml.md + + github-copilot: + name: "GitHub Copilot" + preferred: true + category: ide + description: "GitHub's AI pair programmer" + installer: + targets: + - dir: .github/agents + frontmatter_template: copilot-agent.md + artifact_types: [agents] + - dir: .github/prompts + frontmatter_template: copilot.md + artifact_types: [workflows, tasks, tools] + has_vscode_settings: true + + iflow: + name: "iFlow" preferred: false category: ide - description: "Google's AI development environment" + description: "AI workflow automation" + installer: + target_dir: .iflow/commands + frontmatter_template: common-yaml.md + + kilo: + name: "KiloCoder" + preferred: false + category: ide + description: "AI coding platform" + # Kilo has custom installer (.kilocodemodes YAML format) - not config-driven + + kiro-cli: + name: "Kiro CLI" + preferred: false + category: cli + description: "Kiro command-line interface" + # Kiro CLI has custom installer (YAML->JSON conversion) - not config-driven + + opencode: + name: "OpenCode" + preferred: false + category: ide + description: "OpenCode terminal coding assistant" + installer: + targets: + - dir: .opencode/agent + frontmatter_template: opencode-agent.md + artifact_types: [agents] + - dir: .opencode/command + frontmatter_template: opencode.md + artifact_types: [workflows, tasks, tools] + + qwen: + name: "QwenCoder" + preferred: false + category: ide + description: "Qwen AI coding assistant" + installer: + target_dir: .qwen/commands + file_extension: .toml + frontmatter_template: common-toml.md + + roo: + name: "Roo Code" + preferred: false + category: ide + description: "Enhanced Cline fork" + installer: + target_dir: .roo/commands + frontmatter_template: roo.md + skip_existing: true + + rovo-dev: + name: "Rovo Dev" + preferred: false + category: ide + description: "Atlassian's Rovo development environment" + installer: + target_dir: .rovo-dev/commands + frontmatter_template: common-yaml.md trae: name: "Trae" preferred: false category: ide description: "AI coding tool" + installer: + target_dir: .trae/rules + frontmatter_template: trae.md + + windsurf: + name: "Windsurf" + preferred: true + category: ide + description: "AI-powered IDE with cascade flows" + installer: + target_dir: .windsurf/workflows + frontmatter_template: windsurf.md # Platform categories categories: @@ -155,3 +205,12 @@ conventions: name_format: "Title Case" max_code_length: 20 allowed_characters: "a-z0-9-" + + # New universal file naming standard + file_naming: + agent: "bmad-{module}-{name}.agent.md" + workflow: "bmad-{module}-{name}.workflow.md" + task: "bmad-{module}-{name}.task.md" + tool: "bmad-{module}-{name}.tool.md" + example_agent: "bmad-cis-storymaster.agent.md" + example_workflow: "bmad-bmm-plan-project.workflow.md" diff --git a/tools/cli/installers/lib/ide/qwen.js b/tools/cli/installers/lib/ide/qwen.js deleted file mode 100644 index fab6ee00..00000000 --- a/tools/cli/installers/lib/ide/qwen.js +++ /dev/null @@ -1,219 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); - -/** - * Qwen Code setup handler - * Creates TOML command files in .qwen/commands/ - */ -class QwenSetup extends BaseIdeSetup { - constructor() { - super('qwen', 'Qwen Code'); - this.configDir = '.qwen'; - this.commandsDir = 'commands'; - } - - /** - * Setup Qwen Code configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Create .qwen/commands directory (flat structure, no bmad subfolder) - const qwenDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(qwenDir, this.commandsDir); - - await this.ensureDir(commandsDir); - - // Update existing settings.json if present - await this.updateSettings(qwenDir); - - // Clean up old configuration - await this.cleanupOldConfig(qwenDir); - await this.cleanup(projectDir); - - // Use the unified installer with QWEN template for TOML format - const installer = new UnifiedInstaller(this.bmadFolderName); - const counts = await installer.install( - projectDir, - bmadDir, - { - targetDir: commandsDir, - namingStyle: NamingStyle.FLAT_DASH, - templateType: TemplateType.QWEN, - fileExtension: '.toml', - }, - options.selectedModules || [], - ); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${counts.agents} agents configured`)); - console.log(chalk.dim(` - ${counts.tasks} tasks configured`)); - console.log(chalk.dim(` - ${counts.tools} tools configured`)); - console.log(chalk.dim(` - ${counts.workflows} workflows configured`)); - console.log(chalk.dim(` - ${counts.total} TOML files written to ${path.relative(projectDir, commandsDir)}`)); - - return { - success: true, - ...counts, - }; - } - - /** - * Update settings.json to remove old agent references - */ - async updateSettings(qwenDir) { - const settingsPath = path.join(qwenDir, 'settings.json'); - - if (await fs.pathExists(settingsPath)) { - try { - const settingsContent = await fs.readFile(settingsPath, 'utf8'); - const settings = JSON.parse(settingsContent); - let updated = false; - - // Remove agent file references from contextFileName - if (settings.contextFileName && Array.isArray(settings.contextFileName)) { - const originalLength = settings.contextFileName.length; - settings.contextFileName = settings.contextFileName.filter( - (fileName) => !fileName.startsWith('agents/') && !fileName.startsWith('bmad-method/'), - ); - - if (settings.contextFileName.length !== originalLength) { - updated = true; - } - } - - if (updated) { - await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); - console.log(chalk.green(' ✓ Updated .qwen/settings.json')); - } - } catch (error) { - console.warn(chalk.yellow(' ⚠ Could not update settings.json:'), error.message); - } - } - } - - /** - * Clean up old configuration directories - */ - async cleanupOldConfig(qwenDir) { - const agentsDir = path.join(qwenDir, 'agents'); - const bmadMethodDir = path.join(qwenDir, 'bmad-method'); - const bmadDir = path.join(qwenDir, 'bmadDir'); - - if (await fs.pathExists(agentsDir)) { - await fs.remove(agentsDir); - console.log(chalk.green(' ✓ Removed old agents directory')); - } - - if (await fs.pathExists(bmadMethodDir)) { - await fs.remove(bmadMethodDir); - console.log(chalk.green(' ✓ Removed old bmad-method directory')); - } - - if (await fs.pathExists(bmadDir)) { - await fs.remove(bmadDir); - console.log(chalk.green(' ✓ Removed old BMad directory')); - } - } - - /** - * Cleanup Qwen configuration - */ - async cleanup(projectDir) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - if (await fs.pathExists(commandsDir)) { - // Remove any bmad* files from the commands directory - const entries = await fs.readdir(commandsDir); - for (const entry of entries) { - if (entry.startsWith('bmad')) { - await fs.remove(path.join(commandsDir, entry)); - } - } - } - - // Also remove legacy bmad subfolder if it exists - const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad'); - if (await fs.pathExists(bmadCommandsDir)) { - await fs.remove(bmadCommandsDir); - console.log(chalk.dim(` Cleaned up existing BMAD configuration from Qwen Code`)); - } - - const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method'); - if (await fs.pathExists(oldBmadMethodDir)) { - await fs.remove(oldBmadMethodDir); - console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`)); - } - - const oldBMadDir = path.join(projectDir, this.configDir, 'BMad'); - if (await fs.pathExists(oldBMadDir)) { - await fs.remove(oldBMadDir); - console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`)); - } - } - - /** - * Install a custom agent launcher for Qwen - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - // Create .qwen/commands directory if it doesn't exist - await fs.ensureDir(commandsDir); - - // Create custom agent launcher content - const launcherContent = `# ${agentName} Custom Agent - -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this command to activate ${agentName} - -The agent will follow the persona and instructions from the main agent file. - ---- - -*Generated by BMAD Method*`; - - // Convert to TOML format using the same method as UnifiedInstaller - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '').trim(); - const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`); - - const tomlContent = `description = "BMAD Custom Agent: ${agentName}" -prompt = """ -${escapedContent} -""" -`; - - // Use flat naming: bmad-custom-agent-agentname.toml - const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.toml`; - const launcherPath = path.join(commandsDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, tomlContent, 'utf8'); - - return { - ide: 'qwen', - path: path.relative(projectDir, launcherPath), - command: fileName.replace('.toml', ''), - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { QwenSetup }; diff --git a/tools/cli/installers/lib/ide/roo.js b/tools/cli/installers/lib/ide/roo.js deleted file mode 100644 index 66380464..00000000 --- a/tools/cli/installers/lib/ide/roo.js +++ /dev/null @@ -1,273 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { toDashPath, customAgentDashName } = require('./shared/path-utils'); - -/** - * Roo IDE setup handler - * Creates custom commands in .roo/commands directory - */ -class RooSetup extends BaseIdeSetup { - constructor() { - super('roo', 'Roo Code'); - this.configDir = '.roo'; - this.commandsDir = 'commands'; - } - - /** - * Setup Roo IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Create .roo/commands directory - const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); - await this.ensureDir(rooCommandsDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - let addedCount = 0; - let skippedCount = 0; - - for (const artifact of agentArtifacts) { - // Use shared toDashPath to get consistent naming: bmad_bmm_name.md - const commandName = toDashPath(artifact.relativePath).replace('.md', ''); - const commandPath = path.join(rooCommandsDir, `${commandName}.md`); - - // Skip if already exists - if (await this.pathExists(commandPath)) { - console.log(chalk.dim(` Skipping ${commandName} - already exists`)); - skippedCount++; - continue; - } - - // artifact.sourcePath contains the full path to the agent file - if (!artifact.sourcePath) { - console.error(`Error: Missing sourcePath for artifact ${artifact.name} from module ${artifact.module}`); - console.error(`Artifact object:`, artifact); - throw new Error(`Missing sourcePath for agent: ${artifact.name}`); - } - - const content = await this.readFile(artifact.sourcePath); - - // Create command file that references the actual _bmad agent - await this.createCommandFile( - { module: artifact.module, name: artifact.name, path: artifact.sourcePath }, - content, - commandPath, - projectDir, - ); - - addedCount++; - console.log(chalk.green(` ✓ Added command: ${commandName}`)); - } - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${addedCount} commands added`)); - if (skippedCount > 0) { - console.log(chalk.dim(` - ${skippedCount} commands skipped (already exist)`)); - } - console.log(chalk.dim(` - Commands directory: ${this.configDir}/${this.commandsDir}/`)); - console.log(chalk.dim(` Commands will be available when you open this project in Roo Code`)); - - return { - success: true, - commands: addedCount, - skipped: skippedCount, - }; - } - - /** - * Create a unified command file for agents - * @param {string} commandPath - Path where to write the command file - * @param {Object} options - Command options - * @param {string} options.name - Display name for the command - * @param {string} options.description - Description for the command - * @param {string} options.agentPath - Path to the agent file (relative to project root) - * @param {string} [options.icon] - Icon emoji (defaults to 🤖) - * @param {string} [options.extraContent] - Additional content to include before activation - */ - async createAgentCommandFile(commandPath, options) { - const { name, description, agentPath, icon = '🤖', extraContent = '' } = options; - - // Build command content with YAML frontmatter - let commandContent = `---\n`; - commandContent += `name: '${icon} ${name}'\n`; - commandContent += `description: '${description}'\n`; - commandContent += `---\n\n`; - - commandContent += `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n\n`; - - // Add any extra content (e.g., warnings for custom agents) - if (extraContent) { - commandContent += `${extraContent}\n\n`; - } - - commandContent += `\n`; - commandContent += `1. LOAD the FULL agent file from @${agentPath}\n`; - commandContent += `2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n`; - commandContent += `3. Execute ALL activation steps exactly as written in the agent file\n`; - commandContent += `4. Follow the agent's persona and menu system precisely\n`; - commandContent += `5. Stay in character throughout the session\n`; - commandContent += `\n`; - - // Write command file - await this.writeFile(commandPath, commandContent); - } - - /** - * Create a command file for an agent - */ - async createCommandFile(agent, content, commandPath, projectDir) { - // Extract metadata from agent content - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); - - const iconMatch = content.match(/icon="([^"]+)"/); - const icon = iconMatch ? iconMatch[1] : '🤖'; - - const whenToUseMatch = content.match(/whenToUse="([^"]+)"/); - const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; - - // Get relative path - const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/'); - - // Use unified method - await this.createAgentCommandFile(commandPath, { - name: title, - description: whenToUse, - agentPath: relativePath, - icon: icon, - }); - } - - /** - * Format name as title - */ - formatTitle(name) { - return name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - /** - * Cleanup Roo configuration - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - if (await fs.pathExists(rooCommandsDir)) { - const files = await fs.readdir(rooCommandsDir); - let removedCount = 0; - - for (const file of files) { - if (file.startsWith('bmad') && file.endsWith('.md')) { - await fs.remove(path.join(rooCommandsDir, file)); - removedCount++; - } - } - - if (removedCount > 0) { - console.log(chalk.dim(`Removed ${removedCount} BMAD commands from .roo/commands/`)); - } - } - - // Also clean up old .roomodes file if it exists - const roomodesPath = path.join(projectDir, '.roomodes'); - if (await fs.pathExists(roomodesPath)) { - const content = await fs.readFile(roomodesPath, 'utf8'); - - // Remove BMAD modes only - const lines = content.split('\n'); - const filteredLines = []; - let skipMode = false; - let removedCount = 0; - - for (const line of lines) { - if (/^\s*- slug: bmad/.test(line)) { - skipMode = true; - removedCount++; - } else if (skipMode && /^\s*- slug: /.test(line)) { - skipMode = false; - } - - if (!skipMode) { - filteredLines.push(line); - } - } - - // Write back filtered content - await fs.writeFile(roomodesPath, filteredLines.join('\n')); - if (removedCount > 0) { - console.log(chalk.dim(`Removed ${removedCount} BMAD modes from legacy .roomodes file`)); - } - } - } - - /** - * Install a custom agent launcher for Roo - * @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 (unused, kept for compatibility) - * @returns {Object} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir); - await this.ensureDir(rooCommandsDir); - - // Use underscore format: bmad_custom_fred-commit-poet.md - const commandName = customAgentDashName(agentName).replace('.md', ''); - const commandPath = path.join(rooCommandsDir, `${commandName}.md`); - - // Check if command already exists - if (await this.pathExists(commandPath)) { - return { - ide: 'roo', - path: path.join(this.configDir, this.commandsDir, `${commandName}.md`), - command: commandName, - type: 'custom-agent-launcher', - alreadyExists: true, - }; - } - - // Read the custom agent file to extract metadata (same as regular agents) - const fullAgentPath = path.join(projectDir, agentPath); - const content = await this.readFile(fullAgentPath); - - // Extract metadata from agent content - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName); - - const iconMatch = content.match(/icon="([^"]+)"/); - const icon = iconMatch ? iconMatch[1] : '🤖'; - - const whenToUseMatch = content.match(/whenToUse="([^"]+)"/); - const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; - - // Use unified method without extra content (clean) - await this.createAgentCommandFile(commandPath, { - name: title, - description: whenToUse, - agentPath: agentPath, - icon: icon, - }); - - return { - ide: 'roo', - path: path.join(this.configDir, this.commandsDir, `${commandName}.md`), - command: commandName, - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { RooSetup }; diff --git a/tools/cli/installers/lib/ide/rovo-dev.js b/tools/cli/installers/lib/ide/rovo-dev.js deleted file mode 100644 index 1151a2d5..00000000 --- a/tools/cli/installers/lib/ide/rovo-dev.js +++ /dev/null @@ -1,187 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const chalk = require('chalk'); -const { BaseIdeSetup } = require('./_base-ide'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); - -/** - * Rovo Dev IDE setup handler - * - * Uses UnifiedInstaller for all artifact installation with flat file structure. - * All BMAD artifacts are installed to .rovodev/workflows/ as flat files. - */ -class RovoDevSetup extends BaseIdeSetup { - constructor() { - super('rovo-dev', 'Atlassian Rovo Dev', false); - this.configDir = '.rovodev'; - this.workflowsDir = 'workflows'; - } - - /** - * Setup Rovo Dev 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 .rovodev directory structure - const rovoDevDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - - await this.ensureDir(workflowsDir); - - // Use the unified installer - all artifacts go to workflows folder as flat files - const installer = new UnifiedInstaller(this.bmadFolderName); - const counts = await installer.install( - projectDir, - bmadDir, - { - targetDir: workflowsDir, - namingStyle: NamingStyle.FLAT_DASH, - templateType: TemplateType.CLAUDE, - }, - 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} workflows installed`)); - } - if (counts.tasks + counts.tools > 0) { - console.log(chalk.dim(` - ${counts.tasks + counts.tools} tasks/tools installed (${counts.tasks} tasks, ${counts.tools} tools)`)); - } - console.log(chalk.dim(` - ${counts.total} files written to ${path.relative(projectDir, workflowsDir)}`)); - console.log(chalk.yellow(`\n Note: All BMAD items are available in .rovodev/workflows/`)); - console.log(chalk.dim(` - Access items by typing @ in Rovo Dev to see available files`)); - - return { - success: true, - ...counts, - }; - } - - /** - * Cleanup old BMAD installation before reinstalling - * @param {string} projectDir - Project directory - */ - async cleanup(projectDir) { - const rovoDevDir = path.join(projectDir, this.configDir); - - if (!(await fs.pathExists(rovoDevDir))) { - return; - } - - // Clean BMAD files from workflows directory - const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - if (await fs.pathExists(workflowsDir)) { - const entries = await fs.readdir(workflowsDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(workflowsDir, file)); - } - } - - // Remove legacy subagents directory - const subagentsDir = path.join(rovoDevDir, 'subagents'); - if (await fs.pathExists(subagentsDir)) { - await fs.remove(subagentsDir); - console.log(chalk.dim(` Removed legacy subagents directory`)); - } - - // Remove legacy references directory - const referencesDir = path.join(rovoDevDir, 'references'); - if (await fs.pathExists(referencesDir)) { - await fs.remove(referencesDir); - console.log(chalk.dim(` Removed legacy references directory`)); - } - } - - /** - * Detect whether Rovo Dev is already configured in the project - * @param {string} projectDir - Project directory - * @returns {boolean} - */ - async detect(projectDir) { - const rovoDevDir = path.join(projectDir, this.configDir); - - if (!(await fs.pathExists(rovoDevDir))) { - return false; - } - - // Check for BMAD files in workflows directory - const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - if (await fs.pathExists(workflowsDir)) { - try { - const entries = await fs.readdir(workflowsDir); - if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) { - return true; - } - } catch { - // Continue checking - } - } - - return false; - } - - /** - * Install a custom agent launcher for Rovo Dev - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - - if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; - } - - await this.ensureDir(workflowsDir); - - const launcherContent = `--- -name: ${agentName} -description: Custom BMAD agent: ${agentName} ---- - -# ${agentName} Custom Agent - -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this workflow as ${agentName} - -The agent will follow the persona and instructions from the main agent file. - ---- - -*Generated by BMAD Method*`; - - // Use flat naming: bmad-custom-agent-agentname.md - const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.md`; - const launcherPath = path.join(workflowsDir, fileName); - - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'rovo-dev', - path: path.relative(projectDir, launcherPath), - command: fileName.replace('.md', ''), - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { RovoDevSetup }; diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 1d2b5df8..6e2ca31b 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -32,8 +32,10 @@ class AgentCommandGenerator { const agentPathInModule = agent.relativePath || `${agent.name}.md`; artifacts.push({ type: 'agent-launcher', - module: agent.module, name: agent.name, + displayName: agent.displayName || agent.name, + description: agent.description, + module: agent.module, relativePath: path.join(agent.module, 'agents', agentPathInModule), content: launcherContent, sourcePath: agent.path, diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index eb190589..fbe76099 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -44,9 +44,26 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) { if (content.includes('localskip="true"')) continue; + // Extract description from YAML frontmatter if present + let description = null; + let agentName = file.replace('.md', ''); + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); + if (frontmatterMatch) { + const descMatch = frontmatterMatch[1].match(/description:\s*"([^"]+)"/); + if (descMatch) { + description = descMatch[1]; + } + const nameMatch = frontmatterMatch[1].match(/name:\s*"([^"]+)"/); + if (nameMatch) { + agentName = nameMatch[1]; + } + } + agents.push({ path: filePath, - name: file.replace('.md', ''), + name: agentName, + displayName: agentName, + description: description, module: 'standalone', // Mark as standalone agent }); } @@ -114,9 +131,26 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') { continue; } + // Extract description from YAML frontmatter if present + let description = null; + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); + if (frontmatterMatch) { + const descMatch = frontmatterMatch[1].match(/description:\s*"([^"]+)"/); + if (descMatch) { + description = descMatch[1]; + } + // Also extract name from frontmatter if available + const nameMatch = frontmatterMatch[1].match(/name:\s*"([^"]+)"/); + if (nameMatch) { + entry.name = `${nameMatch[1]}.md`; + } + } + agents.push({ path: fullPath, name: entry.name.replace('.md', ''), + displayName: entry.name.replace('.md', ''), + description: description, module: moduleName, relativePath: newRelativePath, // Keep the .md extension for the full path }); diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index d020f3d3..488d2811 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -3,6 +3,7 @@ * * Provides utilities to convert hierarchical paths to flat naming conventions. * - Underscore format (bmad_module_name.md) - Windows-compatible universal format + * - Suffix-based format (bmad-module-name.agent.md) - New universal standard */ // Default file extension for backward compatibility @@ -12,6 +13,17 @@ const DEFAULT_FILE_EXTENSION = '.md'; const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const AGENT_SEGMENT = 'agents'; +/** + * Artifact type to suffix mapping + * Used for new suffix-based naming convention + */ +const ARTIFACT_SUFFIXES = { + agent: '.agent', + workflow: '.workflow', + task: '.task', + tool: '.tool', +}; + /** * Convert hierarchical path to flat underscore-separated name * Converts: 'bmm', 'agents', 'pm' → 'bmad_bmm_agent_pm.md' @@ -193,6 +205,7 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { // Use dash naming style const isAgent = type === AGENT_SEGMENT; + // For core module, skip the module prefix if (module === 'core') { return isAgent ? `bmad-agent-${name}${fileExtension}` : `bmad-${name}${fileExtension}`; } @@ -201,6 +214,79 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { return isAgent ? `${prefix}${module}-agent-${name}${fileExtension}` : `${prefix}${module}-${name}${fileExtension}`; } +/** + * Convert relative path to suffix-based name (NEW UNIVERSAL STANDARD) + * Converts: 'cis/agents/storymaster.md' → 'bmad-cis-storymaster.agent.md' + * Converts: 'bmm/workflows/plan-project.md' → 'bmad-bmm-plan-project.workflow.md' + * Converts: 'bmm/tasks/create-story.md' → 'bmad-bmm-create-story.task.md' + * Converts: 'bmm/tools/file-ops.md' → 'bmad-bmm-file-ops.tool.md' + * Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix) + * + * @param {string} relativePath - Path like 'cis/agents/storymaster.md' + * @param {string} artifactType - Type of artifact: 'agent', 'workflow', 'task', 'tool' + * @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml') + * @returns {string} Suffix-based filename like 'bmad-cis-storymaster.agent.md' + */ +function toSuffixBasedName(relativePath, artifactType, fileExtension = DEFAULT_FILE_EXTENSION) { + const extMatch = relativePath.match(/\.[^.]+$/); + const originalExt = extMatch ? extMatch[0] : ''; + const withoutExt = relativePath.replace(originalExt, ''); + const parts = withoutExt.split(/[/\\]/); + + const module = parts[0]; + const type = parts[1]; // agents, workflows, tasks, tools + const name = parts.slice(2).join('-'); + + const suffix = ARTIFACT_SUFFIXES[artifactType] || ''; + + // For core module, skip the module prefix (use 'bmad-name.suffix.md') + if (module === 'core') { + return `bmad-${name}${suffix}.${fileExtension.replace('.', '')}`; + } + + // If module already starts with 'bmad-', don't add another prefix + const prefix = module.startsWith('bmad-') ? '' : 'bmad-'; + return `${prefix}${module}-${name}${suffix}.${fileExtension.replace('.', '')}`; +} + +/** + * Get suffix for artifact type + * @param {string} artifactType - Type of artifact: 'agent', 'workflow', 'task', 'tool' + * @returns {string} Suffix like '.agent', '.workflow', etc. + */ +function getArtifactSuffix(artifactType) { + return ARTIFACT_SUFFIXES[artifactType] || ''; +} + +/** + * Parse artifact type from suffix-based filename + * Parses: 'bmad-cis-storymaster.agent.md' → 'agent' + * Parses: 'bmad-bmm-plan-project.workflow.md' → 'workflow' + * + * @param {string} filename - Suffix-based filename + * @returns {string|null} Artifact type or null if not found + */ +function parseArtifactTypeFromFilename(filename) { + for (const [type, suffix] of Object.entries(ARTIFACT_SUFFIXES)) { + if (filename.includes(`${suffix}.`)) { + return type; + } + } + return null; +} + +/** + * Create custom agent suffix-based name + * Creates: 'bmad-custom-fred-commit-poet.agent.md' + * + * @param {string} agentName - Custom agent name + * @param {string} [fileExtension='.md'] - File extension including dot + * @returns {string} Suffix-based filename like 'bmad-custom-fred-commit-poet.agent.md' + */ +function customAgentSuffixName(agentName, fileExtension = DEFAULT_FILE_EXTENSION) { + return `bmad-custom-${agentName}.agent.${fileExtension.replace('.', '')}`; +} + module.exports = { DEFAULT_FILE_EXTENSION, toUnderscoreName, @@ -221,4 +307,10 @@ module.exports = { parseDashName, TYPE_SEGMENTS, AGENT_SEGMENT, + // New suffix-based naming functions (UNIVERSAL STANDARD) + ARTIFACT_SUFFIXES, + toSuffixBasedName, + getArtifactSuffix, + parseArtifactTypeFromFilename, + customAgentSuffixName, }; 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 726356ee..45a30206 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 @@ -2,7 +2,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); const chalk = require('chalk'); -const { toColonName, toColonPath, toDashPath } = require('./path-utils'); +const { toColonName, toColonPath, toDashPath, toSuffixBasedName } = require('./path-utils'); /** * Generates command files for standalone tasks and tools @@ -14,46 +14,6 @@ class TaskToolCommandGenerator { * Use generateColonTaskToolCommands() or generateDashTaskToolCommands() instead. */ - /** - * Generate command content for a task or tool - * @param {Object} item - Task or tool item from manifest - * @param {string} type - 'task' or 'tool' - * @param {string} [format='yaml'] - Output format: 'yaml' or 'toml' - */ - generateCommandContent(item, type, format = 'yaml') { - const description = item.description || `Execute ${item.displayName || item.name}`; - - // Convert path to use {project-root} placeholder - let itemPath = item.path; - if (itemPath.startsWith('bmad/')) { - itemPath = `{project-root}/${itemPath}`; - } - - const content = `# ${item.displayName || item.name} - -LOAD and execute the ${type} at: ${itemPath} - -Follow all instructions in the ${type} file exactly as written. -`; - - if (format === 'toml') { - // Escape any triple quotes in content - const escapedContent = content.replaceAll('"""', String.raw`\"\"\"`); - return `description = "${description}" -prompt = """ -${escapedContent} -""" -`; - } - - // Default YAML format - return `--- -description: '${description.replaceAll("'", "''")}' ---- - -${content}`; - } - /** * Load task manifest CSV */ @@ -257,6 +217,163 @@ ${content}`; return writtenCount; } + + /** + * Generate task and tool commands using suffix-based format (NEW UNIVERSAL STANDARD) + * Creates flat files like: bmad-bmm-create-story.task.md + * + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {string} baseCommandsDir - Base commands directory for the IDE + * @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml') + * @param {string} [templateContent] - Frontmatter template content (from platform-codes.yaml) + * @param {string} [frontmatterTemplate] - Frontmatter template filename + * @param {boolean} [skipExisting=false] - Skip if file already exists + * @returns {Object} Generation results + */ + async generateSuffixBasedTaskToolCommands( + projectDir, + bmadDir, + baseCommandsDir, + fileExtension = '.md', + templateContent = null, + frontmatterTemplate = 'common-yaml.md', + skipExisting = false, + ) { + 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) : []; + + let generatedCount = 0; + let skippedCount = 0; + + // Generate command files for tasks + for (const task of standaloneTasks) { + const commandContent = this.generateCommandContent(task, 'task', templateContent, frontmatterTemplate); + // Use suffix-based format: bmad-bmm-create-story.task.md + const relativePath = `${task.module}/tasks/${task.name}.md`; + const suffixName = toSuffixBasedName(relativePath, 'task', fileExtension); + const commandPath = path.join(baseCommandsDir, suffixName); + + // Skip if already exists + if (skipExisting && (await fs.pathExists(commandPath))) { + skippedCount++; + continue; + } + + await fs.ensureDir(baseCommandsDir); + await fs.writeFile(commandPath, commandContent); + generatedCount++; + } + + // Generate command files for tools + for (const tool of standaloneTools) { + const commandContent = this.generateCommandContent(tool, 'tool', templateContent, frontmatterTemplate); + // Use suffix-based format: bmad-bmm-file-ops.tool.md + const relativePath = `${tool.module}/tools/${tool.name}.md`; + const suffixName = toSuffixBasedName(relativePath, 'tool', fileExtension); + const commandPath = path.join(baseCommandsDir, suffixName); + + // Skip if already exists + if (skipExisting && (await fs.pathExists(commandPath))) { + skippedCount++; + continue; + } + + await fs.ensureDir(baseCommandsDir); + await fs.writeFile(commandPath, commandContent); + generatedCount++; + } + + if (skippedCount > 0) { + console.log(chalk.dim(` Skipped ${skippedCount} existing task/tool files`)); + } + + return { + generated: generatedCount, + tasks: standaloneTasks.length, + tools: standaloneTools.length, + }; + } + + /** + * Generate command content for a task or tool + * @param {Object} item - Task or tool item from manifest + * @param {string} type - 'task' or 'tool' + * @param {string|Object|null} [templateOrFormat] - Template content or format string ('yaml'/'toml') for backward compat + * @param {string} [frontmatterTemplate] - Template filename (for format detection) + */ + generateCommandContent(item, type, templateOrFormat = null, frontmatterTemplate = null) { + const description = item.description || `Execute ${item.displayName || item.name}`; + + // Convert path to use {project-root} placeholder + let itemPath = item.path; + if (itemPath.startsWith('bmad/')) { + itemPath = `{project-root}/${itemPath}`; + } + + const content = `# ${item.displayName || item.name} + +LOAD and execute the ${type} at: ${itemPath} + +Follow all instructions in the ${type} file exactly as written. +`; + + // Handle old calling convention: (item, type, format) where format is 'yaml' or 'toml' + if (typeof templateOrFormat === 'string' && (templateOrFormat === 'yaml' || templateOrFormat === 'toml')) { + if (templateOrFormat === 'toml') { + // TOML format + const escapedContent = content.replaceAll('"""', String.raw`\"\"\"`); + return `description = "${description}" +prompt = """ +${escapedContent} +""" +`; + } + // Default YAML format + return `--- +description: '${description.replaceAll("'", "''")}' +--- + +${content}`; + } + + // New calling convention with template content + const templateContent = templateOrFormat; + if (!templateContent || frontmatterTemplate === 'none' || (templateContent === null && frontmatterTemplate === null)) { + // Default YAML + return `--- +description: '${description.replaceAll("'", "''")}' +--- + +${content}`; + } + + // Apply template variables + const variables = { + name: item.name, + displayName: item.displayName || item.name, + description, + content, + icon: '🤖', + }; + + let result = templateContent; + for (const [key, value] of Object.entries(variables)) { + result = result.replaceAll(`{{${key}}}`, value); + } + + // Handle TOML templates specially + if (frontmatterTemplate && frontmatterTemplate.includes('toml')) { + const escapedContent = content.replaceAll('"""', String.raw`\"\"\"`); + result = result.replace(/prompt = """/, `prompt = """\n${escapedContent}`); + } + + return result; + } } module.exports = { TaskToolCommandGenerator }; diff --git a/tools/cli/installers/lib/ide/shared/unified-installer.js b/tools/cli/installers/lib/ide/shared/unified-installer.js index fec04944..3493ea98 100644 --- a/tools/cli/installers/lib/ide/shared/unified-installer.js +++ b/tools/cli/installers/lib/ide/shared/unified-installer.js @@ -1,47 +1,27 @@ /** * 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) + * ALL IDE configuration comes from platform-codes.yaml + * NO IDE-specific code in this file - just loads and applies templates */ const path = require('node:path'); const fs = require('fs-extra'); +const chalk = require('chalk'); 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'); +const { toColonPath, toDashPath, toSuffixBasedName, getArtifactSuffix } = require('./path-utils'); /** * Naming styles + * @deprecated Use 'suffix-based' for all new installations */ 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 - GEMINI: 'gemini', // TOML frontmatter with description/prompt - QWEN: 'qwen', // TOML frontmatter with description/prompt (same as Gemini) - COPILOT: 'copilot', // YAML with tools array for GitHub Copilot + FLAT_COLON: 'flat-colon', + FLAT_DASH: 'flat-dash', + NESTED: 'nested', + SUFFIX_BASED: 'suffix-based', }; /** @@ -49,18 +29,22 @@ const TemplateType = { * @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 {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml') + * @property {string} [frontmatterTemplate] - Frontmatter template filename (from platform-codes.yaml) + * @property {string} [fileExtension='.md'] - File extension including dot * @property {boolean} includeNestedStructure - For NESTED style, create subdirectories * @property {Function} [customTemplateFn] - Optional custom template function */ /** * Unified BMAD Installer + * + * Driven entirely by platform-codes.yaml configuration + * Frontmatter templates are loaded from templates/frontmatter/ directory */ class UnifiedInstaller { constructor(bmadFolderName = 'bmad') { this.bmadFolderName = bmadFolderName; + this.templateDir = path.join(__dirname, '../templates/frontmatter'); } /** @@ -75,15 +59,19 @@ class UnifiedInstaller { async install(projectDir, bmadDir, config, selectedModules = []) { const { targetDir, - namingStyle = NamingStyle.FLAT_COLON, - templateType = TemplateType.CLAUDE, + namingStyle = NamingStyle.SUFFIX_BASED, + frontmatterTemplate = 'common-yaml.md', fileExtension = '.md', includeNestedStructure = false, customTemplateFn = null, + skipExisting = false, + artifactTypes = null, } = config; - // Clean up any existing BMAD files in target directory - await this.cleanupBmadFiles(targetDir, fileExtension); + // Clean up any existing BMAD files in target directory (unless skipExisting) + if (!skipExisting) { + await this.cleanupBmadFiles(targetDir, fileExtension); + } // Ensure target directory exists await fs.ensureDir(targetDir); @@ -97,49 +85,83 @@ class UnifiedInstaller { total: 0, }; + // Check if we should install agents + const installAgents = !artifactTypes || artifactTypes.includes('agents'); + const installWorkflows = !artifactTypes || artifactTypes.includes('workflows'); + const installTasks = !artifactTypes || artifactTypes.includes('tasks'); + const installTools = !artifactTypes || artifactTypes.includes('tools'); + + // Load frontmatter template once (if not 'none') + let templateContent = null; + if (frontmatterTemplate && frontmatterTemplate !== 'none') { + templateContent = await this.loadFrontmatterTemplate(frontmatterTemplate); + } + // 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, - fileExtension, - customTemplateFn, - 'agent', - ); + if (installAgents) { + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); + counts.agents = await this.writeArtifacts( + agentArtifacts, + targetDir, + namingStyle, + templateContent, + frontmatterTemplate, + fileExtension, + customTemplateFn, + 'agent', + skipExisting, + ); + } // 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, - fileExtension, - customTemplateFn, - 'workflow', - ); + if (installWorkflows) { + 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, + templateContent, + frontmatterTemplate, + fileExtension, + customTemplateFn, + 'workflow', + skipExisting, + ); + } - // 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}`); + // 3. Install Tasks and Tools from manifest CSV + if (installTasks || installTools) { + const ttGen = new TaskToolCommandGenerator(); - // 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, fileExtension) - : await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension); - - counts.tasks = taskToolResult.tasks || 0; - counts.tools = taskToolResult.tools || 0; + // Use suffix-based naming if specified + if (namingStyle === NamingStyle.SUFFIX_BASED) { + const taskToolResult = await ttGen.generateSuffixBasedTaskToolCommands( + projectDir, + bmadDir, + targetDir, + fileExtension, + templateContent, + frontmatterTemplate, + skipExisting, + ); + counts.tasks = taskToolResult.tasks || 0; + counts.tools = taskToolResult.tools || 0; + } else if (namingStyle === NamingStyle.FLAT_DASH) { + const taskToolResult = await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension); + counts.tasks = taskToolResult.tasks || 0; + counts.tools = taskToolResult.tools || 0; + } else { + const taskToolResult = await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension); + counts.tasks = taskToolResult.tasks || 0; + counts.tools = taskToolResult.tools || 0; + } + } counts.total = counts.agents + counts.workflows + counts.tasks + counts.tools; @@ -147,206 +169,89 @@ class UnifiedInstaller { } /** - * Clean up any existing BMAD files in target directory - * @param {string} targetDir - Target directory to clean - * @param {string} [fileExtension='.md'] - File extension to match + * Load frontmatter template from file + * @param {string} templateFile - Template filename + * @returns {Promise} Template content or null if not found */ - async cleanupBmadFiles(targetDir, fileExtension = '.md') { - 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) { - // Only remove files with the matching extension - if (entry.name.startsWith('bmad') && entry.name.endsWith(fileExtension)) { - const entryPath = path.join(targetDir, entry.name); - await fs.remove(entryPath); - } + async loadFrontmatterTemplate(templateFile) { + const templatePath = path.join(this.templateDir, templateFile); + try { + return await fs.readFile(templatePath, 'utf8'); + } catch { + console.warn(chalk.yellow(`Warning: Could not load template ${templateFile}, using default`)); + return null; } } /** - * Write artifacts with specified naming style and template - * @param {Array} artifacts - Artifacts to write - * @param {string} targetDir - Target directory - * @param {NamingStyle} namingStyle - Naming style to use - * @param {TemplateType} templateType - Template type to use - * @param {string} fileExtension - File extension including dot - * @param {Function} customTemplateFn - Optional custom template function - * @param {string} artifactType - Type of artifact for logging - * @returns {Promise} Number of artifacts written + * Apply frontmatter template to content + * @param {Object} artifact - Artifact with metadata + * @param {string} content - Original content + * @param {string} templateContent - Template content + * @param {string} templateFile - Template filename (for special handling) + * @returns {string} Content with frontmatter applied */ - async writeArtifacts(artifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, artifactType) { - console.log( - `[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}, fileExtension=${fileExtension}`, - ); - 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, fileExtension); - targetPath = path.join(targetDir, flatName); - } else if (namingStyle === NamingStyle.FLAT_DASH) { - const flatName = toDashPath(artifact.relativePath, fileExtension); - targetPath = path.join(targetDir, flatName); - } else { - // Fallback: treat as flat even if NESTED specified - const flatName = toColonPath(artifact.relativePath, fileExtension); - targetPath = path.join(targetDir, flatName); - } - - // Apply template transformations if needed - if (customTemplateFn) { - content = customTemplateFn(artifact, content, templateType); - } else { - content = this.applyTemplate(artifact, content, templateType); - } - - // For flat files, just ensure targetDir exists (no nested dirs needed) - await fs.ensureDir(targetDir); - await fs.writeFile(targetPath, content, 'utf8'); - written++; + applyFrontmatterTemplate(artifact, content, templateContent, templateFile) { + if (!templateContent) { + return content; } - 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); - } - - case TemplateType.GEMINI: { - // Add Gemini TOML frontmatter - return this.addGeminiFrontmatter(artifact, content); - } - - case TemplateType.COPILOT: { - // Add Copilot frontmatter with tools array - return this.addCopilotFrontmatter(artifact, content); - } - - case TemplateType.QWEN: { - // Add Qwen TOML frontmatter (same as Gemini) - return this.addGeminiFrontmatter(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; - } - - /** - * Add Gemini TOML frontmatter - * Converts content to TOML format with description and prompt fields - */ - addGeminiFrontmatter(artifact, content) { - // Remove existing YAML frontmatter if present + // Extract existing frontmatter if present const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; const contentWithoutFrontmatter = content.replace(frontmatterRegex, '').trim(); - // Extract description from artifact or content - let description = artifact.name || artifact.displayName || 'BMAD Command'; - if (artifact.module) { - description = `BMAD ${artifact.module.toUpperCase()} ${artifact.type || 'Command'}: ${description}`; + // Get artifact metadata for template substitution + const name = artifact.name || artifact.displayName || 'workflow'; + const title = this.formatTitle(name); + const iconMatch = content.match(/icon="([^"]+)"/); + const icon = iconMatch ? iconMatch[1] : '🤖'; + + // Use artifact's description if available, otherwise generate fallback + const description = artifact.description || `Activates the ${name} ${artifact.type || 'workflow'}.`; + + // Template variables + const variables = { + name, + title, + displayName: name, + description, + icon, + content: contentWithoutFrontmatter, + + // Special variables for certain templates + autoExecMode: this.getAutoExecMode(artifact), + tools: JSON.stringify(this.getCopilotTools()), + }; + + // Apply template substitutions + let result = templateContent; + for (const [key, value] of Object.entries(variables)) { + result = result.replaceAll(`{{${key}}}`, value); } - // Escape any triple quotes in content - const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`); + // Append content after frontmatter (for TOML templates with prompt field) + if (templateFile.includes('toml') && !result.includes('{{content}}')) { + const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`); + result = result.replace(/prompt = """/, `prompt = """\n${escapedContent}`); + } - return `description = "${description}" -prompt = """ -${escapedContent} -""" -`; + return result.trim() + '\n\n' + contentWithoutFrontmatter; } /** - * Add GitHub Copilot frontmatter with tools array + * Get auto_execution_mode for Windsurf based on artifact type */ - addCopilotFrontmatter(artifact, content) { - // Remove existing frontmatter if present - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); + getAutoExecMode(artifact) { + if (artifact.type === 'agent') return '3'; + if (artifact.type === 'task' || artifact.type === 'tool') return '2'; + return '1'; // default for workflows + } - // GitHub Copilot tools array (as specified) - const tools = [ + /** + * Get GitHub Copilot tools array + */ + getCopilotTools() { + return [ 'changes', 'edit', 'fetch', @@ -361,75 +266,110 @@ ${escapedContent} 'todos', 'usages', ]; - - const name = artifact.name || artifact.displayName || 'prompt'; - const description = `Activates the ${name} ${artifact.type || 'workflow'}.`; - - const frontmatter = `--- -description: "${description}" -tools: ${JSON.stringify(tools)} ---- - -`; - - return frontmatter + contentWithoutFrontmatter; } /** - * Get tasks from manifest CSV + * Clean up any existing BMAD files in target directory */ - async getTasksFromManifest(bmadDir) { - const csv = require('csv-parse/sync'); - const manifestPath = path.join(bmadDir, '_config', 'task-manifest.csv'); - - if (!(await fs.pathExists(manifestPath))) { - return []; + async cleanupBmadFiles(targetDir, fileExtension = '.md') { + if (!(await fs.pathExists(targetDir))) { + return; } - const csvContent = await fs.readFile(manifestPath, 'utf8'); - const tasks = csv.parse(csvContent, { - columns: true, - skip_empty_lines: true, - }); + const entries = await fs.readdir(targetDir, { withFileTypes: 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 - })); + for (const entry of entries) { + if (entry.name.startsWith('bmad') && entry.name.endsWith(fileExtension)) { + const entryPath = path.join(targetDir, entry.name); + await fs.remove(entryPath); + } + } } /** - * Get tools from manifest CSV + * Write artifacts with specified naming style and template */ - async getToolsFromManifest(bmadDir) { - const csv = require('csv-parse/sync'); - const manifestPath = path.join(bmadDir, '_config', 'tool-manifest.csv'); + async writeArtifacts( + artifacts, + targetDir, + namingStyle, + templateContent, + templateFile, + fileExtension, + customTemplateFn, + artifactType, + skipExisting = false, + ) { + let written = 0; + let skipped = 0; - if (!(await fs.pathExists(manifestPath))) { - return []; + for (const artifact of artifacts) { + // Determine target path based on naming style + let targetPath; + let content = artifact.content; + + switch (namingStyle) { + case NamingStyle.SUFFIX_BASED: { + const suffixName = toSuffixBasedName(artifact.relativePath, artifactType, fileExtension); + targetPath = path.join(targetDir, suffixName); + + break; + } + case NamingStyle.FLAT_COLON: { + const flatName = toColonPath(artifact.relativePath, fileExtension); + targetPath = path.join(targetDir, flatName); + + break; + } + case NamingStyle.FLAT_DASH: { + const flatName = toDashPath(artifact.relativePath, fileExtension); + targetPath = path.join(targetDir, flatName); + + break; + } + default: { + const flatName = toColonPath(artifact.relativePath, fileExtension); + targetPath = path.join(targetDir, flatName); + } + } + + // Skip if file already exists + if (skipExisting && (await fs.pathExists(targetPath))) { + skipped++; + continue; + } + + // Apply template transformations + if (customTemplateFn) { + content = customTemplateFn(artifact, content, templateFile); + } else if (templateFile !== 'none') { + content = this.applyFrontmatterTemplate(artifact, content, templateContent, templateFile); + } + + await fs.ensureDir(targetDir); + await fs.writeFile(targetPath, content, 'utf8'); + written++; } - const csvContent = await fs.readFile(manifestPath, 'utf8'); - const tools = csv.parse(csvContent, { - columns: true, - skip_empty_lines: true, - }); + if (skipped > 0) { + console.log(chalk.dim(` Skipped ${skipped} existing files`)); + } - // 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 - })); + return written; + } + + /** + * Format name as title + */ + formatTitle(name) { + return name + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); } } 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 18b7d1a2..f1a64467 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -35,6 +35,9 @@ class WorkflowCommandGenerator { const commandContent = await this.generateCommandContent(workflow, bmadDir); artifacts.push({ type: 'workflow-command', + name: workflow.name, + displayName: workflow.displayName || workflow.name, + description: workflow.description, module: workflow.module, relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`), content: commandContent, diff --git a/tools/cli/installers/lib/ide/templates/codex-agent-command-template.md b/tools/cli/installers/lib/ide/templates/codex-agent-command-template.md new file mode 100644 index 00000000..1c82539d --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/codex-agent-command-template.md @@ -0,0 +1,15 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. + + +1. LOAD the FULL agent file from @_bmad/{{relativePath}} +2. READ its entire contents - this contains the complete agent persona, menu, and instructions +3. FOLLOW every step in the section precisely +4. DISPLAY the welcome/greeting as instructed +5. PRESENT the numbered menu +6. WAIT for user input before proceeding + diff --git a/tools/cli/installers/lib/ide/templates/codex-custom-agent-template.md b/tools/cli/installers/lib/ide/templates/codex-custom-agent-template.md new file mode 100644 index 00000000..c1a547fa --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/codex-custom-agent-template.md @@ -0,0 +1,8 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +{{activationHeader}} + +Run @_bmad/{{relativePath}} to load the full agent. diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/common-toml.md b/tools/cli/installers/lib/ide/templates/frontmatter/common-toml.md new file mode 100644 index 00000000..d26a92d3 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/common-toml.md @@ -0,0 +1,4 @@ +description = "{{description}}" +prompt = """ +{{content}} +""" diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/common-yaml.md b/tools/cli/installers/lib/ide/templates/frontmatter/common-yaml.md new file mode 100644 index 00000000..a384374c --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/common-yaml.md @@ -0,0 +1,4 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/copilot-agent.md b/tools/cli/installers/lib/ide/templates/frontmatter/copilot-agent.md new file mode 100644 index 00000000..389b1862 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/copilot-agent.md @@ -0,0 +1,7 @@ +--- +description: "{{description}}" +tools: {{tools}} +--- + +# {{title}} + diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/copilot.md b/tools/cli/installers/lib/ide/templates/frontmatter/copilot.md new file mode 100644 index 00000000..76d696e8 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/copilot.md @@ -0,0 +1,4 @@ +--- +description: "{{description}}" +tools: {{tools}} +--- diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/opencode-agent.md b/tools/cli/installers/lib/ide/templates/frontmatter/opencode-agent.md new file mode 100644 index 00000000..2b633613 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/opencode-agent.md @@ -0,0 +1,5 @@ +--- +name: '{{name}}' +description: 'BMAD {{name}} agent' +mode: 'primary' +--- diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/opencode.md b/tools/cli/installers/lib/ide/templates/frontmatter/opencode.md new file mode 100644 index 00000000..e2ae0a7c --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/opencode.md @@ -0,0 +1,4 @@ +--- +name: '{{name}}' +description: 'BMAD {{name}} command' +--- diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/roo.md b/tools/cli/installers/lib/ide/templates/frontmatter/roo.md new file mode 100644 index 00000000..758f9b8f --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/roo.md @@ -0,0 +1,4 @@ +--- +name: '{{icon}} {{title}}' +description: 'Use for {{title}} tasks' +--- diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/trae.md b/tools/cli/installers/lib/ide/templates/frontmatter/trae.md new file mode 100644 index 00000000..d979b4c3 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/trae.md @@ -0,0 +1,4 @@ +--- +description: "{{name}}" +always: true +--- diff --git a/tools/cli/installers/lib/ide/templates/frontmatter/windsurf.md b/tools/cli/installers/lib/ide/templates/frontmatter/windsurf.md new file mode 100644 index 00000000..e1d48c9e --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/frontmatter/windsurf.md @@ -0,0 +1,4 @@ +--- +description: {{name}} +auto_execution_mode: {{autoExecMode}} +--- diff --git a/tools/cli/installers/lib/ide/trae.js b/tools/cli/installers/lib/ide/trae.js deleted file mode 100644 index c9f8e893..00000000 --- a/tools/cli/installers/lib/ide/trae.js +++ /dev/null @@ -1,313 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); - -/** - * Trae IDE setup handler - */ -class TraeSetup extends BaseIdeSetup { - constructor() { - super('trae', 'Trae'); - this.configDir = '.trae'; - this.rulesDir = 'rules'; - } - - /** - * Setup Trae IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Create .trae/rules directory - const traeDir = path.join(projectDir, this.configDir); - const rulesDir = path.join(traeDir, this.rulesDir); - - await this.ensureDir(rulesDir); - - // Clean up any existing BMAD files before reinstalling - await this.cleanup(projectDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Get tasks, tools, and workflows (standalone only) - const tasks = await this.getTasks(bmadDir, true); - const tools = await this.getTools(bmadDir, true); - const workflows = await this.getWorkflows(bmadDir, true); - - // Process agents as rules with bmad- prefix - let agentCount = 0; - for (const artifact of agentArtifacts) { - const processedContent = await this.createAgentRule(artifact, bmadDir, projectDir); - - // Use bmad- prefix: bmad-agent-{module}-{name}.md - const targetPath = path.join(rulesDir, `bmad-agent-${artifact.module}-${artifact.name}.md`); - await this.writeFile(targetPath, processedContent); - agentCount++; - } - - // Process tasks as rules with bmad- prefix - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const processedContent = this.createTaskRule(task, content); - - // Use bmad- prefix: bmad-task-{module}-{name}.md - const targetPath = path.join(rulesDir, `bmad-task-${task.module}-${task.name}.md`); - await this.writeFile(targetPath, processedContent); - taskCount++; - } - - // Process tools as rules with bmad- prefix - let toolCount = 0; - for (const tool of tools) { - const content = await this.readFile(tool.path); - const processedContent = this.createToolRule(tool, content); - - // Use bmad- prefix: bmad-tool-{module}-{name}.md - const targetPath = path.join(rulesDir, `bmad-tool-${tool.module}-${tool.name}.md`); - await this.writeFile(targetPath, processedContent); - toolCount++; - } - - // Process workflows as rules with bmad- prefix - let workflowCount = 0; - for (const workflow of workflows) { - const content = await this.readFile(workflow.path); - const processedContent = this.createWorkflowRule(workflow, content); - - // Use bmad- prefix: bmad-workflow-{module}-{name}.md - const targetPath = path.join(rulesDir, `bmad-workflow-${workflow.module}-${workflow.name}.md`); - await this.writeFile(targetPath, processedContent); - workflowCount++; - } - - const totalRules = agentCount + taskCount + toolCount + workflowCount; - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agent rules created`)); - console.log(chalk.dim(` - ${taskCount} task rules created`)); - console.log(chalk.dim(` - ${toolCount} tool rules created`)); - console.log(chalk.dim(` - ${workflowCount} workflow rules created`)); - console.log(chalk.dim(` - Total: ${totalRules} rules`)); - console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, rulesDir)}`)); - console.log(chalk.dim(` - Agents can be activated with @{agent-name}`)); - - return { - success: true, - rules: totalRules, - agents: agentCount, - tasks: taskCount, - tools: toolCount, - workflows: workflowCount, - }; - } - - /** - * Create rule content for an agent - */ - async createAgentRule(artifact, bmadDir, projectDir) { - // Strip frontmatter from launcher - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim(); - - // Extract metadata from launcher content - const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); - - // Calculate relative path for reference - const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); - - let ruleContent = `# ${title} Agent Rule - -This rule is triggered when the user types \`@${artifact.name}\` and activates the ${title} agent persona. - -## Agent Activation - -${contentWithoutFrontmatter} - -## File Reference - -The full agent definition is located at: \`${relativePath}\` -`; - - return ruleContent; - } - - /** - * Create rule content for a task - */ - createTaskRule(task, content) { - // Extract task name from content - const nameMatch = content.match(/name="([^"]+)"/); - const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); - - let ruleContent = `# ${taskName} Task Rule - -This rule defines the ${taskName} task workflow. - -## Task Definition - -When this task is triggered, execute the following workflow: - -${content} - -## Usage - -Reference this task with \`@task-${task.name}\` to execute the defined workflow. - -## Module - -Part of the BMAD ${task.module.toUpperCase()} module. -`; - - return ruleContent; - } - - /** - * Create rule content for a tool - */ - createToolRule(tool, content) { - // Extract tool name from content - const nameMatch = content.match(/name="([^"]+)"/); - const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name); - - let ruleContent = `# ${toolName} Tool Rule - -This rule defines the ${toolName} tool. - -## Tool Definition - -When this tool is triggered, execute the following: - -${content} - -## Usage - -Reference this tool with \`@tool-${tool.name}\` to execute it. - -## Module - -Part of the BMAD ${tool.module.toUpperCase()} module. -`; - - return ruleContent; - } - - /** - * Create rule content for a workflow - */ - createWorkflowRule(workflow, content) { - let ruleContent = `# ${workflow.name} Workflow Rule - -This rule defines the ${workflow.name} workflow. - -## Workflow Description - -${workflow.description || 'No description provided'} - -## Workflow Definition - -${content} - -## Usage - -Reference this workflow with \`@workflow-${workflow.name}\` to execute the guided workflow. - -## Module - -Part of the BMAD ${workflow.module.toUpperCase()} module. -`; - - return ruleContent; - } - - /** - * Format agent/task name as title - */ - formatTitle(name) { - return name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - /** - * Cleanup Trae configuration - surgically remove only BMAD files - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - const rulesPath = path.join(projectDir, this.configDir, this.rulesDir); - - if (await fs.pathExists(rulesPath)) { - // Remove any bmad* files (cleans up old bmad- and bmad: formats) - const files = await fs.readdir(rulesPath); - let removed = 0; - - for (const file of files) { - if (file.startsWith('bmad') && file.endsWith('.md')) { - await fs.remove(path.join(rulesPath, file)); - removed++; - } - } - - if (removed > 0) { - console.log(chalk.dim(` Cleaned up ${removed} existing BMAD rules`)); - } - } - } - - /** - * Install a custom agent launcher for Trae - * @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} Installation result - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const traeDir = path.join(projectDir, this.configDir); - const rulesDir = path.join(traeDir, this.rulesDir); - - // Create .trae/rules directory if it doesn't exist - await fs.ensureDir(rulesDir); - - // Create custom agent launcher - const launcherContent = `# ${agentName} Custom Agent - -**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - -This is a launcher for the custom BMAD agent "${agentName}". - -## Usage -1. First run: \`${agentPath}\` to load the complete agent -2. Then use this rule to activate ${agentName} - -The agent will follow the persona and instructions from the main agent file. - ---- - -*Generated by BMAD Method*`; - - const fileName = `bmad-agent-custom-${agentName.toLowerCase()}.md`; - const launcherPath = path.join(rulesDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, launcherContent, 'utf8'); - - return { - ide: 'trae', - path: path.relative(projectDir, launcherPath), - command: agentName, - type: 'custom-agent-launcher', - }; - } -} - -module.exports = { TraeSetup }; diff --git a/tools/cli/installers/lib/ide/windsurf.js b/tools/cli/installers/lib/ide/windsurf.js deleted file mode 100644 index 2be6e189..00000000 --- a/tools/cli/installers/lib/ide/windsurf.js +++ /dev/null @@ -1,244 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); -const fs = require('fs-extra'); - -/** - * Windsurf IDE setup handler - * - * Uses UnifiedInstaller for consistent artifact collection and writing. - * Windsurf-specific configuration: - * - Flat file naming (FLAT_DASH): bmad-bmm-agent-pm.md - * - Windsurf frontmatter with auto_execution_mode - */ -class WindsurfSetup extends BaseIdeSetup { - constructor() { - super('windsurf', 'Windsurf', true); // preferred IDE - this.configDir = '.windsurf'; - this.workflowsDir = 'workflows'; - this.unifiedInstaller = new UnifiedInstaller(this.bmadFolderName); - } - - /** - * Setup Windsurf IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Create .windsurf/workflows directory - const windsurfDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(windsurfDir, this.workflowsDir); - - await this.ensureDir(workflowsDir); - - // Clean up any existing BMAD workflows before reinstalling - await this.cleanup(projectDir); - - // Use UnifiedInstaller with Windsurf-specific configuration - const counts = await this.unifiedInstaller.install( - projectDir, - bmadDir, - { - targetDir: workflowsDir, - namingStyle: NamingStyle.FLAT_DASH, - templateType: TemplateType.WINDSURF, - customTemplateFn: this.windsurfTemplate.bind(this), - }, - options.selectedModules || [], - ); - - // Post-process tasks and tools to add Windsurf auto_execution_mode - // UnifiedInstaller handles agents/workflows correctly, but tasks/tools - // need special handling for proper Windsurf frontmatter - await this.addWindsurfTaskToolFrontmatter(workflowsDir); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${counts.agents} agents installed`)); - console.log(chalk.dim(` - ${counts.tasks} tasks installed`)); - console.log(chalk.dim(` - ${counts.tools} tools installed`)); - console.log(chalk.dim(` - ${counts.workflows} workflows installed`)); - console.log(chalk.dim(` - Total: ${counts.total} items`)); - console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`)); - - // Provide additional configuration hints - if (options.showHints !== false) { - console.log(chalk.dim('\n Windsurf workflow settings:')); - console.log(chalk.dim(' - auto_execution_mode: 3 (recommended for agents)')); - console.log(chalk.dim(' - auto_execution_mode: 2 (recommended for tasks/tools)')); - console.log(chalk.dim(' - auto_execution_mode: 1 (recommended for workflows)')); - console.log(chalk.dim(' - Workflows can be triggered via the Windsurf menu')); - } - - return { - success: true, - ...counts, - }; - } - - /** - * Windsurf-specific template function - * Adds proper Windsurf frontmatter with auto_execution_mode - */ - windsurfTemplate(artifact, content, templateType) { - // Strip existing frontmatter - 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 - let description = artifact.name || artifact.displayName || 'workflow'; - - if (artifact.type === 'agent') { - autoExecMode = '3'; - description = artifact.name || 'agent'; - } else if (artifact.type === 'workflow') { - autoExecMode = '1'; - description = artifact.name || 'workflow'; - } - - return `--- -description: ${description} -auto_execution_mode: ${autoExecMode} ---- - -${contentWithoutFrontmatter}`; - } - - /** - * Add Windsurf auto_execution_mode to task and tool files - * These are generated by TaskToolCommandGenerator with basic YAML - * but need the Windsurf-specific auto_execution_mode field - */ - async addWindsurfTaskToolFrontmatter(workflowsDir) { - if (!(await fs.pathExists(workflowsDir))) { - return; - } - - const entries = await fs.readdir(workflowsDir, { withFileTypes: true }); - let updatedCount = 0; - - for (const entry of entries) { - if (!entry.name.startsWith('bmad-') || !entry.name.endsWith('.md')) { - continue; - } - - const filePath = path.join(workflowsDir, entry.name); - let content = await fs.readFile(filePath, 'utf8'); - - // Check if this is a task or tool file - // They have pattern: bmad-module-task-name.md or bmad-module-tool-name.md - const parts = entry.name.replace('bmad-', '').replace('.md', '').split('-'); - if (parts.length < 2) continue; - - const type = parts.at(-2); // second to last part should be 'task' or 'tool' - - if (type === 'task' || type === 'tool') { - // Check if auto_execution_mode is already present - if (content.includes('auto_execution_mode')) { - continue; - } - - // Extract existing description if present - const descMatch = content.match(/description: '(.+?)'/); - const description = descMatch ? descMatch[1] : entry.name.replace('.md', ''); - - // Strip existing frontmatter and add Windsurf-specific frontmatter - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); - - content = `--- -description: '${description}' -auto_execution_mode: 2 ---- - -${contentWithoutFrontmatter}`; - - await fs.writeFile(filePath, content, 'utf8'); - updatedCount++; - } - } - - if (updatedCount > 0) { - console.log(chalk.dim(` Updated ${updatedCount} task/tool files with Windsurf frontmatter`)); - } - } - - /** - * Cleanup Windsurf configuration - remove only BMAD files - */ - async cleanup(projectDir) { - const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - - if (await fs.pathExists(workflowsDir)) { - // Remove all bmad* files from workflows directory - const entries = await fs.readdir(workflowsDir, { withFileTypes: true }); - let removedCount = 0; - - for (const entry of entries) { - if (entry.name.startsWith('bmad')) { - const entryPath = path.join(workflowsDir, entry.name); - await fs.remove(entryPath); - removedCount++; - } - } - - if (removedCount > 0) { - console.log(chalk.dim(` Cleaned up ${removedCount} existing BMAD workflow files`)); - } - } - } - - /** - * Install a custom agent launcher for Windsurf - * @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 workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - - if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project - } - - await this.ensureDir(workflowsDir); - - 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. - - -1. LOAD the FULL agent file from @${agentPath} -2. READ its entire contents - this contains the complete agent persona, menu, and instructions -3. FOLLOW every step in the section precisely -4. DISPLAY the welcome/greeting as instructed -5. PRESENT the numbered menu -6. WAIT for user input before proceeding - -`; - - // Windsurf uses workflow format with frontmatter - flat naming - const workflowContent = `--- -description: ${metadata.title || agentName} -auto_execution_mode: 3 ---- - -${launcherContent}`; - - // Use flat naming: bmad-custom-agent-agentname.md - const flatName = `bmad-custom-agent-${agentName}.md`; - const launcherPath = path.join(workflowsDir, flatName); - await fs.writeFile(launcherPath, workflowContent); - - return { - path: launcherPath, - command: flatName.replace('.md', ''), - }; - } -} - -module.exports = { WindsurfSetup }; diff --git a/tools/cli/lib/platform-codes.js b/tools/cli/lib/platform-codes.js index bdf0e48c..4fa85e3f 100644 --- a/tools/cli/lib/platform-codes.js +++ b/tools/cli/lib/platform-codes.js @@ -9,7 +9,7 @@ const { getProjectRoot } = require('./project-root'); */ class PlatformCodes { constructor() { - this.configPath = path.join(getProjectRoot(), 'tools', 'platform-codes.yaml'); + this.configPath = path.join(getProjectRoot(), 'tools/cli/installers/lib/ide/platform-codes.yaml'); this.loadConfig(); } diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 614e5016..eee4ad75 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -363,8 +363,8 @@ class UI { const { IdeManager } = require('../installers/lib/ide/manager'); const ideManager = new IdeManager(); - const preferredIdes = ideManager.getPreferredIdes(); - const otherIdes = ideManager.getOtherIdes(); + const preferredIdes = await ideManager.getPreferredIdes(); + const otherIdes = await ideManager.getOtherIdes(); // Build grouped options object for groupMultiselect const groupedOptions = {}; From e7a34a2b61e18d047d675d91e2aa12bc132bb0eb Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sun, 25 Jan 2026 03:56:23 -0600 Subject: [PATCH 11/11] installer updates --- tools/cli/installers/lib/core/installer.js | 63 +++++++------------ .../installers/lib/ide/shared/path-utils.js | 17 +++-- 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 9089775d..005e1faf 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -167,50 +167,31 @@ class Installer { if (newlySelectedIdes.length > 0) { console.log('\n'); // Add spacing before IDE questions + // Use IdeManager to get handlers (supports both config-driven and custom IDEs) + await this.ideManager.ensureInitialized(); + for (const ide of newlySelectedIdes) { - // List of IDEs that have interactive prompts - //TODO: Why is this here, hardcoding this list here is bad, fix me! - const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes( - ide, - ); + try { + const handler = this.ideManager.handlers.get(ide); - if (needsPrompts) { - // Get IDE handler and collect configuration - try { - // Dynamically load the IDE setup module - const ideModule = require(`../ide/${ide}`); - - // Get the setup class (handle different export formats) - let SetupClass; - const className = - ide - .split('-') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join('') + 'Setup'; - - if (ideModule[className]) { - SetupClass = ideModule[className]; - } else if (ideModule.default) { - SetupClass = ideModule.default; - } else { - continue; - } - - const ideSetup = new SetupClass(); - - // Check if this IDE has a collectConfiguration method - if (typeof ideSetup.collectConfiguration === 'function') { - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); - ideConfigurations[ide] = await ideSetup.collectConfiguration({ - selectedModules: selectedModules || [], - projectDir, - bmadDir, - }); - } - } catch { - // IDE doesn't have a setup file or collectConfiguration method - console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`)); + if (!handler) { + // IDE not recognized - skip silently + continue; } + + // Check if this IDE handler has a collectConfiguration method + if (typeof handler.collectConfiguration === 'function') { + console.log(chalk.cyan(`\nConfiguring ${ide}...`)); + ideConfigurations[ide] = await handler.collectConfiguration({ + selectedModules: selectedModules || [], + projectDir, + bmadDir, + }); + } + // Most config-driven IDEs don't need configuration - silently skip + } catch { + // IDE doesn't have collectConfiguration or had an error - skip + continue; } } } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index 488d2811..80b632ca 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -15,13 +15,10 @@ const AGENT_SEGMENT = 'agents'; /** * Artifact type to suffix mapping - * Used for new suffix-based naming convention + * Only agents get the .agent suffix; workflows/tasks/tools use standard .md extension */ const ARTIFACT_SUFFIXES = { agent: '.agent', - workflow: '.workflow', - task: '.task', - tool: '.tool', }; /** @@ -216,10 +213,11 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { /** * Convert relative path to suffix-based name (NEW UNIVERSAL STANDARD) + * Only applies .agent suffix to agents; workflows/tasks/tools get standard .md extension. * Converts: 'cis/agents/storymaster.md' → 'bmad-cis-storymaster.agent.md' - * Converts: 'bmm/workflows/plan-project.md' → 'bmad-bmm-plan-project.workflow.md' - * Converts: 'bmm/tasks/create-story.md' → 'bmad-bmm-create-story.task.md' - * Converts: 'bmm/tools/file-ops.md' → 'bmad-bmm-file-ops.tool.md' + * Converts: 'bmm/workflows/plan-project.md' → 'bmad-bmm-plan-project.md' + * Converts: 'bmm/tasks/create-story.md' → 'bmad-bmm-create-story.md' + * Converts: 'bmm/tools/file-ops.md' → 'bmad-bmm-file-ops.md' * Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix) * * @param {string} relativePath - Path like 'cis/agents/storymaster.md' @@ -237,7 +235,8 @@ function toSuffixBasedName(relativePath, artifactType, fileExtension = DEFAULT_F const type = parts[1]; // agents, workflows, tasks, tools const name = parts.slice(2).join('-'); - const suffix = ARTIFACT_SUFFIXES[artifactType] || ''; + // Only add .agent suffix for agents; workflows/tasks/tools use standard extension + const suffix = artifactType === 'agent' ? ARTIFACT_SUFFIXES.agent : ''; // For core module, skip the module prefix (use 'bmad-name.suffix.md') if (module === 'core') { @@ -261,7 +260,7 @@ function getArtifactSuffix(artifactType) { /** * Parse artifact type from suffix-based filename * Parses: 'bmad-cis-storymaster.agent.md' → 'agent' - * Parses: 'bmad-bmm-plan-project.workflow.md' → 'workflow' + * Returns null for workflows/tasks/tools (no suffix) * * @param {string} filename - Suffix-based filename * @returns {string|null} Artifact type or null if not found