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 = {};