diff --git a/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md b/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md deleted file mode 100644 index f7116cb5..00000000 --- a/tools/cli/installers/lib/ide/STANDARDIZATION_PLAN.md +++ /dev/null @@ -1,208 +0,0 @@ -# IDE Installer Standardization Plan - -## Overview - -Standardize IDE installers to use **flat file naming** with **underscores** (Windows-compatible) and centralize duplicated code in shared utilities. - -**Key Rule: All IDEs use underscore format for Windows compatibility (colons don't work on Windows).** - -## Current State Analysis - -### File Structure Patterns - -| IDE | Current Pattern | Path Format | -|-----|-----------------|-------------| -| **claude-code** | Hierarchical | `.claude/commands/bmad/{module}/agents/{name}.md` | -| **cursor** | Hierarchical | `.cursor/commands/bmad/{module}/agents/{name}.md` | -| **crush** | Hierarchical | `.crush/commands/bmad/{module}/agents/{name}.md` | -| **antigravity** | Flattened (underscores) | `.agent/workflows/bmad_module_agents_name.md` | -| **codex** | Flattened (underscores) | `~/.codex/prompts/bmad_module_agents_name.md` | -| **cline** | Flattened (underscores) | `.clinerules/workflows/bmad_module_type_name.md` | -| **roo** | Flattened (underscores) | `.roo/commands/bmad_module_agent_name.md` | -| **auggie** | Hybrid | `.augment/commands/bmad/agents/{module}-{name}.md` | -| **iflow** | Hybrid | `.iflow/commands/bmad/agents/{module}-{name}.md` | -| **trae** | Different (rules) | `.trae/rules/bmad-agent-{module}-{name}.md` | -| **github-copilot** | Different (agents) | `.github/agents/bmd-custom-{module}-{name}.agent.md` | - -### Shared Generators (in `/shared`) - -1. `agent-command-generator.js` - generates agent launchers -2. `task-tool-command-generator.js` - generates task/tool commands -3. `workflow-command-generator.js` - generates workflow commands - -All currently create artifacts with **nested relative paths** like `{module}/agents/{name}.md` - -### Code Duplication Issues - -1. **Flattening logic** duplicated in multiple IDEs -2. **Agent launcher content creation** duplicated -3. **Path transformation** duplicated - -## Target Standardization - -### For All IDEs (underscore format - Windows-compatible) - -**IDEs affected:** claude-code, cursor, crush, antigravity, codex, cline, roo - -``` -Format: bmad_{module}_{type}_{name}.md - -Examples: -- Agent: bmad_bmm_agents_pm.md -- Agent: bmad_core_agents_dev.md -- Workflow: bmad_bmm_workflows_correct-course.md -- Task: bmad_bmm_tasks_bmad-help.md -- Tool: bmad_core_tools_code-review.md -- Custom: bmad_custom_agents_fred-commit-poet.md -``` - -**Note:** Type segments (agents, workflows, tasks, tools) are filtered out from names: -- `bmm/agents/pm.md` → `bmad_bmm_pm.md` (not `bmad_bmm_agents_pm.md`) - -### For Hybrid IDEs (keep as-is) - -**IDEs affected:** auggie, iflow - -These use `{module}-{name}.md` format within subdirectories - keep as-is. - -### Skip (drastically different) - -**IDEs affected:** trae, github-copilot - -## Implementation Plan - -### Phase 1: Create Shared Utility - -**File:** `shared/path-utils.js` - -```javascript -/** - * Convert hierarchical path to flat underscore-separated name (Windows-compatible) - * @param {string} module - Module name (e.g., 'bmm', 'core') - * @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools') - filtered out - * @param {string} name - Artifact name (e.g., 'pm', 'correct-course') - * @returns {string} Flat filename like 'bmad_bmm_pm.md' - */ -function toUnderscoreName(module, type, name) { - return `bmad_${module}_${name}.md`; -} - -/** - * Convert relative path to flat underscore-separated name (Windows-compatible) - * @param {string} relativePath - Path like 'bmm/agents/pm.md' - * @returns {string} Flat filename like 'bmad_bmm_pm.md' - */ -function toUnderscorePath(relativePath) { - const withoutExt = relativePath.replace('.md', ''); - const parts = withoutExt.split(/[\/\\]/); - // Filter out type segments (agents, workflows, tasks, tools) - const filtered = parts.filter((p) => !TYPE_SEGMENTS.includes(p)); - return `bmad_${filtered.join('_')}.md`; -} - -/** - * Create custom agent underscore name - * @param {string} agentName - Custom agent name - * @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md' - */ -function customAgentUnderscoreName(agentName) { - return `bmad_custom_${agentName}.md`; -} - -// Backward compatibility aliases -const toColonName = toUnderscoreName; -const toColonPath = toUnderscorePath; -const toDashPath = toUnderscorePath; -const customAgentColonName = customAgentUnderscoreName; -const customAgentDashName = customAgentUnderscoreName; - -module.exports = { - toUnderscoreName, - toUnderscorePath, - customAgentUnderscoreName, - // Backward compatibility - toColonName, - toColonPath, - toDashPath, - customAgentColonName, - customAgentDashName, -}; -``` - -### Phase 2: Update Shared Generators - -**Files to modify:** -- `shared/agent-command-generator.js` -- `shared/task-tool-command-generator.js` -- `shared/workflow-command-generator.js` - -**Changes:** -1. Import path utilities -2. Change `relativePath` to use flat format -3. Add method `writeColonArtifacts()` for folder-based IDEs (uses underscore) -4. Add method `writeDashArtifacts()` for flat IDEs (uses underscore) - -### Phase 3: Update All IDEs - -**Files to modify:** -- `claude-code.js` -- `cursor.js` -- `crush.js` -- `antigravity.js` -- `codex.js` -- `cline.js` -- `roo.js` - -**Changes:** -1. Import utilities from path-utils -2. Change from hierarchical to flat underscore naming -3. Update cleanup to handle flat structure (`startsWith('bmad')`) - -### Phase 4: Update Base Class - -**File:** `_base-ide.js` - -**Changes:** -1. Mark `flattenFilename()` as `@deprecated` -2. Add comment pointing to new path-utils - -## Migration Checklist - -### New Files -- [x] Create `shared/path-utils.js` - -### All IDEs (convert to underscore format) -- [x] Update `shared/agent-command-generator.js` - update for underscore -- [x] Update `shared/task-tool-command-generator.js` - update for underscore -- [x] Update `shared/workflow-command-generator.js` - update for underscore -- [x] Update `claude-code.js` - convert to underscore format -- [x] Update `cursor.js` - convert to underscore format -- [x] Update `crush.js` - convert to underscore format -- [ ] Update `antigravity.js` - use underscore format -- [ ] Update `codex.js` - use underscore format -- [ ] Update `cline.js` - use underscore format -- [ ] Update `roo.js` - use underscore format - -### CSV Command Files -- [x] Update `src/core/module-help.csv` - change colons to underscores -- [x] Update `src/bmm/module-help.csv` - change colons to underscores - -### Base Class -- [ ] Update `_base-ide.js` - add deprecation notice - -### Testing -- [ ] Test claude-code installation -- [ ] Test cursor installation -- [ ] Test crush installation -- [ ] Test antigravity installation -- [ ] Test codex installation -- [ ] Test cline installation -- [ ] Test roo installation - -## Notes - -1. **Filter type segments**: agents, workflows, tasks, tools are filtered out from flat names -2. **Underscore format**: Universal underscore format for Windows compatibility -3. **Custom agents**: Follow the same pattern as regular agents -4. **Backward compatibility**: Old function names kept as aliases -5. **Cleanup**: Will remove old `bmad:` format files on next install diff --git a/tools/cli/installers/lib/ide/antigravity.js b/tools/cli/installers/lib/ide/antigravity.js index 73464f0d..4e472c1e 100644 --- a/tools/cli/installers/lib/ide/antigravity.js +++ b/tools/cli/installers/lib/ide/antigravity.js @@ -91,10 +91,13 @@ class AntigravitySetup extends BaseIdeSetup { * @param {string} projectDir - Project directory */ async cleanup(projectDir) { - const bmadWorkflowsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad'); + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - if (await fs.pathExists(bmadWorkflowsDir)) { - await fs.remove(bmadWorkflowsDir); + if (await fs.pathExists(workflowsDir)) { + const bmadFiles = (await fs.readdir(workflowsDir)).filter((f) => f.startsWith('bmad')); + for (const f of bmadFiles) { + await fs.remove(path.join(workflowsDir, f)); + } console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`)); } } @@ -115,11 +118,9 @@ class AntigravitySetup extends BaseIdeSetup { await this.cleanup(projectDir); // Create .agent/workflows directory structure - const agentDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(agentDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - await this.ensureDir(bmadWorkflowsDir); + await this.ensureDir(workflowsDir); // Generate agent launchers using AgentCommandGenerator // This creates small launcher files that reference the actual agents in _bmad/ @@ -129,7 +130,7 @@ class AntigravitySetup extends BaseIdeSetup { // Write agent launcher files with FLATTENED naming using shared utility // Antigravity ignores directory structure, so we flatten to: bmad_module_name.md // This creates slash commands like /bmad_bmm_dev instead of /dev - const agentCount = await agentGen.writeDashArtifacts(bmadWorkflowsDir, agentArtifacts); + const agentCount = await agentGen.writeDashArtifacts(workflowsDir, agentArtifacts); // Process Antigravity specific injections for installed modules // Use pre-collected configuration if available, or skip if already configured @@ -148,12 +149,12 @@ class AntigravitySetup extends BaseIdeSetup { const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); // Write workflow-command artifacts with FLATTENED naming using shared utility - const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts); + const workflowCommandCount = await workflowGen.writeDashArtifacts(workflowsDir, workflowArtifacts); // Generate task and tool commands using FLAT naming (not nested!) // Use the new generateDashTaskToolCommands method with explicit target directory const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, bmadWorkflowsDir); + const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, workflowsDir); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed`)); @@ -167,7 +168,7 @@ class AntigravitySetup extends BaseIdeSetup { ), ); } - console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, bmadWorkflowsDir)}`)); + console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`)); console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad_module_agents_name)`)); return { @@ -430,12 +431,10 @@ class AntigravitySetup extends BaseIdeSetup { * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - // Create .agent/workflows/bmad directory structure (same as regular agents) - const agentDir = path.join(projectDir, this.configDir); - const workflowsDir = path.join(agentDir, this.workflowsDir); - const bmadWorkflowsDir = path.join(workflowsDir, 'bmad'); + // Create .agent/workflows directory structure + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); - await fs.ensureDir(bmadWorkflowsDir); + await fs.ensureDir(workflowsDir); // Create custom agent launcher with same pattern as regular agents const launcherContent = `name: '${agentName}' @@ -458,7 +457,7 @@ usage: | // Use underscore format: bmad_custom_fred-commit-poet.md const fileName = customAgentDashName(agentName); - const launcherPath = path.join(bmadWorkflowsDir, fileName); + const launcherPath = path.join(workflowsDir, fileName); // Write the launcher file await fs.writeFile(launcherPath, launcherContent, 'utf8'); diff --git a/tools/cli/installers/lib/ide/iflow.js b/tools/cli/installers/lib/ide/iflow.js index bbe6d470..0133877d 100644 --- a/tools/cli/installers/lib/ide/iflow.js +++ b/tools/cli/installers/lib/ide/iflow.js @@ -25,30 +25,21 @@ class IFlowSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Create .iflow/commands/bmad directory structure - const iflowDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad'); - const agentsDir = path.join(commandsDir, 'agents'); - const tasksDir = path.join(commandsDir, 'tasks'); - const workflowsDir = path.join(commandsDir, 'workflows'); + // Clean up old BMAD installation first + await this.cleanup(projectDir); - await this.ensureDir(agentsDir); - await this.ensureDir(tasksDir); - await this.ensureDir(workflowsDir); + // Create .iflow/commands directory structure (flat files, no bmad subfolder) + const iflowDir = path.join(projectDir, this.configDir); + const commandsDir = path.join(iflowDir, this.commandsDir); + + await this.ensureDir(commandsDir); // Generate agent launchers const agentGen = new AgentCommandGenerator(this.bmadFolderName); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - // Setup agents as commands - let agentCount = 0; - for (const artifact of agentArtifacts) { - const commandContent = await this.createAgentCommand(artifact); - - const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`); - await this.writeFile(targetPath, commandContent); - agentCount++; - } + // Setup agents as commands (flat files with dash naming) + const agentCount = await agentGen.writeDashArtifacts(commandsDir, agentArtifacts); // Get tasks and workflows (ALL workflows now generate commands) const tasks = await this.getTasks(bmadDir); @@ -57,26 +48,11 @@ class IFlowSetup extends BaseIdeSetup { const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - // Setup tasks as commands - let taskCount = 0; - for (const task of tasks) { - const content = await this.readFile(task.path); - const commandContent = this.createTaskCommand(task, content); + // Setup workflows as commands (flat files with dash naming) + const workflowCount = await workflowGenerator.writeDashArtifacts(commandsDir, workflowArtifacts); - const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - taskCount++; - } - - // Setup workflows as commands (already generated) - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - const targetPath = path.join(workflowsDir, `${artifact.module}-${path.basename(artifact.relativePath, '.md')}.md`); - await this.writeFile(targetPath, artifact.content); - workflowCount++; - } - } + // TODO: tasks not yet implemented with flat naming + const taskCount = 0; console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agent commands created`)); @@ -132,11 +108,20 @@ Part of the BMAD ${task.module.toUpperCase()} module. * Cleanup iFlow configuration */ async cleanup(projectDir) { - const fs = require('fs-extra'); - const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad'); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); + const bmadFolder = path.join(commandsDir, 'bmad'); - if (await fs.pathExists(bmadCommandsDir)) { - await fs.remove(bmadCommandsDir); + // Remove old bmad subfolder if it exists + if (await fs.pathExists(bmadFolder)) { + await fs.remove(bmadFolder); + } + + // Also remove any bmad* files at commands root + if (await fs.pathExists(commandsDir)) { + const bmadFiles = (await fs.readdir(commandsDir)).filter((f) => f.startsWith('bmad')); + for (const f of bmadFiles) { + await fs.remove(path.join(commandsDir, f)); + } console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`)); } } @@ -150,11 +135,10 @@ Part of the BMAD ${task.module.toUpperCase()} module. * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const iflowDir = path.join(projectDir, this.configDir); - const bmadCommandsDir = path.join(iflowDir, this.commandsDir, 'bmad'); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - // Create .iflow/commands/bmad directory if it doesn't exist - await fs.ensureDir(bmadCommandsDir); + // Create .iflow/commands directory if it doesn't exist + await fs.ensureDir(commandsDir); // Create custom agent launcher const launcherContent = `# ${agentName} Custom Agent @@ -173,8 +157,9 @@ The agent will follow the persona and instructions from the main agent file. *Generated by BMAD Method*`; - const fileName = `custom-${agentName.toLowerCase()}.md`; - const launcherPath = path.join(bmadCommandsDir, fileName); + const { customAgentDashName } = require('./shared/path-utils'); + const fileName = customAgentDashName(agentName); + const launcherPath = path.join(commandsDir, fileName); // Write the launcher file await fs.writeFile(launcherPath, launcherContent, 'utf8'); diff --git a/tools/cli/installers/lib/ide/qwen.js b/tools/cli/installers/lib/ide/qwen.js index 7ac72f09..fab6ee00 100644 --- a/tools/cli/installers/lib/ide/qwen.js +++ b/tools/cli/installers/lib/ide/qwen.js @@ -2,19 +2,17 @@ const path = require('node:path'); const fs = require('fs-extra'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); -const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); /** * Qwen Code setup handler - * Creates TOML command files in .qwen/commands/BMad/ + * Creates TOML command files in .qwen/commands/ */ class QwenSetup extends BaseIdeSetup { constructor() { super('qwen', 'Qwen Code'); this.configDir = '.qwen'; this.commandsDir = 'commands'; - this.bmadDir = 'bmad'; } /** @@ -26,118 +24,43 @@ class QwenSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Create .qwen/commands/BMad directory structure + // Create .qwen/commands directory (flat structure, no bmad subfolder) const qwenDir = path.join(projectDir, this.configDir); const commandsDir = path.join(qwenDir, this.commandsDir); - const bmadCommandsDir = path.join(commandsDir, this.bmadDir); - await this.ensureDir(bmadCommandsDir); + await this.ensureDir(commandsDir); // Update existing settings.json if present await this.updateSettings(qwenDir); - // Clean up old configuration if exists + // Clean up old configuration await this.cleanupOldConfig(qwenDir); + await this.cleanup(projectDir); - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - - // Get tasks, tools, and workflows (standalone only for tools/workflows) - const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); - const tools = await this.getTools(bmadDir, true); - const workflows = await this.getWorkflows(bmadDir, true); - - // Create directories for each module (including standalone) - const modules = new Set(); - for (const item of [...agentArtifacts, ...tasks, ...tools, ...workflows]) modules.add(item.module); - - for (const module of modules) { - await this.ensureDir(path.join(bmadCommandsDir, module)); - await this.ensureDir(path.join(bmadCommandsDir, module, 'agents')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'tools')); - await this.ensureDir(path.join(bmadCommandsDir, module, 'workflows')); - } - - // Create TOML files for each agent launcher - let agentCount = 0; - for (const artifact of agentArtifacts) { - // Convert markdown launcher content to TOML format - const tomlContent = this.processAgentLauncherContent(artifact.content, { - module: artifact.module, - name: artifact.name, - }); - - const targetPath = path.join(bmadCommandsDir, artifact.module, 'agents', `${artifact.name}.toml`); - - await this.writeFile(targetPath, tomlContent); - - agentCount++; - console.log(chalk.green(` ✓ Added agent: /bmad_${artifact.module}_agents_${artifact.name}`)); - } - - // Create TOML files for each task - let taskCount = 0; - for (const task of tasks) { - const content = await this.readAndProcess(task.path, { - module: task.module, - name: task.name, - }); - - const targetPath = path.join(bmadCommandsDir, task.module, 'tasks', `${task.name}.toml`); - - await this.writeFile(targetPath, content); - - taskCount++; - console.log(chalk.green(` ✓ Added task: /bmad_${task.module}_tasks_${task.name}`)); - } - - // Create TOML files for each tool - let toolCount = 0; - for (const tool of tools) { - const content = await this.readAndProcess(tool.path, { - module: tool.module, - name: tool.name, - }); - - const targetPath = path.join(bmadCommandsDir, tool.module, 'tools', `${tool.name}.toml`); - - await this.writeFile(targetPath, content); - - toolCount++; - console.log(chalk.green(` ✓ Added tool: /bmad_${tool.module}_tools_${tool.name}`)); - } - - // Create TOML files for each workflow - let workflowCount = 0; - for (const workflow of workflows) { - const content = await this.readAndProcess(workflow.path, { - module: workflow.module, - name: workflow.name, - }); - - const targetPath = path.join(bmadCommandsDir, workflow.module, 'workflows', `${workflow.name}.toml`); - - await this.writeFile(targetPath, content); - - workflowCount++; - console.log(chalk.green(` ✓ Added workflow: /bmad_${workflow.module}_workflows_${workflow.name}`)); - } + // Use the unified installer with QWEN template for TOML format + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: commandsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.QWEN, + fileExtension: '.toml', + }, + options.selectedModules || [], + ); console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents configured`)); - console.log(chalk.dim(` - ${taskCount} tasks configured`)); - console.log(chalk.dim(` - ${toolCount} tools configured`)); - console.log(chalk.dim(` - ${workflowCount} workflows configured`)); - console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`)); + console.log(chalk.dim(` - ${counts.agents} agents configured`)); + console.log(chalk.dim(` - ${counts.tasks} tasks configured`)); + console.log(chalk.dim(` - ${counts.tools} tools configured`)); + console.log(chalk.dim(` - ${counts.workflows} workflows configured`)); + console.log(chalk.dim(` - ${counts.total} TOML files written to ${path.relative(projectDir, commandsDir)}`)); return { success: true, - agents: agentCount, - tasks: taskCount, - tools: toolCount, - workflows: workflowCount, + ...counts, }; } @@ -145,7 +68,6 @@ class QwenSetup extends BaseIdeSetup { * Update settings.json to remove old agent references */ async updateSettings(qwenDir) { - const fs = require('fs-extra'); const settingsPath = path.join(qwenDir, 'settings.json'); if (await fs.pathExists(settingsPath)) { @@ -180,7 +102,6 @@ class QwenSetup extends BaseIdeSetup { * Clean up old configuration directories */ async cleanupOldConfig(qwenDir) { - const fs = require('fs-extra'); const agentsDir = path.join(qwenDir, 'agents'); const bmadMethodDir = path.join(qwenDir, 'bmad-method'); const bmadDir = path.join(qwenDir, 'bmadDir'); @@ -201,117 +122,39 @@ class QwenSetup extends BaseIdeSetup { } } - /** - * Read and process file content - */ - async readAndProcess(filePath, metadata) { - const fs = require('fs-extra'); - const content = await fs.readFile(filePath, 'utf8'); - return this.processContent(content, metadata); - } - - /** - * Process agent launcher content and convert to TOML format - * @param {string} launcherContent - Launcher markdown content - * @param {Object} metadata - File metadata - * @returns {string} TOML formatted content - */ - processAgentLauncherContent(launcherContent, metadata = {}) { - // Strip frontmatter from launcher content - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, ''); - - // Extract title for TOML description - const titleMatch = launcherContent.match(/description:\s*"([^"]+)"/); - const title = titleMatch ? titleMatch[1] : metadata.name; - - // Create TOML with launcher content (without frontmatter) - return `description = "BMAD ${metadata.module.toUpperCase()} Agent: ${title}" -prompt = """ -${contentWithoutFrontmatter.trim()} -""" -`; - } - - /** - * Override processContent to add TOML metadata header for Qwen - * @param {string} content - File content - * @param {Object} metadata - File metadata - * @returns {string} Processed content with Qwen template - */ - processContent(content, metadata = {}) { - // First apply base processing (includes activation injection for agents) - let prompt = super.processContent(content, metadata); - - // Determine the type and description based on content - const isAgent = content.includes(' word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - /** * Cleanup Qwen configuration */ async cleanup(projectDir) { - const fs = require('fs-extra'); - const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, this.bmadDir); - const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method'); - const oldBMadDir = path.join(projectDir, this.configDir, 'BMad'); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); + if (await fs.pathExists(commandsDir)) { + // Remove any bmad* files from the commands directory + const entries = await fs.readdir(commandsDir); + for (const entry of entries) { + if (entry.startsWith('bmad')) { + await fs.remove(path.join(commandsDir, entry)); + } + } + } + + // Also remove legacy bmad subfolder if it exists + const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad'); if (await fs.pathExists(bmadCommandsDir)) { await fs.remove(bmadCommandsDir); - console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`)); + console.log(chalk.dim(` Cleaned up existing BMAD configuration from Qwen Code`)); } + const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method'); if (await fs.pathExists(oldBmadMethodDir)) { await fs.remove(oldBmadMethodDir); - console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`)); + console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`)); } + const oldBMadDir = path.join(projectDir, this.configDir, 'BMad'); if (await fs.pathExists(oldBMadDir)) { await fs.remove(oldBMadDir); - console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`)); + console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`)); } } @@ -324,14 +167,12 @@ ${prompt} * @returns {Object} Installation result */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const qwenDir = path.join(projectDir, this.configDir); - const commandsDir = path.join(qwenDir, this.commandsDir); - const bmadCommandsDir = path.join(commandsDir, this.bmadDir); + const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); - // Create .qwen/commands/BMad directory if it doesn't exist - await fs.ensureDir(bmadCommandsDir); + // Create .qwen/commands directory if it doesn't exist + await fs.ensureDir(commandsDir); - // Create custom agent launcher in TOML format (same pattern as regular agents) + // Create custom agent launcher content const launcherContent = `# ${agentName} Custom Agent **⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! @@ -348,14 +189,20 @@ The agent will follow the persona and instructions from the main agent file. *Generated by BMAD Method*`; - // Use Qwen's TOML conversion method - const tomlContent = this.processAgentLauncherContent(launcherContent, { - name: agentName, - module: 'custom', - }); + // Convert to TOML format using the same method as UnifiedInstaller + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '').trim(); + const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`); - const fileName = `custom-${agentName.toLowerCase()}.toml`; - const launcherPath = path.join(bmadCommandsDir, fileName); + const tomlContent = `description = "BMAD Custom Agent: ${agentName}" +prompt = """ +${escapedContent} +""" +`; + + // Use flat naming: bmad-custom-agent-agentname.toml + const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.toml`; + const launcherPath = path.join(commandsDir, fileName); // Write the launcher file await fs.writeFile(launcherPath, tomlContent, 'utf8'); @@ -363,7 +210,7 @@ The agent will follow the persona and instructions from the main agent file. return { ide: 'qwen', path: path.relative(projectDir, launcherPath), - command: agentName, + command: fileName.replace('.toml', ''), type: 'custom-agent-launcher', }; } diff --git a/tools/cli/installers/lib/ide/rovo-dev.js b/tools/cli/installers/lib/ide/rovo-dev.js index d329e1ad..1151a2d5 100644 --- a/tools/cli/installers/lib/ide/rovo-dev.js +++ b/tools/cli/installers/lib/ide/rovo-dev.js @@ -2,69 +2,19 @@ const path = require('node:path'); const fs = require('fs-extra'); const chalk = require('chalk'); const { BaseIdeSetup } = require('./_base-ide'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); +const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); /** * Rovo Dev IDE setup handler * - * Installs BMAD agents as Rovo Dev subagents in .rovodev/subagents/ - * Installs workflows and tasks/tools as reference guides in .rovodev/ - * Rovo Dev automatically discovers agents and integrates with BMAD like other IDEs + * Uses UnifiedInstaller for all artifact installation with flat file structure. + * All BMAD artifacts are installed to .rovodev/workflows/ as flat files. */ class RovoDevSetup extends BaseIdeSetup { constructor() { super('rovo-dev', 'Atlassian Rovo Dev', false); this.configDir = '.rovodev'; - this.subagentsDir = 'subagents'; this.workflowsDir = 'workflows'; - this.referencesDir = 'references'; - } - - /** - * Cleanup old BMAD installation before reinstalling - * @param {string} projectDir - Project directory - */ - async cleanup(projectDir) { - const rovoDevDir = path.join(projectDir, this.configDir); - - if (!(await fs.pathExists(rovoDevDir))) { - return; - } - - // Clean BMAD agents from subagents directory - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); - if (await fs.pathExists(subagentsDir)) { - const entries = await fs.readdir(subagentsDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(subagentsDir, file)); - } - } - - // Clean BMAD workflows from workflows directory - const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - if (await fs.pathExists(workflowsDir)) { - const entries = await fs.readdir(workflowsDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(workflowsDir, file)); - } - } - - // Clean BMAD tasks/tools from references directory - const referencesDir = path.join(rovoDevDir, this.referencesDir); - if (await fs.pathExists(referencesDir)) { - const entries = await fs.readdir(referencesDir); - const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); - - for (const file of bmadFiles) { - await fs.remove(path.join(referencesDir, file)); - } - } } /** @@ -81,155 +31,76 @@ class RovoDevSetup extends BaseIdeSetup { // Create .rovodev directory structure const rovoDevDir = path.join(projectDir, this.configDir); - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); const workflowsDir = path.join(rovoDevDir, this.workflowsDir); - const referencesDir = path.join(rovoDevDir, this.referencesDir); - await this.ensureDir(subagentsDir); await this.ensureDir(workflowsDir); - await this.ensureDir(referencesDir); - // Generate and install agents - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); + // Use the unified installer - all artifacts go to workflows folder as flat files + const installer = new UnifiedInstaller(this.bmadFolderName); + const counts = await installer.install( + projectDir, + bmadDir, + { + targetDir: workflowsDir, + namingStyle: NamingStyle.FLAT_DASH, + templateType: TemplateType.CLAUDE, + }, + options.selectedModules || [], + ); - let agentCount = 0; - for (const artifact of agentArtifacts) { - const subagentFilename = `bmad-${artifact.module}-${artifact.name}.md`; - const targetPath = path.join(subagentsDir, subagentFilename); - const subagentContent = this.convertToRovoDevSubagent(artifact.content, artifact.name, artifact.module); - await this.writeFile(targetPath, subagentContent); - agentCount++; - } - - // Generate and install workflows - const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGen.collectWorkflowArtifacts(bmadDir); - - let workflowCount = 0; - for (const artifact of workflowArtifacts) { - if (artifact.type === 'workflow-command') { - const workflowFilename = path.basename(artifact.relativePath); - const targetPath = path.join(workflowsDir, workflowFilename); - await this.writeFile(targetPath, artifact.content); - workflowCount++; - } - } - - // Generate and install tasks and tools - const taskToolGen = new TaskToolCommandGenerator(); - const { tasks: taskCount, tools: toolCount } = await this.generateTaskToolReferences(bmadDir, referencesDir, taskToolGen); - - // Summary output console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${agentCount} agents installed to .rovodev/subagents/`)); - if (workflowCount > 0) { - console.log(chalk.dim(` - ${workflowCount} workflows installed to .rovodev/workflows/`)); + console.log(chalk.dim(` - ${counts.agents} agents installed`)); + if (counts.workflows > 0) { + console.log(chalk.dim(` - ${counts.workflows} workflows installed`)); } - if (taskCount + toolCount > 0) { - console.log( - chalk.dim(` - ${taskCount + toolCount} tasks/tools installed to .rovodev/references/ (${taskCount} tasks, ${toolCount} tools)`), - ); + if (counts.tasks + counts.tools > 0) { + console.log(chalk.dim(` - ${counts.tasks + counts.tools} tasks/tools installed (${counts.tasks} tasks, ${counts.tools} tools)`)); } - console.log(chalk.yellow(`\n Note: Agents are automatically discovered by Rovo Dev`)); - console.log(chalk.dim(` - Access agents by typing @ in Rovo Dev to see available options`)); - console.log(chalk.dim(` - Workflows and references are available in .rovodev/ directory`)); + console.log(chalk.dim(` - ${counts.total} files written to ${path.relative(projectDir, workflowsDir)}`)); + console.log(chalk.yellow(`\n Note: All BMAD items are available in .rovodev/workflows/`)); + console.log(chalk.dim(` - Access items by typing @ in Rovo Dev to see available files`)); return { success: true, - agents: agentCount, - workflows: workflowCount, - tasks: taskCount, - tools: toolCount, + ...counts, }; } /** - * Generate task and tool reference guides - * @param {string} bmadDir - BMAD directory - * @param {string} referencesDir - References directory - * @param {TaskToolCommandGenerator} taskToolGen - Generator instance + * Cleanup old BMAD installation before reinstalling + * @param {string} projectDir - Project directory */ - async generateTaskToolReferences(bmadDir, referencesDir, taskToolGen) { - const tasks = await taskToolGen.loadTaskManifest(bmadDir); - const tools = await taskToolGen.loadToolManifest(bmadDir); + async cleanup(projectDir) { + const rovoDevDir = path.join(projectDir, this.configDir); - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - - let taskCount = 0; - for (const task of standaloneTasks) { - const commandContent = taskToolGen.generateCommandContent(task, 'task'); - const targetPath = path.join(referencesDir, `bmad-task-${task.module}-${task.name}.md`); - await this.writeFile(targetPath, commandContent); - taskCount++; + if (!(await fs.pathExists(rovoDevDir))) { + return; } - let toolCount = 0; - for (const tool of standaloneTools) { - const commandContent = taskToolGen.generateCommandContent(tool, 'tool'); - const targetPath = path.join(referencesDir, `bmad-tool-${tool.module}-${tool.name}.md`); - await this.writeFile(targetPath, commandContent); - toolCount++; + // Clean BMAD files from workflows directory + const workflowsDir = path.join(rovoDevDir, this.workflowsDir); + if (await fs.pathExists(workflowsDir)) { + const entries = await fs.readdir(workflowsDir); + const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md')); + + for (const file of bmadFiles) { + await fs.remove(path.join(workflowsDir, file)); + } } - return { tasks: taskCount, tools: toolCount }; - } - - /** - * Convert BMAD agent launcher to Rovo Dev subagent format - * - * Rovo Dev subagents use Markdown files with YAML frontmatter containing: - * - name: Unique identifier for the subagent - * - description: One-line description of the subagent's purpose - * - tools: Array of tools the subagent can use (optional) - * - model: Specific model for this subagent (optional) - * - load_memory: Whether to load memory files (optional, defaults to true) - * - * @param {string} launcherContent - Original agent launcher content - * @param {string} agentName - Name of the agent - * @param {string} moduleName - Name of the module - * @returns {string} Rovo Dev subagent-formatted content - */ - convertToRovoDevSubagent(launcherContent, agentName, moduleName) { - // Extract metadata from the launcher XML - const titleMatch = launcherContent.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName); - - const descriptionMatch = launcherContent.match(/description="([^"]+)"/); - const description = descriptionMatch ? descriptionMatch[1] : `BMAD agent: ${title}`; - - const roleDefinitionMatch = launcherContent.match(/roleDefinition="([^"]+)"/); - const roleDefinition = roleDefinitionMatch ? roleDefinitionMatch[1] : `You are a specialized agent for ${title.toLowerCase()} tasks.`; - - // Extract the main system prompt from the launcher (content after closing tags) - let systemPrompt = roleDefinition; - - // Try to extract additional instructions from the launcher content - const instructionsMatch = launcherContent.match(/([\s\S]*?)<\/instructions>/); - if (instructionsMatch) { - systemPrompt += '\n\n' + instructionsMatch[1].trim(); + // Remove legacy subagents directory + const subagentsDir = path.join(rovoDevDir, 'subagents'); + if (await fs.pathExists(subagentsDir)) { + await fs.remove(subagentsDir); + console.log(chalk.dim(` Removed legacy subagents directory`)); } - // Build YAML frontmatter for Rovo Dev subagent - const frontmatter = { - name: `bmad-${moduleName}-${agentName}`, - description: description, - // Note: tools and model can be added by users in their .rovodev/subagents/*.md files - // We don't enforce specific tools since BMAD agents are flexible - }; - - // Create YAML frontmatter string with proper quoting for special characters - let yamlContent = '---\n'; - yamlContent += `name: ${frontmatter.name}\n`; - // Quote description to handle colons and other special characters in YAML - yamlContent += `description: "${frontmatter.description.replaceAll('"', String.raw`\"`)}"\n`; - yamlContent += '---\n'; - - // Combine frontmatter with system prompt - const subagentContent = yamlContent + systemPrompt; - - return subagentContent; + // Remove legacy references directory + const referencesDir = path.join(rovoDevDir, 'references'); + if (await fs.pathExists(referencesDir)) { + await fs.remove(referencesDir); + console.log(chalk.dim(` Removed legacy references directory`)); + } } /** @@ -244,20 +115,7 @@ class RovoDevSetup extends BaseIdeSetup { return false; } - // Check for BMAD agents in subagents directory - const subagentsDir = path.join(rovoDevDir, this.subagentsDir); - if (await fs.pathExists(subagentsDir)) { - try { - const entries = await fs.readdir(subagentsDir); - if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) { - return true; - } - } catch { - // Continue checking other directories - } - } - - // Check for BMAD workflows in workflows directory + // Check for BMAD files in workflows directory const workflowsDir = path.join(rovoDevDir, this.workflowsDir); if (await fs.pathExists(workflowsDir)) { try { @@ -266,25 +124,64 @@ class RovoDevSetup extends BaseIdeSetup { return true; } } catch { - // Continue checking other directories - } - } - - // Check for BMAD tasks/tools in references directory - const referencesDir = path.join(rovoDevDir, this.referencesDir); - if (await fs.pathExists(referencesDir)) { - try { - const entries = await fs.readdir(referencesDir); - if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) { - return true; - } - } catch { - // Continue + // Continue checking } } return false; } + + /** + * Install a custom agent launcher for Rovo Dev + * @param {string} projectDir - Project directory + * @param {string} agentName - Agent name (e.g., "fred-commit-poet") + * @param {string} agentPath - Path to compiled agent (relative to project root) + * @param {Object} metadata - Agent metadata + * @returns {Object|null} Installation result + */ + async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { + const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir); + + if (!(await this.exists(path.join(projectDir, this.configDir)))) { + return null; + } + + await this.ensureDir(workflowsDir); + + const launcherContent = `--- +name: ${agentName} +description: Custom BMAD agent: ${agentName} +--- + +# ${agentName} Custom Agent + +**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! + +This is a launcher for the custom BMAD agent "${agentName}". + +## Usage +1. First run: \`${agentPath}\` to load the complete agent +2. Then use this workflow as ${agentName} + +The agent will follow the persona and instructions from the main agent file. + +--- + +*Generated by BMAD Method*`; + + // Use flat naming: bmad-custom-agent-agentname.md + const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.md`; + const launcherPath = path.join(workflowsDir, fileName); + + await fs.writeFile(launcherPath, launcherContent, 'utf8'); + + return { + ide: 'rovo-dev', + path: path.relative(projectDir, launcherPath), + command: fileName.replace('.md', ''), + type: 'custom-agent-launcher', + }; + } } module.exports = { RovoDevSetup }; diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 29319af8..1d2b5df8 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -1,6 +1,5 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); /** @@ -65,9 +64,8 @@ class AgentCommandGenerator { .replaceAll('{{name}}', agent.name) .replaceAll('{{module}}', agent.module) .replaceAll('{{path}}', agentPathInModule) - .replaceAll('{{description}}', agent.description || `${agent.name} agent`) - .replaceAll('_bmad', this.bmadFolderName) - .replaceAll('_bmad', '_bmad'); + .replaceAll('{{relativePath}}', path.join(agent.module, 'agents', agentPathInModule)) + .replaceAll('{{description}}', agent.description || `${agent.name} agent`); } /** @@ -109,7 +107,7 @@ class AgentCommandGenerator { // Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md const flatName = toColonPath(artifact.relativePath); const launcherPath = path.join(baseCommandsDir, flatName); - await fs.ensureDir(path.dirname(launcherPath)); + await fs.ensureDir(baseCommandsDir); await fs.writeFile(launcherPath, artifact.content); writtenCount++; } @@ -119,8 +117,8 @@ class AgentCommandGenerator { } /** - * Write agent launcher artifacts using underscore format (Windows-compatible) - * Creates flat files like: bmad_bmm_pm.md + * Write agent launcher artifacts using dash format + * Creates flat files like: bmad-bmm-agent-pm.md * * @param {string} baseCommandsDir - Base commands directory for the IDE * @param {Array} artifacts - Agent launcher artifacts @@ -131,10 +129,10 @@ class AgentCommandGenerator { for (const artifact of artifacts) { if (artifact.type === 'agent-launcher') { - // Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md + // Convert relativePath to dash format: bmm/agents/pm.md → bmad-bmm-agent-pm.md const flatName = toDashPath(artifact.relativePath); const launcherPath = path.join(baseCommandsDir, flatName); - await fs.ensureDir(path.dirname(launcherPath)); + await fs.ensureDir(baseCommandsDir); await fs.writeFile(launcherPath, artifact.content); writtenCount++; } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index 6280f04d..d020f3d3 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -37,6 +37,7 @@ function toUnderscoreName(module, type, name, fileExtension = DEFAULT_FILE_EXTEN * Convert relative path to flat underscore-separated name * Converts: 'bmm/agents/pm.md' → 'bmad_bmm_agent_pm.md' * Converts: 'bmm/workflows/correct-course.md' → 'bmad_bmm_correct-course.md' + * Converts: 'bmad_bmb/agents/agent-builder.md' → 'bmad_bmb_agent_agent-builder.md' (bmad prefix already in module) * Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix) * * @param {string} relativePath - Path like 'bmm/agents/pm.md' @@ -54,8 +55,14 @@ function toUnderscorePath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) const type = parts[1]; const name = parts.slice(2).join('_'); - // Use toUnderscoreName for consistency - return toUnderscoreName(module, type, name, fileExtension); + const isAgent = type === AGENT_SEGMENT; + // For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md' + if (module === 'core') { + return isAgent ? `bmad_agent_${name}${fileExtension}` : `bmad_${name}${fileExtension}`; + } + // If module already starts with 'bmad_', don't add another prefix + const prefix = module.startsWith('bmad_') ? '' : 'bmad_'; + return isAgent ? `${prefix}${module}_agent_${name}${fileExtension}` : `${prefix}${module}_${name}${fileExtension}`; } /** @@ -168,6 +175,7 @@ function toColonPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { * Convert relative path to flat dash-separated name * Converts: 'bmm/agents/pm.md' → 'bmad-bmm-agent-pm.md' * Converts: 'bmm/workflows/correct-course' → 'bmad-bmm-correct-course.md' + * Converts: 'bmad-bmb/agents/agent-builder.md' → 'bmad-bmb-agent-agent-builder.md' (bmad prefix already in module) * @param {string} relativePath - Path like 'bmm/agents/pm.md' * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot * @returns {string} Flat filename like 'bmad-bmm-agent-pm.md' @@ -188,7 +196,9 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) { if (module === 'core') { return isAgent ? `bmad-agent-${name}${fileExtension}` : `bmad-${name}${fileExtension}`; } - return isAgent ? `bmad-${module}-agent-${name}${fileExtension}` : `bmad-${module}-${name}${fileExtension}`; + // If module already starts with 'bmad-', don't add another prefix + const prefix = module.startsWith('bmad-') ? '' : 'bmad-'; + return isAgent ? `${prefix}${module}-agent-${name}${fileExtension}` : `${prefix}${module}-${name}${fileExtension}`; } module.exports = { diff --git a/tools/cli/installers/lib/ide/shared/unified-installer.js b/tools/cli/installers/lib/ide/shared/unified-installer.js index ef93dcd3..fec04944 100644 --- a/tools/cli/installers/lib/ide/shared/unified-installer.js +++ b/tools/cli/installers/lib/ide/shared/unified-installer.js @@ -40,6 +40,7 @@ const TemplateType = { WINDSURF: 'windsurf', // YAML with auto_execution_mode AUGMENT: 'augment', // YAML frontmatter GEMINI: 'gemini', // TOML frontmatter with description/prompt + QWEN: 'qwen', // TOML frontmatter with description/prompt (same as Gemini) COPILOT: 'copilot', // YAML with tools array for GitHub Copilot }; @@ -209,7 +210,8 @@ class UnifiedInstaller { content = this.applyTemplate(artifact, content, templateType); } - await fs.ensureDir(path.dirname(targetPath)); + // For flat files, just ensure targetDir exists (no nested dirs needed) + await fs.ensureDir(targetDir); await fs.writeFile(targetPath, content, 'utf8'); written++; } @@ -254,6 +256,11 @@ class UnifiedInstaller { return this.addCopilotFrontmatter(artifact, content); } + case TemplateType.QWEN: { + // Add Qwen TOML frontmatter (same as Gemini) + return this.addGeminiFrontmatter(artifact, content); + } + default: { return content; } diff --git a/tools/cli/installers/lib/ide/templates/agent-command-template.md b/tools/cli/installers/lib/ide/templates/agent-command-template.md index 89713631..23974f0d 100644 --- a/tools/cli/installers/lib/ide/templates/agent-command-template.md +++ b/tools/cli/installers/lib/ide/templates/agent-command-template.md @@ -6,7 +6,7 @@ description: '{{description}}' You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. -1. LOAD the FULL agent file from @_bmad/{{module}}/agents/{{path}} +1. LOAD the FULL agent file from @_bmad/{{relativePath}} 2. READ its entire contents - this contains the complete agent persona, menu, and instructions 3. Execute ALL activation steps exactly as written in the agent file 4. Follow the agent's persona and menu system precisely diff --git a/tools/cli/installers/lib/ide/templates/workflow-commander.md b/tools/cli/installers/lib/ide/templates/workflow-commander.md index 3645c1a2..7355bc73 100644 --- a/tools/cli/installers/lib/ide/templates/workflow-commander.md +++ b/tools/cli/installers/lib/ide/templates/workflow-commander.md @@ -1,4 +1,5 @@ --- +name: '{{name}}' description: '{{description}}' ---