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
*/
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');

View File

@@ -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');

View File

@@ -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,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
*/
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(bmadCommandsDir)) {
await fs.remove(bmadCommandsDir);
console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`));
if (await fs.pathExists(commandsDir)) {
// Remove any bmad* files from the commands directory
const entries = await fs.readdir(commandsDir);
for (const entry of entries) {
if (entry.startsWith('bmad')) {
await fs.remove(path.join(commandsDir, entry));
}
}
}
// Also remove legacy bmad subfolder if it exists
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
if (await fs.pathExists(bmadCommandsDir)) {
await fs.remove(bmadCommandsDir);
console.log(chalk.dim(` Cleaned up existing BMAD configuration from Qwen Code`));
}
const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method');
if (await fs.pathExists(oldBmadMethodDir)) {
await fs.remove(oldBmadMethodDir);
console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`));
}
const oldBMadDir = path.join(projectDir, this.configDir, 'BMad');
if (await fs.pathExists(oldBMadDir)) {
await fs.remove(oldBMadDir);
console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`));
@@ -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',
};
}

View File

@@ -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 || []);
let agentCount = 0;
for (const artifact of agentArtifacts) {
const subagentFilename = `bmad-${artifact.module}-${artifact.name}.md`;
const targetPath = path.join(subagentsDir, subagentFilename);
const subagentContent = this.convertToRovoDevSubagent(artifact.content, artifact.name, artifact.module);
await this.writeFile(targetPath, subagentContent);
agentCount++;
}
// Generate and install workflows
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
let workflowCount = 0;
for (const artifact of workflowArtifacts) {
if (artifact.type === 'workflow-command') {
const workflowFilename = path.basename(artifact.relativePath);
const targetPath = path.join(workflowsDir, workflowFilename);
await this.writeFile(targetPath, artifact.content);
workflowCount++;
}
}
// Generate and install tasks and tools
const taskToolGen = new TaskToolCommandGenerator();
const { tasks: taskCount, tools: toolCount } = await this.generateTaskToolReferences(bmadDir, referencesDir, taskToolGen);
// Summary output
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${agentCount} agents installed to .rovodev/subagents/`));
if (workflowCount > 0) {
console.log(chalk.dim(` - ${workflowCount} workflows installed to .rovodev/workflows/`));
}
if (taskCount + toolCount > 0) {
console.log(
chalk.dim(` - ${taskCount + toolCount} tasks/tools installed to .rovodev/references/ (${taskCount} tasks, ${toolCount} tools)`),
// Use the unified installer - all artifacts go to workflows folder as flat files
const installer = new UnifiedInstaller(this.bmadFolderName);
const counts = await installer.install(
projectDir,
bmadDir,
{
targetDir: workflowsDir,
namingStyle: NamingStyle.FLAT_DASH,
templateType: TemplateType.CLAUDE,
},
options.selectedModules || [],
);
console.log(chalk.green(`${this.name} configured:`));
console.log(chalk.dim(` - ${counts.agents} agents installed`));
if (counts.workflows > 0) {
console.log(chalk.dim(` - ${counts.workflows} workflows installed`));
}
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`));
if (counts.tasks + counts.tools > 0) {
console.log(chalk.dim(` - ${counts.tasks + counts.tools} tasks/tools installed (${counts.tasks} tasks, ${counts.tools} tools)`));
}
console.log(chalk.dim(` - ${counts.total} files written to ${path.relative(projectDir, workflowsDir)}`));
console.log(chalk.yellow(`\n Note: All BMAD items are available in .rovodev/workflows/`));
console.log(chalk.dim(` - Access items by typing @ in Rovo Dev to see available files`));
return {
success: true,
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 };
// 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`));
}
/**
* 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(/<instructions>([\s\S]*?)<\/instructions>/);
if (instructionsMatch) {
systemPrompt += '\n\n' + instructionsMatch[1].trim();
// 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`));
}
// 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;
}
// 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 };

View File

@@ -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++;
}

View File

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

View File

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

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.
<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
3. Execute ALL activation steps exactly as written in the agent file
4. Follow the agent's persona and menu system precisely

View File

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