installer standardization

This commit is contained in:
Brian Madison
2026-01-24 23:05:43 -06:00
parent b102694c64
commit b4f230f565
10 changed files with 243 additions and 707 deletions

View File

@@ -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

View File

@@ -91,10 +91,13 @@ class AntigravitySetup extends BaseIdeSetup {
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
*/ */
async cleanup(projectDir) { 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)) { if (await fs.pathExists(workflowsDir)) {
await fs.remove(bmadWorkflowsDir); 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}`)); console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`));
} }
} }
@@ -115,11 +118,9 @@ class AntigravitySetup extends BaseIdeSetup {
await this.cleanup(projectDir); await this.cleanup(projectDir);
// Create .agent/workflows directory structure // Create .agent/workflows directory structure
const agentDir = path.join(projectDir, this.configDir); const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
const workflowsDir = path.join(agentDir, this.workflowsDir);
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
await this.ensureDir(bmadWorkflowsDir); await this.ensureDir(workflowsDir);
// Generate agent launchers using AgentCommandGenerator // Generate agent launchers using AgentCommandGenerator
// This creates small launcher files that reference the actual agents in _bmad/ // 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 // Write agent launcher files with FLATTENED naming using shared utility
// Antigravity ignores directory structure, so we flatten to: bmad_module_name.md // Antigravity ignores directory structure, so we flatten to: bmad_module_name.md
// This creates slash commands like /bmad_bmm_dev instead of /dev // 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 // Process Antigravity specific injections for installed modules
// Use pre-collected configuration if available, or skip if already configured // 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); const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
// Write workflow-command artifacts with FLATTENED naming using shared utility // 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!) // Generate task and tool commands using FLAT naming (not nested!)
// Use the new generateDashTaskToolCommands method with explicit target directory // Use the new generateDashTaskToolCommands method with explicit target directory
const taskToolGen = new TaskToolCommandGenerator(); 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed`)); 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)`)); console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad_module_agents_name)`));
return { return {
@@ -430,12 +431,10 @@ class AntigravitySetup extends BaseIdeSetup {
* @returns {Object} Installation result * @returns {Object} Installation result
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
// Create .agent/workflows/bmad directory structure (same as regular agents) // Create .agent/workflows directory structure
const agentDir = path.join(projectDir, this.configDir); const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
const workflowsDir = path.join(agentDir, this.workflowsDir);
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
await fs.ensureDir(bmadWorkflowsDir); await fs.ensureDir(workflowsDir);
// Create custom agent launcher with same pattern as regular agents // Create custom agent launcher with same pattern as regular agents
const launcherContent = `name: '${agentName}' const launcherContent = `name: '${agentName}'
@@ -458,7 +457,7 @@ usage: |
// Use underscore format: bmad_custom_fred-commit-poet.md // Use underscore format: bmad_custom_fred-commit-poet.md
const fileName = customAgentDashName(agentName); const fileName = customAgentDashName(agentName);
const launcherPath = path.join(bmadWorkflowsDir, fileName); const launcherPath = path.join(workflowsDir, fileName);
// Write the launcher file // Write the launcher file
await fs.writeFile(launcherPath, launcherContent, 'utf8'); await fs.writeFile(launcherPath, launcherContent, 'utf8');

View File

@@ -25,30 +25,21 @@ class IFlowSetup extends BaseIdeSetup {
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`)); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .iflow/commands/bmad directory structure // Clean up old BMAD installation first
const iflowDir = path.join(projectDir, this.configDir); await this.cleanup(projectDir);
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); // Create .iflow/commands directory structure (flat files, no bmad subfolder)
await this.ensureDir(tasksDir); const iflowDir = path.join(projectDir, this.configDir);
await this.ensureDir(workflowsDir); const commandsDir = path.join(iflowDir, this.commandsDir);
await this.ensureDir(commandsDir);
// Generate agent launchers // Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Setup agents as commands // Setup agents as commands (flat files with dash naming)
let agentCount = 0; const agentCount = await agentGen.writeDashArtifacts(commandsDir, agentArtifacts);
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) // Get tasks and workflows (ALL workflows now generate commands)
const tasks = await this.getTasks(bmadDir); const tasks = await this.getTasks(bmadDir);
@@ -57,26 +48,11 @@ class IFlowSetup extends BaseIdeSetup {
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
// Setup tasks as commands // Setup workflows as commands (flat files with dash naming)
let taskCount = 0; const workflowCount = await workflowGenerator.writeDashArtifacts(commandsDir, workflowArtifacts);
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`); // TODO: tasks not yet implemented with flat naming
await this.writeFile(targetPath, commandContent); const taskCount = 0;
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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agent commands created`)); console.log(chalk.dim(` - ${agentCount} agent commands created`));
@@ -132,11 +108,20 @@ Part of the BMAD ${task.module.toUpperCase()} module.
* Cleanup iFlow configuration * Cleanup iFlow configuration
*/ */
async cleanup(projectDir) { async cleanup(projectDir) {
const fs = require('fs-extra'); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad'); const bmadFolder = path.join(commandsDir, 'bmad');
if (await fs.pathExists(bmadCommandsDir)) { // Remove old bmad subfolder if it exists
await fs.remove(bmadCommandsDir); 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`)); 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 * @returns {Object} Installation result
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const iflowDir = path.join(projectDir, this.configDir); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
const bmadCommandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
// Create .iflow/commands/bmad directory if it doesn't exist // Create .iflow/commands directory if it doesn't exist
await fs.ensureDir(bmadCommandsDir); await fs.ensureDir(commandsDir);
// Create custom agent launcher // Create custom agent launcher
const launcherContent = `# ${agentName} Custom Agent 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*`; *Generated by BMAD Method*`;
const fileName = `custom-${agentName.toLowerCase()}.md`; const { customAgentDashName } = require('./shared/path-utils');
const launcherPath = path.join(bmadCommandsDir, fileName); const fileName = customAgentDashName(agentName);
const launcherPath = path.join(commandsDir, fileName);
// Write the launcher file // Write the launcher file
await fs.writeFile(launcherPath, launcherContent, 'utf8'); await fs.writeFile(launcherPath, launcherContent, 'utf8');

View File

@@ -2,19 +2,17 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk'); const chalk = require('chalk');
const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts'); const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
/** /**
* Qwen Code setup handler * Qwen Code setup handler
* Creates TOML command files in .qwen/commands/BMad/ * Creates TOML command files in .qwen/commands/
*/ */
class QwenSetup extends BaseIdeSetup { class QwenSetup extends BaseIdeSetup {
constructor() { constructor() {
super('qwen', 'Qwen Code'); super('qwen', 'Qwen Code');
this.configDir = '.qwen'; this.configDir = '.qwen';
this.commandsDir = 'commands'; this.commandsDir = 'commands';
this.bmadDir = 'bmad';
} }
/** /**
@@ -26,118 +24,43 @@ class QwenSetup extends BaseIdeSetup {
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`)); 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 qwenDir = path.join(projectDir, this.configDir);
const commandsDir = path.join(qwenDir, this.commandsDir); 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 // Update existing settings.json if present
await this.updateSettings(qwenDir); await this.updateSettings(qwenDir);
// Clean up old configuration if exists // Clean up old configuration
await this.cleanupOldConfig(qwenDir); await this.cleanupOldConfig(qwenDir);
await this.cleanup(projectDir);
// Generate agent launchers // Use the unified installer with QWEN template for TOML format
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const installer = new UnifiedInstaller(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const counts = await installer.install(
projectDir,
// Get tasks, tools, and workflows (standalone only for tools/workflows) bmadDir,
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); {
const tools = await this.getTools(bmadDir, true); targetDir: commandsDir,
const workflows = await this.getWorkflows(bmadDir, true); namingStyle: NamingStyle.FLAT_DASH,
templateType: TemplateType.QWEN,
// Create directories for each module (including standalone) fileExtension: '.toml',
const modules = new Set(); },
for (const item of [...agentArtifacts, ...tasks, ...tools, ...workflows]) modules.add(item.module); options.selectedModules || [],
);
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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents configured`)); console.log(chalk.dim(` - ${counts.agents} agents configured`));
console.log(chalk.dim(` - ${taskCount} tasks configured`)); console.log(chalk.dim(` - ${counts.tasks} tasks configured`));
console.log(chalk.dim(` - ${toolCount} tools configured`)); console.log(chalk.dim(` - ${counts.tools} tools configured`));
console.log(chalk.dim(` - ${workflowCount} workflows configured`)); console.log(chalk.dim(` - ${counts.workflows} workflows configured`));
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`)); console.log(chalk.dim(` - ${counts.total} TOML files written to ${path.relative(projectDir, commandsDir)}`));
return { return {
success: true, success: true,
agents: agentCount, ...counts,
tasks: taskCount,
tools: toolCount,
workflows: workflowCount,
}; };
} }
@@ -145,7 +68,6 @@ class QwenSetup extends BaseIdeSetup {
* Update settings.json to remove old agent references * Update settings.json to remove old agent references
*/ */
async updateSettings(qwenDir) { async updateSettings(qwenDir) {
const fs = require('fs-extra');
const settingsPath = path.join(qwenDir, 'settings.json'); const settingsPath = path.join(qwenDir, 'settings.json');
if (await fs.pathExists(settingsPath)) { if (await fs.pathExists(settingsPath)) {
@@ -180,7 +102,6 @@ class QwenSetup extends BaseIdeSetup {
* Clean up old configuration directories * Clean up old configuration directories
*/ */
async cleanupOldConfig(qwenDir) { async cleanupOldConfig(qwenDir) {
const fs = require('fs-extra');
const agentsDir = path.join(qwenDir, 'agents'); const agentsDir = path.join(qwenDir, 'agents');
const bmadMethodDir = path.join(qwenDir, 'bmad-method'); const bmadMethodDir = path.join(qwenDir, 'bmad-method');
const bmadDir = path.join(qwenDir, 'bmadDir'); const bmadDir = path.join(qwenDir, 'bmadDir');
@@ -201,114 +122,36 @@ 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('<agent');
const isTask = content.includes('<task');
const isTool = content.includes('<tool');
const isWorkflow = content.includes('workflow:') || content.includes('name:');
let description = '';
if (isAgent) {
// Extract agent title if available
const titleMatch = content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Agent: ${title}`;
} else if (isTask) {
// Extract task name if available
const nameMatch = content.match(/name="([^"]+)"/);
const taskName = nameMatch ? nameMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`;
} else if (isTool) {
// Extract tool name if available
const nameMatch = content.match(/name="([^"]+)"/);
const toolName = nameMatch ? nameMatch[1] : metadata.name;
description = `BMAD ${metadata.module.toUpperCase()} Tool: ${toolName}`;
} else if (isWorkflow) {
// Workflow
description = `BMAD ${metadata.module.toUpperCase()} Workflow: ${metadata.name}`;
} else {
description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`;
}
return `description = "${description}"
prompt = """
${prompt}
"""
`;
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/** /**
* Cleanup Qwen configuration * Cleanup Qwen configuration
*/ */
async cleanup(projectDir) { async cleanup(projectDir) {
const fs = require('fs-extra'); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
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)) { if (await fs.pathExists(commandsDir)) {
await fs.remove(bmadCommandsDir); // Remove any bmad* files from the commands directory
console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`)); const entries = await fs.readdir(commandsDir);
for (const entry of entries) {
if (entry.startsWith('bmad')) {
await fs.remove(path.join(commandsDir, entry));
}
}
} }
// Also remove legacy bmad subfolder if it exists
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
if (await fs.pathExists(bmadCommandsDir)) {
await fs.remove(bmadCommandsDir);
console.log(chalk.dim(` Cleaned up existing BMAD configuration from Qwen Code`));
}
const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method');
if (await fs.pathExists(oldBmadMethodDir)) { if (await fs.pathExists(oldBmadMethodDir)) {
await fs.remove(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)) { if (await fs.pathExists(oldBMadDir)) {
await fs.remove(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 * @returns {Object} Installation result
*/ */
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const qwenDir = path.join(projectDir, this.configDir); const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
const commandsDir = path.join(qwenDir, this.commandsDir);
const bmadCommandsDir = path.join(commandsDir, this.bmadDir);
// Create .qwen/commands/BMad directory if it doesn't exist // Create .qwen/commands directory if it doesn't exist
await fs.ensureDir(bmadCommandsDir); 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 const launcherContent = `# ${agentName} Custom Agent
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete 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*`; *Generated by BMAD Method*`;
// Use Qwen's TOML conversion method // Convert to TOML format using the same method as UnifiedInstaller
const tomlContent = this.processAgentLauncherContent(launcherContent, { const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
name: agentName, const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '').trim();
module: 'custom', const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`);
});
const fileName = `custom-${agentName.toLowerCase()}.toml`; const tomlContent = `description = "BMAD Custom Agent: ${agentName}"
const launcherPath = path.join(bmadCommandsDir, fileName); 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 // Write the launcher file
await fs.writeFile(launcherPath, tomlContent, 'utf8'); await fs.writeFile(launcherPath, tomlContent, 'utf8');
@@ -363,7 +210,7 @@ The agent will follow the persona and instructions from the main agent file.
return { return {
ide: 'qwen', ide: 'qwen',
path: path.relative(projectDir, launcherPath), path: path.relative(projectDir, launcherPath),
command: agentName, command: fileName.replace('.toml', ''),
type: 'custom-agent-launcher', type: 'custom-agent-launcher',
}; };
} }

View File

@@ -2,69 +2,19 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
/** /**
* Rovo Dev IDE setup handler * Rovo Dev IDE setup handler
* *
* Installs BMAD agents as Rovo Dev subagents in .rovodev/subagents/ * Uses UnifiedInstaller for all artifact installation with flat file structure.
* Installs workflows and tasks/tools as reference guides in .rovodev/ * All BMAD artifacts are installed to .rovodev/workflows/ as flat files.
* Rovo Dev automatically discovers agents and integrates with BMAD like other IDEs
*/ */
class RovoDevSetup extends BaseIdeSetup { class RovoDevSetup extends BaseIdeSetup {
constructor() { constructor() {
super('rovo-dev', 'Atlassian Rovo Dev', false); super('rovo-dev', 'Atlassian Rovo Dev', false);
this.configDir = '.rovodev'; this.configDir = '.rovodev';
this.subagentsDir = 'subagents';
this.workflowsDir = 'workflows'; 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 // Create .rovodev directory structure
const rovoDevDir = path.join(projectDir, this.configDir); const rovoDevDir = path.join(projectDir, this.configDir);
const subagentsDir = path.join(rovoDevDir, this.subagentsDir);
const workflowsDir = path.join(rovoDevDir, this.workflowsDir); 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(workflowsDir);
await this.ensureDir(referencesDir);
// Generate and install agents // Use the unified installer - all artifacts go to workflows folder as flat files
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const installer = new UnifiedInstaller(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); const counts = await installer.install(
projectDir,
let agentCount = 0; bmadDir,
for (const artifact of agentArtifacts) { {
const subagentFilename = `bmad-${artifact.module}-${artifact.name}.md`; targetDir: workflowsDir,
const targetPath = path.join(subagentsDir, subagentFilename); namingStyle: NamingStyle.FLAT_DASH,
const subagentContent = this.convertToRovoDevSubagent(artifact.content, artifact.name, artifact.module); templateType: TemplateType.CLAUDE,
await this.writeFile(targetPath, subagentContent); },
agentCount++; options.selectedModules || [],
}
// 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.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${counts.agents} agents installed`));
if (counts.workflows > 0) {
console.log(chalk.dim(` - ${counts.workflows} workflows installed`));
} }
console.log(chalk.yellow(`\n Note: Agents are automatically discovered by Rovo Dev`)); if (counts.tasks + counts.tools > 0) {
console.log(chalk.dim(` - Access agents by typing @ in Rovo Dev to see available options`)); console.log(chalk.dim(` - ${counts.tasks + counts.tools} tasks/tools installed (${counts.tasks} tasks, ${counts.tools} tools)`));
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 { return {
success: true, success: true,
agents: agentCount, ...counts,
workflows: workflowCount,
tasks: taskCount,
tools: toolCount,
}; };
} }
/** /**
* Generate task and tool reference guides * Cleanup old BMAD installation before reinstalling
* @param {string} bmadDir - BMAD directory * @param {string} projectDir - Project directory
* @param {string} referencesDir - References directory
* @param {TaskToolCommandGenerator} taskToolGen - Generator instance
*/ */
async generateTaskToolReferences(bmadDir, referencesDir, taskToolGen) { async cleanup(projectDir) {
const tasks = await taskToolGen.loadTaskManifest(bmadDir); const rovoDevDir = path.join(projectDir, this.configDir);
const tools = await taskToolGen.loadToolManifest(bmadDir);
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; if (!(await fs.pathExists(rovoDevDir))) {
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; return;
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; // Clean BMAD files from workflows directory
for (const tool of standaloneTools) { const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
const commandContent = taskToolGen.generateCommandContent(tool, 'tool'); if (await fs.pathExists(workflowsDir)) {
const targetPath = path.join(referencesDir, `bmad-tool-${tool.module}-${tool.name}.md`); const entries = await fs.readdir(workflowsDir);
await this.writeFile(targetPath, commandContent); const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
toolCount++;
for (const file of bmadFiles) {
await fs.remove(path.join(workflowsDir, file));
}
} }
return { tasks: taskCount, tools: toolCount }; // Remove legacy subagents directory
const subagentsDir = path.join(rovoDevDir, 'subagents');
if (await fs.pathExists(subagentsDir)) {
await fs.remove(subagentsDir);
console.log(chalk.dim(` Removed legacy subagents directory`));
} }
/** // Remove legacy references directory
* Convert BMAD agent launcher to Rovo Dev subagent format const referencesDir = path.join(rovoDevDir, 'references');
* if (await fs.pathExists(referencesDir)) {
* Rovo Dev subagents use Markdown files with YAML frontmatter containing: await fs.remove(referencesDir);
* - name: Unique identifier for the subagent console.log(chalk.dim(` Removed legacy references directory`));
* - 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(/<instructions>([\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;
} }
/** /**
@@ -244,20 +115,7 @@ class RovoDevSetup extends BaseIdeSetup {
return false; return false;
} }
// Check for BMAD agents in subagents directory // Check for BMAD files in workflows 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); const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
if (await fs.pathExists(workflowsDir)) { if (await fs.pathExists(workflowsDir)) {
try { try {
@@ -266,25 +124,64 @@ class RovoDevSetup extends BaseIdeSetup {
return true; return true;
} }
} catch { } catch {
// Continue checking other directories // Continue checking
}
}
// 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; 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 }; module.exports = { RovoDevSetup };

View File

@@ -1,6 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils');
/** /**
@@ -65,9 +64,8 @@ class AgentCommandGenerator {
.replaceAll('{{name}}', agent.name) .replaceAll('{{name}}', agent.name)
.replaceAll('{{module}}', agent.module) .replaceAll('{{module}}', agent.module)
.replaceAll('{{path}}', agentPathInModule) .replaceAll('{{path}}', agentPathInModule)
.replaceAll('{{description}}', agent.description || `${agent.name} agent`) .replaceAll('{{relativePath}}', path.join(agent.module, 'agents', agentPathInModule))
.replaceAll('_bmad', this.bmadFolderName) .replaceAll('{{description}}', agent.description || `${agent.name} agent`);
.replaceAll('_bmad', '_bmad');
} }
/** /**
@@ -109,7 +107,7 @@ class AgentCommandGenerator {
// Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md // Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md
const flatName = toColonPath(artifact.relativePath); const flatName = toColonPath(artifact.relativePath);
const launcherPath = path.join(baseCommandsDir, flatName); const launcherPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(launcherPath)); await fs.ensureDir(baseCommandsDir);
await fs.writeFile(launcherPath, artifact.content); await fs.writeFile(launcherPath, artifact.content);
writtenCount++; writtenCount++;
} }
@@ -119,8 +117,8 @@ class AgentCommandGenerator {
} }
/** /**
* Write agent launcher artifacts using underscore format (Windows-compatible) * Write agent launcher artifacts using dash format
* Creates flat files like: bmad_bmm_pm.md * Creates flat files like: bmad-bmm-agent-pm.md
* *
* @param {string} baseCommandsDir - Base commands directory for the IDE * @param {string} baseCommandsDir - Base commands directory for the IDE
* @param {Array} artifacts - Agent launcher artifacts * @param {Array} artifacts - Agent launcher artifacts
@@ -131,10 +129,10 @@ class AgentCommandGenerator {
for (const artifact of artifacts) { for (const artifact of artifacts) {
if (artifact.type === 'agent-launcher') { 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 flatName = toDashPath(artifact.relativePath);
const launcherPath = path.join(baseCommandsDir, flatName); const launcherPath = path.join(baseCommandsDir, flatName);
await fs.ensureDir(path.dirname(launcherPath)); await fs.ensureDir(baseCommandsDir);
await fs.writeFile(launcherPath, artifact.content); await fs.writeFile(launcherPath, artifact.content);
writtenCount++; writtenCount++;
} }

View File

@@ -37,6 +37,7 @@ function toUnderscoreName(module, type, name, fileExtension = DEFAULT_FILE_EXTEN
* Convert relative path to flat underscore-separated name * Convert relative path to flat underscore-separated name
* Converts: 'bmm/agents/pm.md' → 'bmad_bmm_agent_pm.md' * Converts: 'bmm/agents/pm.md' → 'bmad_bmm_agent_pm.md'
* Converts: 'bmm/workflows/correct-course.md' → 'bmad_bmm_correct-course.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) * Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix)
* *
* @param {string} relativePath - Path like 'bmm/agents/pm.md' * @param {string} relativePath - Path like 'bmm/agents/pm.md'
@@ -54,8 +55,14 @@ function toUnderscorePath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION)
const type = parts[1]; const type = parts[1];
const name = parts.slice(2).join('_'); const name = parts.slice(2).join('_');
// Use toUnderscoreName for consistency const isAgent = type === AGENT_SEGMENT;
return toUnderscoreName(module, type, name, fileExtension); // 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 * Convert relative path to flat dash-separated name
* Converts: 'bmm/agents/pm.md' → 'bmad-bmm-agent-pm.md' * Converts: 'bmm/agents/pm.md' → 'bmad-bmm-agent-pm.md'
* Converts: 'bmm/workflows/correct-course' → 'bmad-bmm-correct-course.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} relativePath - Path like 'bmm/agents/pm.md'
* @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot * @param {string} [fileExtension=DEFAULT_FILE_EXTENSION] - File extension including dot
* @returns {string} Flat filename like 'bmad-bmm-agent-pm.md' * @returns {string} Flat filename like 'bmad-bmm-agent-pm.md'
@@ -188,7 +196,9 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) {
if (module === 'core') { if (module === 'core') {
return isAgent ? `bmad-agent-${name}${fileExtension}` : `bmad-${name}${fileExtension}`; 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 = { module.exports = {

View File

@@ -40,6 +40,7 @@ const TemplateType = {
WINDSURF: 'windsurf', // YAML with auto_execution_mode WINDSURF: 'windsurf', // YAML with auto_execution_mode
AUGMENT: 'augment', // YAML frontmatter AUGMENT: 'augment', // YAML frontmatter
GEMINI: 'gemini', // TOML frontmatter with description/prompt 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 COPILOT: 'copilot', // YAML with tools array for GitHub Copilot
}; };
@@ -209,7 +210,8 @@ class UnifiedInstaller {
content = this.applyTemplate(artifact, content, templateType); 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'); await fs.writeFile(targetPath, content, 'utf8');
written++; written++;
} }
@@ -254,6 +256,11 @@ class UnifiedInstaller {
return this.addCopilotFrontmatter(artifact, content); return this.addCopilotFrontmatter(artifact, content);
} }
case TemplateType.QWEN: {
// Add Qwen TOML frontmatter (same as Gemini)
return this.addGeminiFrontmatter(artifact, content);
}
default: { default: {
return content; return content;
} }

View File

@@ -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. You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
<agent-activation CRITICAL="TRUE"> <agent-activation CRITICAL="TRUE">
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 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 3. Execute ALL activation steps exactly as written in the agent file
4. Follow the agent's persona and menu system precisely 4. Follow the agent's persona and menu system precisely

View File

@@ -1,4 +1,5 @@
--- ---
name: '{{name}}'
description: '{{description}}' description: '{{description}}'
--- ---