From 27c18e0020a72668edd98c357a3e309fbb2cf19f Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Mon, 26 Jan 2026 16:06:00 -0600 Subject: [PATCH] installer fix --- tools/cli/installers/lib/core/installer.js | 65 +-- .../lib/ide/STANDARDIZATION_PLAN.md | 208 ------- .../cli/installers/lib/ide/_config-driven.js | 423 +++++++++++++++ tools/cli/installers/lib/ide/antigravity.js | 474 ---------------- tools/cli/installers/lib/ide/auggie.js | 244 --------- tools/cli/installers/lib/ide/claude-code.js | 506 ------------------ tools/cli/installers/lib/ide/cline.js | 272 ---------- tools/cli/installers/lib/ide/codex.js | 52 +- tools/cli/installers/lib/ide/crush.js | 149 ------ tools/cli/installers/lib/ide/cursor.js | 160 ------ tools/cli/installers/lib/ide/gemini.js | 301 ----------- .../cli/installers/lib/ide/github-copilot.js | 383 ------------- tools/cli/installers/lib/ide/iflow.js | 191 ------- tools/cli/installers/lib/ide/manager.js | 107 ++-- tools/cli/installers/lib/ide/opencode.js | 257 --------- .../cli/installers/lib/ide/platform-codes.js | 100 ++++ .../installers/lib/ide/platform-codes.yaml | 241 +++++++++ tools/cli/installers/lib/ide/qwen.js | 372 ------------- tools/cli/installers/lib/ide/roo.js | 273 ---------- tools/cli/installers/lib/ide/rovo-dev.js | 290 ---------- .../lib/ide/shared/agent-command-generator.js | 24 +- .../lib/ide/shared/bmad-artifacts.js | 5 + .../installers/lib/ide/shared/path-utils.js | 228 ++++++-- .../ide/shared/task-tool-command-generator.js | 10 +- .../ide/shared/workflow-command-generator.js | 24 +- .../lib/ide/templates/combined/antigravity.md | 6 + .../ide/templates/combined/claude-agent.md | 1 + .../combined/claude-workflow-yaml.md | 1 + .../ide/templates/combined/claude-workflow.md | 1 + .../ide/templates/combined/default-agent.md | 15 + .../combined/default-workflow-yaml.md | 14 + .../templates/combined/default-workflow.md | 6 + .../lib/ide/templates/combined/rovodev.md | 9 + .../lib/ide/templates/combined/trae.md | 9 + .../templates/combined/windsurf-workflow.md | 10 + .../lib/ide/templates/split/gemini/body.md | 10 + .../ide/templates/split/gemini/header.toml | 2 + .../lib/ide/templates/split/opencode/body.md | 10 + .../ide/templates/split/opencode/header.md | 4 + tools/cli/installers/lib/ide/trae.js | 313 ----------- tools/cli/installers/lib/ide/windsurf.js | 258 --------- tools/cli/lib/ui.js | 1 + 42 files changed, 1217 insertions(+), 4812 deletions(-) delete mode 100644 tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md create mode 100644 tools/cli/installers/lib/ide/_config-driven.js delete mode 100644 tools/cli/installers/lib/ide/antigravity.js delete mode 100644 tools/cli/installers/lib/ide/auggie.js delete mode 100644 tools/cli/installers/lib/ide/claude-code.js delete mode 100644 tools/cli/installers/lib/ide/cline.js delete mode 100644 tools/cli/installers/lib/ide/crush.js delete mode 100644 tools/cli/installers/lib/ide/cursor.js delete mode 100644 tools/cli/installers/lib/ide/gemini.js delete mode 100644 tools/cli/installers/lib/ide/github-copilot.js delete mode 100644 tools/cli/installers/lib/ide/iflow.js delete mode 100644 tools/cli/installers/lib/ide/opencode.js create mode 100644 tools/cli/installers/lib/ide/platform-codes.js create mode 100644 tools/cli/installers/lib/ide/platform-codes.yaml delete mode 100644 tools/cli/installers/lib/ide/qwen.js delete mode 100644 tools/cli/installers/lib/ide/roo.js delete mode 100644 tools/cli/installers/lib/ide/rovo-dev.js create mode 100644 tools/cli/installers/lib/ide/templates/combined/antigravity.md create mode 120000 tools/cli/installers/lib/ide/templates/combined/claude-agent.md create mode 120000 tools/cli/installers/lib/ide/templates/combined/claude-workflow-yaml.md create mode 120000 tools/cli/installers/lib/ide/templates/combined/claude-workflow.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/default-agent.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/default-workflow.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/rovodev.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/trae.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md create mode 100644 tools/cli/installers/lib/ide/templates/split/gemini/body.md create mode 100644 tools/cli/installers/lib/ide/templates/split/gemini/header.toml create mode 100644 tools/cli/installers/lib/ide/templates/split/opencode/body.md create mode 100644 tools/cli/installers/lib/ide/templates/split/opencode/header.md delete mode 100644 tools/cli/installers/lib/ide/trae.js delete mode 100644 tools/cli/installers/lib/ide/windsurf.js diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 9089775d..b1acc917 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -161,56 +161,39 @@ class Installer { } if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) { + // Ensure IDE manager is initialized + await this.ideManager.ensureInitialized(); + // Determine which IDEs are newly selected (not previously configured) const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide)); if (newlySelectedIdes.length > 0) { console.log('\n'); // Add spacing before IDE questions + // Collect configuration for IDEs that support it for (const ide of newlySelectedIdes) { - // List of IDEs that have interactive prompts - //TODO: Why is this here, hardcoding this list here is bad, fix me! - const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes( - ide, - ); + try { + const handler = this.ideManager.handlers.get(ide); - if (needsPrompts) { - // Get IDE handler and collect configuration - try { - // Dynamically load the IDE setup module - const ideModule = require(`../ide/${ide}`); - - // Get the setup class (handle different export formats) - let SetupClass; - const className = - ide - .split('-') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join('') + 'Setup'; - - if (ideModule[className]) { - SetupClass = ideModule[className]; - } else if (ideModule.default) { - SetupClass = ideModule.default; - } else { - continue; - } - - const ideSetup = new SetupClass(); - - // Check if this IDE has a collectConfiguration method - if (typeof ideSetup.collectConfiguration === 'function') { - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); - ideConfigurations[ide] = await ideSetup.collectConfiguration({ - selectedModules: selectedModules || [], - projectDir, - bmadDir, - }); - } - } catch { - // IDE doesn't have a setup file or collectConfiguration method - console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`)); + if (!handler) { + console.warn(chalk.yellow(`Warning: IDE '${ide}' handler not found`)); + continue; } + + // Check if this IDE handler has a collectConfiguration method + // (custom installers like Codex, Kilo, Kiro-cli may have this) + if (typeof handler.collectConfiguration === 'function') { + console.log(chalk.cyan(`\nConfiguring ${ide}...`)); + ideConfigurations[ide] = await handler.collectConfiguration({ + selectedModules: selectedModules || [], + projectDir, + bmadDir, + }); + } + // Most config-driven IDEs don't need configuration - silently skip + } catch (error) { + // IDE doesn't support configuration or has an error + console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}: ${error.message}`)); } } } diff --git a/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md b/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md deleted file mode 100644 index f7116cb5..00000000 --- a/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md +++ /dev/null @@ -1,208 +0,0 @@ -# IDE Installer Standardization Plan - -## Overview - -Standardize IDE installers to use **flat file naming** with **underscores** (Windows-compatible) and centralize duplicated code in shared utilities. - -**Key Rule: All IDEs use underscore format for Windows compatibility (colons don't work on Windows).** - -## Current State Analysis - -### File Structure Patterns - -| IDE | Current Pattern | Path Format | -|-----|-----------------|-------------| -| **claude-code** | Hierarchical | `.claude/commands/bmad/{module}/agents/{name}.md` | -| **cursor** | Hierarchical | `.cursor/commands/bmad/{module}/agents/{name}.md` | -| **crush** | Hierarchical | `.crush/commands/bmad/{module}/agents/{name}.md` | -| **antigravity** | Flattened (underscores) | `.agent/workflows/bmad_module_agents_name.md` | -| **codex** | Flattened (underscores) | `~/.codex/prompts/bmad_module_agents_name.md` | -| **cline** | Flattened (underscores) | `.clinerules/workflows/bmad_module_type_name.md` | -| **roo** | Flattened (underscores) | `.roo/commands/bmad_module_agent_name.md` | -| **auggie** | Hybrid | `.augment/commands/bmad/agents/{module}-{name}.md` | -| **iflow** | Hybrid | `.iflow/commands/bmad/agents/{module}-{name}.md` | -| **trae** | Different (rules) | `.trae/rules/bmad-agent-{module}-{name}.md` | -| **github-copilot** | Different (agents) | `.github/agents/bmd-custom-{module}-{name}.agent.md` | - -### Shared Generators (in `/shared`) - -1. `agent-command-generator.js` - generates agent launchers -2. `task-tool-command-generator.js` - generates task/tool commands -3. `workflow-command-generator.js` - generates workflow commands - -All currently create artifacts with **nested relative paths** like `{module}/agents/{name}.md` - -### Code Duplication Issues - -1. **Flattening logic** duplicated in multiple IDEs -2. **Agent launcher content creation** duplicated -3. **Path transformation** duplicated - -## Target Standardization - -### For All IDEs (underscore format - Windows-compatible) - -**IDEs affected:** claude-code, cursor, crush, antigravity, codex, cline, roo - -``` -Format: bmad_{module}_{type}_{name}.md - -Examples: -- Agent: bmad_bmm_agents_pm.md -- Agent: bmad_core_agents_dev.md -- Workflow: bmad_bmm_workflows_correct-course.md -- Task: bmad_bmm_tasks_bmad-help.md -- Tool: bmad_core_tools_code-review.md -- Custom: bmad_custom_agents_fred-commit-poet.md -``` - -**Note:** Type segments (agents, workflows, tasks, tools) are filtered out from names: -- `bmm/agents/pm.md` → `bmad_bmm_pm.md` (not `bmad_bmm_agents_pm.md`) - -### For Hybrid IDEs (keep as-is) - -**IDEs affected:** auggie, iflow - -These use `{module}-{name}.md` format within subdirectories - keep as-is. - -### Skip (drastically different) - -**IDEs affected:** trae, github-copilot - -## Implementation Plan - -### Phase 1: Create Shared Utility - -**File:** `shared/path-utils.js` - -```javascript -/** - * Convert hierarchical path to flat underscore-separated name (Windows-compatible) - * @param {string} module - Module name (e.g., 'bmm', 'core') - * @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools') - filtered out - * @param {string} name - Artifact name (e.g., 'pm', 'correct-course') - * @returns {string} Flat filename like 'bmad_bmm_pm.md' - */ -function toUnderscoreName(module, type, name) { - return `bmad_${module}_${name}.md`; -} - -/** - * Convert relative path to flat underscore-separated name (Windows-compatible) - * @param {string} relativePath - Path like 'bmm/agents/pm.md' - * @returns {string} Flat filename like 'bmad_bmm_pm.md' - */ -function toUnderscorePath(relativePath) { - const withoutExt = relativePath.replace('.md', ''); - const parts = withoutExt.split(/[\/\\]/); - // Filter out type segments (agents, workflows, tasks, tools) - const filtered = parts.filter((p) => !TYPE_SEGMENTS.includes(p)); - return `bmad_${filtered.join('_')}.md`; -} - -/** - * Create custom agent underscore name - * @param {string} agentName - Custom agent name - * @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md' - */ -function customAgentUnderscoreName(agentName) { - return `bmad_custom_${agentName}.md`; -} - -// Backward compatibility aliases -const toColonName = toUnderscoreName; -const toColonPath = toUnderscorePath; -const toDashPath = toUnderscorePath; -const customAgentColonName = customAgentUnderscoreName; -const customAgentDashName = customAgentUnderscoreName; - -module.exports = { - toUnderscoreName, - toUnderscorePath, - customAgentUnderscoreName, - // Backward compatibility - toColonName, - toColonPath, - toDashPath, - customAgentColonName, - customAgentDashName, -}; -``` - -### Phase 2: Update Shared Generators - -**Files to modify:** -- `shared/agent-command-generator.js` -- `shared/task-tool-command-generator.js` -- `shared/workflow-command-generator.js` - -**Changes:** -1. Import path utilities -2. Change `relativePath` to use flat format -3. Add method `writeColonArtifacts()` for folder-based IDEs (uses underscore) -4. Add method `writeDashArtifacts()` for flat IDEs (uses underscore) - -### Phase 3: Update All IDEs - -**Files to modify:** -- `claude-code.js` -- `cursor.js` -- `crush.js` -- `antigravity.js` -- `codex.js` -- `cline.js` -- `roo.js` - -**Changes:** -1. Import utilities from path-utils -2. Change from hierarchical to flat underscore naming -3. Update cleanup to handle flat structure (`startsWith('bmad')`) - -### Phase 4: Update Base Class - -**File:** `_base-ide.js` - -**Changes:** -1. Mark `flattenFilename()` as `@deprecated` -2. Add comment pointing to new path-utils - -## Migration Checklist - -### New Files -- [x] Create `shared/path-utils.js` - -### All IDEs (convert to underscore format) -- [x] Update `shared/agent-command-generator.js` - update for underscore -- [x] Update `shared/task-tool-command-generator.js` - update for underscore -- [x] Update `shared/workflow-command-generator.js` - update for underscore -- [x] Update `claude-code.js` - convert to underscore format -- [x] Update `cursor.js` - convert to underscore format -- [x] Update `crush.js` - convert to underscore format -- [ ] Update `antigravity.js` - use underscore format -- [ ] Update `codex.js` - use underscore format -- [ ] Update `cline.js` - use underscore format -- [ ] Update `roo.js` - use underscore format - -### CSV Command Files -- [x] Update `src/core/module-help.csv` - change colons to underscores -- [x] Update `src/bmm/module-help.csv` - change colons to underscores - -### Base Class -- [ ] Update `_base-ide.js` - add deprecation notice - -### Testing -- [ ] Test claude-code installation -- [ ] Test cursor installation -- [ ] Test crush installation -- [ ] Test antigravity installation -- [ ] Test codex installation -- [ ] Test cline installation -- [ ] Test roo installation - -## Notes - -1. **Filter type segments**: agents, workflows, tasks, tools are filtered out from flat names -2. **Underscore format**: Universal underscore format for Windows compatibility -3. **Custom agents**: Follow the same pattern as regular agents -4. **Backward compatibility**: Old function names kept as aliases -5. **Cleanup**: Will remove old `bmad:` format files on next install diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js new file mode 100644 index 00000000..7d637faf --- /dev/null +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -0,0 +1,423 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const chalk = require('chalk'); +const { BaseIdeSetup } = require('./_base-ide'); +const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); + +/** + * Config-driven IDE setup handler + * + * This class provides a standardized way to install BMAD artifacts to IDEs + * based on configuration in platform-codes.yaml. It eliminates the need for + * individual installer files for each IDE. + * + * Features: + * - Config-driven from platform-codes.yaml + * - Template-based content generation + * - Multi-target installation support (e.g., GitHub Copilot) + * - Artifact type filtering (agents, workflows, tasks, tools) + */ +class ConfigDrivenIdeSetup extends BaseIdeSetup { + constructor(platformCode, platformConfig) { + super(platformCode, platformConfig.name, platformConfig.preferred); + this.platformConfig = platformConfig; + this.installerConfig = platformConfig.installer || null; + } + + /** + * Main setup method - called by IdeManager + * @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}...`)); + + // Clean up any old BMAD installation first + await this.cleanup(projectDir); + + if (!this.installerConfig) { + return { success: false, reason: 'no-config' }; + } + + // Handle multi-target installations (e.g., GitHub Copilot) + 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); + } + + return { success: false, reason: 'invalid-config' }; + } + + /** + * Install to a single target directory + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} config - Installation configuration + * @param {Object} options - Setup options + * @returns {Promise} Installation result + */ + async installToTarget(projectDir, bmadDir, config, options) { + const { target_dir, template_type, artifact_types } = config; + const targetPath = path.join(projectDir, target_dir); + await this.ensureDir(targetPath); + + const selectedModules = options.selectedModules || []; + const results = { agents: 0, workflows: 0, tasks: 0, tools: 0 }; + + // Install agents + if (!artifact_types || artifact_types.includes('agents')) { + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); + results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config); + } + + // Install workflows + if (!artifact_types || artifact_types.includes('workflows')) { + const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); + results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); + } + + // Install tasks and tools + if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { + const taskToolGen = new TaskToolCommandGenerator(); + const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); + results.tasks = taskToolResult.tasks || 0; + results.tools = taskToolResult.tools || 0; + } + + this.printSummary(results, target_dir); + return { success: true, results }; + } + + /** + * Install 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} Installation result + */ + async installToMultipleTargets(projectDir, bmadDir, targets, options) { + const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0 }; + + for (const target of targets) { + const result = await this.installToTarget(projectDir, bmadDir, target, options); + if (result.success) { + allResults.agents += result.results.agents || 0; + allResults.workflows += result.results.workflows || 0; + allResults.tasks += result.results.tasks || 0; + allResults.tools += result.results.tools || 0; + } + } + + return { success: true, results: allResults }; + } + + /** + * Write agent artifacts to target directory + * @param {string} targetPath - Target directory path + * @param {Array} artifacts - Agent artifacts + * @param {string} templateType - Template type to use + * @param {Object} config - Installation configuration + * @returns {Promise} Count of artifacts written + */ + async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) { + // Try to load platform-specific template, fall back to default-agent + const template = await this.loadTemplate(templateType, 'agent', config, 'default-agent'); + let count = 0; + + for (const artifact of artifacts) { + const content = this.renderTemplate(template, artifact); + const filename = this.generateFilename(artifact, 'agent'); + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + count++; + } + + return count; + } + + /** + * Write workflow artifacts to target directory + * @param {string} targetPath - Target directory path + * @param {Array} artifacts - Workflow artifacts + * @param {string} templateType - Template type to use + * @param {Object} config - Installation configuration + * @returns {Promise} Count of artifacts written + */ + async writeWorkflowArtifacts(targetPath, artifacts, templateType, config = {}) { + let count = 0; + + for (const artifact of artifacts) { + if (artifact.type === 'workflow-command') { + // Use different template based on workflow type (YAML vs MD) + // Default to 'default' template type, but allow override via config + const workflowTemplateType = artifact.isYamlWorkflow + ? config.yaml_workflow_template || `${templateType}-workflow-yaml` + : config.md_workflow_template || `${templateType}-workflow`; + + // Fall back to default templates if specific ones don't exist + const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow'; + const template = await this.loadTemplate(workflowTemplateType, 'workflow', config, finalTemplateType); + const content = this.renderTemplate(template, artifact); + const filename = this.generateFilename(artifact, 'workflow'); + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + count++; + } + } + + return count; + } + + /** + * Load template based on type and configuration + * @param {string} templateType - Template type (claude, windsurf, etc.) + * @param {string} artifactType - Artifact type (agent, workflow, task, tool) + * @param {Object} config - Installation configuration + * @param {string} fallbackTemplateType - Fallback template type if requested template not found + * @returns {Promise} Template content + */ + async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) { + const { header_template, body_template } = config; + + // Check for separate header/body templates + if (header_template || body_template) { + return await this.loadSplitTemplates(templateType, artifactType, header_template, body_template); + } + + // Load combined template + const templateName = `${templateType}-${artifactType}.md`; + const templatePath = path.join(__dirname, 'templates', 'combined', templateName); + + if (await fs.pathExists(templatePath)) { + return await fs.readFile(templatePath, 'utf8'); + } + + // Fall back to default template (if provided) + if (fallbackTemplateType) { + const fallbackPath = path.join(__dirname, 'templates', 'combined', `${fallbackTemplateType}.md`); + if (await fs.pathExists(fallbackPath)) { + return await fs.readFile(fallbackPath, 'utf8'); + } + } + + // Ultimate fallback - minimal template + return this.getDefaultTemplate(artifactType); + } + + /** + * Load split templates (header + body) + * @param {string} templateType - Template type + * @param {string} artifactType - Artifact type + * @param {string} headerTpl - Header template name + * @param {string} bodyTpl - Body template name + * @returns {Promise} Combined template content + */ + async loadSplitTemplates(templateType, artifactType, headerTpl, bodyTpl) { + let header = ''; + let body = ''; + + // Load header template + if (headerTpl) { + const headerPath = path.join(__dirname, 'templates', 'split', headerTpl); + if (await fs.pathExists(headerPath)) { + header = await fs.readFile(headerPath, 'utf8'); + } + } else { + // Use default header for template type + const defaultHeaderPath = path.join(__dirname, 'templates', 'split', templateType, 'header.md'); + if (await fs.pathExists(defaultHeaderPath)) { + header = await fs.readFile(defaultHeaderPath, 'utf8'); + } + } + + // Load body template + if (bodyTpl) { + const bodyPath = path.join(__dirname, 'templates', 'split', bodyTpl); + if (await fs.pathExists(bodyPath)) { + body = await fs.readFile(bodyPath, 'utf8'); + } + } else { + // Use default body for template type + const defaultBodyPath = path.join(__dirname, 'templates', 'split', templateType, 'body.md'); + if (await fs.pathExists(defaultBodyPath)) { + body = await fs.readFile(defaultBodyPath, 'utf8'); + } + } + + // Combine header and body + return `${header}\n${body}`; + } + + /** + * Get default minimal template + * @param {string} artifactType - Artifact type + * @returns {string} Default template + */ + getDefaultTemplate(artifactType) { + if (artifactType === 'agent') { + return `--- +name: '{{name}}' +description: '{{description}}' +--- + +You must fully embody this agent's persona and follow all activation instructions exactly as specified. + + +1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents - this contains the complete agent persona, menu, and instructions +3. FOLLOW every step in the section precisely + +`; + } + return `--- +name: '{{name}}' +description: '{{description}}' +--- + +# {{name}} + +LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} +`; + } + + /** + * Render template with artifact data + * @param {string} template - Template content + * @param {Object} artifact - Artifact data + * @returns {string} Rendered content + */ + renderTemplate(template, artifact) { + // Use the appropriate path property based on artifact type + let pathToUse = artifact.relativePath || ''; + if (artifact.type === 'agent-launcher') { + pathToUse = artifact.agentPath || artifact.relativePath || ''; + } else if (artifact.type === 'workflow-command') { + pathToUse = artifact.workflowPath || artifact.relativePath || ''; + } + + let rendered = template + .replaceAll('{{name}}', artifact.name || '') + .replaceAll('{{module}}', artifact.module || 'core') + .replaceAll('{{path}}', pathToUse) + .replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`) + .replaceAll('{{workflow_path}}', pathToUse); + + // Replace _bmad placeholder with actual folder name + rendered = rendered.replaceAll('_bmad', this.bmadFolderName); + + // Replace {{bmadFolderName}} placeholder if present + rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName); + + return rendered; + } + + /** + * Generate filename for artifact + * @param {Object} artifact - Artifact data + * @param {string} artifactType - Artifact type (agent, workflow, task, tool) + * @returns {string} Generated filename + */ + generateFilename(artifact, artifactType) { + const { toDashPath } = require('./shared/path-utils'); + // toDashPath already handles the .agent.md suffix for agents correctly + // No need to add it again here + return toDashPath(artifact.relativePath); + } + + /** + * Print installation summary + * @param {Object} results - Installation results + * @param {string} targetDir - Target directory (relative) + */ + printSummary(results, targetDir) { + console.log(chalk.green(`\n✓ ${this.name} configured:`)); + if (results.agents > 0) { + console.log(chalk.dim(` - ${results.agents} agents installed`)); + } + if (results.workflows > 0) { + console.log(chalk.dim(` - ${results.workflows} workflow commands generated`)); + } + if (results.tasks > 0 || results.tools > 0) { + console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`)); + } + console.log(chalk.dim(` - Destination: ${targetDir}`)); + } + + /** + * Cleanup IDE configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + // Clean all target directories + if (this.installerConfig?.targets) { + for (const target of this.installerConfig.targets) { + await this.cleanupTarget(projectDir, target.target_dir); + } + } else if (this.installerConfig?.target_dir) { + await this.cleanupTarget(projectDir, this.installerConfig.target_dir); + } + } + + /** + * Cleanup a specific target directory + * @param {string} projectDir - Project directory + * @param {string} targetDir - Target directory to clean + */ + async cleanupTarget(projectDir, targetDir) { + const targetPath = path.join(projectDir, targetDir); + + if (!(await fs.pathExists(targetPath))) { + return; + } + + // Remove all bmad* files + let entries; + try { + entries = await fs.readdir(targetPath); + } catch { + // Directory exists but can't be read - skip cleanup + return; + } + + if (!entries || !Array.isArray(entries)) { + return; + } + + let removedCount = 0; + + for (const entry of entries) { + // Skip non-strings or undefined entries + if (!entry || typeof entry !== 'string') { + continue; + } + if (entry.startsWith('bmad')) { + const entryPath = path.join(targetPath, entry); + const stat = await fs.stat(entryPath); + if (stat.isFile()) { + await fs.remove(entryPath); + removedCount++; + } else if (stat.isDirectory()) { + await fs.remove(entryPath); + removedCount++; + } + } + } + + if (removedCount > 0) { + console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`)); + } + } +} + +module.exports = { ConfigDrivenIdeSetup }; diff --git a/tools/cli/installers/lib/ide/antigravity.js b/tools/cli/installers/lib/ide/antigravity.js deleted file mode 100644 index 7af2e41b..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 bmadWorkflowsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad'); - - if (await fs.pathExists(bmadWorkflowsDir)) { - await fs.remove(bmadWorkflowsDir); - 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 agentDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(agentDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); - - await this.ensureDir(bmadWorkflowsDir); - - // 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(bmadWorkflowsDir, 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(bmadWorkflowsDir, workflowArtifacts); - - // Generate task and tool commands from manifests (if they exist) - const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir); - - 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, bmadWorkflowsDir)}`)); - 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/bmad directory structure (same as regular agents) - const agentDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(agentDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); - - await fs.ensureDir(bmadWorkflowsDir); - - // 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(bmadWorkflowsDir, 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 04e08788..00000000 --- a/tools/cli/installers/lib/ide/auggie.js +++ /dev/null @@ -1,244 +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'); - -/** - * Auggie CLI setup handler - * Installs to project directory (.augment/commands) - */ -class AuggieSetup extends BaseIdeSetup { - constructor() { - super('auggie', 'Auggie CLI'); - this.detectionPaths = ['.augment']; - } - - /** - * 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}...`)); - - // Always use project directory - const location = path.join(projectDir, '.augment', 'commands'); - - // Clean up old BMAD installation first - await this.cleanup(projectDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Get tasks, tools, and workflows (ALL workflows now generate commands) - const tasks = await this.getTasks(bmadDir, true); - const tools = await this.getTools(bmadDir, true); - - // Get ALL workflows using the new workflow command generator - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Convert workflow artifacts to expected format - const workflows = workflowArtifacts - .filter((artifact) => artifact.type === 'workflow-command') - .map((artifact) => ({ - module: artifact.module, - name: path.basename(artifact.relativePath, '.md'), - path: artifact.sourcePath, - content: artifact.content, - })); - - const bmadCommandsDir = path.join(location, 'bmad'); - const agentsDir = path.join(bmadCommandsDir, 'agents'); - const tasksDir = path.join(bmadCommandsDir, 'tasks'); - const toolsDir = path.join(bmadCommandsDir, 'tools'); - const workflowsDir = path.join(bmadCommandsDir, 'workflows'); - - await this.ensureDir(agentsDir); - await this.ensureDir(tasksDir); - await this.ensureDir(toolsDir); - await this.ensureDir(workflowsDir); - - // Install agent launchers - for (const artifact of agentArtifacts) { - const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`); - await this.writeFile(targetPath, artifact.content); - } - - // Install tasks - for (const task of tasks) { - const content = await this.readFile(task.path); - const commandContent = this.createTaskCommand(task, content); - - const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - } - - // Install tools - for (const tool of tools) { - const content = await this.readFile(tool.path); - const commandContent = this.createToolCommand(tool, content); - - const targetPath = path.join(toolsDir, `${tool.module}-${tool.name}.md`); - await this.writeFile(targetPath, commandContent); - } - - // Install workflows (already generated commands) - for (const workflow of workflows) { - // Use the pre-generated workflow command content - const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`); - await this.writeFile(targetPath, workflow.content); - } - - const totalInstalled = agentArtifacts.length + tasks.length + tools.length + workflows.length; - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentArtifacts.length} agents installed`)); - console.log(chalk.dim(` - ${tasks.length} tasks installed`)); - console.log(chalk.dim(` - ${tools.length} tools installed`)); - console.log(chalk.dim(` - ${workflows.length} workflows installed`)); - console.log(chalk.dim(` - Location: ${path.relative(projectDir, location)}`)); - console.log(chalk.yellow(`\n 💡 Tip: Add 'model: gpt-4o' to command frontmatter to specify AI model`)); - - return { - success: true, - agents: agentArtifacts.length, - tasks: tasks.length, - tools: tools.length, - workflows: workflows.length, - }; - } - - /** - * Create task command content - */ - createTaskCommand(task, content) { - const nameMatch = content.match(/name="([^"]+)"/); - const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); - - return `--- -description: "Execute the ${taskName} task" ---- - -# ${taskName} Task - -${content} - -## Module -BMAD ${task.module.toUpperCase()} module -`; - } - - /** - * Create tool command content - */ - createToolCommand(tool, content) { - const nameMatch = content.match(/name="([^"]+)"/); - const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name); - - return `--- -description: "Use the ${toolName} tool" ---- - -# ${toolName} Tool - -${content} - -## Module -BMAD ${tool.module.toUpperCase()} module -`; - } - - /** - * Create workflow command content - */ - createWorkflowCommand(workflow, content) { - const description = workflow.description || `Execute the ${workflow.name} workflow`; - - return `--- -description: "${description}" ---- - -# ${workflow.name} Workflow - -${content} - -## Module -BMAD ${workflow.module.toUpperCase()} module -`; - } - - /** - * Cleanup Auggie configuration - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - - // Only clean up project directory - const location = path.join(projectDir, '.augment', 'commands'); - const bmadDir = path.join(location, 'bmad'); - - if (await fs.pathExists(bmadDir)) { - await fs.remove(bmadDir); - console.log(chalk.dim(` Removed old BMAD commands`)); - } - } - - /** - * 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 - const location = path.join(projectDir, '.augment', 'commands'); - const bmadCommandsDir = path.join(location, 'bmad'); - const agentsDir = path.join(bmadCommandsDir, 'agents'); - - // Create .augment/commands/bmad/agents directory if it doesn't exist - await fs.ensureDir(agentsDir); - - // Create custom agent launcher - 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 -`; - - const fileName = `custom-${agentName.toLowerCase()}.md`; - const launcherPath = path.join(agentsDir, 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 cf7dedcd..00000000 --- a/tools/cli/installers/lib/ide/claude-code.js +++ /dev/null @@ -1,506 +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 { customAgentColonName } = require('./shared/path-utils'); -const prompts = require('../../../lib/prompts'); - -/** - * Claude Code IDE setup handler - */ -class ClaudeCodeSetup extends BaseIdeSetup { - constructor() { - super('claude-code', 'Claude Code', true); // preferred IDE - this.configDir = '.claude'; - this.commandsDir = 'commands'; - this.agentsDir = 'agents'; - } - - /** - * Prompt for subagent installation location - * @returns {Promise} Selected location ('project' or 'user') - */ - 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', - }); - } - - // /** - // * Collect configuration choices before installation - // * @param {Object} options - Configuration options - // * @returns {Object} Collected configuration - // */ - // async collectConfiguration(options = {}) { - // const config = { - // subagentChoices: null, - // installLocation: null, - // }; - - // const sourceModulesPath = getSourcePath('modules'); - // const modules = options.selectedModules || []; - - // for (const moduleName of modules) { - // // Check for Claude Code sub-module injection config in SOURCE directory - // const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml'); - - // if (await this.exists(injectionConfigPath)) { - // const yaml = require('yaml'); - - // try { - // // Load injection configuration - // const configContent = await fs.readFile(injectionConfigPath, 'utf8'); - // const injectionConfig = yaml.parse(configContent); - - // // Ask about subagents if they exist and we haven't asked yet - // if (injectionConfig.subagents && !config.subagentChoices) { - // config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents); - - // if (config.subagentChoices.install !== 'none') { - // config.installLocation = await this.promptInstallLocation(); - // } - // } - // } catch (error) { - // console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`)); - // } - // } - // } - - // return config; - // } - - /** - * Cleanup old BMAD installation before reinstalling - * @param {string} projectDir - Project directory - */ - async cleanup(projectDir) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - // Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats) - 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++; - } - } - // Also remove legacy bmad folder if it exists - const bmadFolder = path.join(commandsDir, 'bmad'); - if (await fs.pathExists(bmadFolder)) { - await fs.remove(bmadFolder); - console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`)); - } - } - } - - /** - * Clean up legacy folder structure (module/type/name.md) if it exists - * This can be called after migration to remove old nested directories - * @param {string} projectDir - Project directory - */ - async cleanupLegacyFolders(projectDir) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - if (!(await fs.pathExists(commandsDir))) { - return; - } - - // Remove legacy bmad folder if it exists - const bmadFolder = path.join(commandsDir, 'bmad'); - if (await fs.pathExists(bmadFolder)) { - await fs.remove(bmadFolder); - console.log(chalk.dim(` Removed legacy bmad folder from ${this.name}`)); - } - } - - /** - * Setup Claude Code IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - // Store project directory for use in processContent - this.projectDir = projectDir; - - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Clean up old BMAD installation first - await this.cleanup(projectDir); - - // Create .claude/commands directory structure - const claudeDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(claudeDir, this.commandsDir); - await this.ensureDir(commandsDir); - - // Use underscore format: files written directly to commands dir (no bmad subfolder) - // Creates: .claude/commands/bmad_bmm_pm.md - - // Generate agent launchers using AgentCommandGenerator - // This creates small launcher files that reference the actual agents in _bmad/ - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Write agent launcher files using flat underscore naming - // Creates files like: bmad_bmm_pm.md - const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts); - - // Process Claude Code specific injections for installed modules - // Use pre-collected configuration if available, or skip if already configured - if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) { - // IDE is already configured from previous installation, skip prompting - // Just process with default/existing configuration - await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {}); - } else if (options.preCollectedConfig) { - await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig); - } else { - await this.processModuleInjections(projectDir, bmadDir, options); - } - - // Skip CLAUDE.md creation - let user manage their own CLAUDE.md file - // await this.createClaudeConfig(projectDir, modules); - - // Generate workflow commands from manifest (if it exists) - const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); - - // Write workflow-command artifacts using flat underscore naming - // Creates files like: bmad_bmm_correct-course.md - const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts); - - // Generate task and tool commands from manifests (if they exist) - const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed`)); - if (workflowCommandCount > 0) { - console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`)); - } - if (taskToolResult.generated > 0) { - console.log( - chalk.dim( - ` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`, - ), - ); - } - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); - - return { - success: true, - agents: agentCount, - }; - } - - // Method removed - CLAUDE.md file management left to user - - /** - * 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: 'claude-code', - subagentChoices, - installLocation, - interactive: false, - }); - } - - /** - * Process Claude Code specific injections for installed modules - * Looks for injections.yaml in each module's claude-code sub-module - */ - async processModuleInjections(projectDir, bmadDir, options) { - // Get list of installed modules - const modules = options.selectedModules || []; - let subagentChoices = null; - let installLocation = null; - - // Get the actual source directory (not the installation directory) - const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ - projectDir, - modules, - handler: 'claude-code', - 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.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) { - // First ask if they want to install subagents - const install = await prompts.select({ - message: 'Would you like to install Claude Code subagents for enhanced functionality?', - choices: [ - { 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)')}:`, - 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; - - // 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 Claude 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(), '.claude', 'agents'); - console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`)); - } else { - targetDir = path.join(projectDir, '.claude', 'agents'); - console.log(chalk.dim(` Installing subagents to project: .claude/agents/`)); - } - - // Ensure target directory exists - await this.ensureDir(targetDir); - - 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 - * @param {string} projectDir - Project directory - * @param {string} agentName - Agent name (e.g., "fred-commit-poet") - * @param {string} agentPath - Path to compiled agent (relative to project root) - * @param {Object} metadata - Agent metadata - * @returns {Object|null} Info about created command - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project - } - - 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 - -`; - - // Use underscore format: bmad_custom_fred-commit-poet.md - // Written directly to commands dir (no bmad subfolder) - const launcherName = customAgentColonName(agentName); - const launcherPath = path.join(commandsDir, launcherName); - await this.writeFile(launcherPath, launcherContent); - - 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 f2109d88..00000000 --- a/tools/cli/installers/lib/ide/cline.js +++ /dev/null @@ -1,272 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const chalk = require('chalk'); -const { BaseIdeSetup } = require('./_base-ide'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); -const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts'); -const { toDashPath, customAgentDashName } = require('./shared/path-utils'); - -/** - * Cline IDE setup handler - * 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 this.ensureDir(workflowsDir); - - // Clear old BMAD files - await this.clearOldBmadFiles(workflowsDir); - - // Collect all artifacts - const { artifacts, counts } = await this.collectClineArtifacts(projectDir, bmadDir, options); - - // Write flattened files - const written = await this.flattenAndWriteArtifacts(artifacts, workflowsDir); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${counts.agents} agents installed`)); - console.log(chalk.dim(` - ${counts.tasks} tasks installed`)); - console.log(chalk.dim(` - ${counts.workflows} workflow commands installed`)); - if (counts.workflowLaunchers > 0) { - console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers installed`)); - } - console.log(chalk.dim(` - ${written} 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, - agents: counts.agents, - tasks: counts.tasks, - workflows: counts.workflows, - workflowLaunchers: counts.workflowLaunchers, - written, - }; - } - - /** - * 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')); - } - - /** - * Collect all artifacts for Cline export - */ - async collectClineArtifacts(projectDir, bmadDir, options = {}) { - const selectedModules = options.selectedModules || []; - const artifacts = []; - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); - - // Process agent launchers with project-specific paths - for (const agentArtifact of agentArtifacts) { - const content = agentArtifact.content; - - artifacts.push({ - type: 'agent', - module: agentArtifact.module, - sourcePath: agentArtifact.sourcePath, - relativePath: agentArtifact.relativePath, - content, - }); - } - - // Get tasks - const tasks = await getTasksFromBmad(bmadDir, selectedModules); - for (const task of tasks) { - const content = await this.readAndProcessWithProject( - task.path, - { - module: task.module, - name: task.name, - }, - projectDir, - ); - - artifacts.push({ - type: 'task', - module: task.module, - sourcePath: task.path, - relativePath: path.join(task.module, 'tasks', `${task.name}.md`), - content, - }); - } - - // Get workflows - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - artifacts.push(...workflowArtifacts); - - return { - artifacts, - counts: { - agents: agentArtifacts.length, - tasks: tasks.length, - workflows: workflowCounts.commands, - workflowLaunchers: workflowCounts.launchers, - }, - }; - } - - /** - * Flatten file path to bmad_module_type_name.md format - * Uses shared toDashPath utility - */ - flattenFilename(relativePath) { - return toDashPath(relativePath); - } - - /** - * Write all artifacts with flattened names - */ - async flattenAndWriteArtifacts(artifacts, destDir) { - let written = 0; - - for (const artifact of artifacts) { - const flattenedName = this.flattenFilename(artifact.relativePath); - const targetPath = path.join(destDir, flattenedName); - await fs.writeFile(targetPath, artifact.content); - written++; - } - - return written; - } - - /** - * Clear old BMAD files from the workflows directory - */ - 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); - } - } - } - - /** - * Read and process file with project-specific paths - */ - async readAndProcessWithProject(filePath, metadata, projectDir) { - const content = await fs.readFile(filePath, 'utf8'); - return super.processContent(content, metadata, projectDir); - } - - /** - * Cleanup Cline configuration - */ - 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', - }; - } - - /** - * Utility: Ensure directory exists - */ - async ensureDir(dirPath) { - await fs.ensureDir(dirPath); - } -} - -module.exports = { ClineSetup }; diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index b632d4b7..60250a39 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -154,17 +154,25 @@ class CodexSetup extends BaseIdeSetup { // Check global location if (await fs.pathExists(globalDir)) { - const entries = await fs.readdir(globalDir); - if (entries.some((entry) => entry.startsWith('bmad'))) { - return true; + try { + const entries = await fs.readdir(globalDir); + if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) { + return true; + } + } catch { + // Ignore errors } } // Check project-specific location if (await fs.pathExists(projectSpecificDir)) { - const entries = await fs.readdir(projectSpecificDir); - if (entries.some((entry) => entry.startsWith('bmad'))) { - return true; + try { + const entries = await fs.readdir(projectSpecificDir); + if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) { + return true; + } + } catch { + // Ignore errors } } @@ -253,19 +261,39 @@ class CodexSetup extends BaseIdeSetup { return; } - const entries = await fs.readdir(destDir); + let entries; + try { + entries = await fs.readdir(destDir); + } catch (error) { + // Directory exists but can't be read - skip cleanup + console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`)); + return; + } + + if (!entries || !Array.isArray(entries)) { + return; + } for (const entry of entries) { + // Skip non-strings or undefined entries + if (!entry || typeof entry !== 'string') { + continue; + } 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); + try { + const stat = await fs.stat(entryPath); + if (stat.isFile()) { + await fs.remove(entryPath); + } else if (stat.isDirectory()) { + await fs.remove(entryPath); + } + } catch (error) { + // Skip files that can't be processed + console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`)); } } } diff --git a/tools/cli/installers/lib/ide/crush.js b/tools/cli/installers/lib/ide/crush.js deleted file mode 100644 index b9312d67..00000000 --- a/tools/cli/installers/lib/ide/crush.js +++ /dev/null @@ -1,149 +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'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); -const { customAgentColonName } = require('./shared/path-utils'); - -/** - * Crush IDE setup handler - * Creates commands in .crush/commands/ directory structure using flat colon naming - */ -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 crushDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(crushDir, this.commandsDir); - await this.ensureDir(commandsDir); - - // Use underscore format: files written directly to commands dir (no bmad subfolder) - // Creates: .crush/commands/bmad_bmm_pm.md - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Write agent launcher files using flat underscore naming - // Creates files like: bmad_bmm_pm.md - const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts); - - // Get ALL workflows using the new workflow command generator - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Write workflow-command artifacts using flat underscore naming - // Creates files like: bmad_bmm_correct-course.md - const workflowCount = await workflowGenerator.writeColonArtifacts(commandsDir, workflowArtifacts); - - // Generate task and tool commands using flat underscore naming - const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agent commands created`)); - console.log(chalk.dim(` - ${taskToolResult.tasks} task commands created`)); - console.log(chalk.dim(` - ${taskToolResult.tools} tool commands created`)); - console.log(chalk.dim(` - ${workflowCount} workflow commands created`)); - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); - console.log(chalk.dim('\n Commands can be accessed via Crush command palette')); - - return { - success: true, - agents: agentCount, - tasks: taskToolResult.tasks || 0, - tools: taskToolResult.tools || 0, - workflows: workflowCount, - }; - } - - /** - * 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 771bba72..00000000 --- a/tools/cli/installers/lib/ide/cursor.js +++ /dev/null @@ -1,160 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); -const { customAgentColonName } = require('./shared/path-utils'); - -/** - * Cursor IDE setup handler - */ -class CursorSetup extends BaseIdeSetup { - constructor() { - super('cursor', 'Cursor', true); // preferred IDE - this.configDir = '.cursor'; - this.rulesDir = 'rules'; - this.commandsDir = 'commands'; - } - - /** - * Cleanup old BMAD installation before reinstalling - * @param {string} projectDir - Project directory - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - // Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats) - if (await fs.pathExists(commandsDir)) { - const entries = await fs.readdir(commandsDir); - for (const entry of entries) { - 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}`)); - } - } - - /** - * Setup Cursor IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Clean up old BMAD installation first - await this.cleanup(projectDir); - - // Create .cursor/commands directory structure - const cursorDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(cursorDir, this.commandsDir); - await this.ensureDir(commandsDir); - - // Use underscore format: files written directly to commands dir (no bmad subfolder) - // Creates: .cursor/commands/bmad_bmm_pm.md - - // Generate agent launchers using AgentCommandGenerator - // This creates small launcher files that reference the actual agents in _bmad/ - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Write agent launcher files using flat underscore naming - // Creates files like: bmad_bmm_pm.md - const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts); - - // Generate workflow commands from manifest (if it exists) - const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); - - // Write workflow-command artifacts using flat underscore naming - // Creates files like: bmad_bmm_correct-course.md - const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts); - - // Generate task and tool commands from manifests (if they exist) - const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir); - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed`)); - if (workflowCommandCount > 0) { - console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`)); - } - if (taskToolResult.generated > 0) { - console.log( - chalk.dim( - ` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`, - ), - ); - } - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); - - return { - success: true, - agents: agentCount, - tasks: taskToolResult.tasks || 0, - tools: taskToolResult.tools || 0, - workflows: workflowCommandCount, - }; - } - - /** - * Install a custom agent launcher for Cursor - * @param {string} projectDir - Project directory - * @param {string} agentName - Agent name (e.g., "fred-commit-poet") - * @param {string} agentPath - Path to compiled agent (relative to project root) - * @param {Object} metadata - Agent metadata - * @returns {Object|null} Info about created command - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - - if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project - } - - await this.ensureDir(commandsDir); - - const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. - - -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 - -`; - - // Cursor uses YAML frontmatter matching Claude Code format - const commandContent = `--- -name: '${agentName}' -description: '${agentName} agent' ---- - -${launcherContent} -`; - - // Use underscore format: bmad_custom_fred-commit-poet.md - // Written directly to commands dir (no bmad subfolder) - const launcherName = customAgentColonName(agentName); - const launcherPath = path.join(commandsDir, launcherName); - await this.writeFile(launcherPath, commandContent); - - 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 a1673573..00000000 --- a/tools/cli/installers/lib/ide/gemini.js +++ /dev/null @@ -1,301 +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 { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); - -/** - * 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'; - this.agentTemplatePath = path.join(__dirname, 'templates', 'gemini-agent-command.toml'); - this.taskTemplatePath = path.join(__dirname, 'templates', 'gemini-task-command.toml'); - } - - /** - * 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); - - // 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 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); - - // Install agents as TOML files with bmad- prefix (flat structure) - let agentCount = 0; - for (const artifact of agentArtifacts) { - const tomlContent = await this.createAgentLauncherToml(artifact); - - // Flat structure: bmad-agent-{module}-{name}.toml - const tomlPath = path.join(commandsDir, `bmad-agent-${artifact.module}-${artifact.name}.toml`); - await this.writeFile(tomlPath, tomlContent); - agentCount++; - - console.log(chalk.green(` ✓ Added agent: /bmad_agents_${artifact.module}_${artifact.name}`)); - } - - // Install tasks as TOML files with bmad- prefix (flat structure) - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const tomlContent = await this.createTaskToml(task, content); - - // Flat structure: bmad-task-{module}-{name}.toml - const tomlPath = path.join(commandsDir, `bmad-task-${task.module}-${task.name}.toml`); - await this.writeFile(tomlPath, tomlContent); - taskCount++; - - console.log(chalk.green(` ✓ Added task: /bmad_tasks_${task.module}_${task.name}`)); - } - - // Install workflows as TOML files with bmad- prefix (flat structure) - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - // Create TOML wrapper around workflow command content - const tomlContent = await this.createWorkflowToml(artifact); - - // Flat structure: bmad-workflow-{module}-{name}.toml - const workflowName = path.basename(artifact.relativePath, '.md'); - const tomlPath = path.join(commandsDir, `bmad-workflow-${artifact.module}-${workflowName}.toml`); - await this.writeFile(tomlPath, tomlContent); - workflowCount++; - - console.log(chalk.green(` ✓ Added workflow: /bmad_workflows_${artifact.module}_${workflowName}`)); - } - } - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents configured`)); - console.log(chalk.dim(` - ${taskCount} tasks configured`)); - console.log(chalk.dim(` - ${workflowCount} workflows configured`)); - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); - console.log(chalk.dim(` - Agent activation: /bmad_agents_{agent-name}`)); - console.log(chalk.dim(` - Task activation: /bmad_tasks_{task-name}`)); - console.log(chalk.dim(` - Workflow activation: /bmad_workflows_{workflow-name}`)); - - return { - success: true, - agents: agentCount, - tasks: taskCount, - workflows: workflowCount, - }; - } - - /** - * Create agent launcher TOML content from artifact - */ - async createAgentLauncherToml(artifact) { - // Strip frontmatter from launcher content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim(); - - // Extract title from launcher frontmatter - const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); - - // Create TOML wrapper around launcher content (without frontmatter) - const description = `BMAD ${artifact.module.toUpperCase()} Agent: ${title}`; - - return `description = "${description}" -prompt = """ -${contentWithoutFrontmatter} -""" -`; - } - - /** - * Create agent TOML content using template - */ - async createAgentToml(agent, content) { - // Extract metadata - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); - - // Load template - const template = await fs.readFile(this.agentTemplatePath, 'utf8'); - - // Replace template variables - // Note: {user_name} and other {config_values} are left as-is for runtime substitution by Gemini - const tomlContent = template - .replaceAll('{{title}}', title) - .replaceAll('{_bmad}', '_bmad') - .replaceAll('{_bmad}', this.bmadFolderName) - .replaceAll('{{module}}', agent.module) - .replaceAll('{{name}}', agent.name); - - return tomlContent; - } - - /** - * Create task TOML content using template - */ - async createTaskToml(task, content) { - // Extract task name from XML if available - const nameMatch = content.match(/([^<]+)<\/name>/); - const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name); - - // Load template - const template = await fs.readFile(this.taskTemplatePath, 'utf8'); - - // Replace template variables - const tomlContent = template - .replaceAll('{{taskName}}', taskName) - .replaceAll('{_bmad}', '_bmad') - .replaceAll('{_bmad}', this.bmadFolderName) - .replaceAll('{{module}}', task.module) - .replaceAll('{{filename}}', task.filename); - - return tomlContent; - } - - /** - * Create workflow TOML content from artifact - */ - async createWorkflowToml(artifact) { - // Extract description from artifact content - const descriptionMatch = artifact.content.match(/description:\s*"([^"]+)"/); - const description = descriptionMatch - ? descriptionMatch[1] - : `BMAD ${artifact.module.toUpperCase()} Workflow: ${path.basename(artifact.relativePath, '.md')}`; - - // Strip frontmatter from command content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim(); - - return `description = "${description}" -prompt = """ -${contentWithoutFrontmatter} -""" -`; - } - - /** - * Cleanup Gemini configuration - surgically remove only BMAD files - */ - 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 a7c6c925..00000000 --- a/tools/cli/installers/lib/ide/github-copilot.js +++ /dev/null @@ -1,383 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -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.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 directory - const githubDir = path.join(projectDir, this.configDir); - const agentsDir = path.join(githubDir, this.agentsDir); - await this.ensureDir(agentsDir); - - // 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 || []); - - // Create agent files with bmd- prefix - let agentCount = 0; - for (const artifact of agentArtifacts) { - const content = artifact.content; - const agentContent = await this.createAgentContent({ module: artifact.module, name: artifact.name }, content); - - // Use bmd- prefix: bmd-custom-{module}-{name}.agent.md - const targetPath = path.join(agentsDir, `bmd-custom-${artifact.module}-${artifact.name}.agent.md`); - await this.writeFile(targetPath, agentContent); - agentCount++; - - console.log(chalk.green(` ✓ Created agent: bmd-custom-${artifact.module}-${artifact.name}`)); - } - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents created`)); - console.log(chalk.dim(` - Agents directory: ${path.relative(projectDir, agentsDir)}`)); - console.log(chalk.dim(` - VS Code settings configured`)); - console.log(chalk.dim('\n Agents available in VS Code Chat view')); - - return { - success: true, - agents: agentCount, - 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 new agents directory - const agentsDir = path.join(projectDir, this.configDir, this.agentsDir); - if (await fs.pathExists(agentsDir)) { - const files = await fs.readdir(agentsDir); - let removed = 0; - - for (const file of files) { - if (file.startsWith('bmd-') && file.endsWith('.agent.md')) { - await fs.remove(path.join(agentsDir, file)); - removed++; - } - } - - if (removed > 0) { - console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`)); - } - } - } - - /** - * 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, `bmd-custom-${agentName}.agent.md`); - await this.writeFile(agentFilePath, agentContent); - - return { - path: agentFilePath, - command: `bmd-custom-${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 bbe6d470..00000000 --- a/tools/cli/installers/lib/ide/iflow.js +++ /dev/null @@ -1,191 +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}...`)); - - // Create .iflow/commands/bmad directory structure - const iflowDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad'); - const agentsDir = path.join(commandsDir, 'agents'); - const tasksDir = path.join(commandsDir, 'tasks'); - const workflowsDir = path.join(commandsDir, 'workflows'); - - await this.ensureDir(agentsDir); - await this.ensureDir(tasksDir); - await this.ensureDir(workflowsDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Setup agents as commands - let agentCount = 0; - for (const artifact of agentArtifacts) { - const commandContent = await this.createAgentCommand(artifact); - - const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`); - await this.writeFile(targetPath, commandContent); - agentCount++; - } - - // 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 tasks as commands - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const commandContent = this.createTaskCommand(task, content); - - const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - taskCount++; - } - - // Setup workflows as commands (already generated) - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - const targetPath = path.join(workflowsDir, `${artifact.module}-${path.basename(artifact.relativePath, '.md')}.md`); - await this.writeFile(targetPath, artifact.content); - workflowCount++; - } - } - - 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 fs = require('fs-extra'); - const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad'); - - if (await fs.pathExists(bmadCommandsDir)) { - await fs.remove(bmadCommandsDir); - console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`)); - } - } - - /** - * 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 iflowDir = path.join(projectDir, this.configDir); - const bmadCommandsDir = path.join(iflowDir, this.commandsDir, 'bmad'); - - // Create .iflow/commands/bmad directory if it doesn't exist - await fs.ensureDir(bmadCommandsDir); - - // 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 fileName = `custom-${agentName.toLowerCase()}.md`; - const launcherPath = path.join(bmadCommandsDir, 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/manager.js b/tools/cli/installers/lib/ide/manager.js index 97462746..2b68dfad 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -5,11 +5,15 @@ const chalk = require('chalk'); /** * IDE Manager - handles IDE-specific setup * Dynamically discovers and loads IDE handlers + * + * Loading strategy: + * 1. Custom installer files (codex.js, kilo.js, kiro-cli.js) - for platforms with unique installation logic + * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns */ class IdeManager { constructor() { this.handlers = new Map(); - this.loadHandlers(); + this._initialized = false; this.bmadFolderName = 'bmad'; // Default, can be overridden } @@ -28,53 +32,76 @@ class IdeManager { } /** - * Dynamically load all IDE handlers from directory + * Ensure handlers are loaded (lazy loading) */ - loadHandlers() { + async ensureInitialized() { + if (!this._initialized) { + await this.loadHandlers(); + this._initialized = true; + } + } + + /** + * Dynamically load all IDE handlers + * 1. Load custom installer files first (codex.js, kilo.js, kiro-cli.js) + * 2. Load config-driven handlers from platform-codes.yaml + */ + async loadHandlers() { + // Load custom installer files + this.loadCustomInstallerFiles(); + + // Load config-driven handlers from platform-codes.yaml + await this.loadConfigDrivenHandlers(); + } + + /** + * Load custom installer files (unique installation logic) + * These files have special installation patterns that don't fit the config-driven model + */ + loadCustomInstallerFiles() { const ideDir = __dirname; + const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js']; - try { - // Get all JS files in the IDE directory - const files = fs.readdirSync(ideDir).filter((file) => { - // Skip base class, manager, utility files (starting with _), and helper modules - return ( - file.endsWith('.js') && - !file.startsWith('_') && - file !== 'manager.js' && - file !== 'workflow-command-generator.js' && - file !== 'task-tool-command-generator.js' - ); - }); + for (const file of customFiles) { + const filePath = path.join(ideDir, file); + if (!fs.existsSync(filePath)) continue; - // Sort alphabetically for consistent ordering - files.sort(); + try { + const HandlerModule = require(filePath); + const HandlerClass = HandlerModule.default || Object.values(HandlerModule)[0]; - for (const file of files) { - const moduleName = path.basename(file, '.js'); - - try { - const modulePath = path.join(ideDir, file); - const HandlerModule = require(modulePath); - - // Get the first exported class (handles various export styles) - const HandlerClass = HandlerModule.default || HandlerModule[Object.keys(HandlerModule)[0]]; - - if (HandlerClass) { - const instance = new HandlerClass(); - // Use the name property from the instance (set in constructor) - // Only add if the instance has a valid name - if (instance.name && typeof instance.name === 'string') { - this.handlers.set(instance.name, instance); - } else { - console.log(chalk.yellow(` Warning: ${moduleName} handler missing valid 'name' property`)); - } + if (HandlerClass) { + const instance = new HandlerClass(); + if (instance.name && typeof instance.name === 'string') { + this.handlers.set(instance.name, instance); } - } catch (error) { - console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`)); } + } catch (error) { + console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`)); } - } catch (error) { - console.error(chalk.red('Failed to load IDE handlers:'), error.message); + } + } + + /** + * Load config-driven handlers from platform-codes.yaml + * This creates ConfigDrivenIdeSetup instances for platforms with installer config + */ + async loadConfigDrivenHandlers() { + const { loadPlatformCodes } = require('./platform-codes'); + const platformConfig = await loadPlatformCodes(); + + const { ConfigDrivenIdeSetup } = require('./_config-driven'); + + for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) { + // Skip if already loaded by custom installer + if (this.handlers.has(platformCode)) continue; + + // Skip if no installer config (platform may not need installation) + if (!platformInfo.installer) continue; + + const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); + handler.setBmadFolderName(this.bmadFolderName); + this.handlers.set(platformCode, 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/cli/installers/lib/ide/platform-codes.js b/tools/cli/installers/lib/ide/platform-codes.js new file mode 100644 index 00000000..d5d8e0a4 --- /dev/null +++ b/tools/cli/installers/lib/ide/platform-codes.js @@ -0,0 +1,100 @@ +const fs = require('fs-extra'); +const path = require('node:path'); +const yaml = require('yaml'); + +const PLATFORM_CODES_PATH = path.join(__dirname, 'platform-codes.yaml'); + +let _cachedPlatformCodes = null; + +/** + * Load the platform codes configuration from YAML + * @returns {Object} Platform codes configuration + */ +async function loadPlatformCodes() { + if (_cachedPlatformCodes) { + return _cachedPlatformCodes; + } + + if (!(await fs.pathExists(PLATFORM_CODES_PATH))) { + throw new Error(`Platform codes configuration not found at: ${PLATFORM_CODES_PATH}`); + } + + const content = await fs.readFile(PLATFORM_CODES_PATH, 'utf8'); + _cachedPlatformCodes = yaml.parse(content); + return _cachedPlatformCodes; +} + +/** + * Get platform information by code + * @param {string} platformCode - Platform code (e.g., 'claude-code', 'cursor') + * @returns {Object|null} Platform info or null if not found + */ +function getPlatformInfo(platformCode) { + if (!_cachedPlatformCodes) { + throw new Error('Platform codes not loaded. Call loadPlatformCodes() first.'); + } + + return _cachedPlatformCodes.platforms[platformCode] || null; +} + +/** + * Get all preferred platforms + * @returns {Promise} Array of preferred platform codes + */ +async function getPreferredPlatforms() { + const config = await loadPlatformCodes(); + return Object.entries(config.platforms) + .filter(([_, info]) => info.preferred) + .map(([code, _]) => code); +} + +/** + * Get all platform codes by category + * @param {string} category - Category to filter by (ide, cli, tool, etc.) + * @returns {Promise} Array of platform codes in the category + */ +async function getPlatformsByCategory(category) { + const config = await loadPlatformCodes(); + return Object.entries(config.platforms) + .filter(([_, info]) => info.category === category) + .map(([code, _]) => code); +} + +/** + * Get all platforms with installer config + * @returns {Promise} Array of platform codes that have installer config + */ +async function getConfigDrivenPlatforms() { + const config = await loadPlatformCodes(); + return Object.entries(config.platforms) + .filter(([_, info]) => info.installer) + .map(([code, _]) => code); +} + +/** + * Get platforms that use custom installers (no installer config) + * @returns {Promise} Array of platform codes with custom installers + */ +async function getCustomInstallerPlatforms() { + const config = await loadPlatformCodes(); + return Object.entries(config.platforms) + .filter(([_, info]) => !info.installer) + .map(([code, _]) => code); +} + +/** + * Clear the cached platform codes (useful for testing) + */ +function clearCache() { + _cachedPlatformCodes = null; +} + +module.exports = { + loadPlatformCodes, + getPlatformInfo, + getPreferredPlatforms, + getPlatformsByCategory, + getConfigDrivenPlatforms, + getCustomInstallerPlatforms, + clearCache, +}; diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml new file mode 100644 index 00000000..e3aea054 --- /dev/null +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -0,0 +1,241 @@ +# BMAD Platform Codes Configuration +# Central configuration for all platform/IDE codes used in the BMAD system +# +# This file defines: +# 1. Platform metadata (name, preferred status, category, description) +# 2. Installer configuration (target directories, templates, artifact types) +# +# Format: +# code: Platform identifier used internally +# name: Display name shown to users +# preferred: Whether this platform is shown as a recommended option on install +# category: Type of platform (ide, cli, tool, service) +# description: Brief description of the platform +# installer: Installation configuration (optional - omit for custom installers) + +platforms: + # ============================================================================ + # CLI Tools + # ============================================================================ + + claude-code: + name: "Claude Code" + preferred: true + category: cli + description: "Anthropic's official CLI for Claude" + installer: + target_dir: .claude/commands + template_type: default + + auggie: + name: "Auggie" + preferred: false + category: cli + description: "AI development tool" + installer: + target_dir: .augment/commands + template_type: default + + gemini: + name: "Gemini CLI" + preferred: false + category: cli + description: "Google's CLI for Gemini" + installer: + target_dir: .gemini/commands + template_type: gemini + + # ============================================================================ + # IDEs + # ============================================================================ + + cursor: + name: "Cursor" + preferred: true + category: ide + description: "AI-first code editor" + installer: + target_dir: .cursor/commands + template_type: default + + windsurf: + name: "Windsurf" + preferred: true + category: ide + description: "AI-powered IDE with cascade flows" + installer: + target_dir: .windsurf/workflows + template_type: windsurf + + cline: + name: "Cline" + preferred: false + category: ide + description: "AI coding assistant" + installer: + target_dir: .clinerules/workflows + template_type: windsurf + + roo: + name: "Roo Cline" + preferred: false + category: ide + description: "Enhanced Cline fork" + installer: + target_dir: .roo/commands + template_type: default + + github-copilot: + name: "GitHub Copilot" + preferred: false + category: ide + description: "GitHub's AI pair programmer" + installer: + targets: + - target_dir: .github/agents + template_type: copilot_agents + artifact_types: [agents] + - target_dir: .vscode + template_type: vscode_settings + artifact_types: [] + + opencode: + name: "OpenCode" + preferred: false + category: ide + description: "OpenCode terminal coding assistant" + installer: + target_dir: .opencode/command + template_type: opencode + + crush: + name: "Crush" + preferred: false + category: ide + description: "AI development assistant" + installer: + target_dir: .crush/commands + template_type: default + + iflow: + name: "iFlow" + preferred: false + category: ide + description: "AI workflow automation" + installer: + target_dir: .iflow/commands + template_type: default + + qwen: + name: "QwenCoder" + preferred: false + category: ide + description: "Qwen AI coding assistant" + installer: + target_dir: .qwen/commands + template_type: default + + rovo-dev: + name: "Rovo Dev" + preferred: false + category: ide + description: "Atlassian's Rovo development environment" + installer: + target_dir: .rovodev/workflows + template_type: rovodev + + trae: + name: "Trae" + preferred: false + category: ide + description: "AI coding tool" + installer: + target_dir: .trae/rules + template_type: trae + + antigravity: + name: "Google Antigravity" + preferred: false + category: ide + description: "Google's AI development environment" + installer: + target_dir: .agent/workflows + template_type: antigravity + # Note: Antigravity uses .agent/workflows/ directory (not .antigravity/) + + # ============================================================================ + # Custom Installers (no installer config - use custom file) + # These have unique installation logic that doesn't fit the config-driven model + # ============================================================================ + + codex: + name: "Codex" + preferred: false + category: cli + description: "OpenAI Codex integration" + # No installer config - uses custom codex.js + + kilo: + name: "KiloCoder" + preferred: false + category: ide + description: "AI coding platform" + # No installer config - uses custom kilo.js (creates .kilocodemodes file) + + kiro-cli: + name: "Kiro CLI" + preferred: false + category: cli + description: "Kiro command-line interface" + # No installer config - uses custom kiro-cli.js (YAML→JSON conversion) + +# ============================================================================ +# Installer Config Schema +# ============================================================================ +# +# installer: +# target_dir: string # Directory where artifacts are installed +# template_type: string # Default template type to use +# header_template: string (optional) # Override for header/frontmatter template +# body_template: string (optional) # Override for body/content template +# targets: array (optional) # For multi-target installations +# - target_dir: string +# template_type: string +# artifact_types: [agents, workflows, tasks, tools] +# artifact_types: array (optional) # Filter which artifacts to install (default: all) +# skip_existing: boolean (optional) # Skip files that already exist (default: false) + +# ============================================================================ +# Platform Categories +# ============================================================================ + +categories: + ide: + name: "Integrated Development Environment" + description: "Full-featured code editors with AI assistance" + + cli: + name: "Command Line Interface" + description: "Terminal-based tools" + + tool: + name: "Development Tool" + description: "Standalone development utilities" + + service: + name: "Cloud Service" + description: "Cloud-based development platforms" + + extension: + name: "Editor Extension" + description: "Plugins for existing editors" + +# ============================================================================ +# Naming Conventions and Rules +# ============================================================================ + +conventions: + code_format: "lowercase-kebab-case" + name_format: "Title Case" + max_code_length: 20 + allowed_characters: "a-z0-9-" diff --git a/tools/cli/installers/lib/ide/qwen.js b/tools/cli/installers/lib/ide/qwen.js deleted file mode 100644 index 7ac72f09..00000000 --- a/tools/cli/installers/lib/ide/qwen.js +++ /dev/null @@ -1,372 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); - -/** - * Qwen Code setup handler - * Creates TOML command files in .qwen/commands/BMad/ - */ -class QwenSetup extends BaseIdeSetup { - constructor() { - super('qwen', 'Qwen Code'); - this.configDir = '.qwen'; - this.commandsDir = 'commands'; - this.bmadDir = 'bmad'; - } - - /** - * 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/BMad directory structure - const qwenDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(qwenDir, this.commandsDir); - const bmadCommandsDir = path.join(commandsDir, this.bmadDir); - - await this.ensureDir(bmadCommandsDir); - - // Update existing settings.json if present - await this.updateSettings(qwenDir); - - // Clean up old configuration if exists - await this.cleanupOldConfig(qwenDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Get tasks, tools, and workflows (standalone only for tools/workflows) - const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); - const tools = await this.getTools(bmadDir, true); - const workflows = await this.getWorkflows(bmadDir, true); - - // Create directories for each module (including standalone) - const modules = new Set(); - for (const item of [...agentArtifacts, ...tasks, ...tools, ...workflows]) modules.add(item.module); - - for (const module of modules) { - await this.ensureDir(path.join(bmadCommandsDir, module)); - await this.ensureDir(path.join(bmadCommandsDir, module, 'agents')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'tools')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'workflows')); - } - - // Create TOML files for each agent launcher - let agentCount = 0; - for (const artifact of agentArtifacts) { - // Convert markdown launcher content to TOML format - const tomlContent = this.processAgentLauncherContent(artifact.content, { - module: artifact.module, - name: artifact.name, - }); - - const targetPath = path.join(bmadCommandsDir, artifact.module, 'agents', `${artifact.name}.toml`); - - await this.writeFile(targetPath, tomlContent); - - agentCount++; - console.log(chalk.green(` ✓ Added agent: /bmad_${artifact.module}_agents_${artifact.name}`)); - } - - // Create TOML files for each task - let taskCount = 0; - for (const task of tasks) { - const content = await this.readAndProcess(task.path, { - module: task.module, - name: task.name, - }); - - const targetPath = path.join(bmadCommandsDir, task.module, 'tasks', `${task.name}.toml`); - - await this.writeFile(targetPath, content); - - taskCount++; - console.log(chalk.green(` ✓ Added task: /bmad_${task.module}_tasks_${task.name}`)); - } - - // Create TOML files for each tool - let toolCount = 0; - for (const tool of tools) { - const content = await this.readAndProcess(tool.path, { - module: tool.module, - name: tool.name, - }); - - const targetPath = path.join(bmadCommandsDir, tool.module, 'tools', `${tool.name}.toml`); - - await this.writeFile(targetPath, content); - - toolCount++; - console.log(chalk.green(` ✓ Added tool: /bmad_${tool.module}_tools_${tool.name}`)); - } - - // Create TOML files for each workflow - let workflowCount = 0; - for (const workflow of workflows) { - const content = await this.readAndProcess(workflow.path, { - module: workflow.module, - name: workflow.name, - }); - - const targetPath = path.join(bmadCommandsDir, workflow.module, 'workflows', `${workflow.name}.toml`); - - await this.writeFile(targetPath, content); - - workflowCount++; - console.log(chalk.green(` ✓ Added workflow: /bmad_${workflow.module}_workflows_${workflow.name}`)); - } - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents configured`)); - console.log(chalk.dim(` - ${taskCount} tasks configured`)); - console.log(chalk.dim(` - ${toolCount} tools configured`)); - console.log(chalk.dim(` - ${workflowCount} workflows configured`)); - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`)); - - return { - success: true, - agents: agentCount, - tasks: taskCount, - tools: toolCount, - workflows: workflowCount, - }; - } - - /** - * Update settings.json to remove old agent references - */ - async updateSettings(qwenDir) { - const fs = require('fs-extra'); - const settingsPath = path.join(qwenDir, 'settings.json'); - - if (await fs.pathExists(settingsPath)) { - try { - const settingsContent = await fs.readFile(settingsPath, 'utf8'); - const settings = JSON.parse(settingsContent); - let updated = false; - - // Remove agent file references from contextFileName - if (settings.contextFileName && Array.isArray(settings.contextFileName)) { - const originalLength = settings.contextFileName.length; - settings.contextFileName = settings.contextFileName.filter( - (fileName) => !fileName.startsWith('agents/') && !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 fs = require('fs-extra'); - 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')); - } - } - - /** - * Read and process file content - */ - async readAndProcess(filePath, metadata) { - const fs = require('fs-extra'); - const content = await fs.readFile(filePath, 'utf8'); - return this.processContent(content, metadata); - } - - /** - * Process agent launcher content and convert to TOML format - * @param {string} launcherContent - Launcher markdown content - * @param {Object} metadata - File metadata - * @returns {string} TOML formatted content - */ - processAgentLauncherContent(launcherContent, metadata = {}) { - // Strip frontmatter from launcher content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, ''); - - // Extract title for TOML description - const titleMatch = launcherContent.match(/description:\s*"([^"]+)"/); - const title = titleMatch ? titleMatch[1] : metadata.name; - - // Create TOML with launcher content (without frontmatter) - return `description = "BMAD ${metadata.module.toUpperCase()} Agent: ${title}" -prompt = """ -${contentWithoutFrontmatter.trim()} -""" -`; - } - - /** - * Override processContent to add TOML metadata header for Qwen - * @param {string} content - File content - * @param {Object} metadata - File metadata - * @returns {string} Processed content with Qwen template - */ - processContent(content, metadata = {}) { - // First apply base processing (includes activation injection for agents) - let prompt = super.processContent(content, metadata); - - // Determine the type and description based on content - const isAgent = content.includes(' word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - /** - * Cleanup Qwen configuration - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, this.bmadDir); - const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method'); - const oldBMadDir = path.join(projectDir, this.configDir, 'BMad'); - - if (await fs.pathExists(bmadCommandsDir)) { - await fs.remove(bmadCommandsDir); - console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`)); - } - - if (await fs.pathExists(oldBmadMethodDir)) { - await fs.remove(oldBmadMethodDir); - console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`)); - } - - 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 qwenDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(qwenDir, this.commandsDir); - const bmadCommandsDir = path.join(commandsDir, this.bmadDir); - - // Create .qwen/commands/BMad directory if it doesn't exist - await fs.ensureDir(bmadCommandsDir); - - // Create custom agent launcher in TOML format (same pattern as regular agents) - 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 Qwen's TOML conversion method - const tomlContent = this.processAgentLauncherContent(launcherContent, { - name: agentName, - module: 'custom', - }); - - const fileName = `custom-${agentName.toLowerCase()}.toml`; - const launcherPath = path.join(bmadCommandsDir, fileName); - - // Write the launcher file - await fs.writeFile(launcherPath, tomlContent, 'utf8'); - - return { - ide: 'qwen', - path: path.relative(projectDir, launcherPath), - command: agentName, - 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 d329e1ad..00000000 --- a/tools/cli/installers/lib/ide/rovo-dev.js +++ /dev/null @@ -1,290 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const chalk = require('chalk'); -const { BaseIdeSetup } = require('./_base-ide'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); - -/** - * Rovo Dev IDE setup handler - * - * Installs BMAD agents as Rovo Dev subagents in .rovodev/subagents/ - * Installs workflows and tasks/tools as reference guides in .rovodev/ - * Rovo Dev automatically discovers agents and integrates with BMAD like other IDEs - */ -class RovoDevSetup extends BaseIdeSetup { - constructor() { - super('rovo-dev', 'Atlassian Rovo Dev', false); - this.configDir = '.rovodev'; - this.subagentsDir = 'subagents'; - this.workflowsDir = 'workflows'; - this.referencesDir = 'references'; - } - - /** - * Cleanup old BMAD installation before reinstalling - * @param {string} projectDir - Project directory - */ - async cleanup(projectDir) { - const rovoDevDir = path.join(projectDir, this.configDir); - - if (!(await fs.pathExists(rovoDevDir))) { - return; - } - - // Clean BMAD agents from subagents directory - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); - if (await fs.pathExists(subagentsDir)) { - const entries = await fs.readdir(subagentsDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(subagentsDir, file)); - } - } - - // Clean BMAD workflows from workflows directory - const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - if (await fs.pathExists(workflowsDir)) { - const entries = await fs.readdir(workflowsDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(workflowsDir, file)); - } - } - - // Clean BMAD tasks/tools from references directory - const referencesDir = path.join(rovoDevDir, this.referencesDir); - if (await fs.pathExists(referencesDir)) { - const entries = await fs.readdir(referencesDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(referencesDir, file)); - } - } - } - - /** - * 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 subagentsDir = path.join(rovoDevDir, this.subagentsDir); - const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - const referencesDir = path.join(rovoDevDir, this.referencesDir); - - await this.ensureDir(subagentsDir); - await this.ensureDir(workflowsDir); - await this.ensureDir(referencesDir); - - // Generate and install agents - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - let agentCount = 0; - for (const artifact of agentArtifacts) { - const subagentFilename = `bmad-${artifact.module}-${artifact.name}.md`; - const targetPath = path.join(subagentsDir, subagentFilename); - const subagentContent = this.convertToRovoDevSubagent(artifact.content, artifact.name, artifact.module); - await this.writeFile(targetPath, subagentContent); - agentCount++; - } - - // Generate and install workflows - const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGen.collectWorkflowArtifacts(bmadDir); - - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - const workflowFilename = path.basename(artifact.relativePath); - const targetPath = path.join(workflowsDir, workflowFilename); - await this.writeFile(targetPath, artifact.content); - workflowCount++; - } - } - - // Generate and install tasks and tools - const taskToolGen = new TaskToolCommandGenerator(); - const { tasks: taskCount, tools: toolCount } = await this.generateTaskToolReferences(bmadDir, referencesDir, taskToolGen); - - // Summary output - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed to .rovodev/subagents/`)); - if (workflowCount > 0) { - console.log(chalk.dim(` - ${workflowCount} workflows installed to .rovodev/workflows/`)); - } - if (taskCount + toolCount > 0) { - console.log( - chalk.dim(` - ${taskCount + toolCount} tasks/tools installed to .rovodev/references/ (${taskCount} tasks, ${toolCount} tools)`), - ); - } - console.log(chalk.yellow(`\n Note: Agents are automatically discovered by Rovo Dev`)); - console.log(chalk.dim(` - Access agents by typing @ in Rovo Dev to see available options`)); - console.log(chalk.dim(` - Workflows and references are available in .rovodev/ directory`)); - - return { - success: true, - agents: agentCount, - workflows: workflowCount, - tasks: taskCount, - tools: toolCount, - }; - } - - /** - * Generate task and tool reference guides - * @param {string} bmadDir - BMAD directory - * @param {string} referencesDir - References directory - * @param {TaskToolCommandGenerator} taskToolGen - Generator instance - */ - async generateTaskToolReferences(bmadDir, referencesDir, taskToolGen) { - const tasks = await taskToolGen.loadTaskManifest(bmadDir); - const tools = await taskToolGen.loadToolManifest(bmadDir); - - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - - let taskCount = 0; - for (const task of standaloneTasks) { - const commandContent = taskToolGen.generateCommandContent(task, 'task'); - const targetPath = path.join(referencesDir, `bmad-task-${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - taskCount++; - } - - let toolCount = 0; - for (const tool of standaloneTools) { - const commandContent = taskToolGen.generateCommandContent(tool, 'tool'); - const targetPath = path.join(referencesDir, `bmad-tool-${tool.module}-${tool.name}.md`); - await this.writeFile(targetPath, commandContent); - toolCount++; - } - - return { tasks: taskCount, tools: toolCount }; - } - - /** - * Convert BMAD agent launcher to Rovo Dev subagent format - * - * Rovo Dev subagents use Markdown files with YAML frontmatter containing: - * - name: Unique identifier for the subagent - * - description: One-line description of the subagent's purpose - * - tools: Array of tools the subagent can use (optional) - * - model: Specific model for this subagent (optional) - * - load_memory: Whether to load memory files (optional, defaults to true) - * - * @param {string} launcherContent - Original agent launcher content - * @param {string} agentName - Name of the agent - * @param {string} moduleName - Name of the module - * @returns {string} Rovo Dev subagent-formatted content - */ - convertToRovoDevSubagent(launcherContent, agentName, moduleName) { - // Extract metadata from the launcher XML - const titleMatch = launcherContent.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName); - - const descriptionMatch = launcherContent.match(/description="([^"]+)"/); - const description = descriptionMatch ? descriptionMatch[1] : `BMAD agent: ${title}`; - - const roleDefinitionMatch = launcherContent.match(/roleDefinition="([^"]+)"/); - const roleDefinition = roleDefinitionMatch ? roleDefinitionMatch[1] : `You are a specialized agent for ${title.toLowerCase()} tasks.`; - - // Extract the main system prompt from the launcher (content after closing tags) - let systemPrompt = roleDefinition; - - // Try to extract additional instructions from the launcher content - const instructionsMatch = launcherContent.match(/([\s\S]*?)<\/instructions>/); - if (instructionsMatch) { - systemPrompt += '\n\n' + instructionsMatch[1].trim(); - } - - // Build YAML frontmatter for Rovo Dev subagent - const frontmatter = { - name: `bmad-${moduleName}-${agentName}`, - description: description, - // Note: tools and model can be added by users in their .rovodev/subagents/*.md files - // We don't enforce specific tools since BMAD agents are flexible - }; - - // Create YAML frontmatter string with proper quoting for special characters - let yamlContent = '---\n'; - yamlContent += `name: ${frontmatter.name}\n`; - // Quote description to handle colons and other special characters in YAML - yamlContent += `description: "${frontmatter.description.replaceAll('"', String.raw`\"`)}"\n`; - yamlContent += '---\n'; - - // Combine frontmatter with system prompt - const subagentContent = yamlContent + systemPrompt; - - return subagentContent; - } - - /** - * 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 agents in subagents directory - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); - if (await fs.pathExists(subagentsDir)) { - try { - const entries = await fs.readdir(subagentsDir); - if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) { - return true; - } - } catch { - // Continue checking other directories - } - } - - // Check for BMAD workflows in workflows directory - 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 other directories - } - } - - // Check for BMAD tasks/tools in references directory - const referencesDir = path.join(rovoDevDir, this.referencesDir); - if (await fs.pathExists(referencesDir)) { - try { - const entries = await fs.readdir(referencesDir); - if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) { - return true; - } - } catch { - // Continue - } - } - - return false; - } -} - -module.exports = { RovoDevSetup }; diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 29319af8..084ed048 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -31,11 +31,23 @@ class AgentCommandGenerator { const launcherContent = await this.generateLauncherContent(agent); // Use relativePath if available (for nested agents), otherwise just name with .md const agentPathInModule = agent.relativePath || `${agent.name}.md`; + // Calculate the relative agent path (e.g., bmm/agents/pm.md) + let agentRelPath = agent.path; + // Remove _bmad/ prefix if present to get relative path from project root + // Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...) + if (agentRelPath.includes('_bmad/')) { + const parts = agentRelPath.split(/_bmad\//); + if (parts.length > 1) { + agentRelPath = parts.slice(1).join('/'); + } + } artifacts.push({ type: 'agent-launcher', - module: agent.module, name: agent.name, - relativePath: path.join(agent.module, 'agents', agentPathInModule), + description: agent.description || `${agent.name} agent`, + module: agent.module, + relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename + agentPath: agentRelPath, // Relative path to actual agent file content: launcherContent, sourcePath: agent.path, }); @@ -119,8 +131,10 @@ class AgentCommandGenerator { } /** - * Write agent launcher artifacts using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_pm.md + * Write agent launcher artifacts using dash format (NEW STANDARD) + * Creates flat files like: bmad-bmm-pm.agent.md + * + * The .agent.md suffix distinguishes agents from workflows/tasks/tools. * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Agent launcher artifacts @@ -131,7 +145,7 @@ class AgentCommandGenerator { for (const artifact of artifacts) { if (artifact.type === 'agent-launcher') { - // Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md + // Convert relativePath to dash format: bmm/agents/pm.md → bmad-bmm-pm.agent.md const flatName = toDashPath(artifact.relativePath); const launcherPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(launcherPath)); diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index eb190589..e88a64f5 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -86,6 +86,11 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { + // Skip if entry.name is undefined or not a string + if (!entry.name || typeof entry.name !== 'string') { + continue; + } + const fullPath = path.join(dirPath, entry.name); const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index dc564774..7c335d4b 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -2,7 +2,16 @@ * Path transformation utilities for IDE installer standardization * * Provides utilities to convert hierarchical paths to flat naming conventions. - * - Underscore format (bmad_module_name.md) - Windows-compatible universal format + * + * DASH-BASED NAMING (new standard): + * - Agents: bmad-module-name.agent.md (with .agent.md suffix) + * - Workflows/Tasks/Tools: bmad-module-name.md + * + * Example outputs: + * - cis/agents/storymaster.md → bmad-cis-storymaster.agent.md + * - bmm/workflows/plan-project.md → bmad-bmm-plan-project.md + * - bmm/tasks/create-story.md → bmad-bmm-create-story.md + * - core/agents/brainstorming.md → bmad-brainstorming.agent.md */ // Type segments - agents are included in naming, others are filtered out @@ -10,111 +19,120 @@ const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const AGENT_SEGMENT = 'agents'; /** - * Convert hierarchical path to flat underscore-separated name - * Converts: 'bmm', 'agents', 'pm' → 'bmad_bmm_agent_pm.md' - * Converts: 'bmm', 'workflows', 'correct-course' → 'bmad_bmm_correct-course.md' - * Converts: 'core', 'agents', 'brainstorming' → 'bmad_agent_brainstorming.md' (core items skip module prefix) + * Convert hierarchical path to flat dash-separated name (NEW STANDARD) + * Converts: 'bmm', 'agents', 'pm' → 'bmad-bmm-pm.agent.md' + * Converts: 'bmm', 'workflows', 'correct-course' → 'bmad-bmm-correct-course.md' + * Converts: 'core', 'agents', 'brainstorming' → 'bmad-brainstorming.agent.md' (core items skip module prefix) * * @param {string} module - Module name (e.g., 'bmm', 'core') * @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools') * @param {string} name - Artifact name (e.g., 'pm', 'brainstorming') - * @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_bmm_correct-course.md' + * @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-bmm-correct-course.md' */ -function toUnderscoreName(module, type, name) { +function toDashName(module, type, name) { const isAgent = type === AGENT_SEGMENT; - // For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md' + + // For core module, skip the module prefix: use 'bmad-name.md' instead of 'bmad-core-name.md' if (module === 'core') { - return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`; + return isAgent ? `bmad-${name}.agent.md` : `bmad-${name}.md`; } - return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`; + + // Module artifacts: bmad-module-name.md or bmad-module-name.agent.md + // eslint-disable-next-line unicorn/prefer-string-replace-all -- regex replace is intentional here + const dashName = name.replace(/\//g, '-'); // Flatten nested paths + return isAgent ? `bmad-${module}-${dashName}.agent.md` : `bmad-${module}-${dashName}.md`; } /** - * Convert relative path to flat underscore-separated name - * Converts: 'bmm/agents/pm.md' → 'bmad_bmm_agent_pm.md' - * Converts: 'bmm/workflows/correct-course.md' → 'bmad_bmm_correct-course.md' - * Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix) + * Convert relative path to flat dash-separated name + * Converts: 'bmm/agents/pm.md' → 'bmad-bmm-pm.agent.md' + * Converts: 'bmm/workflows/correct-course.md' → 'bmad-bmm-correct-course.md' + * Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix) * * @param {string} relativePath - Path like 'bmm/agents/pm.md' - * @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_brainstorming.md' + * @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-brainstorming.md' */ -function toUnderscorePath(relativePath) { +function toDashPath(relativePath) { + if (!relativePath || typeof relativePath !== 'string') { + // Return a safe default for invalid input + return 'bmad-unknown.md'; + } + const withoutExt = relativePath.replace('.md', ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; const type = parts[1]; - const name = parts.slice(2).join('_'); + const name = parts.slice(2).join('-'); - // Use toUnderscoreName for consistency - return toUnderscoreName(module, type, name); + return toDashName(module, type, name); } /** - * Create custom agent underscore name - * Creates: 'bmad_custom_fred-commit-poet.md' + * Create custom agent dash name + * Creates: 'bmad-custom-fred-commit-poet.agent.md' * * @param {string} agentName - Custom agent name - * @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md' + * @returns {string} Flat filename like 'bmad-custom-fred-commit-poet.agent.md' */ -function customAgentUnderscoreName(agentName) { - return `bmad_custom_${agentName}.md`; +function customAgentDashName(agentName) { + return `bmad-custom-${agentName}.agent.md`; } /** - * Check if a filename uses underscore format + * Check if a filename uses dash format * @param {string} filename - Filename to check - * @returns {boolean} True if filename uses underscore format + * @returns {boolean} True if filename uses dash format */ -function isUnderscoreFormat(filename) { - return filename.startsWith('bmad_') && filename.includes('_'); +function isDashFormat(filename) { + return filename.startsWith('bmad-') && filename.includes('-'); } /** - * Extract parts from an underscore-formatted filename - * Parses: 'bmad_bmm_agent_pm.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' } - * Parses: 'bmad_bmm_correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' } - * Parses: 'bmad_agent_brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents) - * Parses: 'bmad_brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows) + * Extract parts from a dash-formatted filename + * Parses: 'bmad-bmm-pm.agent.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' } + * Parses: 'bmad-bmm-correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' } + * Parses: 'bmad-brainstorming.agent.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents) + * Parses: 'bmad-brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows) * - * @param {string} filename - Underscore-formatted filename + * @param {string} filename - Dash-formatted filename * @returns {Object|null} Parsed parts or null if invalid format */ -function parseUnderscoreName(filename) { +function parseDashName(filename) { const withoutExt = filename.replace('.md', ''); - const parts = withoutExt.split('_'); + const parts = withoutExt.split('-'); if (parts.length < 2 || parts[0] !== 'bmad') { return null; } - // Check if this is an agent file (has 'agent' as one of the parts) - const agentIndex = parts.indexOf('agent'); + // Check if this is an agent file (has .agent suffix) + const isAgent = withoutExt.endsWith('.agent'); - if (agentIndex !== -1) { + if (isAgent) { // This is an agent file - // Format: bmad_agent_name (core) or bmad_module_agent_name - if (agentIndex === 1) { - // Core agent: bmad_agent_name + // Format: bmad-name.agent (core) or bmad-module-name.agent + if (parts.length === 3) { + // Core agent: bmad-name.agent return { prefix: parts[0], module: 'core', type: 'agents', - name: parts.slice(agentIndex + 1).join('_'), + name: parts[1], }; } else { - // Module agent: bmad_module_agent_name + // Module agent: bmad-module-name.agent return { prefix: parts[0], module: parts[1], type: 'agents', - name: parts.slice(agentIndex + 1).join('_'), + name: parts.slice(2).join('-'), }; } } // Not an agent file - must be a workflow/tool/task - // If only 2 parts (bmad_name), it's a core workflow/tool/task + // If only 2 parts (bmad-name), it's a core workflow/tool/task if (parts.length === 2) { return { prefix: parts[0], @@ -124,42 +142,140 @@ function parseUnderscoreName(filename) { }; } - // Otherwise, it's a module workflow/tool/task (bmad_module_name) + // Otherwise, it's a module workflow/tool/task (bmad-module-name) return { prefix: parts[0], module: parts[1], type: 'workflows', // Default to workflows for non-agent module items + name: parts.slice(2).join('-'), + }; +} + +// ============================================================================ +// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility +// ============================================================================ + +/** + * Convert hierarchical path to flat underscore-separated name (LEGACY) + * @deprecated Use toDashName instead + */ +function toUnderscoreName(module, type, name) { + const isAgent = type === AGENT_SEGMENT; + if (module === 'core') { + return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`; + } + return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`; +} + +/** + * Convert relative path to flat underscore-separated name (LEGACY) + * @deprecated Use toDashPath instead + */ +function toUnderscorePath(relativePath) { + const withoutExt = relativePath.replace('.md', ''); + const parts = withoutExt.split(/[/\\]/); + + const module = parts[0]; + const type = parts[1]; + const name = parts.slice(2).join('_'); + + return toUnderscoreName(module, type, name); +} + +/** + * Create custom agent underscore name (LEGACY) + * @deprecated Use customAgentDashName instead + */ +function customAgentUnderscoreName(agentName) { + return `bmad_custom_${agentName}.md`; +} + +/** + * Check if a filename uses underscore format (LEGACY) + * @deprecated Use isDashFormat instead + */ +function isUnderscoreFormat(filename) { + return filename.startsWith('bmad_') && filename.includes('_'); +} + +/** + * Extract parts from an underscore-formatted filename (LEGACY) + * @deprecated Use parseDashName instead + */ +function parseUnderscoreName(filename) { + const withoutExt = filename.replace('.md', ''); + const parts = withoutExt.split('_'); + + if (parts.length < 2 || parts[0] !== 'bmad') { + return null; + } + + const agentIndex = parts.indexOf('agent'); + + if (agentIndex !== -1) { + if (agentIndex === 1) { + return { + prefix: parts[0], + module: 'core', + type: 'agents', + name: parts.slice(agentIndex + 1).join('_'), + }; + } else { + return { + prefix: parts[0], + module: parts[1], + type: 'agents', + name: parts.slice(agentIndex + 1).join('_'), + }; + } + } + + if (parts.length === 2) { + return { + prefix: parts[0], + module: 'core', + type: 'workflows', + name: parts[1], + }; + } + + return { + prefix: parts[0], + module: parts[1], + type: 'workflows', name: parts.slice(2).join('_'), }; } -// Backward compatibility aliases (deprecated) +// Backward compatibility aliases (colon format was same as underscore) const toColonName = toUnderscoreName; const toColonPath = toUnderscorePath; -const toDashPath = toUnderscorePath; const customAgentColonName = customAgentUnderscoreName; -const customAgentDashName = customAgentUnderscoreName; const isColonFormat = isUnderscoreFormat; -const isDashFormat = isUnderscoreFormat; const parseColonName = parseUnderscoreName; -const parseDashName = parseUnderscoreName; module.exports = { + // New standard (dash-based) + toDashName, + toDashPath, + customAgentDashName, + isDashFormat, + parseDashName, + + // Legacy (underscore-based) - kept for backward compatibility toUnderscoreName, toUnderscorePath, customAgentUnderscoreName, isUnderscoreFormat, parseUnderscoreName, + // Backward compatibility aliases toColonName, toColonPath, - toDashPath, customAgentColonName, - customAgentDashName, isColonFormat, - isDashFormat, parseColonName, - parseDashName, + TYPE_SEGMENTS, AGENT_SEGMENT, }; diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index fd5f45d5..4c816ae4 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 @@ -66,7 +66,7 @@ class TaskToolCommandGenerator { // Convert path to use {project-root} placeholder let itemPath = item.path; - if (itemPath.startsWith('bmad/')) { + if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) { itemPath = `{project-root}/${itemPath}`; } @@ -239,8 +239,10 @@ Follow all instructions in the ${type} file exactly as written. } /** - * Write task/tool artifacts using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_bmad-help.md + * Write task/tool artifacts using dash format (NEW STANDARD) + * Creates flat files like: bmad-bmm-bmad-help.md + * + * Note: Tasks/tools do NOT have .agent.md suffix - only agents do. * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Task/tool artifacts with relativePath @@ -252,7 +254,7 @@ Follow all instructions in the ${type} file exactly as written. for (const artifact of artifacts) { if (artifact.type === 'task' || artifact.type === 'tool') { const commandContent = this.generateCommandContent(artifact, artifact.type); - // Use underscore format: bmad_module_name.md + // Use dash format: bmad-module-name.md const flatName = toDashPath(artifact.relativePath); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index ebf8b7f5..b3410b0f 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -67,10 +67,26 @@ class WorkflowCommandGenerator { for (const workflow of allWorkflows) { const commandContent = await this.generateCommandContent(workflow, bmadDir); + // Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.yaml) + let workflowRelPath = workflow.path; + // Remove _bmad/ prefix if present to get relative path from project root + // Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...) + if (workflowRelPath.includes('_bmad/')) { + const parts = workflowRelPath.split(/_bmad\//); + if (parts.length > 1) { + workflowRelPath = parts.slice(1).join('/'); + } + } + // Determine if this is a YAML workflow + const isYamlWorkflow = workflow.path.endsWith('.yaml') || workflow.path.endsWith('.yml'); artifacts.push({ type: 'workflow-command', + isYamlWorkflow: isYamlWorkflow, // For template selection + name: workflow.name, + description: workflow.description || `${workflow.name} workflow`, module: workflow.module, relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`), + workflowPath: workflowRelPath, // Relative path to actual workflow file content: commandContent, sourcePath: workflow.path, }); @@ -265,8 +281,10 @@ When running any workflow: } /** - * Write workflow command artifacts using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_correct-course.md + * Write workflow command artifacts using dash format (NEW STANDARD) + * Creates flat files like: bmad-bmm-correct-course.md + * + * Note: Workflows do NOT have .agent.md suffix - only agents do. * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Workflow artifacts @@ -277,7 +295,7 @@ When running any workflow: for (const artifact of artifacts) { if (artifact.type === 'workflow-command') { - // Convert relativePath to underscore format: bmm/workflows/correct-course.md → bmad_bmm_correct-course.md + // Convert relativePath to dash format: bmm/workflows/correct-course.md → bmad-bmm-correct-course.md const flatName = toDashPath(artifact.relativePath); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); diff --git a/tools/cli/installers/lib/ide/templates/combined/antigravity.md b/tools/cli/installers/lib/ide/templates/combined/antigravity.md new file mode 100644 index 00000000..cbb3dd4e --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/antigravity.md @@ -0,0 +1,6 @@ +name: '{{name}}' +description: '{{description}}' + +LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}} + +Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/claude-agent.md b/tools/cli/installers/lib/ide/templates/combined/claude-agent.md new file mode 120000 index 00000000..9f6c17b4 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/claude-agent.md @@ -0,0 +1 @@ +default-agent.md \ No newline at end of file diff --git a/tools/cli/installers/lib/ide/templates/combined/claude-workflow-yaml.md b/tools/cli/installers/lib/ide/templates/combined/claude-workflow-yaml.md new file mode 120000 index 00000000..11f78e1d --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/claude-workflow-yaml.md @@ -0,0 +1 @@ +default-workflow-yaml.md \ No newline at end of file diff --git a/tools/cli/installers/lib/ide/templates/combined/claude-workflow.md b/tools/cli/installers/lib/ide/templates/combined/claude-workflow.md new file mode 120000 index 00000000..8d4ae523 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/claude-workflow.md @@ -0,0 +1 @@ +default-workflow.md \ No newline at end of file diff --git a/tools/cli/installers/lib/ide/templates/combined/default-agent.md b/tools/cli/installers/lib/ide/templates/combined/default-agent.md new file mode 100644 index 00000000..f8ad9380 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-agent.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 {project-root}/_bmad/{{path}} +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/combined/default-workflow-yaml.md b/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md new file mode 100644 index 00000000..eca90437 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md @@ -0,0 +1,14 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded: + + +1. Always LOAD the FULL @{project-root}/{{bmadFolderName}}/core/tasks/workflow.xml +2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config @{project-root}/{{bmadFolderName}}/{{path}} +3. Pass the yaml path @{project-root}/{{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions +4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions +5. Save outputs after EACH section when generating any documents from templates + diff --git a/tools/cli/installers/lib/ide/templates/combined/default-workflow.md b/tools/cli/installers/lib/ide/templates/combined/default-workflow.md new file mode 100644 index 00000000..afb0dea5 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-workflow.md @@ -0,0 +1,6 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly! diff --git a/tools/cli/installers/lib/ide/templates/combined/rovodev.md b/tools/cli/installers/lib/ide/templates/combined/rovodev.md new file mode 100644 index 00000000..a46b811d --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/rovodev.md @@ -0,0 +1,9 @@ +# {{name}} + +{{description}} + +--- + +LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}} + +Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/trae.md b/tools/cli/installers/lib/ide/templates/combined/trae.md new file mode 100644 index 00000000..c4357874 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/trae.md @@ -0,0 +1,9 @@ +# {{name}} + +{{description}} + +## Instructions + +LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}} + +Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md b/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md new file mode 100644 index 00000000..d61b819a --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md @@ -0,0 +1,10 @@ +--- +description: '{{description}}' +auto_execution_mode: "iterate" +--- + +# {{name}} + +LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}} + +Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/split/gemini/body.md b/tools/cli/installers/lib/ide/templates/split/gemini/body.md new file mode 100644 index 00000000..b20f6651 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/split/gemini/body.md @@ -0,0 +1,10 @@ +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 {project-root}/_bmad/{{path}} +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/split/gemini/header.toml b/tools/cli/installers/lib/ide/templates/split/gemini/header.toml new file mode 100644 index 00000000..099b1e26 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/split/gemini/header.toml @@ -0,0 +1,2 @@ +name = "{{name}}" +description = "{{description}}" diff --git a/tools/cli/installers/lib/ide/templates/split/opencode/body.md b/tools/cli/installers/lib/ide/templates/split/opencode/body.md new file mode 100644 index 00000000..b20f6651 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/split/opencode/body.md @@ -0,0 +1,10 @@ +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 {project-root}/_bmad/{{path}} +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/split/opencode/header.md b/tools/cli/installers/lib/ide/templates/split/opencode/header.md new file mode 100644 index 00000000..a384374c --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/split/opencode/header.md @@ -0,0 +1,4 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- 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 92596db3..00000000 --- a/tools/cli/installers/lib/ide/windsurf.js +++ /dev/null @@ -1,258 +0,0 @@ -const path = require('node:path'); -const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); - -/** - * Windsurf IDE setup handler - */ -class WindsurfSetup extends BaseIdeSetup { - constructor() { - super('windsurf', 'Windsurf', true); // preferred IDE - this.configDir = '.windsurf'; - this.workflowsDir = 'workflows'; - } - - /** - * Setup Windsurf IDE configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); - - // Create .windsurf/workflows/bmad directory structure - const windsurfDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(windsurfDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); - - await this.ensureDir(bmadWorkflowsDir); - - // Clean up any existing BMAD workflows before reinstalling - await this.cleanup(projectDir); - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Convert artifacts to agent format for module organization - const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name })); - - // Get tasks, tools, and workflows (standalone only) - const tasks = await this.getTasks(bmadDir, true); - const tools = await this.getTools(bmadDir, true); - const workflows = await this.getWorkflows(bmadDir, true); - - // Create directories for each module under bmad/ - const modules = new Set(); - for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module); - - for (const module of modules) { - await this.ensureDir(path.join(bmadWorkflowsDir, module)); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'agents')); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tasks')); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tools')); - await this.ensureDir(path.join(bmadWorkflowsDir, module, 'workflows')); - } - - // Process agent launchers as workflows with organized structure - let agentCount = 0; - for (const artifact of agentArtifacts) { - const processedContent = this.createWorkflowContent({ module: artifact.module, name: artifact.name }, artifact.content); - - // Organized path: bmad/module/agents/agent-name.md - const targetPath = path.join(bmadWorkflowsDir, artifact.module, 'agents', `${artifact.name}.md`); - await this.writeFile(targetPath, processedContent); - agentCount++; - } - - // Process tasks as workflows with organized structure - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const processedContent = this.createTaskWorkflowContent(task, content); - - // Organized path: bmad/module/tasks/task-name.md - const targetPath = path.join(bmadWorkflowsDir, task.module, 'tasks', `${task.name}.md`); - await this.writeFile(targetPath, processedContent); - taskCount++; - } - - // Process tools as workflows with organized structure - let toolCount = 0; - for (const tool of tools) { - const content = await this.readFile(tool.path); - const processedContent = this.createToolWorkflowContent(tool, content); - - // Organized path: bmad/module/tools/tool-name.md - const targetPath = path.join(bmadWorkflowsDir, tool.module, 'tools', `${tool.name}.md`); - await this.writeFile(targetPath, processedContent); - toolCount++; - } - - // Process workflows with organized structure - let workflowCount = 0; - for (const workflow of workflows) { - const content = await this.readFile(workflow.path); - const processedContent = this.createWorkflowWorkflowContent(workflow, content); - - // Organized path: bmad/module/workflows/workflow-name.md - const targetPath = path.join(bmadWorkflowsDir, workflow.module, 'workflows', `${workflow.name}.md`); - await this.writeFile(targetPath, processedContent); - workflowCount++; - } - - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed`)); - console.log(chalk.dim(` - ${taskCount} tasks installed`)); - console.log(chalk.dim(` - ${toolCount} tools installed`)); - console.log(chalk.dim(` - ${workflowCount} workflows installed`)); - console.log(chalk.dim(` - Organized in modules: ${[...modules].join(', ')}`)); - console.log(chalk.dim(` - 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, - agents: agentCount, - tasks: taskCount, - tools: toolCount, - workflows: workflowCount, - }; - } - - /** - * Create workflow content for an agent - */ - createWorkflowContent(agent, content) { - // Strip existing frontmatter from launcher - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); - - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: ${agent.name} -auto_execution_mode: 3 ---- - -${contentWithoutFrontmatter}`; - - return workflowContent; - } - - /** - * Create workflow content for a task - */ - createTaskWorkflowContent(task, content) { - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: task-${task.name} -auto_execution_mode: 2 ---- - -${content}`; - - return workflowContent; - } - - /** - * Create workflow content for a tool - */ - createToolWorkflowContent(tool, content) { - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: tool-${tool.name} -auto_execution_mode: 2 ---- - -${content}`; - - return workflowContent; - } - - /** - * Create workflow content for a workflow - */ - createWorkflowWorkflowContent(workflow, content) { - // Create simple Windsurf frontmatter matching original format - let workflowContent = `--- -description: ${workflow.name} -auto_execution_mode: 1 ---- - -${content}`; - - return workflowContent; - } - - /** - * Cleanup Windsurf configuration - surgically remove only BMAD files - */ - async cleanup(projectDir) { - const fs = require('fs-extra'); - const bmadPath = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad'); - - if (await fs.pathExists(bmadPath)) { - // Remove the entire bmad folder - this is our territory - await fs.remove(bmadPath); - console.log(chalk.dim(` Cleaned up existing BMAD workflows`)); - } - } - - /** - * 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 fs = require('fs-extra'); - const customAgentsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad', 'custom', 'agents'); - - if (!(await this.exists(path.join(projectDir, this.configDir)))) { - return null; // IDE not configured for this project - } - - await this.ensureDir(customAgentsDir); - - 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 - const workflowContent = `--- -description: ${metadata.title || agentName} -auto_execution_mode: 3 ---- - -${launcherContent}`; - - const launcherPath = path.join(customAgentsDir, `${agentName}.md`); - await fs.writeFile(launcherPath, workflowContent); - - return { - path: launcherPath, - command: `bmad/custom/agents/${agentName}`, - }; - } -} - -module.exports = { WindsurfSetup }; diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 614e5016..dc12d4d5 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -362,6 +362,7 @@ class UI { // Get IDE manager to fetch available IDEs dynamically const { IdeManager } = require('../installers/lib/ide/manager'); const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); // IMPORTANT: Must initialize before getting IDEs const preferredIdes = ideManager.getPreferredIdes(); const otherIdes = ideManager.getOtherIdes();