mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
installer fix
This commit is contained in:
@@ -161,56 +161,39 @@ class Installer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
|
if (!toolConfig.skipIde && toolConfig.ides && toolConfig.ides.length > 0) {
|
||||||
|
// Ensure IDE manager is initialized
|
||||||
|
await this.ideManager.ensureInitialized();
|
||||||
|
|
||||||
// Determine which IDEs are newly selected (not previously configured)
|
// Determine which IDEs are newly selected (not previously configured)
|
||||||
const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
|
const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide));
|
||||||
|
|
||||||
if (newlySelectedIdes.length > 0) {
|
if (newlySelectedIdes.length > 0) {
|
||||||
console.log('\n'); // Add spacing before IDE questions
|
console.log('\n'); // Add spacing before IDE questions
|
||||||
|
|
||||||
|
// Collect configuration for IDEs that support it
|
||||||
for (const ide of newlySelectedIdes) {
|
for (const ide of newlySelectedIdes) {
|
||||||
// List of IDEs that have interactive prompts
|
|
||||||
//TODO: Why is this here, hardcoding this list here is bad, fix me!
|
|
||||||
const needsPrompts = ['claude-code', 'github-copilot', 'roo', 'cline', 'auggie', 'codex', 'qwen', 'gemini', 'rovo-dev'].includes(
|
|
||||||
ide,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (needsPrompts) {
|
|
||||||
// Get IDE handler and collect configuration
|
|
||||||
try {
|
try {
|
||||||
// Dynamically load the IDE setup module
|
const handler = this.ideManager.handlers.get(ide);
|
||||||
const ideModule = require(`../ide/${ide}`);
|
|
||||||
|
|
||||||
// Get the setup class (handle different export formats)
|
if (!handler) {
|
||||||
let SetupClass;
|
console.warn(chalk.yellow(`Warning: IDE '${ide}' handler not found`));
|
||||||
const className =
|
|
||||||
ide
|
|
||||||
.split('-')
|
|
||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
||||||
.join('') + 'Setup';
|
|
||||||
|
|
||||||
if (ideModule[className]) {
|
|
||||||
SetupClass = ideModule[className];
|
|
||||||
} else if (ideModule.default) {
|
|
||||||
SetupClass = ideModule.default;
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ideSetup = new SetupClass();
|
// Check if this IDE handler has a collectConfiguration method
|
||||||
|
// (custom installers like Codex, Kilo, Kiro-cli may have this)
|
||||||
// Check if this IDE has a collectConfiguration method
|
if (typeof handler.collectConfiguration === 'function') {
|
||||||
if (typeof ideSetup.collectConfiguration === 'function') {
|
|
||||||
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
console.log(chalk.cyan(`\nConfiguring ${ide}...`));
|
||||||
ideConfigurations[ide] = await ideSetup.collectConfiguration({
|
ideConfigurations[ide] = await handler.collectConfiguration({
|
||||||
selectedModules: selectedModules || [],
|
selectedModules: selectedModules || [],
|
||||||
projectDir,
|
projectDir,
|
||||||
bmadDir,
|
bmadDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
// Most config-driven IDEs don't need configuration - silently skip
|
||||||
// IDE doesn't have a setup file or collectConfiguration method
|
} catch (error) {
|
||||||
console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}`));
|
// IDE doesn't support configuration or has an error
|
||||||
}
|
console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}: ${error.message}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
423
tools/cli/installers/lib/ide/_config-driven.js
Normal file
423
tools/cli/installers/lib/ide/_config-driven.js
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
|
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||||
|
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config-driven IDE setup handler
|
||||||
|
*
|
||||||
|
* This class provides a standardized way to install BMAD artifacts to IDEs
|
||||||
|
* based on configuration in platform-codes.yaml. It eliminates the need for
|
||||||
|
* individual installer files for each IDE.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Config-driven from platform-codes.yaml
|
||||||
|
* - Template-based content generation
|
||||||
|
* - Multi-target installation support (e.g., GitHub Copilot)
|
||||||
|
* - Artifact type filtering (agents, workflows, tasks, tools)
|
||||||
|
*/
|
||||||
|
class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
|
constructor(platformCode, platformConfig) {
|
||||||
|
super(platformCode, platformConfig.name, platformConfig.preferred);
|
||||||
|
this.platformConfig = platformConfig;
|
||||||
|
this.installerConfig = platformConfig.installer || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main setup method - called by IdeManager
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {Object} options - Setup options
|
||||||
|
* @returns {Promise<Object>} Setup result
|
||||||
|
*/
|
||||||
|
async setup(projectDir, bmadDir, options = {}) {
|
||||||
|
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||||
|
|
||||||
|
// Clean up any old BMAD installation first
|
||||||
|
await this.cleanup(projectDir);
|
||||||
|
|
||||||
|
if (!this.installerConfig) {
|
||||||
|
return { success: false, reason: 'no-config' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multi-target installations (e.g., GitHub Copilot)
|
||||||
|
if (this.installerConfig.targets) {
|
||||||
|
return this.installToMultipleTargets(projectDir, bmadDir, this.installerConfig.targets, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single-target installations
|
||||||
|
if (this.installerConfig.target_dir) {
|
||||||
|
return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, reason: 'invalid-config' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install to a single target directory
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {Object} config - Installation configuration
|
||||||
|
* @param {Object} options - Setup options
|
||||||
|
* @returns {Promise<Object>} Installation result
|
||||||
|
*/
|
||||||
|
async installToTarget(projectDir, bmadDir, config, options) {
|
||||||
|
const { target_dir, template_type, artifact_types } = config;
|
||||||
|
const targetPath = path.join(projectDir, target_dir);
|
||||||
|
await this.ensureDir(targetPath);
|
||||||
|
|
||||||
|
const selectedModules = options.selectedModules || [];
|
||||||
|
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
||||||
|
|
||||||
|
// Install agents
|
||||||
|
if (!artifact_types || artifact_types.includes('agents')) {
|
||||||
|
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||||
|
const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
||||||
|
results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install workflows
|
||||||
|
if (!artifact_types || artifact_types.includes('workflows')) {
|
||||||
|
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||||
|
const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||||
|
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install tasks and tools
|
||||||
|
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
||||||
|
const taskToolGen = new TaskToolCommandGenerator();
|
||||||
|
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath);
|
||||||
|
results.tasks = taskToolResult.tasks || 0;
|
||||||
|
results.tools = taskToolResult.tools || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.printSummary(results, target_dir);
|
||||||
|
return { success: true, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install to multiple target directories
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {Array} targets - Array of target configurations
|
||||||
|
* @param {Object} options - Setup options
|
||||||
|
* @returns {Promise<Object>} Installation result
|
||||||
|
*/
|
||||||
|
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
||||||
|
const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
||||||
|
if (result.success) {
|
||||||
|
allResults.agents += result.results.agents || 0;
|
||||||
|
allResults.workflows += result.results.workflows || 0;
|
||||||
|
allResults.tasks += result.results.tasks || 0;
|
||||||
|
allResults.tools += result.results.tools || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, results: allResults };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write agent artifacts to target directory
|
||||||
|
* @param {string} targetPath - Target directory path
|
||||||
|
* @param {Array} artifacts - Agent artifacts
|
||||||
|
* @param {string} templateType - Template type to use
|
||||||
|
* @param {Object} config - Installation configuration
|
||||||
|
* @returns {Promise<number>} Count of artifacts written
|
||||||
|
*/
|
||||||
|
async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||||
|
// Try to load platform-specific template, fall back to default-agent
|
||||||
|
const template = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const artifact of artifacts) {
|
||||||
|
const content = this.renderTemplate(template, artifact);
|
||||||
|
const filename = this.generateFilename(artifact, 'agent');
|
||||||
|
const filePath = path.join(targetPath, filename);
|
||||||
|
await this.writeFile(filePath, content);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write workflow artifacts to target directory
|
||||||
|
* @param {string} targetPath - Target directory path
|
||||||
|
* @param {Array} artifacts - Workflow artifacts
|
||||||
|
* @param {string} templateType - Template type to use
|
||||||
|
* @param {Object} config - Installation configuration
|
||||||
|
* @returns {Promise<number>} Count of artifacts written
|
||||||
|
*/
|
||||||
|
async writeWorkflowArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const artifact of artifacts) {
|
||||||
|
if (artifact.type === 'workflow-command') {
|
||||||
|
// Use different template based on workflow type (YAML vs MD)
|
||||||
|
// Default to 'default' template type, but allow override via config
|
||||||
|
const workflowTemplateType = artifact.isYamlWorkflow
|
||||||
|
? config.yaml_workflow_template || `${templateType}-workflow-yaml`
|
||||||
|
: config.md_workflow_template || `${templateType}-workflow`;
|
||||||
|
|
||||||
|
// Fall back to default templates if specific ones don't exist
|
||||||
|
const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow';
|
||||||
|
const template = await this.loadTemplate(workflowTemplateType, 'workflow', config, finalTemplateType);
|
||||||
|
const content = this.renderTemplate(template, artifact);
|
||||||
|
const filename = this.generateFilename(artifact, 'workflow');
|
||||||
|
const filePath = path.join(targetPath, filename);
|
||||||
|
await this.writeFile(filePath, content);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load template based on type and configuration
|
||||||
|
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
||||||
|
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||||
|
* @param {Object} config - Installation configuration
|
||||||
|
* @param {string} fallbackTemplateType - Fallback template type if requested template not found
|
||||||
|
* @returns {Promise<string>} Template content
|
||||||
|
*/
|
||||||
|
async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) {
|
||||||
|
const { header_template, body_template } = config;
|
||||||
|
|
||||||
|
// Check for separate header/body templates
|
||||||
|
if (header_template || body_template) {
|
||||||
|
return await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load combined template
|
||||||
|
const templateName = `${templateType}-${artifactType}.md`;
|
||||||
|
const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
|
||||||
|
|
||||||
|
if (await fs.pathExists(templatePath)) {
|
||||||
|
return await fs.readFile(templatePath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default template (if provided)
|
||||||
|
if (fallbackTemplateType) {
|
||||||
|
const fallbackPath = path.join(__dirname, 'templates', 'combined', `${fallbackTemplateType}.md`);
|
||||||
|
if (await fs.pathExists(fallbackPath)) {
|
||||||
|
return await fs.readFile(fallbackPath, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ultimate fallback - minimal template
|
||||||
|
return this.getDefaultTemplate(artifactType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load split templates (header + body)
|
||||||
|
* @param {string} templateType - Template type
|
||||||
|
* @param {string} artifactType - Artifact type
|
||||||
|
* @param {string} headerTpl - Header template name
|
||||||
|
* @param {string} bodyTpl - Body template name
|
||||||
|
* @returns {Promise<string>} Combined template content
|
||||||
|
*/
|
||||||
|
async loadSplitTemplates(templateType, artifactType, headerTpl, bodyTpl) {
|
||||||
|
let header = '';
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
// Load header template
|
||||||
|
if (headerTpl) {
|
||||||
|
const headerPath = path.join(__dirname, 'templates', 'split', headerTpl);
|
||||||
|
if (await fs.pathExists(headerPath)) {
|
||||||
|
header = await fs.readFile(headerPath, 'utf8');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use default header for template type
|
||||||
|
const defaultHeaderPath = path.join(__dirname, 'templates', 'split', templateType, 'header.md');
|
||||||
|
if (await fs.pathExists(defaultHeaderPath)) {
|
||||||
|
header = await fs.readFile(defaultHeaderPath, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load body template
|
||||||
|
if (bodyTpl) {
|
||||||
|
const bodyPath = path.join(__dirname, 'templates', 'split', bodyTpl);
|
||||||
|
if (await fs.pathExists(bodyPath)) {
|
||||||
|
body = await fs.readFile(bodyPath, 'utf8');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use default body for template type
|
||||||
|
const defaultBodyPath = path.join(__dirname, 'templates', 'split', templateType, 'body.md');
|
||||||
|
if (await fs.pathExists(defaultBodyPath)) {
|
||||||
|
body = await fs.readFile(defaultBodyPath, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine header and body
|
||||||
|
return `${header}\n${body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default minimal template
|
||||||
|
* @param {string} artifactType - Artifact type
|
||||||
|
* @returns {string} Default template
|
||||||
|
*/
|
||||||
|
getDefaultTemplate(artifactType) {
|
||||||
|
if (artifactType === 'agent') {
|
||||||
|
return `---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
|
You must fully embody this agent's persona and follow all activation instructions exactly as specified.
|
||||||
|
|
||||||
|
<agent-activation CRITICAL="TRUE">
|
||||||
|
1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||||
|
3. FOLLOW every step in the <activation> section precisely
|
||||||
|
</agent-activation>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{name}}
|
||||||
|
|
||||||
|
LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render template with artifact data
|
||||||
|
* @param {string} template - Template content
|
||||||
|
* @param {Object} artifact - Artifact data
|
||||||
|
* @returns {string} Rendered content
|
||||||
|
*/
|
||||||
|
renderTemplate(template, artifact) {
|
||||||
|
// Use the appropriate path property based on artifact type
|
||||||
|
let pathToUse = artifact.relativePath || '';
|
||||||
|
if (artifact.type === 'agent-launcher') {
|
||||||
|
pathToUse = artifact.agentPath || artifact.relativePath || '';
|
||||||
|
} else if (artifact.type === 'workflow-command') {
|
||||||
|
pathToUse = artifact.workflowPath || artifact.relativePath || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let rendered = template
|
||||||
|
.replaceAll('{{name}}', artifact.name || '')
|
||||||
|
.replaceAll('{{module}}', artifact.module || 'core')
|
||||||
|
.replaceAll('{{path}}', pathToUse)
|
||||||
|
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
|
||||||
|
.replaceAll('{{workflow_path}}', pathToUse);
|
||||||
|
|
||||||
|
// Replace _bmad placeholder with actual folder name
|
||||||
|
rendered = rendered.replaceAll('_bmad', this.bmadFolderName);
|
||||||
|
|
||||||
|
// Replace {{bmadFolderName}} placeholder if present
|
||||||
|
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
|
||||||
|
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate filename for artifact
|
||||||
|
* @param {Object} artifact - Artifact data
|
||||||
|
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||||
|
* @returns {string} Generated filename
|
||||||
|
*/
|
||||||
|
generateFilename(artifact, artifactType) {
|
||||||
|
const { toDashPath } = require('./shared/path-utils');
|
||||||
|
// toDashPath already handles the .agent.md suffix for agents correctly
|
||||||
|
// No need to add it again here
|
||||||
|
return toDashPath(artifact.relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print installation summary
|
||||||
|
* @param {Object} results - Installation results
|
||||||
|
* @param {string} targetDir - Target directory (relative)
|
||||||
|
*/
|
||||||
|
printSummary(results, targetDir) {
|
||||||
|
console.log(chalk.green(`\n✓ ${this.name} configured:`));
|
||||||
|
if (results.agents > 0) {
|
||||||
|
console.log(chalk.dim(` - ${results.agents} agents installed`));
|
||||||
|
}
|
||||||
|
if (results.workflows > 0) {
|
||||||
|
console.log(chalk.dim(` - ${results.workflows} workflow commands generated`));
|
||||||
|
}
|
||||||
|
if (results.tasks > 0 || results.tools > 0) {
|
||||||
|
console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`));
|
||||||
|
}
|
||||||
|
console.log(chalk.dim(` - Destination: ${targetDir}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup IDE configuration
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
*/
|
||||||
|
async cleanup(projectDir) {
|
||||||
|
// Clean all target directories
|
||||||
|
if (this.installerConfig?.targets) {
|
||||||
|
for (const target of this.installerConfig.targets) {
|
||||||
|
await this.cleanupTarget(projectDir, target.target_dir);
|
||||||
|
}
|
||||||
|
} else if (this.installerConfig?.target_dir) {
|
||||||
|
await this.cleanupTarget(projectDir, this.installerConfig.target_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup a specific target directory
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} targetDir - Target directory to clean
|
||||||
|
*/
|
||||||
|
async cleanupTarget(projectDir, targetDir) {
|
||||||
|
const targetPath = path.join(projectDir, targetDir);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(targetPath))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all bmad* files
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await fs.readdir(targetPath);
|
||||||
|
} catch {
|
||||||
|
// Directory exists but can't be read - skip cleanup
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entries || !Array.isArray(entries)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let removedCount = 0;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
// Skip non-strings or undefined entries
|
||||||
|
if (!entry || typeof entry !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.startsWith('bmad')) {
|
||||||
|
const entryPath = path.join(targetPath, entry);
|
||||||
|
const stat = await fs.stat(entryPath);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
await fs.remove(entryPath);
|
||||||
|
removedCount++;
|
||||||
|
} else if (stat.isDirectory()) {
|
||||||
|
await fs.remove(entryPath);
|
||||||
|
removedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0) {
|
||||||
|
console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { ConfigDrivenIdeSetup };
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const {
|
|
||||||
loadModuleInjectionConfig,
|
|
||||||
shouldApplyInjection,
|
|
||||||
filterAgentInstructions,
|
|
||||||
resolveSubagentFiles,
|
|
||||||
} = require('./shared/module-injections');
|
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
|
||||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Antigravity IDE setup handler
|
|
||||||
*
|
|
||||||
* Uses .agent/workflows/ directory for slash commands
|
|
||||||
*/
|
|
||||||
class AntigravitySetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('antigravity', 'Google Antigravity', true);
|
|
||||||
this.configDir = '.agent';
|
|
||||||
this.workflowsDir = 'workflows';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt for subagent installation location
|
|
||||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
|
||||||
*/
|
|
||||||
async _promptInstallLocation() {
|
|
||||||
return prompts.select({
|
|
||||||
message: 'Where would you like to install Antigravity subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect configuration choices before installation
|
|
||||||
* @param {Object} options - Configuration options
|
|
||||||
* @returns {Object} Collected configuration
|
|
||||||
*/
|
|
||||||
async collectConfiguration(options = {}) {
|
|
||||||
// const config = {
|
|
||||||
// subagentChoices: null,
|
|
||||||
// installLocation: null,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const sourceModulesPath = getSourcePath('modules');
|
|
||||||
// const modules = options.selectedModules || [];
|
|
||||||
|
|
||||||
// for (const moduleName of modules) {
|
|
||||||
// // Check for Antigravity sub-module injection config in SOURCE directory
|
|
||||||
// const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'antigravity', 'injections.yaml');
|
|
||||||
|
|
||||||
// if (await this.exists(injectionConfigPath)) {
|
|
||||||
// const yaml = require('yaml');
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Load injection configuration
|
|
||||||
// const configContent = await fs.readFile(injectionConfigPath, 'utf8');
|
|
||||||
// const injectionConfig = yaml.parse(configContent);
|
|
||||||
|
|
||||||
// // Ask about subagents if they exist and we haven't asked yet
|
|
||||||
// if (injectionConfig.subagents && !config.subagentChoices) {
|
|
||||||
// config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
|
||||||
|
|
||||||
// if (config.subagentChoices.install !== 'none') {
|
|
||||||
// config.installLocation = await this._promptInstallLocation();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup old BMAD installation before reinstalling
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const bmadWorkflowsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad');
|
|
||||||
|
|
||||||
if (await fs.pathExists(bmadWorkflowsDir)) {
|
|
||||||
await fs.remove(bmadWorkflowsDir);
|
|
||||||
console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Antigravity IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
// Store project directory for use in processContent
|
|
||||||
this.projectDir = projectDir;
|
|
||||||
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Create .agent/workflows directory structure
|
|
||||||
const agentDir = path.join(projectDir, this.configDir);
|
|
||||||
const workflowsDir = path.join(agentDir, this.workflowsDir);
|
|
||||||
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
|
|
||||||
|
|
||||||
await this.ensureDir(bmadWorkflowsDir);
|
|
||||||
|
|
||||||
// Generate agent launchers using AgentCommandGenerator
|
|
||||||
// This creates small launcher files that reference the actual agents in _bmad/
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Write agent launcher files with FLATTENED naming using shared utility
|
|
||||||
// Antigravity ignores directory structure, so we flatten to: bmad_module_name.md
|
|
||||||
// This creates slash commands like /bmad_bmm_dev instead of /dev
|
|
||||||
const agentCount = await agentGen.writeDashArtifacts(bmadWorkflowsDir, agentArtifacts);
|
|
||||||
|
|
||||||
// Process Antigravity specific injections for installed modules
|
|
||||||
// Use pre-collected configuration if available, or skip if already configured
|
|
||||||
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
|
|
||||||
// IDE is already configured from previous installation, skip prompting
|
|
||||||
// Just process with default/existing configuration
|
|
||||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {});
|
|
||||||
} else if (options.preCollectedConfig) {
|
|
||||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
|
|
||||||
} else {
|
|
||||||
await this.processModuleInjections(projectDir, bmadDir, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate workflow commands from manifest (if it exists)
|
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Write workflow-command artifacts with FLATTENED naming using shared utility
|
|
||||||
const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts);
|
|
||||||
|
|
||||||
// Generate task and tool commands from manifests (if they exist)
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
|
||||||
if (workflowCommandCount > 0) {
|
|
||||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
|
||||||
}
|
|
||||||
if (taskToolResult.generated > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.dim(
|
|
||||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, bmadWorkflowsDir)}`));
|
|
||||||
console.log(chalk.yellow(`\n Note: Antigravity uses flattened slash commands (e.g., /bmad_module_agents_name)`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read and process file content
|
|
||||||
*/
|
|
||||||
async readAndProcess(filePath, metadata) {
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
return this.processContent(content, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override processContent to keep {project-root} placeholder
|
|
||||||
*/
|
|
||||||
processContent(content, metadata = {}) {
|
|
||||||
// Use the base class method WITHOUT projectDir to preserve {project-root} placeholder
|
|
||||||
return super.processContent(content, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get agents from source modules (not installed location)
|
|
||||||
*/
|
|
||||||
async getAgentsFromSource(sourceDir, selectedModules) {
|
|
||||||
const agents = [];
|
|
||||||
|
|
||||||
// Add core agents
|
|
||||||
const corePath = getModulePath('core');
|
|
||||||
if (await fs.pathExists(path.join(corePath, 'agents'))) {
|
|
||||||
const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core');
|
|
||||||
agents.push(...coreAgents);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add module agents
|
|
||||||
for (const moduleName of selectedModules) {
|
|
||||||
const modulePath = path.join(sourceDir, moduleName);
|
|
||||||
const agentsPath = path.join(modulePath, 'agents');
|
|
||||||
|
|
||||||
if (await fs.pathExists(agentsPath)) {
|
|
||||||
const moduleAgents = await getAgentsFromDir(agentsPath, moduleName);
|
|
||||||
agents.push(...moduleAgents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process module injections with pre-collected configuration
|
|
||||||
*/
|
|
||||||
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
|
|
||||||
// Get list of installed modules
|
|
||||||
const modules = options.selectedModules || [];
|
|
||||||
const { subagentChoices, installLocation } = preCollectedConfig;
|
|
||||||
|
|
||||||
// Get the actual source directory (not the installation directory)
|
|
||||||
await this.processModuleInjectionsInternal({
|
|
||||||
projectDir,
|
|
||||||
modules,
|
|
||||||
handler: 'antigravity',
|
|
||||||
subagentChoices,
|
|
||||||
installLocation,
|
|
||||||
interactive: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process Antigravity specific injections for installed modules
|
|
||||||
* Looks for injections.yaml in each module's antigravity sub-module
|
|
||||||
*/
|
|
||||||
async processModuleInjections(projectDir, bmadDir, options) {
|
|
||||||
// Get list of installed modules
|
|
||||||
const modules = options.selectedModules || [];
|
|
||||||
let subagentChoices = null;
|
|
||||||
let installLocation = null;
|
|
||||||
|
|
||||||
// Get the actual source directory (not the installation directory)
|
|
||||||
const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({
|
|
||||||
projectDir,
|
|
||||||
modules,
|
|
||||||
handler: 'antigravity',
|
|
||||||
subagentChoices,
|
|
||||||
installLocation,
|
|
||||||
interactive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updatedChoices) {
|
|
||||||
subagentChoices = updatedChoices;
|
|
||||||
}
|
|
||||||
if (updatedLocation) {
|
|
||||||
installLocation = updatedLocation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) {
|
|
||||||
let choices = subagentChoices;
|
|
||||||
let location = installLocation;
|
|
||||||
|
|
||||||
for (const moduleName of modules) {
|
|
||||||
const configData = await loadModuleInjectionConfig(handler, moduleName);
|
|
||||||
|
|
||||||
if (!configData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { config, handlerBaseDir } = configData;
|
|
||||||
|
|
||||||
if (interactive) {
|
|
||||||
console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler} features...`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (interactive && config.subagents && !choices) {
|
|
||||||
// choices = await this.promptSubagentInstallation(config.subagents);
|
|
||||||
|
|
||||||
// if (choices.install !== 'none') {
|
|
||||||
// location = await this._promptInstallLocation();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (config.injections && choices && choices.install !== 'none') {
|
|
||||||
for (const injection of config.injections) {
|
|
||||||
if (shouldApplyInjection(injection, choices)) {
|
|
||||||
await this.injectContent(projectDir, injection, choices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.subagents && choices && choices.install !== 'none') {
|
|
||||||
await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { subagentChoices: choices, installLocation: location };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt user for subagent installation preferences
|
|
||||||
*/
|
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
|
||||||
// First ask if they want to install subagents
|
|
||||||
const install = await prompts.select({
|
|
||||||
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
|
||||||
],
|
|
||||||
default: 'all',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (install === 'selective') {
|
|
||||||
// Show list of available subagents with descriptions
|
|
||||||
const subagentInfo = {
|
|
||||||
'market-researcher.md': 'Market research and competitive analysis',
|
|
||||||
'requirements-analyst.md': 'Requirements extraction and validation',
|
|
||||||
'technical-evaluator.md': 'Technology stack evaluation',
|
|
||||||
'epic-optimizer.md': 'Epic and story breakdown optimization',
|
|
||||||
'document-reviewer.md': 'Document quality review',
|
|
||||||
};
|
|
||||||
|
|
||||||
const selected = await prompts.multiselect({
|
|
||||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
|
||||||
choices: subagentConfig.files.map((file) => ({
|
|
||||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
|
||||||
value: file,
|
|
||||||
checked: true,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { install };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject content at specified point in file
|
|
||||||
*/
|
|
||||||
async injectContent(projectDir, injection, subagentChoices = null) {
|
|
||||||
const targetPath = path.join(projectDir, injection.file);
|
|
||||||
|
|
||||||
if (await this.exists(targetPath)) {
|
|
||||||
let content = await fs.readFile(targetPath, 'utf8');
|
|
||||||
const marker = `<!-- IDE-INJECT-POINT: ${injection.point} -->`;
|
|
||||||
|
|
||||||
if (content.includes(marker)) {
|
|
||||||
let injectionContent = injection.content;
|
|
||||||
|
|
||||||
// Filter content if selective subagents chosen
|
|
||||||
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
|
|
||||||
injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
content = content.replace(marker, injectionContent);
|
|
||||||
await fs.writeFile(targetPath, content);
|
|
||||||
console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy selected subagents to appropriate Antigravity agents directory
|
|
||||||
*/
|
|
||||||
async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) {
|
|
||||||
const os = require('node:os');
|
|
||||||
|
|
||||||
// Determine target directory based on user choice
|
|
||||||
let targetDir;
|
|
||||||
if (location === 'user') {
|
|
||||||
targetDir = path.join(os.homedir(), '.agent', 'agents');
|
|
||||||
console.log(chalk.dim(` Installing subagents globally to: ~/.agent/agents/`));
|
|
||||||
} else {
|
|
||||||
targetDir = path.join(projectDir, '.agent', 'agents');
|
|
||||||
console.log(chalk.dim(` Installing subagents to project: .agent/agents/`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure target directory exists
|
|
||||||
await this.ensureDir(targetDir);
|
|
||||||
|
|
||||||
const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices);
|
|
||||||
|
|
||||||
let copiedCount = 0;
|
|
||||||
for (const resolved of resolvedFiles) {
|
|
||||||
try {
|
|
||||||
const sourcePath = resolved.absolutePath;
|
|
||||||
|
|
||||||
const subFolder = path.dirname(resolved.relativePath);
|
|
||||||
let targetPath;
|
|
||||||
if (subFolder && subFolder !== '.') {
|
|
||||||
const targetSubDir = path.join(targetDir, subFolder);
|
|
||||||
await this.ensureDir(targetSubDir);
|
|
||||||
targetPath = path.join(targetSubDir, path.basename(resolved.file));
|
|
||||||
} else {
|
|
||||||
targetPath = path.join(targetDir, path.basename(resolved.file));
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.copyFile(sourcePath, targetPath);
|
|
||||||
console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`));
|
|
||||||
copiedCount++;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (copiedCount > 0) {
|
|
||||||
console.log(chalk.dim(` Total subagents installed: ${copiedCount}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Antigravity
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
// Create .agent/workflows/bmad directory structure (same as regular agents)
|
|
||||||
const agentDir = path.join(projectDir, this.configDir);
|
|
||||||
const workflowsDir = path.join(agentDir, this.workflowsDir);
|
|
||||||
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
|
|
||||||
|
|
||||||
await fs.ensureDir(bmadWorkflowsDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher with same pattern as regular agents
|
|
||||||
const launcherContent = `name: '${agentName}'
|
|
||||||
description: '${agentName} agent'
|
|
||||||
usage: |
|
|
||||||
Custom BMAD agent: ${agentName}
|
|
||||||
|
|
||||||
Launch with: /${agentName}
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from @${agentPath}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. EXECUTE as ${agentName} with full persona adoption
|
|
||||||
</agent-activation>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
⚠️ **IMPORTANT**: Run @${agentPath} to load the complete agent before using this launcher!`;
|
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
const fileName = customAgentDashName(agentName);
|
|
||||||
const launcherPath = path.join(bmadWorkflowsDir, fileName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'antigravity',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: `/${fileName.replace('.md', '')}`,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { AntigravitySetup };
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auggie CLI setup handler
|
|
||||||
* Installs to project directory (.augment/commands)
|
|
||||||
*/
|
|
||||||
class AuggieSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('auggie', 'Auggie CLI');
|
|
||||||
this.detectionPaths = ['.augment'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Auggie CLI configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Always use project directory
|
|
||||||
const location = path.join(projectDir, '.augment', 'commands');
|
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Get tasks, tools, and workflows (ALL workflows now generate commands)
|
|
||||||
const tasks = await this.getTasks(bmadDir, true);
|
|
||||||
const tools = await this.getTools(bmadDir, true);
|
|
||||||
|
|
||||||
// Get ALL workflows using the new workflow command generator
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Convert workflow artifacts to expected format
|
|
||||||
const workflows = workflowArtifacts
|
|
||||||
.filter((artifact) => artifact.type === 'workflow-command')
|
|
||||||
.map((artifact) => ({
|
|
||||||
module: artifact.module,
|
|
||||||
name: path.basename(artifact.relativePath, '.md'),
|
|
||||||
path: artifact.sourcePath,
|
|
||||||
content: artifact.content,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const bmadCommandsDir = path.join(location, 'bmad');
|
|
||||||
const agentsDir = path.join(bmadCommandsDir, 'agents');
|
|
||||||
const tasksDir = path.join(bmadCommandsDir, 'tasks');
|
|
||||||
const toolsDir = path.join(bmadCommandsDir, 'tools');
|
|
||||||
const workflowsDir = path.join(bmadCommandsDir, 'workflows');
|
|
||||||
|
|
||||||
await this.ensureDir(agentsDir);
|
|
||||||
await this.ensureDir(tasksDir);
|
|
||||||
await this.ensureDir(toolsDir);
|
|
||||||
await this.ensureDir(workflowsDir);
|
|
||||||
|
|
||||||
// Install agent launchers
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`);
|
|
||||||
await this.writeFile(targetPath, artifact.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install tasks
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readFile(task.path);
|
|
||||||
const commandContent = this.createTaskCommand(task, content);
|
|
||||||
|
|
||||||
const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install tools
|
|
||||||
for (const tool of tools) {
|
|
||||||
const content = await this.readFile(tool.path);
|
|
||||||
const commandContent = this.createToolCommand(tool, content);
|
|
||||||
|
|
||||||
const targetPath = path.join(toolsDir, `${tool.module}-${tool.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install workflows (already generated commands)
|
|
||||||
for (const workflow of workflows) {
|
|
||||||
// Use the pre-generated workflow command content
|
|
||||||
const targetPath = path.join(workflowsDir, `${workflow.module}-${workflow.name}.md`);
|
|
||||||
await this.writeFile(targetPath, workflow.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalInstalled = agentArtifacts.length + tasks.length + tools.length + workflows.length;
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentArtifacts.length} agents installed`));
|
|
||||||
console.log(chalk.dim(` - ${tasks.length} tasks installed`));
|
|
||||||
console.log(chalk.dim(` - ${tools.length} tools installed`));
|
|
||||||
console.log(chalk.dim(` - ${workflows.length} workflows installed`));
|
|
||||||
console.log(chalk.dim(` - Location: ${path.relative(projectDir, location)}`));
|
|
||||||
console.log(chalk.yellow(`\n 💡 Tip: Add 'model: gpt-4o' to command frontmatter to specify AI model`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentArtifacts.length,
|
|
||||||
tasks: tasks.length,
|
|
||||||
tools: tools.length,
|
|
||||||
workflows: workflows.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create task command content
|
|
||||||
*/
|
|
||||||
createTaskCommand(task, content) {
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
|
||||||
|
|
||||||
return `---
|
|
||||||
description: "Execute the ${taskName} task"
|
|
||||||
---
|
|
||||||
|
|
||||||
# ${taskName} Task
|
|
||||||
|
|
||||||
${content}
|
|
||||||
|
|
||||||
## Module
|
|
||||||
BMAD ${task.module.toUpperCase()} module
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create tool command content
|
|
||||||
*/
|
|
||||||
createToolCommand(tool, content) {
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name);
|
|
||||||
|
|
||||||
return `---
|
|
||||||
description: "Use the ${toolName} tool"
|
|
||||||
---
|
|
||||||
|
|
||||||
# ${toolName} Tool
|
|
||||||
|
|
||||||
${content}
|
|
||||||
|
|
||||||
## Module
|
|
||||||
BMAD ${tool.module.toUpperCase()} module
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create workflow command content
|
|
||||||
*/
|
|
||||||
createWorkflowCommand(workflow, content) {
|
|
||||||
const description = workflow.description || `Execute the ${workflow.name} workflow`;
|
|
||||||
|
|
||||||
return `---
|
|
||||||
description: "${description}"
|
|
||||||
---
|
|
||||||
|
|
||||||
# ${workflow.name} Workflow
|
|
||||||
|
|
||||||
${content}
|
|
||||||
|
|
||||||
## Module
|
|
||||||
BMAD ${workflow.module.toUpperCase()} module
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup Auggie configuration
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
|
|
||||||
// Only clean up project directory
|
|
||||||
const location = path.join(projectDir, '.augment', 'commands');
|
|
||||||
const bmadDir = path.join(location, 'bmad');
|
|
||||||
|
|
||||||
if (await fs.pathExists(bmadDir)) {
|
|
||||||
await fs.remove(bmadDir);
|
|
||||||
console.log(chalk.dim(` Removed old BMAD commands`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Auggie
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
// Auggie uses .augment/commands directory
|
|
||||||
const location = path.join(projectDir, '.augment', 'commands');
|
|
||||||
const bmadCommandsDir = path.join(location, 'bmad');
|
|
||||||
const agentsDir = path.join(bmadCommandsDir, 'agents');
|
|
||||||
|
|
||||||
// Create .augment/commands/bmad/agents directory if it doesn't exist
|
|
||||||
await fs.ensureDir(agentsDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher
|
|
||||||
const launcherContent = `---
|
|
||||||
description: "Use the ${agentName} custom agent"
|
|
||||||
---
|
|
||||||
|
|
||||||
# ${agentName} Custom Agent
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
|
||||||
|
|
||||||
This is a launcher for the custom BMAD agent "${agentName}".
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. First run: \`${agentPath}\` to load the complete agent
|
|
||||||
2. Then use this command to activate ${agentName}
|
|
||||||
|
|
||||||
The agent will follow the persona and instructions from the main agent file.
|
|
||||||
|
|
||||||
## Module
|
|
||||||
BMAD Custom agent
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fileName = `custom-${agentName.toLowerCase()}.md`;
|
|
||||||
const launcherPath = path.join(agentsDir, fileName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'auggie',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: agentName,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { AuggieSetup };
|
|
||||||
@@ -1,506 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const {
|
|
||||||
loadModuleInjectionConfig,
|
|
||||||
shouldApplyInjection,
|
|
||||||
filterAgentInstructions,
|
|
||||||
resolveSubagentFiles,
|
|
||||||
} = require('./shared/module-injections');
|
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
|
||||||
const { customAgentColonName } = require('./shared/path-utils');
|
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Claude Code IDE setup handler
|
|
||||||
*/
|
|
||||||
class ClaudeCodeSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('claude-code', 'Claude Code', true); // preferred IDE
|
|
||||||
this.configDir = '.claude';
|
|
||||||
this.commandsDir = 'commands';
|
|
||||||
this.agentsDir = 'agents';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt for subagent installation location
|
|
||||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
|
||||||
*/
|
|
||||||
async promptInstallLocation() {
|
|
||||||
return prompts.select({
|
|
||||||
message: 'Where would you like to install Claude Code subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Collect configuration choices before installation
|
|
||||||
// * @param {Object} options - Configuration options
|
|
||||||
// * @returns {Object} Collected configuration
|
|
||||||
// */
|
|
||||||
// async collectConfiguration(options = {}) {
|
|
||||||
// const config = {
|
|
||||||
// subagentChoices: null,
|
|
||||||
// installLocation: null,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const sourceModulesPath = getSourcePath('modules');
|
|
||||||
// const modules = options.selectedModules || [];
|
|
||||||
|
|
||||||
// for (const moduleName of modules) {
|
|
||||||
// // Check for Claude Code sub-module injection config in SOURCE directory
|
|
||||||
// const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml');
|
|
||||||
|
|
||||||
// if (await this.exists(injectionConfigPath)) {
|
|
||||||
// const yaml = require('yaml');
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Load injection configuration
|
|
||||||
// const configContent = await fs.readFile(injectionConfigPath, 'utf8');
|
|
||||||
// const injectionConfig = yaml.parse(configContent);
|
|
||||||
|
|
||||||
// // Ask about subagents if they exist and we haven't asked yet
|
|
||||||
// if (injectionConfig.subagents && !config.subagentChoices) {
|
|
||||||
// config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
|
||||||
|
|
||||||
// if (config.subagentChoices.install !== 'none') {
|
|
||||||
// config.installLocation = await this.promptInstallLocation();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return config;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup old BMAD installation before reinstalling
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
|
|
||||||
if (await fs.pathExists(commandsDir)) {
|
|
||||||
const entries = await fs.readdir(commandsDir);
|
|
||||||
let removedCount = 0;
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.startsWith('bmad')) {
|
|
||||||
await fs.remove(path.join(commandsDir, entry));
|
|
||||||
removedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also remove legacy bmad folder if it exists
|
|
||||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
|
||||||
if (await fs.pathExists(bmadFolder)) {
|
|
||||||
await fs.remove(bmadFolder);
|
|
||||||
console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up legacy folder structure (module/type/name.md) if it exists
|
|
||||||
* This can be called after migration to remove old nested directories
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
*/
|
|
||||||
async cleanupLegacyFolders(projectDir) {
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(commandsDir))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove legacy bmad folder if it exists
|
|
||||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
|
||||||
if (await fs.pathExists(bmadFolder)) {
|
|
||||||
await fs.remove(bmadFolder);
|
|
||||||
console.log(chalk.dim(` Removed legacy bmad folder from ${this.name}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Claude Code IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
// Store project directory for use in processContent
|
|
||||||
this.projectDir = projectDir;
|
|
||||||
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Create .claude/commands directory structure
|
|
||||||
const claudeDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(claudeDir, this.commandsDir);
|
|
||||||
await this.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
|
||||||
// Creates: .claude/commands/bmad_bmm_pm.md
|
|
||||||
|
|
||||||
// Generate agent launchers using AgentCommandGenerator
|
|
||||||
// This creates small launcher files that reference the actual agents in _bmad/
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Write agent launcher files using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_pm.md
|
|
||||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
|
||||||
|
|
||||||
// Process Claude Code specific injections for installed modules
|
|
||||||
// Use pre-collected configuration if available, or skip if already configured
|
|
||||||
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
|
|
||||||
// IDE is already configured from previous installation, skip prompting
|
|
||||||
// Just process with default/existing configuration
|
|
||||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {});
|
|
||||||
} else if (options.preCollectedConfig) {
|
|
||||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
|
|
||||||
} else {
|
|
||||||
await this.processModuleInjections(projectDir, bmadDir, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip CLAUDE.md creation - let user manage their own CLAUDE.md file
|
|
||||||
// await this.createClaudeConfig(projectDir, modules);
|
|
||||||
|
|
||||||
// Generate workflow commands from manifest (if it exists)
|
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Write workflow-command artifacts using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_correct-course.md
|
|
||||||
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
|
|
||||||
|
|
||||||
// Generate task and tool commands from manifests (if they exist)
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
|
||||||
if (workflowCommandCount > 0) {
|
|
||||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
|
||||||
}
|
|
||||||
if (taskToolResult.generated > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.dim(
|
|
||||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method removed - CLAUDE.md file management left to user
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read and process file content
|
|
||||||
*/
|
|
||||||
async readAndProcess(filePath, metadata) {
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
return this.processContent(content, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override processContent to keep {project-root} placeholder
|
|
||||||
*/
|
|
||||||
processContent(content, metadata = {}) {
|
|
||||||
// Use the base class method WITHOUT projectDir to preserve {project-root} placeholder
|
|
||||||
return super.processContent(content, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get agents from source modules (not installed location)
|
|
||||||
*/
|
|
||||||
async getAgentsFromSource(sourceDir, selectedModules) {
|
|
||||||
const agents = [];
|
|
||||||
|
|
||||||
// Add core agents
|
|
||||||
const corePath = getModulePath('core');
|
|
||||||
if (await fs.pathExists(path.join(corePath, 'agents'))) {
|
|
||||||
const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core');
|
|
||||||
agents.push(...coreAgents);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add module agents
|
|
||||||
for (const moduleName of selectedModules) {
|
|
||||||
const modulePath = path.join(sourceDir, moduleName);
|
|
||||||
const agentsPath = path.join(modulePath, 'agents');
|
|
||||||
|
|
||||||
if (await fs.pathExists(agentsPath)) {
|
|
||||||
const moduleAgents = await getAgentsFromDir(agentsPath, moduleName);
|
|
||||||
agents.push(...moduleAgents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process module injections with pre-collected configuration
|
|
||||||
*/
|
|
||||||
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
|
|
||||||
// Get list of installed modules
|
|
||||||
const modules = options.selectedModules || [];
|
|
||||||
const { subagentChoices, installLocation } = preCollectedConfig;
|
|
||||||
|
|
||||||
// Get the actual source directory (not the installation directory)
|
|
||||||
await this.processModuleInjectionsInternal({
|
|
||||||
projectDir,
|
|
||||||
modules,
|
|
||||||
handler: 'claude-code',
|
|
||||||
subagentChoices,
|
|
||||||
installLocation,
|
|
||||||
interactive: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process Claude Code specific injections for installed modules
|
|
||||||
* Looks for injections.yaml in each module's claude-code sub-module
|
|
||||||
*/
|
|
||||||
async processModuleInjections(projectDir, bmadDir, options) {
|
|
||||||
// Get list of installed modules
|
|
||||||
const modules = options.selectedModules || [];
|
|
||||||
let subagentChoices = null;
|
|
||||||
let installLocation = null;
|
|
||||||
|
|
||||||
// Get the actual source directory (not the installation directory)
|
|
||||||
const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({
|
|
||||||
projectDir,
|
|
||||||
modules,
|
|
||||||
handler: 'claude-code',
|
|
||||||
subagentChoices,
|
|
||||||
installLocation,
|
|
||||||
interactive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updatedChoices) {
|
|
||||||
subagentChoices = updatedChoices;
|
|
||||||
}
|
|
||||||
if (updatedLocation) {
|
|
||||||
installLocation = updatedLocation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) {
|
|
||||||
let choices = subagentChoices;
|
|
||||||
let location = installLocation;
|
|
||||||
|
|
||||||
for (const moduleName of modules) {
|
|
||||||
const configData = await loadModuleInjectionConfig(handler, moduleName);
|
|
||||||
|
|
||||||
if (!configData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { config, handlerBaseDir } = configData;
|
|
||||||
|
|
||||||
if (interactive) {
|
|
||||||
console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler.replace('-', ' ')} features...`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interactive && config.subagents && !choices) {
|
|
||||||
// choices = await this.promptSubagentInstallation(config.subagents);
|
|
||||||
// if (choices.install !== 'none') {
|
|
||||||
// location = await this.promptInstallLocation();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.injections && choices && choices.install !== 'none') {
|
|
||||||
for (const injection of config.injections) {
|
|
||||||
if (shouldApplyInjection(injection, choices)) {
|
|
||||||
await this.injectContent(projectDir, injection, choices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.subagents && choices && choices.install !== 'none') {
|
|
||||||
await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { subagentChoices: choices, installLocation: location };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt user for subagent installation preferences
|
|
||||||
*/
|
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
|
||||||
// First ask if they want to install subagents
|
|
||||||
const install = await prompts.select({
|
|
||||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
|
||||||
],
|
|
||||||
default: 'all',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (install === 'selective') {
|
|
||||||
// Show list of available subagents with descriptions
|
|
||||||
const subagentInfo = {
|
|
||||||
'market-researcher.md': 'Market research and competitive analysis',
|
|
||||||
'requirements-analyst.md': 'Requirements extraction and validation',
|
|
||||||
'technical-evaluator.md': 'Technology stack evaluation',
|
|
||||||
'epic-optimizer.md': 'Epic and story breakdown optimization',
|
|
||||||
'document-reviewer.md': 'Document quality review',
|
|
||||||
};
|
|
||||||
|
|
||||||
const selected = await prompts.multiselect({
|
|
||||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
|
||||||
options: subagentConfig.files.map((file) => ({
|
|
||||||
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
|
||||||
value: file,
|
|
||||||
})),
|
|
||||||
initialValues: subagentConfig.files,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { install };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject content at specified point in file
|
|
||||||
*/
|
|
||||||
async injectContent(projectDir, injection, subagentChoices = null) {
|
|
||||||
const targetPath = path.join(projectDir, injection.file);
|
|
||||||
|
|
||||||
if (await this.exists(targetPath)) {
|
|
||||||
let content = await fs.readFile(targetPath, 'utf8');
|
|
||||||
const marker = `<!-- IDE-INJECT-POINT: ${injection.point} -->`;
|
|
||||||
|
|
||||||
if (content.includes(marker)) {
|
|
||||||
let injectionContent = injection.content;
|
|
||||||
|
|
||||||
// Filter content if selective subagents chosen
|
|
||||||
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
|
|
||||||
injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
content = content.replace(marker, injectionContent);
|
|
||||||
await fs.writeFile(targetPath, content);
|
|
||||||
console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy selected subagents to appropriate Claude agents directory
|
|
||||||
*/
|
|
||||||
async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) {
|
|
||||||
const os = require('node:os');
|
|
||||||
|
|
||||||
// Determine target directory based on user choice
|
|
||||||
let targetDir;
|
|
||||||
if (location === 'user') {
|
|
||||||
targetDir = path.join(os.homedir(), '.claude', 'agents');
|
|
||||||
console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`));
|
|
||||||
} else {
|
|
||||||
targetDir = path.join(projectDir, '.claude', 'agents');
|
|
||||||
console.log(chalk.dim(` Installing subagents to project: .claude/agents/`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure target directory exists
|
|
||||||
await this.ensureDir(targetDir);
|
|
||||||
|
|
||||||
const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices);
|
|
||||||
|
|
||||||
let copiedCount = 0;
|
|
||||||
for (const resolved of resolvedFiles) {
|
|
||||||
try {
|
|
||||||
const sourcePath = resolved.absolutePath;
|
|
||||||
|
|
||||||
const subFolder = path.dirname(resolved.relativePath);
|
|
||||||
let targetPath;
|
|
||||||
if (subFolder && subFolder !== '.') {
|
|
||||||
const targetSubDir = path.join(targetDir, subFolder);
|
|
||||||
await this.ensureDir(targetSubDir);
|
|
||||||
targetPath = path.join(targetSubDir, path.basename(resolved.file));
|
|
||||||
} else {
|
|
||||||
targetPath = path.join(targetDir, path.basename(resolved.file));
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.copyFile(sourcePath, targetPath);
|
|
||||||
console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`));
|
|
||||||
copiedCount++;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (copiedCount > 0) {
|
|
||||||
console.log(chalk.dim(` Total subagents installed: ${copiedCount}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Claude Code
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object|null} Info about created command
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
|
||||||
return null; // IDE not configured for this project
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
const launcherContent = `---
|
|
||||||
name: '${agentName}'
|
|
||||||
description: '${agentName} agent'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from @${agentPath}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
// Written directly to commands dir (no bmad subfolder)
|
|
||||||
const launcherName = customAgentColonName(agentName);
|
|
||||||
const launcherPath = path.join(commandsDir, launcherName);
|
|
||||||
await this.writeFile(launcherPath, launcherContent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: launcherPath,
|
|
||||||
command: `/${launcherName.replace('.md', '')}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { ClaudeCodeSetup };
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts');
|
|
||||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cline IDE setup handler
|
|
||||||
* Installs BMAD artifacts to .clinerules/workflows with flattened naming
|
|
||||||
*/
|
|
||||||
class ClineSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('cline', 'Cline', false);
|
|
||||||
this.configDir = '.clinerules';
|
|
||||||
this.workflowsDir = 'workflows';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Cline IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Create .clinerules/workflows directory
|
|
||||||
const clineDir = path.join(projectDir, this.configDir);
|
|
||||||
const workflowsDir = path.join(clineDir, this.workflowsDir);
|
|
||||||
|
|
||||||
await this.ensureDir(workflowsDir);
|
|
||||||
|
|
||||||
// Clear old BMAD files
|
|
||||||
await this.clearOldBmadFiles(workflowsDir);
|
|
||||||
|
|
||||||
// Collect all artifacts
|
|
||||||
const { artifacts, counts } = await this.collectClineArtifacts(projectDir, bmadDir, options);
|
|
||||||
|
|
||||||
// Write flattened files
|
|
||||||
const written = await this.flattenAndWriteArtifacts(artifacts, workflowsDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
|
||||||
console.log(chalk.dim(` - ${counts.tasks} tasks installed`));
|
|
||||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands installed`));
|
|
||||||
if (counts.workflowLaunchers > 0) {
|
|
||||||
console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers installed`));
|
|
||||||
}
|
|
||||||
console.log(chalk.dim(` - ${written} files written to ${path.relative(projectDir, workflowsDir)}`));
|
|
||||||
|
|
||||||
// Usage instructions
|
|
||||||
console.log(chalk.yellow('\n ⚠️ How to Use Cline Workflows'));
|
|
||||||
console.log(chalk.cyan(' BMAD workflows are available as slash commands in Cline'));
|
|
||||||
console.log(chalk.dim(' Usage:'));
|
|
||||||
console.log(chalk.dim(' - Type / to see available commands'));
|
|
||||||
console.log(chalk.dim(' - All BMAD items start with "bmad_"'));
|
|
||||||
console.log(chalk.dim(' - Example: /bmad_bmm_pm'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: counts.agents,
|
|
||||||
tasks: counts.tasks,
|
|
||||||
workflows: counts.workflows,
|
|
||||||
workflowLaunchers: counts.workflowLaunchers,
|
|
||||||
written,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect Cline installation by checking for .clinerules/workflows directory
|
|
||||||
*/
|
|
||||||
async detect(projectDir) {
|
|
||||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(workflowsDir))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(workflowsDir);
|
|
||||||
return entries.some((entry) => entry.startsWith('bmad'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect all artifacts for Cline export
|
|
||||||
*/
|
|
||||||
async collectClineArtifacts(projectDir, bmadDir, options = {}) {
|
|
||||||
const selectedModules = options.selectedModules || [];
|
|
||||||
const artifacts = [];
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
|
||||||
|
|
||||||
// Process agent launchers with project-specific paths
|
|
||||||
for (const agentArtifact of agentArtifacts) {
|
|
||||||
const content = agentArtifact.content;
|
|
||||||
|
|
||||||
artifacts.push({
|
|
||||||
type: 'agent',
|
|
||||||
module: agentArtifact.module,
|
|
||||||
sourcePath: agentArtifact.sourcePath,
|
|
||||||
relativePath: agentArtifact.relativePath,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tasks
|
|
||||||
const tasks = await getTasksFromBmad(bmadDir, selectedModules);
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readAndProcessWithProject(
|
|
||||||
task.path,
|
|
||||||
{
|
|
||||||
module: task.module,
|
|
||||||
name: task.name,
|
|
||||||
},
|
|
||||||
projectDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
artifacts.push({
|
|
||||||
type: 'task',
|
|
||||||
module: task.module,
|
|
||||||
sourcePath: task.path,
|
|
||||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get workflows
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
artifacts.push(...workflowArtifacts);
|
|
||||||
|
|
||||||
return {
|
|
||||||
artifacts,
|
|
||||||
counts: {
|
|
||||||
agents: agentArtifacts.length,
|
|
||||||
tasks: tasks.length,
|
|
||||||
workflows: workflowCounts.commands,
|
|
||||||
workflowLaunchers: workflowCounts.launchers,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flatten file path to bmad_module_type_name.md format
|
|
||||||
* Uses shared toDashPath utility
|
|
||||||
*/
|
|
||||||
flattenFilename(relativePath) {
|
|
||||||
return toDashPath(relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write all artifacts with flattened names
|
|
||||||
*/
|
|
||||||
async flattenAndWriteArtifacts(artifacts, destDir) {
|
|
||||||
let written = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
const flattenedName = this.flattenFilename(artifact.relativePath);
|
|
||||||
const targetPath = path.join(destDir, flattenedName);
|
|
||||||
await fs.writeFile(targetPath, artifact.content);
|
|
||||||
written++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear old BMAD files from the workflows directory
|
|
||||||
*/
|
|
||||||
async clearOldBmadFiles(destDir) {
|
|
||||||
if (!(await fs.pathExists(destDir))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(destDir);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.startsWith('bmad')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entryPath = path.join(destDir, entry);
|
|
||||||
const stat = await fs.stat(entryPath);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
await fs.remove(entryPath);
|
|
||||||
} else if (stat.isDirectory()) {
|
|
||||||
await fs.remove(entryPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read and process file with project-specific paths
|
|
||||||
*/
|
|
||||||
async readAndProcessWithProject(filePath, metadata, projectDir) {
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
return super.processContent(content, metadata, projectDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup Cline configuration
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
|
||||||
await this.clearOldBmadFiles(workflowsDir);
|
|
||||||
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Cline
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const clineDir = path.join(projectDir, this.configDir);
|
|
||||||
const workflowsDir = path.join(clineDir, this.workflowsDir);
|
|
||||||
|
|
||||||
// Create .clinerules/workflows directory if it doesn't exist
|
|
||||||
await fs.ensureDir(workflowsDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher workflow
|
|
||||||
const launcherContent = `name: ${agentName}
|
|
||||||
description: Custom BMAD agent: ${agentName}
|
|
||||||
|
|
||||||
# ${agentName} Custom Agent
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
|
||||||
|
|
||||||
This is a launcher for the custom BMAD agent "${agentName}".
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. First run: \`${agentPath}\` to load the complete agent
|
|
||||||
2. Then use this workflow as ${agentName}
|
|
||||||
|
|
||||||
The agent will follow the persona and instructions from the main agent file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generated by BMAD Method*`;
|
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
const fileName = customAgentDashName(agentName);
|
|
||||||
const launcherPath = path.join(workflowsDir, fileName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'cline',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: fileName.replace('.md', ''),
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility: Ensure directory exists
|
|
||||||
*/
|
|
||||||
async ensureDir(dirPath) {
|
|
||||||
await fs.ensureDir(dirPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { ClineSetup };
|
|
||||||
@@ -154,18 +154,26 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
|
|
||||||
// Check global location
|
// Check global location
|
||||||
if (await fs.pathExists(globalDir)) {
|
if (await fs.pathExists(globalDir)) {
|
||||||
|
try {
|
||||||
const entries = await fs.readdir(globalDir);
|
const entries = await fs.readdir(globalDir);
|
||||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check project-specific location
|
// Check project-specific location
|
||||||
if (await fs.pathExists(projectSpecificDir)) {
|
if (await fs.pathExists(projectSpecificDir)) {
|
||||||
|
try {
|
||||||
const entries = await fs.readdir(projectSpecificDir);
|
const entries = await fs.readdir(projectSpecificDir);
|
||||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -253,20 +261,40 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = await fs.readdir(destDir);
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await fs.readdir(destDir);
|
||||||
|
} catch (error) {
|
||||||
|
// Directory exists but can't be read - skip cleanup
|
||||||
|
console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entries || !Array.isArray(entries)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
// Skip non-strings or undefined entries
|
||||||
|
if (!entry || typeof entry !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!entry.startsWith('bmad')) {
|
if (!entry.startsWith('bmad')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryPath = path.join(destDir, entry);
|
const entryPath = path.join(destDir, entry);
|
||||||
|
try {
|
||||||
const stat = await fs.stat(entryPath);
|
const stat = await fs.stat(entryPath);
|
||||||
if (stat.isFile()) {
|
if (stat.isFile()) {
|
||||||
await fs.remove(entryPath);
|
await fs.remove(entryPath);
|
||||||
} else if (stat.isDirectory()) {
|
} else if (stat.isDirectory()) {
|
||||||
await fs.remove(entryPath);
|
await fs.remove(entryPath);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Skip files that can't be processed
|
||||||
|
console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { customAgentColonName } = require('./shared/path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crush IDE setup handler
|
|
||||||
* Creates commands in .crush/commands/ directory structure using flat colon naming
|
|
||||||
*/
|
|
||||||
class CrushSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('crush', 'Crush');
|
|
||||||
this.configDir = '.crush';
|
|
||||||
this.commandsDir = 'commands';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Crush IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Create .crush/commands directory
|
|
||||||
const crushDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(crushDir, this.commandsDir);
|
|
||||||
await this.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
|
||||||
// Creates: .crush/commands/bmad_bmm_pm.md
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Write agent launcher files using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_pm.md
|
|
||||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
|
||||||
|
|
||||||
// Get ALL workflows using the new workflow command generator
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Write workflow-command artifacts using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_correct-course.md
|
|
||||||
const workflowCount = await workflowGenerator.writeColonArtifacts(commandsDir, workflowArtifacts);
|
|
||||||
|
|
||||||
// Generate task and tool commands using flat underscore naming
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agent commands created`));
|
|
||||||
console.log(chalk.dim(` - ${taskToolResult.tasks} task commands created`));
|
|
||||||
console.log(chalk.dim(` - ${taskToolResult.tools} tool commands created`));
|
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflow commands created`));
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
|
||||||
console.log(chalk.dim('\n Commands can be accessed via Crush command palette'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskToolResult.tasks || 0,
|
|
||||||
tools: taskToolResult.tools || 0,
|
|
||||||
workflows: workflowCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup Crush configuration
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
|
|
||||||
if (await fs.pathExists(commandsDir)) {
|
|
||||||
const entries = await fs.readdir(commandsDir);
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.startsWith('bmad')) {
|
|
||||||
await fs.remove(path.join(commandsDir, entry));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also remove legacy bmad folder if it exists
|
|
||||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
|
||||||
if (await fs.pathExists(bmadFolder)) {
|
|
||||||
await fs.remove(bmadFolder);
|
|
||||||
console.log(chalk.dim(`Removed BMAD commands from Crush`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Crush
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
// Create .crush/commands directory if it doesn't exist
|
|
||||||
await fs.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher
|
|
||||||
const launcherContent = `# ${agentName} Custom Agent
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
|
||||||
|
|
||||||
This is a launcher for the custom BMAD agent "${agentName}".
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. First run: \`${agentPath}\` to load the complete agent
|
|
||||||
2. Then use this command to activate ${agentName}
|
|
||||||
|
|
||||||
The agent will follow the persona and instructions from the main agent file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generated by BMAD Method*`;
|
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
// Written directly to commands dir (no bmad subfolder)
|
|
||||||
const launcherName = customAgentColonName(agentName);
|
|
||||||
const launcherPath = path.join(commandsDir, launcherName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'crush',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: launcherName.replace('.md', ''),
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { CrushSetup };
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { customAgentColonName } = require('./shared/path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cursor IDE setup handler
|
|
||||||
*/
|
|
||||||
class CursorSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('cursor', 'Cursor', true); // preferred IDE
|
|
||||||
this.configDir = '.cursor';
|
|
||||||
this.rulesDir = 'rules';
|
|
||||||
this.commandsDir = 'commands';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup old BMAD installation before reinstalling
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
|
|
||||||
if (await fs.pathExists(commandsDir)) {
|
|
||||||
const entries = await fs.readdir(commandsDir);
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.startsWith('bmad')) {
|
|
||||||
await fs.remove(path.join(commandsDir, entry));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also remove legacy bmad folder if it exists
|
|
||||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
|
||||||
if (await fs.pathExists(bmadFolder)) {
|
|
||||||
await fs.remove(bmadFolder);
|
|
||||||
console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Cursor IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Create .cursor/commands directory structure
|
|
||||||
const cursorDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(cursorDir, this.commandsDir);
|
|
||||||
await this.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
|
||||||
// Creates: .cursor/commands/bmad_bmm_pm.md
|
|
||||||
|
|
||||||
// Generate agent launchers using AgentCommandGenerator
|
|
||||||
// This creates small launcher files that reference the actual agents in _bmad/
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Write agent launcher files using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_pm.md
|
|
||||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
|
||||||
|
|
||||||
// Generate workflow commands from manifest (if it exists)
|
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Write workflow-command artifacts using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_correct-course.md
|
|
||||||
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
|
|
||||||
|
|
||||||
// Generate task and tool commands from manifests (if they exist)
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
|
||||||
if (workflowCommandCount > 0) {
|
|
||||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
|
||||||
}
|
|
||||||
if (taskToolResult.generated > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.dim(
|
|
||||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskToolResult.tasks || 0,
|
|
||||||
tools: taskToolResult.tools || 0,
|
|
||||||
workflows: workflowCommandCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Cursor
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object|null} Info about created command
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
|
||||||
return null; // IDE not configured for this project
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from @${agentPath}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Cursor uses YAML frontmatter matching Claude Code format
|
|
||||||
const commandContent = `---
|
|
||||||
name: '${agentName}'
|
|
||||||
description: '${agentName} agent'
|
|
||||||
---
|
|
||||||
|
|
||||||
${launcherContent}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
// Written directly to commands dir (no bmad subfolder)
|
|
||||||
const launcherName = customAgentColonName(agentName);
|
|
||||||
const launcherPath = path.join(commandsDir, launcherName);
|
|
||||||
await this.writeFile(launcherPath, commandContent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: launcherPath,
|
|
||||||
command: `/${launcherName.replace('.md', '')}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { CursorSetup };
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gemini CLI setup handler
|
|
||||||
* Creates TOML files in .gemini/commands/ structure
|
|
||||||
*/
|
|
||||||
class GeminiSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('gemini', 'Gemini CLI', false);
|
|
||||||
this.configDir = '.gemini';
|
|
||||||
this.commandsDir = 'commands';
|
|
||||||
this.agentTemplatePath = path.join(__dirname, 'templates', 'gemini-agent-command.toml');
|
|
||||||
this.taskTemplatePath = path.join(__dirname, 'templates', 'gemini-task-command.toml');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load config values from bmad installation
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @returns {Object} Config values
|
|
||||||
*/
|
|
||||||
async loadConfigValues(bmadDir) {
|
|
||||||
const configValues = {
|
|
||||||
user_name: 'User', // Default fallback
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to load core config.yaml
|
|
||||||
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
|
||||||
if (await fs.pathExists(coreConfigPath)) {
|
|
||||||
try {
|
|
||||||
const configContent = await fs.readFile(coreConfigPath, 'utf8');
|
|
||||||
const config = yaml.parse(configContent);
|
|
||||||
|
|
||||||
if (config.user_name) {
|
|
||||||
configValues.user_name = config.user_name;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(chalk.yellow(` Warning: Could not load config values: ${error.message}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return configValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Gemini CLI configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Create .gemini/commands directory (flat structure with bmad- prefix)
|
|
||||||
const geminiDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(geminiDir, this.commandsDir);
|
|
||||||
|
|
||||||
await this.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
// Clean up any existing BMAD files before reinstalling
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Get tasks and workflows (ALL workflows now generate commands)
|
|
||||||
const tasks = await this.getTasks(bmadDir);
|
|
||||||
|
|
||||||
// Get ALL workflows using the new workflow command generator
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Install agents as TOML files with bmad- prefix (flat structure)
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const tomlContent = await this.createAgentLauncherToml(artifact);
|
|
||||||
|
|
||||||
// Flat structure: bmad-agent-{module}-{name}.toml
|
|
||||||
const tomlPath = path.join(commandsDir, `bmad-agent-${artifact.module}-${artifact.name}.toml`);
|
|
||||||
await this.writeFile(tomlPath, tomlContent);
|
|
||||||
agentCount++;
|
|
||||||
|
|
||||||
console.log(chalk.green(` ✓ Added agent: /bmad_agents_${artifact.module}_${artifact.name}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install tasks as TOML files with bmad- prefix (flat structure)
|
|
||||||
let taskCount = 0;
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readFile(task.path);
|
|
||||||
const tomlContent = await this.createTaskToml(task, content);
|
|
||||||
|
|
||||||
// Flat structure: bmad-task-{module}-{name}.toml
|
|
||||||
const tomlPath = path.join(commandsDir, `bmad-task-${task.module}-${task.name}.toml`);
|
|
||||||
await this.writeFile(tomlPath, tomlContent);
|
|
||||||
taskCount++;
|
|
||||||
|
|
||||||
console.log(chalk.green(` ✓ Added task: /bmad_tasks_${task.module}_${task.name}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install workflows as TOML files with bmad- prefix (flat structure)
|
|
||||||
let workflowCount = 0;
|
|
||||||
for (const artifact of workflowArtifacts) {
|
|
||||||
if (artifact.type === 'workflow-command') {
|
|
||||||
// Create TOML wrapper around workflow command content
|
|
||||||
const tomlContent = await this.createWorkflowToml(artifact);
|
|
||||||
|
|
||||||
// Flat structure: bmad-workflow-{module}-{name}.toml
|
|
||||||
const workflowName = path.basename(artifact.relativePath, '.md');
|
|
||||||
const tomlPath = path.join(commandsDir, `bmad-workflow-${artifact.module}-${workflowName}.toml`);
|
|
||||||
await this.writeFile(tomlPath, tomlContent);
|
|
||||||
workflowCount++;
|
|
||||||
|
|
||||||
console.log(chalk.green(` ✓ Added workflow: /bmad_workflows_${artifact.module}_${workflowName}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents configured`));
|
|
||||||
console.log(chalk.dim(` - ${taskCount} tasks configured`));
|
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflows configured`));
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
|
||||||
console.log(chalk.dim(` - Agent activation: /bmad_agents_{agent-name}`));
|
|
||||||
console.log(chalk.dim(` - Task activation: /bmad_tasks_{task-name}`));
|
|
||||||
console.log(chalk.dim(` - Workflow activation: /bmad_workflows_{workflow-name}`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskCount,
|
|
||||||
workflows: workflowCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create agent launcher TOML content from artifact
|
|
||||||
*/
|
|
||||||
async createAgentLauncherToml(artifact) {
|
|
||||||
// Strip frontmatter from launcher content
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
|
|
||||||
|
|
||||||
// Extract title from launcher frontmatter
|
|
||||||
const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
|
|
||||||
|
|
||||||
// Create TOML wrapper around launcher content (without frontmatter)
|
|
||||||
const description = `BMAD ${artifact.module.toUpperCase()} Agent: ${title}`;
|
|
||||||
|
|
||||||
return `description = "${description}"
|
|
||||||
prompt = """
|
|
||||||
${contentWithoutFrontmatter}
|
|
||||||
"""
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create agent TOML content using template
|
|
||||||
*/
|
|
||||||
async createAgentToml(agent, content) {
|
|
||||||
// Extract metadata
|
|
||||||
const titleMatch = content.match(/title="([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
|
|
||||||
|
|
||||||
// Load template
|
|
||||||
const template = await fs.readFile(this.agentTemplatePath, 'utf8');
|
|
||||||
|
|
||||||
// Replace template variables
|
|
||||||
// Note: {user_name} and other {config_values} are left as-is for runtime substitution by Gemini
|
|
||||||
const tomlContent = template
|
|
||||||
.replaceAll('{{title}}', title)
|
|
||||||
.replaceAll('{_bmad}', '_bmad')
|
|
||||||
.replaceAll('{_bmad}', this.bmadFolderName)
|
|
||||||
.replaceAll('{{module}}', agent.module)
|
|
||||||
.replaceAll('{{name}}', agent.name);
|
|
||||||
|
|
||||||
return tomlContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create task TOML content using template
|
|
||||||
*/
|
|
||||||
async createTaskToml(task, content) {
|
|
||||||
// Extract task name from XML if available
|
|
||||||
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
|
|
||||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
|
||||||
|
|
||||||
// Load template
|
|
||||||
const template = await fs.readFile(this.taskTemplatePath, 'utf8');
|
|
||||||
|
|
||||||
// Replace template variables
|
|
||||||
const tomlContent = template
|
|
||||||
.replaceAll('{{taskName}}', taskName)
|
|
||||||
.replaceAll('{_bmad}', '_bmad')
|
|
||||||
.replaceAll('{_bmad}', this.bmadFolderName)
|
|
||||||
.replaceAll('{{module}}', task.module)
|
|
||||||
.replaceAll('{{filename}}', task.filename);
|
|
||||||
|
|
||||||
return tomlContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create workflow TOML content from artifact
|
|
||||||
*/
|
|
||||||
async createWorkflowToml(artifact) {
|
|
||||||
// Extract description from artifact content
|
|
||||||
const descriptionMatch = artifact.content.match(/description:\s*"([^"]+)"/);
|
|
||||||
const description = descriptionMatch
|
|
||||||
? descriptionMatch[1]
|
|
||||||
: `BMAD ${artifact.module.toUpperCase()} Workflow: ${path.basename(artifact.relativePath, '.md')}`;
|
|
||||||
|
|
||||||
// Strip frontmatter from command content
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
|
|
||||||
|
|
||||||
return `description = "${description}"
|
|
||||||
prompt = """
|
|
||||||
${contentWithoutFrontmatter}
|
|
||||||
"""
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup Gemini configuration - surgically remove only BMAD files
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
if (await fs.pathExists(commandsDir)) {
|
|
||||||
// Remove any bmad* files (cleans up old bmad- and bmad: formats)
|
|
||||||
const files = await fs.readdir(commandsDir);
|
|
||||||
let removed = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('bmad') && file.endsWith('.toml')) {
|
|
||||||
await fs.remove(path.join(commandsDir, file));
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Gemini
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const geminiDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(geminiDir, this.commandsDir);
|
|
||||||
|
|
||||||
// Create .gemini/commands directory if it doesn't exist
|
|
||||||
await fs.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher in TOML format
|
|
||||||
const launcherContent = `description = "Custom BMAD Agent: ${agentName}"
|
|
||||||
prompt = """
|
|
||||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
|
||||||
|
|
||||||
This is a launcher for the custom BMAD agent "${agentName}".
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. First run: \`${agentPath}\` to load the complete agent
|
|
||||||
2. Then use this command to activate ${agentName}
|
|
||||||
|
|
||||||
The agent will follow the persona and instructions from the main agent file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generated by BMAD Method*
|
|
||||||
"""`;
|
|
||||||
|
|
||||||
const fileName = `bmad-custom-${agentName.toLowerCase()}.toml`;
|
|
||||||
const launcherPath = path.join(commandsDir, fileName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'gemini',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: agentName,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { GeminiSetup };
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GitHub Copilot setup handler
|
|
||||||
* Creates agents in .github/agents/ and configures VS Code settings
|
|
||||||
*/
|
|
||||||
class GitHubCopilotSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('github-copilot', 'GitHub Copilot', true); // preferred IDE
|
|
||||||
this.configDir = '.github';
|
|
||||||
this.agentsDir = 'agents';
|
|
||||||
this.vscodeDir = '.vscode';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect configuration choices before installation
|
|
||||||
* @param {Object} options - Configuration options
|
|
||||||
* @returns {Object} Collected configuration
|
|
||||||
*/
|
|
||||||
async collectConfiguration(options = {}) {
|
|
||||||
const config = {};
|
|
||||||
|
|
||||||
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
|
||||||
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
|
||||||
|
|
||||||
config.vsCodeConfig = await prompts.select({
|
|
||||||
message: 'How would you like to configure VS Code settings?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
|
||||||
{ name: 'Configure each setting manually', value: 'manual' },
|
|
||||||
{ name: 'Skip settings configuration', value: 'skip' },
|
|
||||||
],
|
|
||||||
default: 'defaults',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.vsCodeConfig === 'manual') {
|
|
||||||
config.manualSettings = await prompts.prompt([
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'maxRequests',
|
|
||||||
message: 'Maximum requests per session (1-50)?',
|
|
||||||
default: '15',
|
|
||||||
validate: (input) => {
|
|
||||||
const num = parseInt(input, 10);
|
|
||||||
if (isNaN(num)) return 'Enter a valid number 1-50';
|
|
||||||
if (num < 1 || num > 50) return 'Enter a number between 1-50';
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'runTasks',
|
|
||||||
message: 'Allow running workspace tasks?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'mcpDiscovery',
|
|
||||||
message: 'Enable MCP server discovery?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'autoFix',
|
|
||||||
message: 'Enable automatic error fixing?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'autoApprove',
|
|
||||||
message: 'Auto-approve tools (less secure)?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup GitHub Copilot configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Configure VS Code settings using pre-collected config if available
|
|
||||||
const config = options.preCollectedConfig || {};
|
|
||||||
await this.configureVsCodeSettings(projectDir, { ...options, ...config });
|
|
||||||
|
|
||||||
// Create .github/agents directory
|
|
||||||
const githubDir = path.join(projectDir, this.configDir);
|
|
||||||
const agentsDir = path.join(githubDir, this.agentsDir);
|
|
||||||
await this.ensureDir(agentsDir);
|
|
||||||
|
|
||||||
// Clean up any existing BMAD files before reinstalling
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Create agent files with bmd- prefix
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const content = artifact.content;
|
|
||||||
const agentContent = await this.createAgentContent({ module: artifact.module, name: artifact.name }, content);
|
|
||||||
|
|
||||||
// Use bmd- prefix: bmd-custom-{module}-{name}.agent.md
|
|
||||||
const targetPath = path.join(agentsDir, `bmd-custom-${artifact.module}-${artifact.name}.agent.md`);
|
|
||||||
await this.writeFile(targetPath, agentContent);
|
|
||||||
agentCount++;
|
|
||||||
|
|
||||||
console.log(chalk.green(` ✓ Created agent: bmd-custom-${artifact.module}-${artifact.name}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents created`));
|
|
||||||
console.log(chalk.dim(` - Agents directory: ${path.relative(projectDir, agentsDir)}`));
|
|
||||||
console.log(chalk.dim(` - VS Code settings configured`));
|
|
||||||
console.log(chalk.dim('\n Agents available in VS Code Chat view'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
settings: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure VS Code settings for GitHub Copilot
|
|
||||||
*/
|
|
||||||
async configureVsCodeSettings(projectDir, options) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const vscodeDir = path.join(projectDir, this.vscodeDir);
|
|
||||||
const settingsPath = path.join(vscodeDir, 'settings.json');
|
|
||||||
|
|
||||||
await this.ensureDir(vscodeDir);
|
|
||||||
|
|
||||||
// Read existing settings
|
|
||||||
let existingSettings = {};
|
|
||||||
if (await fs.pathExists(settingsPath)) {
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(settingsPath, 'utf8');
|
|
||||||
existingSettings = JSON.parse(content);
|
|
||||||
console.log(chalk.yellow(' Found existing .vscode/settings.json'));
|
|
||||||
} catch {
|
|
||||||
console.warn(chalk.yellow(' Could not parse settings.json, creating new'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use pre-collected configuration or skip if not available
|
|
||||||
let configChoice = options.vsCodeConfig;
|
|
||||||
if (!configChoice) {
|
|
||||||
// If no pre-collected config, skip configuration
|
|
||||||
console.log(chalk.yellow(' ⚠ No configuration collected, skipping VS Code settings'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configChoice === 'skip') {
|
|
||||||
console.log(chalk.yellow(' ⚠ Skipping VS Code settings'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bmadSettings = {};
|
|
||||||
|
|
||||||
if (configChoice === 'defaults') {
|
|
||||||
bmadSettings = {
|
|
||||||
'chat.agent.enabled': true,
|
|
||||||
'chat.agent.maxRequests': 15,
|
|
||||||
'github.copilot.chat.agent.runTasks': true,
|
|
||||||
'chat.mcp.discovery.enabled': true,
|
|
||||||
'github.copilot.chat.agent.autoFix': true,
|
|
||||||
'chat.tools.autoApprove': false,
|
|
||||||
};
|
|
||||||
console.log(chalk.green(' ✓ Using recommended defaults'));
|
|
||||||
} else {
|
|
||||||
// Manual configuration - use pre-collected settings
|
|
||||||
const manual = options.manualSettings || {};
|
|
||||||
|
|
||||||
const maxRequests = parseInt(manual.maxRequests || '15', 10);
|
|
||||||
bmadSettings = {
|
|
||||||
'chat.agent.enabled': true,
|
|
||||||
'chat.agent.maxRequests': isNaN(maxRequests) ? 15 : maxRequests,
|
|
||||||
'github.copilot.chat.agent.runTasks': manual.runTasks === undefined ? true : manual.runTasks,
|
|
||||||
'chat.mcp.discovery.enabled': manual.mcpDiscovery === undefined ? true : manual.mcpDiscovery,
|
|
||||||
'github.copilot.chat.agent.autoFix': manual.autoFix === undefined ? true : manual.autoFix,
|
|
||||||
'chat.tools.autoApprove': manual.autoApprove || false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge settings (existing take precedence)
|
|
||||||
const mergedSettings = { ...bmadSettings, ...existingSettings };
|
|
||||||
|
|
||||||
// Write settings
|
|
||||||
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
||||||
console.log(chalk.green(' ✓ VS Code settings configured'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create agent content
|
|
||||||
*/
|
|
||||||
async createAgentContent(agent, content) {
|
|
||||||
// Extract metadata from launcher frontmatter if present
|
|
||||||
const descMatch = content.match(/description:\s*"([^"]+)"/);
|
|
||||||
const title = descMatch ? descMatch[1] : this.formatTitle(agent.name);
|
|
||||||
|
|
||||||
const description = `Activates the ${title} agent persona.`;
|
|
||||||
|
|
||||||
// Strip any existing frontmatter from the content
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
let cleanContent = content;
|
|
||||||
if (frontmatterRegex.test(content)) {
|
|
||||||
cleanContent = content.replace(frontmatterRegex, '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Available GitHub Copilot tools (November 2025 - Official VS Code Documentation)
|
|
||||||
// Reference: https://code.visualstudio.com/docs/copilot/reference/copilot-vscode-features#_chat-tools
|
|
||||||
const tools = [
|
|
||||||
'changes', // List of source control changes
|
|
||||||
'edit', // Edit files in your workspace including: createFile, createDirectory, editNotebook, newJupyterNotebook and editFiles
|
|
||||||
'fetch', // Fetch content from web page
|
|
||||||
'githubRepo', // Perform code search in GitHub repo
|
|
||||||
'problems', // Add workspace issues from Problems panel
|
|
||||||
'runCommands', // Runs commands in the terminal including: getTerminalOutput, terminalSelection, terminalLastCommand and runInTerminal
|
|
||||||
'runTasks', // Runs tasks and gets their output for your workspace
|
|
||||||
'runTests', // Run unit tests in workspace
|
|
||||||
'search', // Search and read files in your workspace, including:fileSearch, textSearch, listDirectory, readFile, codebase and searchResults
|
|
||||||
'runSubagent', // Runs a task within an isolated subagent context. Enables efficient organization of tasks and context window management.
|
|
||||||
'testFailure', // Get unit test failure information
|
|
||||||
'todos', // Tool for managing and tracking todo items for task planning
|
|
||||||
'usages', // Find references and navigate definitions
|
|
||||||
];
|
|
||||||
|
|
||||||
let agentContent = `---
|
|
||||||
description: "${description.replaceAll('"', String.raw`\"`)}"
|
|
||||||
tools: ${JSON.stringify(tools)}
|
|
||||||
---
|
|
||||||
|
|
||||||
# ${title} Agent
|
|
||||||
|
|
||||||
${cleanContent}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
return agentContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format name as title
|
|
||||||
*/
|
|
||||||
formatTitle(name) {
|
|
||||||
return name
|
|
||||||
.split('-')
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup GitHub Copilot configuration - surgically remove only BMAD files
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
|
|
||||||
// Clean up old chatmodes directory
|
|
||||||
const chatmodesDir = path.join(projectDir, this.configDir, 'chatmodes');
|
|
||||||
if (await fs.pathExists(chatmodesDir)) {
|
|
||||||
const files = await fs.readdir(chatmodesDir);
|
|
||||||
let removed = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('bmad') && file.endsWith('.chatmode.md')) {
|
|
||||||
await fs.remove(path.join(chatmodesDir, file));
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
console.log(chalk.dim(` Cleaned up ${removed} old BMAD chat modes`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up new agents directory
|
|
||||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
|
||||||
if (await fs.pathExists(agentsDir)) {
|
|
||||||
const files = await fs.readdir(agentsDir);
|
|
||||||
let removed = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('bmd-') && file.endsWith('.agent.md')) {
|
|
||||||
await fs.remove(path.join(agentsDir, file));
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for GitHub Copilot
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object|null} Info about created command
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
|
||||||
return null; // IDE not configured for this project
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.ensureDir(agentsDir);
|
|
||||||
|
|
||||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from @${agentPath}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// GitHub Copilot needs specific tools in frontmatter
|
|
||||||
const copilotTools = [
|
|
||||||
'changes',
|
|
||||||
'codebase',
|
|
||||||
'createDirectory',
|
|
||||||
'createFile',
|
|
||||||
'editFiles',
|
|
||||||
'fetch',
|
|
||||||
'fileSearch',
|
|
||||||
'githubRepo',
|
|
||||||
'listDirectory',
|
|
||||||
'problems',
|
|
||||||
'readFile',
|
|
||||||
'runInTerminal',
|
|
||||||
'runTask',
|
|
||||||
'runTests',
|
|
||||||
'runVscodeCommand',
|
|
||||||
'search',
|
|
||||||
'searchResults',
|
|
||||||
'terminalLastCommand',
|
|
||||||
'terminalSelection',
|
|
||||||
'testFailure',
|
|
||||||
'textSearch',
|
|
||||||
'usages',
|
|
||||||
];
|
|
||||||
|
|
||||||
const agentContent = `---
|
|
||||||
description: "Activates the ${metadata.title || agentName} agent persona."
|
|
||||||
tools: ${JSON.stringify(copilotTools)}
|
|
||||||
---
|
|
||||||
|
|
||||||
# ${metadata.title || agentName} Agent
|
|
||||||
|
|
||||||
${launcherContent}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const agentFilePath = path.join(agentsDir, `bmd-custom-${agentName}.agent.md`);
|
|
||||||
await this.writeFile(agentFilePath, agentContent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: agentFilePath,
|
|
||||||
command: `bmd-custom-${agentName}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { GitHubCopilotSetup };
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* iFlow CLI setup handler
|
|
||||||
* Creates commands in .iflow/commands/ directory structure
|
|
||||||
*/
|
|
||||||
class IFlowSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('iflow', 'iFlow CLI');
|
|
||||||
this.configDir = '.iflow';
|
|
||||||
this.commandsDir = 'commands';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup iFlow CLI configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Create .iflow/commands/bmad directory structure
|
|
||||||
const iflowDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
|
|
||||||
const agentsDir = path.join(commandsDir, 'agents');
|
|
||||||
const tasksDir = path.join(commandsDir, 'tasks');
|
|
||||||
const workflowsDir = path.join(commandsDir, 'workflows');
|
|
||||||
|
|
||||||
await this.ensureDir(agentsDir);
|
|
||||||
await this.ensureDir(tasksDir);
|
|
||||||
await this.ensureDir(workflowsDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Setup agents as commands
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const commandContent = await this.createAgentCommand(artifact);
|
|
||||||
|
|
||||||
const targetPath = path.join(agentsDir, `${artifact.module}-${artifact.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
agentCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tasks and workflows (ALL workflows now generate commands)
|
|
||||||
const tasks = await this.getTasks(bmadDir);
|
|
||||||
|
|
||||||
// Get ALL workflows using the new workflow command generator
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Setup tasks as commands
|
|
||||||
let taskCount = 0;
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readFile(task.path);
|
|
||||||
const commandContent = this.createTaskCommand(task, content);
|
|
||||||
|
|
||||||
const targetPath = path.join(tasksDir, `${task.module}-${task.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
taskCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup workflows as commands (already generated)
|
|
||||||
let workflowCount = 0;
|
|
||||||
for (const artifact of workflowArtifacts) {
|
|
||||||
if (artifact.type === 'workflow-command') {
|
|
||||||
const targetPath = path.join(workflowsDir, `${artifact.module}-${path.basename(artifact.relativePath, '.md')}.md`);
|
|
||||||
await this.writeFile(targetPath, artifact.content);
|
|
||||||
workflowCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agent commands created`));
|
|
||||||
console.log(chalk.dim(` - ${taskCount} task commands created`));
|
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflow commands created`));
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskCount,
|
|
||||||
workflows: workflowCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create agent command content
|
|
||||||
*/
|
|
||||||
async createAgentCommand(artifact) {
|
|
||||||
// The launcher content is already complete - just return it as-is
|
|
||||||
return artifact.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create task command content
|
|
||||||
*/
|
|
||||||
createTaskCommand(task, content) {
|
|
||||||
// Extract task name
|
|
||||||
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
|
|
||||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
|
||||||
|
|
||||||
let commandContent = `# /task-${task.name} Command
|
|
||||||
|
|
||||||
When this command is used, execute the following task:
|
|
||||||
|
|
||||||
## ${taskName} Task
|
|
||||||
|
|
||||||
${content}
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
This command executes the ${taskName} task from the BMAD ${task.module.toUpperCase()} module.
|
|
||||||
|
|
||||||
## Module
|
|
||||||
|
|
||||||
Part of the BMAD ${task.module.toUpperCase()} module.
|
|
||||||
`;
|
|
||||||
|
|
||||||
return commandContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup iFlow configuration
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
|
|
||||||
|
|
||||||
if (await fs.pathExists(bmadCommandsDir)) {
|
|
||||||
await fs.remove(bmadCommandsDir);
|
|
||||||
console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for iFlow
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const iflowDir = path.join(projectDir, this.configDir);
|
|
||||||
const bmadCommandsDir = path.join(iflowDir, this.commandsDir, 'bmad');
|
|
||||||
|
|
||||||
// Create .iflow/commands/bmad directory if it doesn't exist
|
|
||||||
await fs.ensureDir(bmadCommandsDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher
|
|
||||||
const launcherContent = `# ${agentName} Custom Agent
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
|
||||||
|
|
||||||
This is a launcher for the custom BMAD agent "${agentName}".
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. First run: \`${agentPath}\` to load the complete agent
|
|
||||||
2. Then use this command to activate ${agentName}
|
|
||||||
|
|
||||||
The agent will follow the persona and instructions from the main agent file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generated by BMAD Method*`;
|
|
||||||
|
|
||||||
const fileName = `custom-${agentName.toLowerCase()}.md`;
|
|
||||||
const launcherPath = path.join(bmadCommandsDir, fileName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'iflow',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: agentName,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { IFlowSetup };
|
|
||||||
@@ -5,11 +5,15 @@ const chalk = require('chalk');
|
|||||||
/**
|
/**
|
||||||
* IDE Manager - handles IDE-specific setup
|
* IDE Manager - handles IDE-specific setup
|
||||||
* Dynamically discovers and loads IDE handlers
|
* Dynamically discovers and loads IDE handlers
|
||||||
|
*
|
||||||
|
* Loading strategy:
|
||||||
|
* 1. Custom installer files (codex.js, kilo.js, kiro-cli.js) - for platforms with unique installation logic
|
||||||
|
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||||
*/
|
*/
|
||||||
class IdeManager {
|
class IdeManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.handlers = new Map();
|
this.handlers = new Map();
|
||||||
this.loadHandlers();
|
this._initialized = false;
|
||||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,53 +32,76 @@ class IdeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically load all IDE handlers from directory
|
* Ensure handlers are loaded (lazy loading)
|
||||||
*/
|
*/
|
||||||
loadHandlers() {
|
async ensureInitialized() {
|
||||||
|
if (!this._initialized) {
|
||||||
|
await this.loadHandlers();
|
||||||
|
this._initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically load all IDE handlers
|
||||||
|
* 1. Load custom installer files first (codex.js, kilo.js, kiro-cli.js)
|
||||||
|
* 2. Load config-driven handlers from platform-codes.yaml
|
||||||
|
*/
|
||||||
|
async loadHandlers() {
|
||||||
|
// Load custom installer files
|
||||||
|
this.loadCustomInstallerFiles();
|
||||||
|
|
||||||
|
// Load config-driven handlers from platform-codes.yaml
|
||||||
|
await this.loadConfigDrivenHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load custom installer files (unique installation logic)
|
||||||
|
* These files have special installation patterns that don't fit the config-driven model
|
||||||
|
*/
|
||||||
|
loadCustomInstallerFiles() {
|
||||||
const ideDir = __dirname;
|
const ideDir = __dirname;
|
||||||
|
const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js'];
|
||||||
|
|
||||||
|
for (const file of customFiles) {
|
||||||
|
const filePath = path.join(ideDir, file);
|
||||||
|
if (!fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all JS files in the IDE directory
|
const HandlerModule = require(filePath);
|
||||||
const files = fs.readdirSync(ideDir).filter((file) => {
|
const HandlerClass = HandlerModule.default || Object.values(HandlerModule)[0];
|
||||||
// Skip base class, manager, utility files (starting with _), and helper modules
|
|
||||||
return (
|
|
||||||
file.endsWith('.js') &&
|
|
||||||
!file.startsWith('_') &&
|
|
||||||
file !== 'manager.js' &&
|
|
||||||
file !== 'workflow-command-generator.js' &&
|
|
||||||
file !== 'task-tool-command-generator.js'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort alphabetically for consistent ordering
|
|
||||||
files.sort();
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const moduleName = path.basename(file, '.js');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const modulePath = path.join(ideDir, file);
|
|
||||||
const HandlerModule = require(modulePath);
|
|
||||||
|
|
||||||
// Get the first exported class (handles various export styles)
|
|
||||||
const HandlerClass = HandlerModule.default || HandlerModule[Object.keys(HandlerModule)[0]];
|
|
||||||
|
|
||||||
if (HandlerClass) {
|
if (HandlerClass) {
|
||||||
const instance = new HandlerClass();
|
const instance = new HandlerClass();
|
||||||
// Use the name property from the instance (set in constructor)
|
|
||||||
// Only add if the instance has a valid name
|
|
||||||
if (instance.name && typeof instance.name === 'string') {
|
if (instance.name && typeof instance.name === 'string') {
|
||||||
this.handlers.set(instance.name, instance);
|
this.handlers.set(instance.name, instance);
|
||||||
} else {
|
|
||||||
console.log(chalk.yellow(` Warning: ${moduleName} handler missing valid 'name' property`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`));
|
console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
console.error(chalk.red('Failed to load IDE handlers:'), error.message);
|
|
||||||
|
/**
|
||||||
|
* Load config-driven handlers from platform-codes.yaml
|
||||||
|
* This creates ConfigDrivenIdeSetup instances for platforms with installer config
|
||||||
|
*/
|
||||||
|
async loadConfigDrivenHandlers() {
|
||||||
|
const { loadPlatformCodes } = require('./platform-codes');
|
||||||
|
const platformConfig = await loadPlatformCodes();
|
||||||
|
|
||||||
|
const { ConfigDrivenIdeSetup } = require('./_config-driven');
|
||||||
|
|
||||||
|
for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) {
|
||||||
|
// Skip if already loaded by custom installer
|
||||||
|
if (this.handlers.has(platformCode)) continue;
|
||||||
|
|
||||||
|
// Skip if no installer config (platform may not need installation)
|
||||||
|
if (!platformInfo.installer) continue;
|
||||||
|
|
||||||
|
const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
|
||||||
|
handler.setBmadFolderName(this.bmadFolderName);
|
||||||
|
this.handlers.set(platformCode, handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const os = require('node:os');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenCode IDE setup handler
|
|
||||||
*/
|
|
||||||
class OpenCodeSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('opencode', 'OpenCode', true); // Mark as preferred/recommended
|
|
||||||
this.configDir = '.opencode';
|
|
||||||
this.commandsDir = 'command';
|
|
||||||
this.agentsDir = 'agent';
|
|
||||||
}
|
|
||||||
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
const baseDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsBaseDir = path.join(baseDir, this.commandsDir);
|
|
||||||
const agentsBaseDir = path.join(baseDir, this.agentsDir);
|
|
||||||
|
|
||||||
await this.ensureDir(commandsBaseDir);
|
|
||||||
await this.ensureDir(agentsBaseDir);
|
|
||||||
|
|
||||||
// Clean up any existing BMAD files before reinstalling
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Install primary agents with flat naming: bmad-agent-{module}-{name}.md
|
|
||||||
// OpenCode agents go in the agent folder (not command folder)
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const agentContent = artifact.content;
|
|
||||||
// Flat structure in agent folder: bmad-agent-{module}-{name}.md
|
|
||||||
const targetPath = path.join(agentsBaseDir, `bmad-agent-${artifact.module}-${artifact.name}.md`);
|
|
||||||
await this.writeFile(targetPath, agentContent);
|
|
||||||
agentCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install workflow commands with flat naming: bmad-{module}-{workflow-name}
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
let workflowCommandCount = 0;
|
|
||||||
for (const artifact of workflowArtifacts) {
|
|
||||||
if (artifact.type === 'workflow-command') {
|
|
||||||
const commandContent = artifact.content;
|
|
||||||
// Flat structure: bmad-{module}-{workflow-name}.md
|
|
||||||
// artifact.relativePath is like: bmm/workflows/plan-project.md
|
|
||||||
const workflowName = path.basename(artifact.relativePath, '.md');
|
|
||||||
const targetPath = path.join(commandsBaseDir, `bmad-${artifact.module}-${workflowName}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
workflowCommandCount++;
|
|
||||||
}
|
|
||||||
// Skip workflow launcher READMEs as they're not needed in flat structure
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install task and tool commands with flat naming
|
|
||||||
const { tasks, tools } = await this.generateFlatTaskToolCommands(bmadDir, commandsBaseDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed to .opencode/agent/`));
|
|
||||||
if (workflowCommandCount > 0) {
|
|
||||||
console.log(chalk.dim(` - ${workflowCommandCount} workflows installed to .opencode/command/`));
|
|
||||||
}
|
|
||||||
if (tasks + tools > 0) {
|
|
||||||
console.log(chalk.dim(` - ${tasks + tools} tasks/tools installed to .opencode/command/ (${tasks} tasks, ${tools} tools)`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
workflows: workflowCommandCount,
|
|
||||||
workflowCounts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate flat task and tool commands for OpenCode
|
|
||||||
* OpenCode doesn't support nested command directories
|
|
||||||
*/
|
|
||||||
async generateFlatTaskToolCommands(bmadDir, commandsBaseDir) {
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const tasks = await taskToolGen.loadTaskManifest(bmadDir);
|
|
||||||
const tools = await taskToolGen.loadToolManifest(bmadDir);
|
|
||||||
|
|
||||||
// Filter to only standalone items
|
|
||||||
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
|
||||||
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
|
||||||
|
|
||||||
// Generate command files for tasks with flat naming: bmad-task-{module}-{name}.md
|
|
||||||
for (const task of standaloneTasks) {
|
|
||||||
const commandContent = taskToolGen.generateCommandContent(task, 'task');
|
|
||||||
const targetPath = path.join(commandsBaseDir, `bmad-task-${task.module}-${task.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate command files for tools with flat naming: bmad-tool-{module}-{name}.md
|
|
||||||
for (const tool of standaloneTools) {
|
|
||||||
const commandContent = taskToolGen.generateCommandContent(tool, 'tool');
|
|
||||||
const targetPath = path.join(commandsBaseDir, `bmad-tool-${tool.module}-${tool.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
tasks: standaloneTasks.length,
|
|
||||||
tools: standaloneTools.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async readAndProcess(filePath, metadata) {
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
return this.processContent(content, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createAgentContent(content, metadata) {
|
|
||||||
const { frontmatter = {}, body } = this.parseFrontmatter(content);
|
|
||||||
|
|
||||||
frontmatter.description =
|
|
||||||
frontmatter.description && String(frontmatter.description).trim().length > 0
|
|
||||||
? frontmatter.description
|
|
||||||
: `BMAD ${metadata.module} agent: ${metadata.name}`;
|
|
||||||
|
|
||||||
// OpenCode agents use: 'primary' mode for main agents
|
|
||||||
frontmatter.mode = 'primary';
|
|
||||||
|
|
||||||
const frontmatterString = this.stringifyFrontmatter(frontmatter);
|
|
||||||
|
|
||||||
// Get the activation header from central template
|
|
||||||
const activationHeader = await this.getAgentCommandHeader();
|
|
||||||
|
|
||||||
return `${frontmatterString}\n\n${activationHeader}\n\n${body}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseFrontmatter(content) {
|
|
||||||
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
||||||
if (!match) {
|
|
||||||
return { data: {}, body: content };
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = content.slice(match[0].length);
|
|
||||||
|
|
||||||
let frontmatter = {};
|
|
||||||
try {
|
|
||||||
frontmatter = yaml.parse(match[1]) || {};
|
|
||||||
} catch {
|
|
||||||
frontmatter = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { frontmatter, body };
|
|
||||||
}
|
|
||||||
|
|
||||||
stringifyFrontmatter(frontmatter) {
|
|
||||||
const yamlText = yaml
|
|
||||||
.dump(frontmatter, {
|
|
||||||
indent: 2,
|
|
||||||
lineWidth: -1,
|
|
||||||
noRefs: true,
|
|
||||||
sortKeys: false,
|
|
||||||
})
|
|
||||||
.trimEnd();
|
|
||||||
|
|
||||||
return `---\n${yamlText}\n---`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup OpenCode configuration - surgically remove only BMAD files
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
let removed = 0;
|
|
||||||
|
|
||||||
// Clean up agent folder
|
|
||||||
if (await fs.pathExists(agentsDir)) {
|
|
||||||
const files = await fs.readdir(agentsDir);
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
|
||||||
await fs.remove(path.join(agentsDir, file));
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up command folder
|
|
||||||
if (await fs.pathExists(commandsDir)) {
|
|
||||||
const files = await fs.readdir(commandsDir);
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
|
||||||
await fs.remove(path.join(commandsDir, file));
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for OpenCode
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object|null} Info about created command
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const agentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
|
||||||
return null; // IDE not configured for this project
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.ensureDir(agentsDir);
|
|
||||||
|
|
||||||
const launcherContent = `---
|
|
||||||
name: '${agentName}'
|
|
||||||
description: '${metadata.title || agentName} agent'
|
|
||||||
mode: 'primary'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from @${agentPath}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// OpenCode uses flat naming: bmad-agent-custom-{name}.md
|
|
||||||
const launcherPath = path.join(agentsDir, `bmad-agent-custom-${agentName}.md`);
|
|
||||||
await this.writeFile(launcherPath, launcherContent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: launcherPath,
|
|
||||||
command: `bmad-agent-custom-${agentName}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { OpenCodeSetup };
|
|
||||||
100
tools/cli/installers/lib/ide/platform-codes.js
Normal file
100
tools/cli/installers/lib/ide/platform-codes.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('node:path');
|
||||||
|
const yaml = require('yaml');
|
||||||
|
|
||||||
|
const PLATFORM_CODES_PATH = path.join(__dirname, 'platform-codes.yaml');
|
||||||
|
|
||||||
|
let _cachedPlatformCodes = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the platform codes configuration from YAML
|
||||||
|
* @returns {Object} Platform codes configuration
|
||||||
|
*/
|
||||||
|
async function loadPlatformCodes() {
|
||||||
|
if (_cachedPlatformCodes) {
|
||||||
|
return _cachedPlatformCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(PLATFORM_CODES_PATH))) {
|
||||||
|
throw new Error(`Platform codes configuration not found at: ${PLATFORM_CODES_PATH}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(PLATFORM_CODES_PATH, 'utf8');
|
||||||
|
_cachedPlatformCodes = yaml.parse(content);
|
||||||
|
return _cachedPlatformCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get platform information by code
|
||||||
|
* @param {string} platformCode - Platform code (e.g., 'claude-code', 'cursor')
|
||||||
|
* @returns {Object|null} Platform info or null if not found
|
||||||
|
*/
|
||||||
|
function getPlatformInfo(platformCode) {
|
||||||
|
if (!_cachedPlatformCodes) {
|
||||||
|
throw new Error('Platform codes not loaded. Call loadPlatformCodes() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _cachedPlatformCodes.platforms[platformCode] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all preferred platforms
|
||||||
|
* @returns {Promise<Array>} Array of preferred platform codes
|
||||||
|
*/
|
||||||
|
async function getPreferredPlatforms() {
|
||||||
|
const config = await loadPlatformCodes();
|
||||||
|
return Object.entries(config.platforms)
|
||||||
|
.filter(([_, info]) => info.preferred)
|
||||||
|
.map(([code, _]) => code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all platform codes by category
|
||||||
|
* @param {string} category - Category to filter by (ide, cli, tool, etc.)
|
||||||
|
* @returns {Promise<Array>} Array of platform codes in the category
|
||||||
|
*/
|
||||||
|
async function getPlatformsByCategory(category) {
|
||||||
|
const config = await loadPlatformCodes();
|
||||||
|
return Object.entries(config.platforms)
|
||||||
|
.filter(([_, info]) => info.category === category)
|
||||||
|
.map(([code, _]) => code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all platforms with installer config
|
||||||
|
* @returns {Promise<Array>} Array of platform codes that have installer config
|
||||||
|
*/
|
||||||
|
async function getConfigDrivenPlatforms() {
|
||||||
|
const config = await loadPlatformCodes();
|
||||||
|
return Object.entries(config.platforms)
|
||||||
|
.filter(([_, info]) => info.installer)
|
||||||
|
.map(([code, _]) => code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get platforms that use custom installers (no installer config)
|
||||||
|
* @returns {Promise<Array>} Array of platform codes with custom installers
|
||||||
|
*/
|
||||||
|
async function getCustomInstallerPlatforms() {
|
||||||
|
const config = await loadPlatformCodes();
|
||||||
|
return Object.entries(config.platforms)
|
||||||
|
.filter(([_, info]) => !info.installer)
|
||||||
|
.map(([code, _]) => code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cached platform codes (useful for testing)
|
||||||
|
*/
|
||||||
|
function clearCache() {
|
||||||
|
_cachedPlatformCodes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadPlatformCodes,
|
||||||
|
getPlatformInfo,
|
||||||
|
getPreferredPlatforms,
|
||||||
|
getPlatformsByCategory,
|
||||||
|
getConfigDrivenPlatforms,
|
||||||
|
getCustomInstallerPlatforms,
|
||||||
|
clearCache,
|
||||||
|
};
|
||||||
241
tools/cli/installers/lib/ide/platform-codes.yaml
Normal file
241
tools/cli/installers/lib/ide/platform-codes.yaml
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# BMAD Platform Codes Configuration
|
||||||
|
# Central configuration for all platform/IDE codes used in the BMAD system
|
||||||
|
#
|
||||||
|
# This file defines:
|
||||||
|
# 1. Platform metadata (name, preferred status, category, description)
|
||||||
|
# 2. Installer configuration (target directories, templates, artifact types)
|
||||||
|
#
|
||||||
|
# Format:
|
||||||
|
# code: Platform identifier used internally
|
||||||
|
# name: Display name shown to users
|
||||||
|
# preferred: Whether this platform is shown as a recommended option on install
|
||||||
|
# category: Type of platform (ide, cli, tool, service)
|
||||||
|
# description: Brief description of the platform
|
||||||
|
# installer: Installation configuration (optional - omit for custom installers)
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
# ============================================================================
|
||||||
|
# CLI Tools
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
claude-code:
|
||||||
|
name: "Claude Code"
|
||||||
|
preferred: true
|
||||||
|
category: cli
|
||||||
|
description: "Anthropic's official CLI for Claude"
|
||||||
|
installer:
|
||||||
|
target_dir: .claude/commands
|
||||||
|
template_type: default
|
||||||
|
|
||||||
|
auggie:
|
||||||
|
name: "Auggie"
|
||||||
|
preferred: false
|
||||||
|
category: cli
|
||||||
|
description: "AI development tool"
|
||||||
|
installer:
|
||||||
|
target_dir: .augment/commands
|
||||||
|
template_type: default
|
||||||
|
|
||||||
|
gemini:
|
||||||
|
name: "Gemini CLI"
|
||||||
|
preferred: false
|
||||||
|
category: cli
|
||||||
|
description: "Google's CLI for Gemini"
|
||||||
|
installer:
|
||||||
|
target_dir: .gemini/commands
|
||||||
|
template_type: gemini
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# IDEs
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
cursor:
|
||||||
|
name: "Cursor"
|
||||||
|
preferred: true
|
||||||
|
category: ide
|
||||||
|
description: "AI-first code editor"
|
||||||
|
installer:
|
||||||
|
target_dir: .cursor/commands
|
||||||
|
template_type: default
|
||||||
|
|
||||||
|
windsurf:
|
||||||
|
name: "Windsurf"
|
||||||
|
preferred: true
|
||||||
|
category: ide
|
||||||
|
description: "AI-powered IDE with cascade flows"
|
||||||
|
installer:
|
||||||
|
target_dir: .windsurf/workflows
|
||||||
|
template_type: windsurf
|
||||||
|
|
||||||
|
cline:
|
||||||
|
name: "Cline"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "AI coding assistant"
|
||||||
|
installer:
|
||||||
|
target_dir: .clinerules/workflows
|
||||||
|
template_type: windsurf
|
||||||
|
|
||||||
|
roo:
|
||||||
|
name: "Roo Cline"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "Enhanced Cline fork"
|
||||||
|
installer:
|
||||||
|
target_dir: .roo/commands
|
||||||
|
template_type: default
|
||||||
|
|
||||||
|
github-copilot:
|
||||||
|
name: "GitHub Copilot"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "GitHub's AI pair programmer"
|
||||||
|
installer:
|
||||||
|
targets:
|
||||||
|
- target_dir: .github/agents
|
||||||
|
template_type: copilot_agents
|
||||||
|
artifact_types: [agents]
|
||||||
|
- target_dir: .vscode
|
||||||
|
template_type: vscode_settings
|
||||||
|
artifact_types: []
|
||||||
|
|
||||||
|
opencode:
|
||||||
|
name: "OpenCode"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "OpenCode terminal coding assistant"
|
||||||
|
installer:
|
||||||
|
target_dir: .opencode/command
|
||||||
|
template_type: opencode
|
||||||
|
|
||||||
|
crush:
|
||||||
|
name: "Crush"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "AI development assistant"
|
||||||
|
installer:
|
||||||
|
target_dir: .crush/commands
|
||||||
|
template_type: default
|
||||||
|
|
||||||
|
iflow:
|
||||||
|
name: "iFlow"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "AI workflow automation"
|
||||||
|
installer:
|
||||||
|
target_dir: .iflow/commands
|
||||||
|
template_type: default
|
||||||
|
|
||||||
|
qwen:
|
||||||
|
name: "QwenCoder"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "Qwen AI coding assistant"
|
||||||
|
installer:
|
||||||
|
target_dir: .qwen/commands
|
||||||
|
template_type: default
|
||||||
|
|
||||||
|
rovo-dev:
|
||||||
|
name: "Rovo Dev"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "Atlassian's Rovo development environment"
|
||||||
|
installer:
|
||||||
|
target_dir: .rovodev/workflows
|
||||||
|
template_type: rovodev
|
||||||
|
|
||||||
|
trae:
|
||||||
|
name: "Trae"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "AI coding tool"
|
||||||
|
installer:
|
||||||
|
target_dir: .trae/rules
|
||||||
|
template_type: trae
|
||||||
|
|
||||||
|
antigravity:
|
||||||
|
name: "Google Antigravity"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "Google's AI development environment"
|
||||||
|
installer:
|
||||||
|
target_dir: .agent/workflows
|
||||||
|
template_type: antigravity
|
||||||
|
# Note: Antigravity uses .agent/workflows/ directory (not .antigravity/)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Custom Installers (no installer config - use custom file)
|
||||||
|
# These have unique installation logic that doesn't fit the config-driven model
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
codex:
|
||||||
|
name: "Codex"
|
||||||
|
preferred: false
|
||||||
|
category: cli
|
||||||
|
description: "OpenAI Codex integration"
|
||||||
|
# No installer config - uses custom codex.js
|
||||||
|
|
||||||
|
kilo:
|
||||||
|
name: "KiloCoder"
|
||||||
|
preferred: false
|
||||||
|
category: ide
|
||||||
|
description: "AI coding platform"
|
||||||
|
# No installer config - uses custom kilo.js (creates .kilocodemodes file)
|
||||||
|
|
||||||
|
kiro-cli:
|
||||||
|
name: "Kiro CLI"
|
||||||
|
preferred: false
|
||||||
|
category: cli
|
||||||
|
description: "Kiro command-line interface"
|
||||||
|
# No installer config - uses custom kiro-cli.js (YAML→JSON conversion)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Installer Config Schema
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# installer:
|
||||||
|
# target_dir: string # Directory where artifacts are installed
|
||||||
|
# template_type: string # Default template type to use
|
||||||
|
# header_template: string (optional) # Override for header/frontmatter template
|
||||||
|
# body_template: string (optional) # Override for body/content template
|
||||||
|
# targets: array (optional) # For multi-target installations
|
||||||
|
# - target_dir: string
|
||||||
|
# template_type: string
|
||||||
|
# artifact_types: [agents, workflows, tasks, tools]
|
||||||
|
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
||||||
|
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Platform Categories
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
categories:
|
||||||
|
ide:
|
||||||
|
name: "Integrated Development Environment"
|
||||||
|
description: "Full-featured code editors with AI assistance"
|
||||||
|
|
||||||
|
cli:
|
||||||
|
name: "Command Line Interface"
|
||||||
|
description: "Terminal-based tools"
|
||||||
|
|
||||||
|
tool:
|
||||||
|
name: "Development Tool"
|
||||||
|
description: "Standalone development utilities"
|
||||||
|
|
||||||
|
service:
|
||||||
|
name: "Cloud Service"
|
||||||
|
description: "Cloud-based development platforms"
|
||||||
|
|
||||||
|
extension:
|
||||||
|
name: "Editor Extension"
|
||||||
|
description: "Plugins for existing editors"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Naming Conventions and Rules
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
conventions:
|
||||||
|
code_format: "lowercase-kebab-case"
|
||||||
|
name_format: "Title Case"
|
||||||
|
max_code_length: 20
|
||||||
|
allowed_characters: "a-z0-9-"
|
||||||
@@ -1,372 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Qwen Code setup handler
|
|
||||||
* Creates TOML command files in .qwen/commands/BMad/
|
|
||||||
*/
|
|
||||||
class QwenSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('qwen', 'Qwen Code');
|
|
||||||
this.configDir = '.qwen';
|
|
||||||
this.commandsDir = 'commands';
|
|
||||||
this.bmadDir = 'bmad';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Qwen Code configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Create .qwen/commands/BMad directory structure
|
|
||||||
const qwenDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(qwenDir, this.commandsDir);
|
|
||||||
const bmadCommandsDir = path.join(commandsDir, this.bmadDir);
|
|
||||||
|
|
||||||
await this.ensureDir(bmadCommandsDir);
|
|
||||||
|
|
||||||
// Update existing settings.json if present
|
|
||||||
await this.updateSettings(qwenDir);
|
|
||||||
|
|
||||||
// Clean up old configuration if exists
|
|
||||||
await this.cleanupOldConfig(qwenDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Get tasks, tools, and workflows (standalone only for tools/workflows)
|
|
||||||
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []);
|
|
||||||
const tools = await this.getTools(bmadDir, true);
|
|
||||||
const workflows = await this.getWorkflows(bmadDir, true);
|
|
||||||
|
|
||||||
// Create directories for each module (including standalone)
|
|
||||||
const modules = new Set();
|
|
||||||
for (const item of [...agentArtifacts, ...tasks, ...tools, ...workflows]) modules.add(item.module);
|
|
||||||
|
|
||||||
for (const module of modules) {
|
|
||||||
await this.ensureDir(path.join(bmadCommandsDir, module));
|
|
||||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'agents'));
|
|
||||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks'));
|
|
||||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'tools'));
|
|
||||||
await this.ensureDir(path.join(bmadCommandsDir, module, 'workflows'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create TOML files for each agent launcher
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
// Convert markdown launcher content to TOML format
|
|
||||||
const tomlContent = this.processAgentLauncherContent(artifact.content, {
|
|
||||||
module: artifact.module,
|
|
||||||
name: artifact.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetPath = path.join(bmadCommandsDir, artifact.module, 'agents', `${artifact.name}.toml`);
|
|
||||||
|
|
||||||
await this.writeFile(targetPath, tomlContent);
|
|
||||||
|
|
||||||
agentCount++;
|
|
||||||
console.log(chalk.green(` ✓ Added agent: /bmad_${artifact.module}_agents_${artifact.name}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create TOML files for each task
|
|
||||||
let taskCount = 0;
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readAndProcess(task.path, {
|
|
||||||
module: task.module,
|
|
||||||
name: task.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetPath = path.join(bmadCommandsDir, task.module, 'tasks', `${task.name}.toml`);
|
|
||||||
|
|
||||||
await this.writeFile(targetPath, content);
|
|
||||||
|
|
||||||
taskCount++;
|
|
||||||
console.log(chalk.green(` ✓ Added task: /bmad_${task.module}_tasks_${task.name}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create TOML files for each tool
|
|
||||||
let toolCount = 0;
|
|
||||||
for (const tool of tools) {
|
|
||||||
const content = await this.readAndProcess(tool.path, {
|
|
||||||
module: tool.module,
|
|
||||||
name: tool.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetPath = path.join(bmadCommandsDir, tool.module, 'tools', `${tool.name}.toml`);
|
|
||||||
|
|
||||||
await this.writeFile(targetPath, content);
|
|
||||||
|
|
||||||
toolCount++;
|
|
||||||
console.log(chalk.green(` ✓ Added tool: /bmad_${tool.module}_tools_${tool.name}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create TOML files for each workflow
|
|
||||||
let workflowCount = 0;
|
|
||||||
for (const workflow of workflows) {
|
|
||||||
const content = await this.readAndProcess(workflow.path, {
|
|
||||||
module: workflow.module,
|
|
||||||
name: workflow.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const targetPath = path.join(bmadCommandsDir, workflow.module, 'workflows', `${workflow.name}.toml`);
|
|
||||||
|
|
||||||
await this.writeFile(targetPath, content);
|
|
||||||
|
|
||||||
workflowCount++;
|
|
||||||
console.log(chalk.green(` ✓ Added workflow: /bmad_${workflow.module}_workflows_${workflow.name}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents configured`));
|
|
||||||
console.log(chalk.dim(` - ${taskCount} tasks configured`));
|
|
||||||
console.log(chalk.dim(` - ${toolCount} tools configured`));
|
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflows configured`));
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskCount,
|
|
||||||
tools: toolCount,
|
|
||||||
workflows: workflowCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update settings.json to remove old agent references
|
|
||||||
*/
|
|
||||||
async updateSettings(qwenDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const settingsPath = path.join(qwenDir, 'settings.json');
|
|
||||||
|
|
||||||
if (await fs.pathExists(settingsPath)) {
|
|
||||||
try {
|
|
||||||
const settingsContent = await fs.readFile(settingsPath, 'utf8');
|
|
||||||
const settings = JSON.parse(settingsContent);
|
|
||||||
let updated = false;
|
|
||||||
|
|
||||||
// Remove agent file references from contextFileName
|
|
||||||
if (settings.contextFileName && Array.isArray(settings.contextFileName)) {
|
|
||||||
const originalLength = settings.contextFileName.length;
|
|
||||||
settings.contextFileName = settings.contextFileName.filter(
|
|
||||||
(fileName) => !fileName.startsWith('agents/') && !fileName.startsWith('bmad-method/'),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (settings.contextFileName.length !== originalLength) {
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
||||||
console.log(chalk.green(' ✓ Updated .qwen/settings.json'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(chalk.yellow(' ⚠ Could not update settings.json:'), error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up old configuration directories
|
|
||||||
*/
|
|
||||||
async cleanupOldConfig(qwenDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const agentsDir = path.join(qwenDir, 'agents');
|
|
||||||
const bmadMethodDir = path.join(qwenDir, 'bmad-method');
|
|
||||||
const bmadDir = path.join(qwenDir, 'bmadDir');
|
|
||||||
|
|
||||||
if (await fs.pathExists(agentsDir)) {
|
|
||||||
await fs.remove(agentsDir);
|
|
||||||
console.log(chalk.green(' ✓ Removed old agents directory'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await fs.pathExists(bmadMethodDir)) {
|
|
||||||
await fs.remove(bmadMethodDir);
|
|
||||||
console.log(chalk.green(' ✓ Removed old bmad-method directory'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await fs.pathExists(bmadDir)) {
|
|
||||||
await fs.remove(bmadDir);
|
|
||||||
console.log(chalk.green(' ✓ Removed old BMad directory'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read and process file content
|
|
||||||
*/
|
|
||||||
async readAndProcess(filePath, metadata) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
return this.processContent(content, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process agent launcher content and convert to TOML format
|
|
||||||
* @param {string} launcherContent - Launcher markdown content
|
|
||||||
* @param {Object} metadata - File metadata
|
|
||||||
* @returns {string} TOML formatted content
|
|
||||||
*/
|
|
||||||
processAgentLauncherContent(launcherContent, metadata = {}) {
|
|
||||||
// Strip frontmatter from launcher content
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '');
|
|
||||||
|
|
||||||
// Extract title for TOML description
|
|
||||||
const titleMatch = launcherContent.match(/description:\s*"([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : metadata.name;
|
|
||||||
|
|
||||||
// Create TOML with launcher content (without frontmatter)
|
|
||||||
return `description = "BMAD ${metadata.module.toUpperCase()} Agent: ${title}"
|
|
||||||
prompt = """
|
|
||||||
${contentWithoutFrontmatter.trim()}
|
|
||||||
"""
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override processContent to add TOML metadata header for Qwen
|
|
||||||
* @param {string} content - File content
|
|
||||||
* @param {Object} metadata - File metadata
|
|
||||||
* @returns {string} Processed content with Qwen template
|
|
||||||
*/
|
|
||||||
processContent(content, metadata = {}) {
|
|
||||||
// First apply base processing (includes activation injection for agents)
|
|
||||||
let prompt = super.processContent(content, metadata);
|
|
||||||
|
|
||||||
// Determine the type and description based on content
|
|
||||||
const isAgent = content.includes('<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');
|
|
||||||
|
|
||||||
if (await fs.pathExists(bmadCommandsDir)) {
|
|
||||||
await fs.remove(bmadCommandsDir);
|
|
||||||
console.log(chalk.dim(`Removed BMAD configuration from Qwen Code`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await fs.pathExists(oldBmadMethodDir)) {
|
|
||||||
await fs.remove(oldBmadMethodDir);
|
|
||||||
console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await fs.pathExists(oldBMadDir)) {
|
|
||||||
await fs.remove(oldBMadDir);
|
|
||||||
console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Qwen
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const qwenDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(qwenDir, this.commandsDir);
|
|
||||||
const bmadCommandsDir = path.join(commandsDir, this.bmadDir);
|
|
||||||
|
|
||||||
// Create .qwen/commands/BMad directory if it doesn't exist
|
|
||||||
await fs.ensureDir(bmadCommandsDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher in TOML format (same pattern as regular agents)
|
|
||||||
const launcherContent = `# ${agentName} Custom Agent
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
|
||||||
|
|
||||||
This is a launcher for the custom BMAD agent "${agentName}".
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. First run: \`${agentPath}\` to load the complete agent
|
|
||||||
2. Then use this command to activate ${agentName}
|
|
||||||
|
|
||||||
The agent will follow the persona and instructions from the main agent file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generated by BMAD Method*`;
|
|
||||||
|
|
||||||
// Use Qwen's TOML conversion method
|
|
||||||
const tomlContent = this.processAgentLauncherContent(launcherContent, {
|
|
||||||
name: agentName,
|
|
||||||
module: 'custom',
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileName = `custom-${agentName.toLowerCase()}.toml`;
|
|
||||||
const launcherPath = path.join(bmadCommandsDir, fileName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, tomlContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'qwen',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: agentName,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { QwenSetup };
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Roo IDE setup handler
|
|
||||||
* Creates custom commands in .roo/commands directory
|
|
||||||
*/
|
|
||||||
class RooSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('roo', 'Roo Code');
|
|
||||||
this.configDir = '.roo';
|
|
||||||
this.commandsDir = 'commands';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Roo IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Create .roo/commands directory
|
|
||||||
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
await this.ensureDir(rooCommandsDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
let addedCount = 0;
|
|
||||||
let skippedCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
// Use shared toDashPath to get consistent naming: bmad_bmm_name.md
|
|
||||||
const commandName = toDashPath(artifact.relativePath).replace('.md', '');
|
|
||||||
const commandPath = path.join(rooCommandsDir, `${commandName}.md`);
|
|
||||||
|
|
||||||
// Skip if already exists
|
|
||||||
if (await this.pathExists(commandPath)) {
|
|
||||||
console.log(chalk.dim(` Skipping ${commandName} - already exists`));
|
|
||||||
skippedCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// artifact.sourcePath contains the full path to the agent file
|
|
||||||
if (!artifact.sourcePath) {
|
|
||||||
console.error(`Error: Missing sourcePath for artifact ${artifact.name} from module ${artifact.module}`);
|
|
||||||
console.error(`Artifact object:`, artifact);
|
|
||||||
throw new Error(`Missing sourcePath for agent: ${artifact.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await this.readFile(artifact.sourcePath);
|
|
||||||
|
|
||||||
// Create command file that references the actual _bmad agent
|
|
||||||
await this.createCommandFile(
|
|
||||||
{ module: artifact.module, name: artifact.name, path: artifact.sourcePath },
|
|
||||||
content,
|
|
||||||
commandPath,
|
|
||||||
projectDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
addedCount++;
|
|
||||||
console.log(chalk.green(` ✓ Added command: ${commandName}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${addedCount} commands added`));
|
|
||||||
if (skippedCount > 0) {
|
|
||||||
console.log(chalk.dim(` - ${skippedCount} commands skipped (already exist)`));
|
|
||||||
}
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${this.configDir}/${this.commandsDir}/`));
|
|
||||||
console.log(chalk.dim(` Commands will be available when you open this project in Roo Code`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
commands: addedCount,
|
|
||||||
skipped: skippedCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a unified command file for agents
|
|
||||||
* @param {string} commandPath - Path where to write the command file
|
|
||||||
* @param {Object} options - Command options
|
|
||||||
* @param {string} options.name - Display name for the command
|
|
||||||
* @param {string} options.description - Description for the command
|
|
||||||
* @param {string} options.agentPath - Path to the agent file (relative to project root)
|
|
||||||
* @param {string} [options.icon] - Icon emoji (defaults to 🤖)
|
|
||||||
* @param {string} [options.extraContent] - Additional content to include before activation
|
|
||||||
*/
|
|
||||||
async createAgentCommandFile(commandPath, options) {
|
|
||||||
const { name, description, agentPath, icon = '🤖', extraContent = '' } = options;
|
|
||||||
|
|
||||||
// Build command content with YAML frontmatter
|
|
||||||
let commandContent = `---\n`;
|
|
||||||
commandContent += `name: '${icon} ${name}'\n`;
|
|
||||||
commandContent += `description: '${description}'\n`;
|
|
||||||
commandContent += `---\n\n`;
|
|
||||||
|
|
||||||
commandContent += `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n\n`;
|
|
||||||
|
|
||||||
// Add any extra content (e.g., warnings for custom agents)
|
|
||||||
if (extraContent) {
|
|
||||||
commandContent += `${extraContent}\n\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
commandContent += `<agent-activation CRITICAL="TRUE">\n`;
|
|
||||||
commandContent += `1. LOAD the FULL agent file from @${agentPath}\n`;
|
|
||||||
commandContent += `2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n`;
|
|
||||||
commandContent += `3. Execute ALL activation steps exactly as written in the agent file\n`;
|
|
||||||
commandContent += `4. Follow the agent's persona and menu system precisely\n`;
|
|
||||||
commandContent += `5. Stay in character throughout the session\n`;
|
|
||||||
commandContent += `</agent-activation>\n`;
|
|
||||||
|
|
||||||
// Write command file
|
|
||||||
await this.writeFile(commandPath, commandContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a command file for an agent
|
|
||||||
*/
|
|
||||||
async createCommandFile(agent, content, commandPath, projectDir) {
|
|
||||||
// Extract metadata from agent content
|
|
||||||
const titleMatch = content.match(/title="([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name);
|
|
||||||
|
|
||||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
|
||||||
const icon = iconMatch ? iconMatch[1] : '🤖';
|
|
||||||
|
|
||||||
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
|
|
||||||
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
|
|
||||||
|
|
||||||
// Get relative path
|
|
||||||
const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/');
|
|
||||||
|
|
||||||
// Use unified method
|
|
||||||
await this.createAgentCommandFile(commandPath, {
|
|
||||||
name: title,
|
|
||||||
description: whenToUse,
|
|
||||||
agentPath: relativePath,
|
|
||||||
icon: icon,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format name as title
|
|
||||||
*/
|
|
||||||
formatTitle(name) {
|
|
||||||
return name
|
|
||||||
.split('-')
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup Roo configuration
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
if (await fs.pathExists(rooCommandsDir)) {
|
|
||||||
const files = await fs.readdir(rooCommandsDir);
|
|
||||||
let removedCount = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
|
||||||
await fs.remove(path.join(rooCommandsDir, file));
|
|
||||||
removedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removedCount > 0) {
|
|
||||||
console.log(chalk.dim(`Removed ${removedCount} BMAD commands from .roo/commands/`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also clean up old .roomodes file if it exists
|
|
||||||
const roomodesPath = path.join(projectDir, '.roomodes');
|
|
||||||
if (await fs.pathExists(roomodesPath)) {
|
|
||||||
const content = await fs.readFile(roomodesPath, 'utf8');
|
|
||||||
|
|
||||||
// Remove BMAD modes only
|
|
||||||
const lines = content.split('\n');
|
|
||||||
const filteredLines = [];
|
|
||||||
let skipMode = false;
|
|
||||||
let removedCount = 0;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (/^\s*- slug: bmad/.test(line)) {
|
|
||||||
skipMode = true;
|
|
||||||
removedCount++;
|
|
||||||
} else if (skipMode && /^\s*- slug: /.test(line)) {
|
|
||||||
skipMode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipMode) {
|
|
||||||
filteredLines.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write back filtered content
|
|
||||||
await fs.writeFile(roomodesPath, filteredLines.join('\n'));
|
|
||||||
if (removedCount > 0) {
|
|
||||||
console.log(chalk.dim(`Removed ${removedCount} BMAD modes from legacy .roomodes file`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Roo
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata (unused, kept for compatibility)
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const rooCommandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
await this.ensureDir(rooCommandsDir);
|
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
const commandName = customAgentDashName(agentName).replace('.md', '');
|
|
||||||
const commandPath = path.join(rooCommandsDir, `${commandName}.md`);
|
|
||||||
|
|
||||||
// Check if command already exists
|
|
||||||
if (await this.pathExists(commandPath)) {
|
|
||||||
return {
|
|
||||||
ide: 'roo',
|
|
||||||
path: path.join(this.configDir, this.commandsDir, `${commandName}.md`),
|
|
||||||
command: commandName,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
alreadyExists: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the custom agent file to extract metadata (same as regular agents)
|
|
||||||
const fullAgentPath = path.join(projectDir, agentPath);
|
|
||||||
const content = await this.readFile(fullAgentPath);
|
|
||||||
|
|
||||||
// Extract metadata from agent content
|
|
||||||
const titleMatch = content.match(/title="([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName);
|
|
||||||
|
|
||||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
|
||||||
const icon = iconMatch ? iconMatch[1] : '🤖';
|
|
||||||
|
|
||||||
const whenToUseMatch = content.match(/whenToUse="([^"]+)"/);
|
|
||||||
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
|
|
||||||
|
|
||||||
// Use unified method without extra content (clean)
|
|
||||||
await this.createAgentCommandFile(commandPath, {
|
|
||||||
name: title,
|
|
||||||
description: whenToUse,
|
|
||||||
agentPath: agentPath,
|
|
||||||
icon: icon,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'roo',
|
|
||||||
path: path.join(this.configDir, this.commandsDir, `${commandName}.md`),
|
|
||||||
command: commandName,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { RooSetup };
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rovo Dev IDE setup handler
|
|
||||||
*
|
|
||||||
* Installs BMAD agents as Rovo Dev subagents in .rovodev/subagents/
|
|
||||||
* Installs workflows and tasks/tools as reference guides in .rovodev/
|
|
||||||
* Rovo Dev automatically discovers agents and integrates with BMAD like other IDEs
|
|
||||||
*/
|
|
||||||
class RovoDevSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('rovo-dev', 'Atlassian Rovo Dev', false);
|
|
||||||
this.configDir = '.rovodev';
|
|
||||||
this.subagentsDir = 'subagents';
|
|
||||||
this.workflowsDir = 'workflows';
|
|
||||||
this.referencesDir = 'references';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup old BMAD installation before reinstalling
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const rovoDevDir = path.join(projectDir, this.configDir);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(rovoDevDir))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean BMAD agents from subagents directory
|
|
||||||
const subagentsDir = path.join(rovoDevDir, this.subagentsDir);
|
|
||||||
if (await fs.pathExists(subagentsDir)) {
|
|
||||||
const entries = await fs.readdir(subagentsDir);
|
|
||||||
const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
|
|
||||||
|
|
||||||
for (const file of bmadFiles) {
|
|
||||||
await fs.remove(path.join(subagentsDir, file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean BMAD workflows from workflows directory
|
|
||||||
const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
|
||||||
if (await fs.pathExists(workflowsDir)) {
|
|
||||||
const entries = await fs.readdir(workflowsDir);
|
|
||||||
const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
|
|
||||||
|
|
||||||
for (const file of bmadFiles) {
|
|
||||||
await fs.remove(path.join(workflowsDir, file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean BMAD tasks/tools from references directory
|
|
||||||
const referencesDir = path.join(rovoDevDir, this.referencesDir);
|
|
||||||
if (await fs.pathExists(referencesDir)) {
|
|
||||||
const entries = await fs.readdir(referencesDir);
|
|
||||||
const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
|
|
||||||
|
|
||||||
for (const file of bmadFiles) {
|
|
||||||
await fs.remove(path.join(referencesDir, file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Rovo Dev configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Create .rovodev directory structure
|
|
||||||
const rovoDevDir = path.join(projectDir, this.configDir);
|
|
||||||
const subagentsDir = path.join(rovoDevDir, this.subagentsDir);
|
|
||||||
const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
|
||||||
const referencesDir = path.join(rovoDevDir, this.referencesDir);
|
|
||||||
|
|
||||||
await this.ensureDir(subagentsDir);
|
|
||||||
await this.ensureDir(workflowsDir);
|
|
||||||
await this.ensureDir(referencesDir);
|
|
||||||
|
|
||||||
// Generate and install agents
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const subagentFilename = `bmad-${artifact.module}-${artifact.name}.md`;
|
|
||||||
const targetPath = path.join(subagentsDir, subagentFilename);
|
|
||||||
const subagentContent = this.convertToRovoDevSubagent(artifact.content, artifact.name, artifact.module);
|
|
||||||
await this.writeFile(targetPath, subagentContent);
|
|
||||||
agentCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate and install workflows
|
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
let workflowCount = 0;
|
|
||||||
for (const artifact of workflowArtifacts) {
|
|
||||||
if (artifact.type === 'workflow-command') {
|
|
||||||
const workflowFilename = path.basename(artifact.relativePath);
|
|
||||||
const targetPath = path.join(workflowsDir, workflowFilename);
|
|
||||||
await this.writeFile(targetPath, artifact.content);
|
|
||||||
workflowCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate and install tasks and tools
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const { tasks: taskCount, tools: toolCount } = await this.generateTaskToolReferences(bmadDir, referencesDir, taskToolGen);
|
|
||||||
|
|
||||||
// Summary output
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed to .rovodev/subagents/`));
|
|
||||||
if (workflowCount > 0) {
|
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflows installed to .rovodev/workflows/`));
|
|
||||||
}
|
|
||||||
if (taskCount + toolCount > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.dim(` - ${taskCount + toolCount} tasks/tools installed to .rovodev/references/ (${taskCount} tasks, ${toolCount} tools)`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log(chalk.yellow(`\n Note: Agents are automatically discovered by Rovo Dev`));
|
|
||||||
console.log(chalk.dim(` - Access agents by typing @ in Rovo Dev to see available options`));
|
|
||||||
console.log(chalk.dim(` - Workflows and references are available in .rovodev/ directory`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
workflows: workflowCount,
|
|
||||||
tasks: taskCount,
|
|
||||||
tools: toolCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate task and tool reference guides
|
|
||||||
* @param {string} bmadDir - BMAD directory
|
|
||||||
* @param {string} referencesDir - References directory
|
|
||||||
* @param {TaskToolCommandGenerator} taskToolGen - Generator instance
|
|
||||||
*/
|
|
||||||
async generateTaskToolReferences(bmadDir, referencesDir, taskToolGen) {
|
|
||||||
const tasks = await taskToolGen.loadTaskManifest(bmadDir);
|
|
||||||
const tools = await taskToolGen.loadToolManifest(bmadDir);
|
|
||||||
|
|
||||||
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
|
||||||
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
|
|
||||||
|
|
||||||
let taskCount = 0;
|
|
||||||
for (const task of standaloneTasks) {
|
|
||||||
const commandContent = taskToolGen.generateCommandContent(task, 'task');
|
|
||||||
const targetPath = path.join(referencesDir, `bmad-task-${task.module}-${task.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
taskCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
let toolCount = 0;
|
|
||||||
for (const tool of standaloneTools) {
|
|
||||||
const commandContent = taskToolGen.generateCommandContent(tool, 'tool');
|
|
||||||
const targetPath = path.join(referencesDir, `bmad-tool-${tool.module}-${tool.name}.md`);
|
|
||||||
await this.writeFile(targetPath, commandContent);
|
|
||||||
toolCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { tasks: taskCount, tools: toolCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert BMAD agent launcher to Rovo Dev subagent format
|
|
||||||
*
|
|
||||||
* Rovo Dev subagents use Markdown files with YAML frontmatter containing:
|
|
||||||
* - name: Unique identifier for the subagent
|
|
||||||
* - description: One-line description of the subagent's purpose
|
|
||||||
* - tools: Array of tools the subagent can use (optional)
|
|
||||||
* - model: Specific model for this subagent (optional)
|
|
||||||
* - load_memory: Whether to load memory files (optional, defaults to true)
|
|
||||||
*
|
|
||||||
* @param {string} launcherContent - Original agent launcher content
|
|
||||||
* @param {string} agentName - Name of the agent
|
|
||||||
* @param {string} moduleName - Name of the module
|
|
||||||
* @returns {string} Rovo Dev subagent-formatted content
|
|
||||||
*/
|
|
||||||
convertToRovoDevSubagent(launcherContent, agentName, moduleName) {
|
|
||||||
// Extract metadata from the launcher XML
|
|
||||||
const titleMatch = launcherContent.match(/title="([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(agentName);
|
|
||||||
|
|
||||||
const descriptionMatch = launcherContent.match(/description="([^"]+)"/);
|
|
||||||
const description = descriptionMatch ? descriptionMatch[1] : `BMAD agent: ${title}`;
|
|
||||||
|
|
||||||
const roleDefinitionMatch = launcherContent.match(/roleDefinition="([^"]+)"/);
|
|
||||||
const roleDefinition = roleDefinitionMatch ? roleDefinitionMatch[1] : `You are a specialized agent for ${title.toLowerCase()} tasks.`;
|
|
||||||
|
|
||||||
// Extract the main system prompt from the launcher (content after closing tags)
|
|
||||||
let systemPrompt = roleDefinition;
|
|
||||||
|
|
||||||
// Try to extract additional instructions from the launcher content
|
|
||||||
const instructionsMatch = launcherContent.match(/<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect whether Rovo Dev is already configured in the project
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
async detect(projectDir) {
|
|
||||||
const rovoDevDir = path.join(projectDir, this.configDir);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(rovoDevDir))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for BMAD agents in subagents directory
|
|
||||||
const subagentsDir = path.join(rovoDevDir, this.subagentsDir);
|
|
||||||
if (await fs.pathExists(subagentsDir)) {
|
|
||||||
try {
|
|
||||||
const entries = await fs.readdir(subagentsDir);
|
|
||||||
if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Continue checking other directories
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for BMAD workflows in workflows directory
|
|
||||||
const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
|
||||||
if (await fs.pathExists(workflowsDir)) {
|
|
||||||
try {
|
|
||||||
const entries = await fs.readdir(workflowsDir);
|
|
||||||
if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Continue checking other directories
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for BMAD tasks/tools in references directory
|
|
||||||
const referencesDir = path.join(rovoDevDir, this.referencesDir);
|
|
||||||
if (await fs.pathExists(referencesDir)) {
|
|
||||||
try {
|
|
||||||
const entries = await fs.readdir(referencesDir);
|
|
||||||
if (entries.some((entry) => entry.startsWith('bmad') && entry.endsWith('.md'))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { RovoDevSetup };
|
|
||||||
@@ -31,11 +31,23 @@ class AgentCommandGenerator {
|
|||||||
const launcherContent = await this.generateLauncherContent(agent);
|
const launcherContent = await this.generateLauncherContent(agent);
|
||||||
// Use relativePath if available (for nested agents), otherwise just name with .md
|
// Use relativePath if available (for nested agents), otherwise just name with .md
|
||||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
||||||
|
// Calculate the relative agent path (e.g., bmm/agents/pm.md)
|
||||||
|
let agentRelPath = agent.path;
|
||||||
|
// Remove _bmad/ prefix if present to get relative path from project root
|
||||||
|
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||||
|
if (agentRelPath.includes('_bmad/')) {
|
||||||
|
const parts = agentRelPath.split(/_bmad\//);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
agentRelPath = parts.slice(1).join('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
type: 'agent-launcher',
|
type: 'agent-launcher',
|
||||||
module: agent.module,
|
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
relativePath: path.join(agent.module, 'agents', agentPathInModule),
|
description: agent.description || `${agent.name} agent`,
|
||||||
|
module: agent.module,
|
||||||
|
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
|
||||||
|
agentPath: agentRelPath, // Relative path to actual agent file
|
||||||
content: launcherContent,
|
content: launcherContent,
|
||||||
sourcePath: agent.path,
|
sourcePath: agent.path,
|
||||||
});
|
});
|
||||||
@@ -119,8 +131,10 @@ class AgentCommandGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write agent launcher artifacts using underscore format (Windows-compatible)
|
* Write agent launcher artifacts using dash format (NEW STANDARD)
|
||||||
* Creates flat files like: bmad_bmm_pm.md
|
* Creates flat files like: bmad-bmm-pm.agent.md
|
||||||
|
*
|
||||||
|
* The .agent.md suffix distinguishes agents from workflows/tasks/tools.
|
||||||
*
|
*
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||||
* @param {Array} artifacts - Agent launcher artifacts
|
* @param {Array} artifacts - Agent launcher artifacts
|
||||||
@@ -131,7 +145,7 @@ 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-pm.agent.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(path.dirname(launcherPath));
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
|||||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
// Skip if entry.name is undefined or not a string
|
||||||
|
if (!entry.name || typeof entry.name !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const fullPath = path.join(dirPath, entry.name);
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
* Path transformation utilities for IDE installer standardization
|
* Path transformation utilities for IDE installer standardization
|
||||||
*
|
*
|
||||||
* Provides utilities to convert hierarchical paths to flat naming conventions.
|
* Provides utilities to convert hierarchical paths to flat naming conventions.
|
||||||
* - Underscore format (bmad_module_name.md) - Windows-compatible universal format
|
*
|
||||||
|
* DASH-BASED NAMING (new standard):
|
||||||
|
* - Agents: bmad-module-name.agent.md (with .agent.md suffix)
|
||||||
|
* - Workflows/Tasks/Tools: bmad-module-name.md
|
||||||
|
*
|
||||||
|
* Example outputs:
|
||||||
|
* - cis/agents/storymaster.md → bmad-cis-storymaster.agent.md
|
||||||
|
* - bmm/workflows/plan-project.md → bmad-bmm-plan-project.md
|
||||||
|
* - bmm/tasks/create-story.md → bmad-bmm-create-story.md
|
||||||
|
* - core/agents/brainstorming.md → bmad-brainstorming.agent.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Type segments - agents are included in naming, others are filtered out
|
// Type segments - agents are included in naming, others are filtered out
|
||||||
@@ -10,111 +19,120 @@ const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
|||||||
const AGENT_SEGMENT = 'agents';
|
const AGENT_SEGMENT = 'agents';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert hierarchical path to flat underscore-separated name
|
* Convert hierarchical path to flat dash-separated name (NEW STANDARD)
|
||||||
* Converts: 'bmm', 'agents', 'pm' → 'bmad_bmm_agent_pm.md'
|
* Converts: 'bmm', 'agents', 'pm' → 'bmad-bmm-pm.agent.md'
|
||||||
* Converts: 'bmm', 'workflows', 'correct-course' → 'bmad_bmm_correct-course.md'
|
* Converts: 'bmm', 'workflows', 'correct-course' → 'bmad-bmm-correct-course.md'
|
||||||
* Converts: 'core', 'agents', 'brainstorming' → 'bmad_agent_brainstorming.md' (core items skip module prefix)
|
* Converts: 'core', 'agents', 'brainstorming' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||||
*
|
*
|
||||||
* @param {string} module - Module name (e.g., 'bmm', 'core')
|
* @param {string} module - Module name (e.g., 'bmm', 'core')
|
||||||
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools')
|
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools')
|
||||||
* @param {string} name - Artifact name (e.g., 'pm', 'brainstorming')
|
* @param {string} name - Artifact name (e.g., 'pm', 'brainstorming')
|
||||||
* @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_bmm_correct-course.md'
|
* @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-bmm-correct-course.md'
|
||||||
*/
|
*/
|
||||||
function toUnderscoreName(module, type, name) {
|
function toDashName(module, type, name) {
|
||||||
const isAgent = type === AGENT_SEGMENT;
|
const isAgent = type === AGENT_SEGMENT;
|
||||||
// For core module, skip the module prefix: use 'bmad_name.md' instead of 'bmad_core_name.md'
|
|
||||||
|
// For core module, skip the module prefix: use 'bmad-name.md' instead of 'bmad-core-name.md'
|
||||||
if (module === 'core') {
|
if (module === 'core') {
|
||||||
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
|
return isAgent ? `bmad-${name}.agent.md` : `bmad-${name}.md`;
|
||||||
}
|
}
|
||||||
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
|
|
||||||
|
// Module artifacts: bmad-module-name.md or bmad-module-name.agent.md
|
||||||
|
// eslint-disable-next-line unicorn/prefer-string-replace-all -- regex replace is intentional here
|
||||||
|
const dashName = name.replace(/\//g, '-'); // Flatten nested paths
|
||||||
|
return isAgent ? `bmad-${module}-${dashName}.agent.md` : `bmad-${module}-${dashName}.md`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert relative path to flat underscore-separated name
|
* 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-pm.agent.md'
|
||||||
* Converts: 'bmm/workflows/correct-course.md' → 'bmad_bmm_correct-course.md'
|
* Converts: 'bmm/workflows/correct-course.md' → 'bmad-bmm-correct-course.md'
|
||||||
* Converts: 'core/agents/brainstorming.md' → 'bmad_agent_brainstorming.md' (core items skip module prefix)
|
* Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||||
*
|
*
|
||||||
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
|
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
|
||||||
* @returns {string} Flat filename like 'bmad_bmm_agent_pm.md' or 'bmad_brainstorming.md'
|
* @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-brainstorming.md'
|
||||||
*/
|
*/
|
||||||
function toUnderscorePath(relativePath) {
|
function toDashPath(relativePath) {
|
||||||
|
if (!relativePath || typeof relativePath !== 'string') {
|
||||||
|
// Return a safe default for invalid input
|
||||||
|
return 'bmad-unknown.md';
|
||||||
|
}
|
||||||
|
|
||||||
const withoutExt = relativePath.replace('.md', '');
|
const withoutExt = relativePath.replace('.md', '');
|
||||||
const parts = withoutExt.split(/[/\\]/);
|
const parts = withoutExt.split(/[/\\]/);
|
||||||
|
|
||||||
const module = parts[0];
|
const module = parts[0];
|
||||||
const type = parts[1];
|
const type = parts[1];
|
||||||
const name = parts.slice(2).join('_');
|
const name = parts.slice(2).join('-');
|
||||||
|
|
||||||
// Use toUnderscoreName for consistency
|
return toDashName(module, type, name);
|
||||||
return toUnderscoreName(module, type, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create custom agent underscore name
|
* Create custom agent dash name
|
||||||
* Creates: 'bmad_custom_fred-commit-poet.md'
|
* Creates: 'bmad-custom-fred-commit-poet.agent.md'
|
||||||
*
|
*
|
||||||
* @param {string} agentName - Custom agent name
|
* @param {string} agentName - Custom agent name
|
||||||
* @returns {string} Flat filename like 'bmad_custom_fred-commit-poet.md'
|
* @returns {string} Flat filename like 'bmad-custom-fred-commit-poet.agent.md'
|
||||||
*/
|
*/
|
||||||
function customAgentUnderscoreName(agentName) {
|
function customAgentDashName(agentName) {
|
||||||
return `bmad_custom_${agentName}.md`;
|
return `bmad-custom-${agentName}.agent.md`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a filename uses underscore format
|
* Check if a filename uses dash format
|
||||||
* @param {string} filename - Filename to check
|
* @param {string} filename - Filename to check
|
||||||
* @returns {boolean} True if filename uses underscore format
|
* @returns {boolean} True if filename uses dash format
|
||||||
*/
|
*/
|
||||||
function isUnderscoreFormat(filename) {
|
function isDashFormat(filename) {
|
||||||
return filename.startsWith('bmad_') && filename.includes('_');
|
return filename.startsWith('bmad-') && filename.includes('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract parts from an underscore-formatted filename
|
* Extract parts from a dash-formatted filename
|
||||||
* Parses: 'bmad_bmm_agent_pm.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' }
|
* Parses: 'bmad-bmm-pm.agent.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' }
|
||||||
* Parses: 'bmad_bmm_correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' }
|
* Parses: 'bmad-bmm-correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' }
|
||||||
* Parses: 'bmad_agent_brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents)
|
* Parses: 'bmad-brainstorming.agent.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents)
|
||||||
* Parses: 'bmad_brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows)
|
* Parses: 'bmad-brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows)
|
||||||
*
|
*
|
||||||
* @param {string} filename - Underscore-formatted filename
|
* @param {string} filename - Dash-formatted filename
|
||||||
* @returns {Object|null} Parsed parts or null if invalid format
|
* @returns {Object|null} Parsed parts or null if invalid format
|
||||||
*/
|
*/
|
||||||
function parseUnderscoreName(filename) {
|
function parseDashName(filename) {
|
||||||
const withoutExt = filename.replace('.md', '');
|
const withoutExt = filename.replace('.md', '');
|
||||||
const parts = withoutExt.split('_');
|
const parts = withoutExt.split('-');
|
||||||
|
|
||||||
if (parts.length < 2 || parts[0] !== 'bmad') {
|
if (parts.length < 2 || parts[0] !== 'bmad') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is an agent file (has 'agent' as one of the parts)
|
// Check if this is an agent file (has .agent suffix)
|
||||||
const agentIndex = parts.indexOf('agent');
|
const isAgent = withoutExt.endsWith('.agent');
|
||||||
|
|
||||||
if (agentIndex !== -1) {
|
if (isAgent) {
|
||||||
// This is an agent file
|
// This is an agent file
|
||||||
// Format: bmad_agent_name (core) or bmad_module_agent_name
|
// Format: bmad-name.agent (core) or bmad-module-name.agent
|
||||||
if (agentIndex === 1) {
|
if (parts.length === 3) {
|
||||||
// Core agent: bmad_agent_name
|
// Core agent: bmad-name.agent
|
||||||
return {
|
return {
|
||||||
prefix: parts[0],
|
prefix: parts[0],
|
||||||
module: 'core',
|
module: 'core',
|
||||||
type: 'agents',
|
type: 'agents',
|
||||||
name: parts.slice(agentIndex + 1).join('_'),
|
name: parts[1],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Module agent: bmad_module_agent_name
|
// Module agent: bmad-module-name.agent
|
||||||
return {
|
return {
|
||||||
prefix: parts[0],
|
prefix: parts[0],
|
||||||
module: parts[1],
|
module: parts[1],
|
||||||
type: 'agents',
|
type: 'agents',
|
||||||
name: parts.slice(agentIndex + 1).join('_'),
|
name: parts.slice(2).join('-'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not an agent file - must be a workflow/tool/task
|
// Not an agent file - must be a workflow/tool/task
|
||||||
// If only 2 parts (bmad_name), it's a core workflow/tool/task
|
// If only 2 parts (bmad-name), it's a core workflow/tool/task
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
return {
|
return {
|
||||||
prefix: parts[0],
|
prefix: parts[0],
|
||||||
@@ -124,42 +142,140 @@ function parseUnderscoreName(filename) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, it's a module workflow/tool/task (bmad_module_name)
|
// Otherwise, it's a module workflow/tool/task (bmad-module-name)
|
||||||
return {
|
return {
|
||||||
prefix: parts[0],
|
prefix: parts[0],
|
||||||
module: parts[1],
|
module: parts[1],
|
||||||
type: 'workflows', // Default to workflows for non-agent module items
|
type: 'workflows', // Default to workflows for non-agent module items
|
||||||
|
name: parts.slice(2).join('-'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hierarchical path to flat underscore-separated name (LEGACY)
|
||||||
|
* @deprecated Use toDashName instead
|
||||||
|
*/
|
||||||
|
function toUnderscoreName(module, type, name) {
|
||||||
|
const isAgent = type === AGENT_SEGMENT;
|
||||||
|
if (module === 'core') {
|
||||||
|
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
|
||||||
|
}
|
||||||
|
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert relative path to flat underscore-separated name (LEGACY)
|
||||||
|
* @deprecated Use toDashPath instead
|
||||||
|
*/
|
||||||
|
function toUnderscorePath(relativePath) {
|
||||||
|
const withoutExt = relativePath.replace('.md', '');
|
||||||
|
const parts = withoutExt.split(/[/\\]/);
|
||||||
|
|
||||||
|
const module = parts[0];
|
||||||
|
const type = parts[1];
|
||||||
|
const name = parts.slice(2).join('_');
|
||||||
|
|
||||||
|
return toUnderscoreName(module, type, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom agent underscore name (LEGACY)
|
||||||
|
* @deprecated Use customAgentDashName instead
|
||||||
|
*/
|
||||||
|
function customAgentUnderscoreName(agentName) {
|
||||||
|
return `bmad_custom_${agentName}.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a filename uses underscore format (LEGACY)
|
||||||
|
* @deprecated Use isDashFormat instead
|
||||||
|
*/
|
||||||
|
function isUnderscoreFormat(filename) {
|
||||||
|
return filename.startsWith('bmad_') && filename.includes('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract parts from an underscore-formatted filename (LEGACY)
|
||||||
|
* @deprecated Use parseDashName instead
|
||||||
|
*/
|
||||||
|
function parseUnderscoreName(filename) {
|
||||||
|
const withoutExt = filename.replace('.md', '');
|
||||||
|
const parts = withoutExt.split('_');
|
||||||
|
|
||||||
|
if (parts.length < 2 || parts[0] !== 'bmad') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentIndex = parts.indexOf('agent');
|
||||||
|
|
||||||
|
if (agentIndex !== -1) {
|
||||||
|
if (agentIndex === 1) {
|
||||||
|
return {
|
||||||
|
prefix: parts[0],
|
||||||
|
module: 'core',
|
||||||
|
type: 'agents',
|
||||||
|
name: parts.slice(agentIndex + 1).join('_'),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
prefix: parts[0],
|
||||||
|
module: parts[1],
|
||||||
|
type: 'agents',
|
||||||
|
name: parts.slice(agentIndex + 1).join('_'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return {
|
||||||
|
prefix: parts[0],
|
||||||
|
module: 'core',
|
||||||
|
type: 'workflows',
|
||||||
|
name: parts[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prefix: parts[0],
|
||||||
|
module: parts[1],
|
||||||
|
type: 'workflows',
|
||||||
name: parts.slice(2).join('_'),
|
name: parts.slice(2).join('_'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility aliases (deprecated)
|
// Backward compatibility aliases (colon format was same as underscore)
|
||||||
const toColonName = toUnderscoreName;
|
const toColonName = toUnderscoreName;
|
||||||
const toColonPath = toUnderscorePath;
|
const toColonPath = toUnderscorePath;
|
||||||
const toDashPath = toUnderscorePath;
|
|
||||||
const customAgentColonName = customAgentUnderscoreName;
|
const customAgentColonName = customAgentUnderscoreName;
|
||||||
const customAgentDashName = customAgentUnderscoreName;
|
|
||||||
const isColonFormat = isUnderscoreFormat;
|
const isColonFormat = isUnderscoreFormat;
|
||||||
const isDashFormat = isUnderscoreFormat;
|
|
||||||
const parseColonName = parseUnderscoreName;
|
const parseColonName = parseUnderscoreName;
|
||||||
const parseDashName = parseUnderscoreName;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
// New standard (dash-based)
|
||||||
|
toDashName,
|
||||||
|
toDashPath,
|
||||||
|
customAgentDashName,
|
||||||
|
isDashFormat,
|
||||||
|
parseDashName,
|
||||||
|
|
||||||
|
// Legacy (underscore-based) - kept for backward compatibility
|
||||||
toUnderscoreName,
|
toUnderscoreName,
|
||||||
toUnderscorePath,
|
toUnderscorePath,
|
||||||
customAgentUnderscoreName,
|
customAgentUnderscoreName,
|
||||||
isUnderscoreFormat,
|
isUnderscoreFormat,
|
||||||
parseUnderscoreName,
|
parseUnderscoreName,
|
||||||
|
|
||||||
// Backward compatibility aliases
|
// Backward compatibility aliases
|
||||||
toColonName,
|
toColonName,
|
||||||
toColonPath,
|
toColonPath,
|
||||||
toDashPath,
|
|
||||||
customAgentColonName,
|
customAgentColonName,
|
||||||
customAgentDashName,
|
|
||||||
isColonFormat,
|
isColonFormat,
|
||||||
isDashFormat,
|
|
||||||
parseColonName,
|
parseColonName,
|
||||||
parseDashName,
|
|
||||||
TYPE_SEGMENTS,
|
TYPE_SEGMENTS,
|
||||||
AGENT_SEGMENT,
|
AGENT_SEGMENT,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class TaskToolCommandGenerator {
|
|||||||
|
|
||||||
// Convert path to use {project-root} placeholder
|
// Convert path to use {project-root} placeholder
|
||||||
let itemPath = item.path;
|
let itemPath = item.path;
|
||||||
if (itemPath.startsWith('bmad/')) {
|
if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) {
|
||||||
itemPath = `{project-root}/${itemPath}`;
|
itemPath = `{project-root}/${itemPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,8 +239,10 @@ Follow all instructions in the ${type} file exactly as written.
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write task/tool artifacts using underscore format (Windows-compatible)
|
* Write task/tool artifacts using dash format (NEW STANDARD)
|
||||||
* Creates flat files like: bmad_bmm_bmad-help.md
|
* Creates flat files like: bmad-bmm-bmad-help.md
|
||||||
|
*
|
||||||
|
* Note: Tasks/tools do NOT have .agent.md suffix - only agents do.
|
||||||
*
|
*
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||||
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
||||||
@@ -252,7 +254,7 @@ Follow all instructions in the ${type} file exactly as written.
|
|||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
if (artifact.type === 'task' || artifact.type === 'tool') {
|
if (artifact.type === 'task' || artifact.type === 'tool') {
|
||||||
const commandContent = this.generateCommandContent(artifact, artifact.type);
|
const commandContent = this.generateCommandContent(artifact, artifact.type);
|
||||||
// Use underscore format: bmad_module_name.md
|
// Use dash format: bmad-module-name.md
|
||||||
const flatName = toDashPath(artifact.relativePath);
|
const flatName = toDashPath(artifact.relativePath);
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
const commandPath = path.join(baseCommandsDir, flatName);
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
await fs.ensureDir(path.dirname(commandPath));
|
||||||
|
|||||||
@@ -67,10 +67,26 @@ class WorkflowCommandGenerator {
|
|||||||
|
|
||||||
for (const workflow of allWorkflows) {
|
for (const workflow of allWorkflows) {
|
||||||
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
||||||
|
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.yaml)
|
||||||
|
let workflowRelPath = workflow.path;
|
||||||
|
// Remove _bmad/ prefix if present to get relative path from project root
|
||||||
|
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||||
|
if (workflowRelPath.includes('_bmad/')) {
|
||||||
|
const parts = workflowRelPath.split(/_bmad\//);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
workflowRelPath = parts.slice(1).join('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Determine if this is a YAML workflow
|
||||||
|
const isYamlWorkflow = workflow.path.endsWith('.yaml') || workflow.path.endsWith('.yml');
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
type: 'workflow-command',
|
type: 'workflow-command',
|
||||||
|
isYamlWorkflow: isYamlWorkflow, // For template selection
|
||||||
|
name: workflow.name,
|
||||||
|
description: workflow.description || `${workflow.name} workflow`,
|
||||||
module: workflow.module,
|
module: workflow.module,
|
||||||
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
||||||
|
workflowPath: workflowRelPath, // Relative path to actual workflow file
|
||||||
content: commandContent,
|
content: commandContent,
|
||||||
sourcePath: workflow.path,
|
sourcePath: workflow.path,
|
||||||
});
|
});
|
||||||
@@ -265,8 +281,10 @@ When running any workflow:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write workflow command artifacts using underscore format (Windows-compatible)
|
* Write workflow command artifacts using dash format (NEW STANDARD)
|
||||||
* Creates flat files like: bmad_bmm_correct-course.md
|
* Creates flat files like: bmad-bmm-correct-course.md
|
||||||
|
*
|
||||||
|
* Note: Workflows do NOT have .agent.md suffix - only agents do.
|
||||||
*
|
*
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||||
* @param {Array} artifacts - Workflow artifacts
|
* @param {Array} artifacts - Workflow artifacts
|
||||||
@@ -277,7 +295,7 @@ When running any workflow:
|
|||||||
|
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
if (artifact.type === 'workflow-command') {
|
if (artifact.type === 'workflow-command') {
|
||||||
// Convert relativePath to underscore format: bmm/workflows/correct-course.md → bmad_bmm_correct-course.md
|
// Convert relativePath to dash format: bmm/workflows/correct-course.md → bmad-bmm-correct-course.md
|
||||||
const flatName = toDashPath(artifact.relativePath);
|
const flatName = toDashPath(artifact.relativePath);
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
const commandPath = path.join(baseCommandsDir, flatName);
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
await fs.ensureDir(path.dirname(commandPath));
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
|
||||||
|
LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}}
|
||||||
|
|
||||||
|
Follow all instructions in the workflow file exactly as written.
|
||||||
1
tools/cli/installers/lib/ide/templates/combined/claude-agent.md
Symbolic link
1
tools/cli/installers/lib/ide/templates/combined/claude-agent.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
default-agent.md
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
default-workflow-yaml.md
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
default-workflow.md
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
|
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||||
|
|
||||||
|
<agent-activation CRITICAL="TRUE">
|
||||||
|
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
||||||
|
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||||
|
3. FOLLOW every step in the <activation> section precisely
|
||||||
|
4. DISPLAY the welcome/greeting as instructed
|
||||||
|
5. PRESENT the numbered menu
|
||||||
|
6. WAIT for user input before proceeding
|
||||||
|
</agent-activation>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
|
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
|
||||||
|
|
||||||
|
<steps CRITICAL="TRUE">
|
||||||
|
1. Always LOAD the FULL @{project-root}/{{bmadFolderName}}/core/tasks/workflow.xml
|
||||||
|
2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config @{project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
3. Pass the yaml path @{project-root}/{{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions
|
||||||
|
4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions
|
||||||
|
5. Save outputs after EACH section when generating any documents from templates
|
||||||
|
</steps>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
|
|
||||||
|
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# {{name}}
|
||||||
|
|
||||||
|
{{description}}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}}
|
||||||
|
|
||||||
|
Follow all instructions in the workflow file exactly as written.
|
||||||
9
tools/cli/installers/lib/ide/templates/combined/trae.md
Normal file
9
tools/cli/installers/lib/ide/templates/combined/trae.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# {{name}}
|
||||||
|
|
||||||
|
{{description}}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}}
|
||||||
|
|
||||||
|
Follow all instructions in the workflow file exactly as written.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description: '{{description}}'
|
||||||
|
auto_execution_mode: "iterate"
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{name}}
|
||||||
|
|
||||||
|
LOAD and execute the workflow at: {project-root}/_bmad/{{workflow_path}}
|
||||||
|
|
||||||
|
Follow all instructions in the workflow file exactly as written.
|
||||||
10
tools/cli/installers/lib/ide/templates/split/gemini/body.md
Normal file
10
tools/cli/installers/lib/ide/templates/split/gemini/body.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||||
|
|
||||||
|
<agent-activation CRITICAL="TRUE">
|
||||||
|
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
||||||
|
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||||
|
3. FOLLOW every step in the <activation> section precisely
|
||||||
|
4. DISPLAY the welcome/greeting as instructed
|
||||||
|
5. PRESENT the numbered menu
|
||||||
|
6. WAIT for user input before proceeding
|
||||||
|
</agent-activation>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
name = "{{name}}"
|
||||||
|
description = "{{description}}"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||||
|
|
||||||
|
<agent-activation CRITICAL="TRUE">
|
||||||
|
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
||||||
|
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||||
|
3. FOLLOW every step in the <activation> section precisely
|
||||||
|
4. DISPLAY the welcome/greeting as instructed
|
||||||
|
5. PRESENT the numbered menu
|
||||||
|
6. WAIT for user input before proceeding
|
||||||
|
</agent-activation>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
name: '{{name}}'
|
||||||
|
description: '{{description}}'
|
||||||
|
---
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trae IDE setup handler
|
|
||||||
*/
|
|
||||||
class TraeSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('trae', 'Trae');
|
|
||||||
this.configDir = '.trae';
|
|
||||||
this.rulesDir = 'rules';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Trae IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Create .trae/rules directory
|
|
||||||
const traeDir = path.join(projectDir, this.configDir);
|
|
||||||
const rulesDir = path.join(traeDir, this.rulesDir);
|
|
||||||
|
|
||||||
await this.ensureDir(rulesDir);
|
|
||||||
|
|
||||||
// Clean up any existing BMAD files before reinstalling
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Get tasks, tools, and workflows (standalone only)
|
|
||||||
const tasks = await this.getTasks(bmadDir, true);
|
|
||||||
const tools = await this.getTools(bmadDir, true);
|
|
||||||
const workflows = await this.getWorkflows(bmadDir, true);
|
|
||||||
|
|
||||||
// Process agents as rules with bmad- prefix
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const processedContent = await this.createAgentRule(artifact, bmadDir, projectDir);
|
|
||||||
|
|
||||||
// Use bmad- prefix: bmad-agent-{module}-{name}.md
|
|
||||||
const targetPath = path.join(rulesDir, `bmad-agent-${artifact.module}-${artifact.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
agentCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process tasks as rules with bmad- prefix
|
|
||||||
let taskCount = 0;
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readFile(task.path);
|
|
||||||
const processedContent = this.createTaskRule(task, content);
|
|
||||||
|
|
||||||
// Use bmad- prefix: bmad-task-{module}-{name}.md
|
|
||||||
const targetPath = path.join(rulesDir, `bmad-task-${task.module}-${task.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
taskCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process tools as rules with bmad- prefix
|
|
||||||
let toolCount = 0;
|
|
||||||
for (const tool of tools) {
|
|
||||||
const content = await this.readFile(tool.path);
|
|
||||||
const processedContent = this.createToolRule(tool, content);
|
|
||||||
|
|
||||||
// Use bmad- prefix: bmad-tool-{module}-{name}.md
|
|
||||||
const targetPath = path.join(rulesDir, `bmad-tool-${tool.module}-${tool.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
toolCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process workflows as rules with bmad- prefix
|
|
||||||
let workflowCount = 0;
|
|
||||||
for (const workflow of workflows) {
|
|
||||||
const content = await this.readFile(workflow.path);
|
|
||||||
const processedContent = this.createWorkflowRule(workflow, content);
|
|
||||||
|
|
||||||
// Use bmad- prefix: bmad-workflow-{module}-{name}.md
|
|
||||||
const targetPath = path.join(rulesDir, `bmad-workflow-${workflow.module}-${workflow.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
workflowCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalRules = agentCount + taskCount + toolCount + workflowCount;
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agent rules created`));
|
|
||||||
console.log(chalk.dim(` - ${taskCount} task rules created`));
|
|
||||||
console.log(chalk.dim(` - ${toolCount} tool rules created`));
|
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflow rules created`));
|
|
||||||
console.log(chalk.dim(` - Total: ${totalRules} rules`));
|
|
||||||
console.log(chalk.dim(` - Rules directory: ${path.relative(projectDir, rulesDir)}`));
|
|
||||||
console.log(chalk.dim(` - Agents can be activated with @{agent-name}`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
rules: totalRules,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskCount,
|
|
||||||
tools: toolCount,
|
|
||||||
workflows: workflowCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create rule content for an agent
|
|
||||||
*/
|
|
||||||
async createAgentRule(artifact, bmadDir, projectDir) {
|
|
||||||
// Strip frontmatter from launcher
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
const contentWithoutFrontmatter = artifact.content.replace(frontmatterRegex, '').trim();
|
|
||||||
|
|
||||||
// Extract metadata from launcher content
|
|
||||||
const titleMatch = artifact.content.match(/description:\s*"([^"]+)"/);
|
|
||||||
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
|
|
||||||
|
|
||||||
// Calculate relative path for reference
|
|
||||||
const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/');
|
|
||||||
|
|
||||||
let ruleContent = `# ${title} Agent Rule
|
|
||||||
|
|
||||||
This rule is triggered when the user types \`@${artifact.name}\` and activates the ${title} agent persona.
|
|
||||||
|
|
||||||
## Agent Activation
|
|
||||||
|
|
||||||
${contentWithoutFrontmatter}
|
|
||||||
|
|
||||||
## File Reference
|
|
||||||
|
|
||||||
The full agent definition is located at: \`${relativePath}\`
|
|
||||||
`;
|
|
||||||
|
|
||||||
return ruleContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create rule content for a task
|
|
||||||
*/
|
|
||||||
createTaskRule(task, content) {
|
|
||||||
// Extract task name from content
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
const taskName = nameMatch ? nameMatch[1] : this.formatTitle(task.name);
|
|
||||||
|
|
||||||
let ruleContent = `# ${taskName} Task Rule
|
|
||||||
|
|
||||||
This rule defines the ${taskName} task workflow.
|
|
||||||
|
|
||||||
## Task Definition
|
|
||||||
|
|
||||||
When this task is triggered, execute the following workflow:
|
|
||||||
|
|
||||||
${content}
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Reference this task with \`@task-${task.name}\` to execute the defined workflow.
|
|
||||||
|
|
||||||
## Module
|
|
||||||
|
|
||||||
Part of the BMAD ${task.module.toUpperCase()} module.
|
|
||||||
`;
|
|
||||||
|
|
||||||
return ruleContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create rule content for a tool
|
|
||||||
*/
|
|
||||||
createToolRule(tool, content) {
|
|
||||||
// Extract tool name from content
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
const toolName = nameMatch ? nameMatch[1] : this.formatTitle(tool.name);
|
|
||||||
|
|
||||||
let ruleContent = `# ${toolName} Tool Rule
|
|
||||||
|
|
||||||
This rule defines the ${toolName} tool.
|
|
||||||
|
|
||||||
## Tool Definition
|
|
||||||
|
|
||||||
When this tool is triggered, execute the following:
|
|
||||||
|
|
||||||
${content}
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Reference this tool with \`@tool-${tool.name}\` to execute it.
|
|
||||||
|
|
||||||
## Module
|
|
||||||
|
|
||||||
Part of the BMAD ${tool.module.toUpperCase()} module.
|
|
||||||
`;
|
|
||||||
|
|
||||||
return ruleContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create rule content for a workflow
|
|
||||||
*/
|
|
||||||
createWorkflowRule(workflow, content) {
|
|
||||||
let ruleContent = `# ${workflow.name} Workflow Rule
|
|
||||||
|
|
||||||
This rule defines the ${workflow.name} workflow.
|
|
||||||
|
|
||||||
## Workflow Description
|
|
||||||
|
|
||||||
${workflow.description || 'No description provided'}
|
|
||||||
|
|
||||||
## Workflow Definition
|
|
||||||
|
|
||||||
${content}
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Reference this workflow with \`@workflow-${workflow.name}\` to execute the guided workflow.
|
|
||||||
|
|
||||||
## Module
|
|
||||||
|
|
||||||
Part of the BMAD ${workflow.module.toUpperCase()} module.
|
|
||||||
`;
|
|
||||||
|
|
||||||
return ruleContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format agent/task name as title
|
|
||||||
*/
|
|
||||||
formatTitle(name) {
|
|
||||||
return name
|
|
||||||
.split('-')
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup Trae configuration - surgically remove only BMAD files
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const rulesPath = path.join(projectDir, this.configDir, this.rulesDir);
|
|
||||||
|
|
||||||
if (await fs.pathExists(rulesPath)) {
|
|
||||||
// Remove any bmad* files (cleans up old bmad- and bmad: formats)
|
|
||||||
const files = await fs.readdir(rulesPath);
|
|
||||||
let removed = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith('bmad') && file.endsWith('.md')) {
|
|
||||||
await fs.remove(path.join(rulesPath, file));
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD rules`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Trae
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object} Installation result
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const traeDir = path.join(projectDir, this.configDir);
|
|
||||||
const rulesDir = path.join(traeDir, this.rulesDir);
|
|
||||||
|
|
||||||
// Create .trae/rules directory if it doesn't exist
|
|
||||||
await fs.ensureDir(rulesDir);
|
|
||||||
|
|
||||||
// Create custom agent launcher
|
|
||||||
const launcherContent = `# ${agentName} Custom Agent
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
|
||||||
|
|
||||||
This is a launcher for the custom BMAD agent "${agentName}".
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. First run: \`${agentPath}\` to load the complete agent
|
|
||||||
2. Then use this rule to activate ${agentName}
|
|
||||||
|
|
||||||
The agent will follow the persona and instructions from the main agent file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generated by BMAD Method*`;
|
|
||||||
|
|
||||||
const fileName = `bmad-agent-custom-${agentName.toLowerCase()}.md`;
|
|
||||||
const launcherPath = path.join(rulesDir, fileName);
|
|
||||||
|
|
||||||
// Write the launcher file
|
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ide: 'trae',
|
|
||||||
path: path.relative(projectDir, launcherPath),
|
|
||||||
command: agentName,
|
|
||||||
type: 'custom-agent-launcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { TraeSetup };
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Windsurf IDE setup handler
|
|
||||||
*/
|
|
||||||
class WindsurfSetup extends BaseIdeSetup {
|
|
||||||
constructor() {
|
|
||||||
super('windsurf', 'Windsurf', true); // preferred IDE
|
|
||||||
this.configDir = '.windsurf';
|
|
||||||
this.workflowsDir = 'workflows';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Windsurf IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Create .windsurf/workflows/bmad directory structure
|
|
||||||
const windsurfDir = path.join(projectDir, this.configDir);
|
|
||||||
const workflowsDir = path.join(windsurfDir, this.workflowsDir);
|
|
||||||
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
|
|
||||||
|
|
||||||
await this.ensureDir(bmadWorkflowsDir);
|
|
||||||
|
|
||||||
// Clean up any existing BMAD workflows before reinstalling
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Convert artifacts to agent format for module organization
|
|
||||||
const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name }));
|
|
||||||
|
|
||||||
// Get tasks, tools, and workflows (standalone only)
|
|
||||||
const tasks = await this.getTasks(bmadDir, true);
|
|
||||||
const tools = await this.getTools(bmadDir, true);
|
|
||||||
const workflows = await this.getWorkflows(bmadDir, true);
|
|
||||||
|
|
||||||
// Create directories for each module under bmad/
|
|
||||||
const modules = new Set();
|
|
||||||
for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module);
|
|
||||||
|
|
||||||
for (const module of modules) {
|
|
||||||
await this.ensureDir(path.join(bmadWorkflowsDir, module));
|
|
||||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'agents'));
|
|
||||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tasks'));
|
|
||||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tools'));
|
|
||||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'workflows'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process agent launchers as workflows with organized structure
|
|
||||||
let agentCount = 0;
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
const processedContent = this.createWorkflowContent({ module: artifact.module, name: artifact.name }, artifact.content);
|
|
||||||
|
|
||||||
// Organized path: bmad/module/agents/agent-name.md
|
|
||||||
const targetPath = path.join(bmadWorkflowsDir, artifact.module, 'agents', `${artifact.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
agentCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process tasks as workflows with organized structure
|
|
||||||
let taskCount = 0;
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readFile(task.path);
|
|
||||||
const processedContent = this.createTaskWorkflowContent(task, content);
|
|
||||||
|
|
||||||
// Organized path: bmad/module/tasks/task-name.md
|
|
||||||
const targetPath = path.join(bmadWorkflowsDir, task.module, 'tasks', `${task.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
taskCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process tools as workflows with organized structure
|
|
||||||
let toolCount = 0;
|
|
||||||
for (const tool of tools) {
|
|
||||||
const content = await this.readFile(tool.path);
|
|
||||||
const processedContent = this.createToolWorkflowContent(tool, content);
|
|
||||||
|
|
||||||
// Organized path: bmad/module/tools/tool-name.md
|
|
||||||
const targetPath = path.join(bmadWorkflowsDir, tool.module, 'tools', `${tool.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
toolCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process workflows with organized structure
|
|
||||||
let workflowCount = 0;
|
|
||||||
for (const workflow of workflows) {
|
|
||||||
const content = await this.readFile(workflow.path);
|
|
||||||
const processedContent = this.createWorkflowWorkflowContent(workflow, content);
|
|
||||||
|
|
||||||
// Organized path: bmad/module/workflows/workflow-name.md
|
|
||||||
const targetPath = path.join(bmadWorkflowsDir, workflow.module, 'workflows', `${workflow.name}.md`);
|
|
||||||
await this.writeFile(targetPath, processedContent);
|
|
||||||
workflowCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
|
||||||
console.log(chalk.dim(` - ${taskCount} tasks installed`));
|
|
||||||
console.log(chalk.dim(` - ${toolCount} tools installed`));
|
|
||||||
console.log(chalk.dim(` - ${workflowCount} workflows installed`));
|
|
||||||
console.log(chalk.dim(` - Organized in modules: ${[...modules].join(', ')}`));
|
|
||||||
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`));
|
|
||||||
|
|
||||||
// Provide additional configuration hints
|
|
||||||
if (options.showHints !== false) {
|
|
||||||
console.log(chalk.dim('\n Windsurf workflow settings:'));
|
|
||||||
console.log(chalk.dim(' - auto_execution_mode: 3 (recommended for agents)'));
|
|
||||||
console.log(chalk.dim(' - auto_execution_mode: 2 (recommended for tasks/tools)'));
|
|
||||||
console.log(chalk.dim(' - auto_execution_mode: 1 (recommended for workflows)'));
|
|
||||||
console.log(chalk.dim(' - Workflows can be triggered via the Windsurf menu'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskCount,
|
|
||||||
tools: toolCount,
|
|
||||||
workflows: workflowCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create workflow content for an agent
|
|
||||||
*/
|
|
||||||
createWorkflowContent(agent, content) {
|
|
||||||
// Strip existing frontmatter from launcher
|
|
||||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
|
||||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
|
||||||
|
|
||||||
// Create simple Windsurf frontmatter matching original format
|
|
||||||
let workflowContent = `---
|
|
||||||
description: ${agent.name}
|
|
||||||
auto_execution_mode: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
${contentWithoutFrontmatter}`;
|
|
||||||
|
|
||||||
return workflowContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create workflow content for a task
|
|
||||||
*/
|
|
||||||
createTaskWorkflowContent(task, content) {
|
|
||||||
// Create simple Windsurf frontmatter matching original format
|
|
||||||
let workflowContent = `---
|
|
||||||
description: task-${task.name}
|
|
||||||
auto_execution_mode: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
${content}`;
|
|
||||||
|
|
||||||
return workflowContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create workflow content for a tool
|
|
||||||
*/
|
|
||||||
createToolWorkflowContent(tool, content) {
|
|
||||||
// Create simple Windsurf frontmatter matching original format
|
|
||||||
let workflowContent = `---
|
|
||||||
description: tool-${tool.name}
|
|
||||||
auto_execution_mode: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
${content}`;
|
|
||||||
|
|
||||||
return workflowContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create workflow content for a workflow
|
|
||||||
*/
|
|
||||||
createWorkflowWorkflowContent(workflow, content) {
|
|
||||||
// Create simple Windsurf frontmatter matching original format
|
|
||||||
let workflowContent = `---
|
|
||||||
description: ${workflow.name}
|
|
||||||
auto_execution_mode: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
${content}`;
|
|
||||||
|
|
||||||
return workflowContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup Windsurf configuration - surgically remove only BMAD files
|
|
||||||
*/
|
|
||||||
async cleanup(projectDir) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const bmadPath = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad');
|
|
||||||
|
|
||||||
if (await fs.pathExists(bmadPath)) {
|
|
||||||
// Remove the entire bmad folder - this is our territory
|
|
||||||
await fs.remove(bmadPath);
|
|
||||||
console.log(chalk.dim(` Cleaned up existing BMAD workflows`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install a custom agent launcher for Windsurf
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object|null} Info about created command
|
|
||||||
*/
|
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const customAgentsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad', 'custom', 'agents');
|
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
|
||||||
return null; // IDE not configured for this project
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.ensureDir(customAgentsDir);
|
|
||||||
|
|
||||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from @${agentPath}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Windsurf uses workflow format with frontmatter
|
|
||||||
const workflowContent = `---
|
|
||||||
description: ${metadata.title || agentName}
|
|
||||||
auto_execution_mode: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
${launcherContent}`;
|
|
||||||
|
|
||||||
const launcherPath = path.join(customAgentsDir, `${agentName}.md`);
|
|
||||||
await fs.writeFile(launcherPath, workflowContent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: launcherPath,
|
|
||||||
command: `bmad/custom/agents/${agentName}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { WindsurfSetup };
|
|
||||||
@@ -362,6 +362,7 @@ class UI {
|
|||||||
// Get IDE manager to fetch available IDEs dynamically
|
// Get IDE manager to fetch available IDEs dynamically
|
||||||
const { IdeManager } = require('../installers/lib/ide/manager');
|
const { IdeManager } = require('../installers/lib/ide/manager');
|
||||||
const ideManager = new IdeManager();
|
const ideManager = new IdeManager();
|
||||||
|
await ideManager.ensureInitialized(); // IMPORTANT: Must initialize before getting IDEs
|
||||||
|
|
||||||
const preferredIdes = ideManager.getPreferredIdes();
|
const preferredIdes = ideManager.getPreferredIdes();
|
||||||
const otherIdes = ideManager.getOtherIdes();
|
const otherIdes = ideManager.getOtherIdes();
|
||||||
|
|||||||
Reference in New Issue
Block a user