mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
normalize commands
This commit is contained in:
446
tools/cli/installers/lib/ide/_config-driven.js
Normal file
446
tools/cli/installers/lib/ide/_config-driven.js
Normal file
@@ -0,0 +1,446 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const yaml = require('yaml');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { UnifiedInstaller } = require('./shared/unified-installer');
|
||||
const { toSuffixBasedName, getArtifactSuffix, customAgentSuffixName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Load platform codes configuration from platform-codes.yaml
|
||||
* @returns {Object} Platform configuration object
|
||||
*/
|
||||
async function loadPlatformCodes() {
|
||||
const platformCodesPath = path.join(__dirname, 'platform-codes.yaml');
|
||||
|
||||
if (!(await fs.pathExists(platformCodesPath))) {
|
||||
console.warn(chalk.yellow('Warning: platform-codes.yaml not found'));
|
||||
return { platforms: {} };
|
||||
}
|
||||
|
||||
const content = await fs.readFile(platformCodesPath, 'utf8');
|
||||
const config = yaml.parse(content);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Config-driven IDE setup handler
|
||||
*
|
||||
* Reads installer configuration from platform-codes.yaml and uses
|
||||
* UnifiedInstaller to perform the actual installation.
|
||||
*
|
||||
* This eliminates the need for separate installer files for most IDEs.
|
||||
*/
|
||||
class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||
/**
|
||||
* @param {string} platformCode - Platform code (e.g., 'claude-code', 'cursor')
|
||||
* @param {Object} platformConfig - Platform configuration from platform-codes.yaml
|
||||
*/
|
||||
constructor(platformCode, platformConfig) {
|
||||
super(platformCode, platformConfig.name, platformConfig.preferred);
|
||||
this.platformConfig = platformConfig;
|
||||
this.installerConfig = platformConfig.installer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup IDE configuration using config-driven approach
|
||||
* @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}...`));
|
||||
|
||||
if (!this.installerConfig) {
|
||||
console.warn(chalk.yellow(`No installer configuration found for ${this.name}`));
|
||||
return { success: false, reason: 'no-config' };
|
||||
}
|
||||
|
||||
// Handle multi-target installations (like github-copilot, opencode)
|
||||
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);
|
||||
}
|
||||
|
||||
console.warn(chalk.yellow(`Invalid installer configuration for ${this.name}`));
|
||||
return { success: false, reason: 'invalid-config' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install artifacts to a single target directory
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} targetConfig - Target configuration
|
||||
* @param {Object} options - Setup options
|
||||
* @returns {Promise<Object>} Setup result
|
||||
*/
|
||||
async installToTarget(projectDir, bmadDir, targetConfig, options) {
|
||||
const targetDir = path.join(projectDir, targetConfig.dir || targetConfig.target_dir);
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanupTarget(targetDir, targetConfig.file_extension || '.md');
|
||||
|
||||
// Ensure target directory exists
|
||||
await this.ensureDir(targetDir);
|
||||
|
||||
// Get frontmatter template from config (defaults to common-yaml.md)
|
||||
const frontmatterTemplate = targetConfig.frontmatter_template || 'common-yaml.md';
|
||||
|
||||
// Use the unified installer
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir,
|
||||
namingStyle: 'suffix-based',
|
||||
frontmatterTemplate,
|
||||
fileExtension: targetConfig.file_extension || '.md',
|
||||
skipExisting: targetConfig.skip_existing || false,
|
||||
artifactTypes: targetConfig.artifact_types,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
if (counts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||
}
|
||||
if (counts.tasks + counts.tools > 0) {
|
||||
console.log(
|
||||
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - Target directory: ${path.relative(projectDir, targetDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: counts.agents,
|
||||
tasks: counts.tasks,
|
||||
tools: counts.tools,
|
||||
workflows: counts.workflows,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install artifacts 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>} Setup result
|
||||
*/
|
||||
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
||||
const totalCounts = {
|
||||
agents: 0,
|
||||
workflows: 0,
|
||||
tasks: 0,
|
||||
tools: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const targetNames = [];
|
||||
|
||||
for (const targetConfig of targets) {
|
||||
const targetDir = path.join(projectDir, targetConfig.dir);
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanupTarget(targetDir, targetConfig.file_extension || '.md');
|
||||
|
||||
// Ensure target directory exists
|
||||
await this.ensureDir(targetDir);
|
||||
|
||||
// Get frontmatter template from config (defaults to common-yaml.md)
|
||||
const frontmatterTemplate = targetConfig.frontmatter_template || 'common-yaml.md';
|
||||
|
||||
// Use the unified installer for this target
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir,
|
||||
namingStyle: 'suffix-based',
|
||||
frontmatterTemplate,
|
||||
fileExtension: targetConfig.file_extension || '.md',
|
||||
skipExisting: targetConfig.skip_existing || false,
|
||||
artifactTypes: targetConfig.artifact_types,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
// Accumulate counts
|
||||
totalCounts.agents += counts.agents;
|
||||
totalCounts.workflows += counts.workflows;
|
||||
totalCounts.tasks += counts.tasks;
|
||||
totalCounts.tools += counts.tools;
|
||||
|
||||
targetNames.push(path.relative(projectDir, targetDir));
|
||||
}
|
||||
|
||||
totalCounts.total = totalCounts.agents + totalCounts.workflows + totalCounts.tasks + totalCounts.tools;
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${totalCounts.agents} agents installed`));
|
||||
if (totalCounts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${totalCounts.workflows} workflow commands generated`));
|
||||
}
|
||||
if (totalCounts.tasks + totalCounts.tools > 0) {
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` - ${totalCounts.tasks + totalCounts.tools} task/tool commands generated (${totalCounts.tasks} tasks, ${totalCounts.tools} tools)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - Target directories: ${targetNames.join(', ')}`));
|
||||
|
||||
// Handle VS Code settings if needed (for github-copilot)
|
||||
if (this.installerConfig.has_vscode_settings) {
|
||||
await this.configureVsCodeSettings(projectDir, options);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...totalCounts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure VS Code settings for GitHub Copilot
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {Object} options - Setup options
|
||||
*/
|
||||
async configureVsCodeSettings(projectDir, options) {
|
||||
const vscodeDir = path.join(projectDir, '.vscode');
|
||||
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);
|
||||
} catch {
|
||||
console.warn(chalk.yellow(' Could not parse settings.json, creating new'));
|
||||
}
|
||||
}
|
||||
|
||||
// BMAD VS Code settings
|
||||
const 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,
|
||||
};
|
||||
|
||||
// Merge settings (existing take precedence)
|
||||
const mergedSettings = { ...bmadSettings, ...existingSettings };
|
||||
|
||||
// Write settings
|
||||
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
||||
console.log(chalk.dim(` - VS Code settings configured`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a specific target directory
|
||||
* @param {string} targetDir - Target directory to clean
|
||||
* @param {string} [fileExtension='.md'] - File extension to match
|
||||
*/
|
||||
async cleanupTarget(targetDir, fileExtension = '.md') {
|
||||
if (!(await fs.pathExists(targetDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(targetDir);
|
||||
let removed = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
// Remove bmad* files with the matching extension
|
||||
if (entry.startsWith('bmad') && entry.endsWith(fileExtension)) {
|
||||
await fs.remove(path.join(targetDir, entry));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD files`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup IDE configuration
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
if (!this.installerConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle multi-target cleanup
|
||||
if (this.installerConfig.targets) {
|
||||
for (const targetConfig of this.installerConfig.targets) {
|
||||
const targetDir = path.join(projectDir, targetConfig.dir);
|
||||
await this.cleanupTarget(targetDir, targetConfig.file_extension || '.md');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle single-target cleanup
|
||||
if (this.installerConfig.target_dir) {
|
||||
const targetDir = path.join(projectDir, this.installerConfig.target_dir);
|
||||
await this.cleanupTarget(targetDir, this.installerConfig.file_extension || '.md');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for this IDE
|
||||
* @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) {
|
||||
if (!this.installerConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine target directory for agents
|
||||
let targetDir;
|
||||
let fileExtension = '.md';
|
||||
let frontmatterTemplate = 'common-yaml.md';
|
||||
|
||||
if (this.installerConfig.targets) {
|
||||
// For multi-target IDEs like github-copilot, find the agents target
|
||||
const agentsTarget = this.installerConfig.targets.find((t) => t.artifact_types && t.artifact_types.includes('agents'));
|
||||
if (!agentsTarget) {
|
||||
return null; // No agents target found
|
||||
}
|
||||
targetDir = path.join(projectDir, agentsTarget.dir);
|
||||
fileExtension = agentsTarget.file_extension || '.md';
|
||||
frontmatterTemplate = agentsTarget.frontmatter_template || 'common-yaml.md';
|
||||
} else if (this.installerConfig.target_dir) {
|
||||
targetDir = path.join(projectDir, this.installerConfig.target_dir);
|
||||
fileExtension = this.installerConfig.file_extension || '.md';
|
||||
frontmatterTemplate = this.installerConfig.frontmatter_template || 'common-yaml.md';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(await this.exists(targetDir))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.ensureDir(targetDir);
|
||||
|
||||
// Create launcher content using frontmatter template
|
||||
const launcherContent = await this.createLauncherContent(agentName, agentPath, metadata, frontmatterTemplate);
|
||||
|
||||
// Use suffix-based naming for custom agents
|
||||
const fileName = customAgentSuffixName(agentName, fileExtension);
|
||||
const launcherPath = path.join(targetDir, fileName);
|
||||
await this.writeFile(launcherPath, launcherContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: fileName.replace(fileExtension, ''),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create launcher content using frontmatter template
|
||||
* @param {string} agentName - Agent name
|
||||
* @param {string} agentPath - Path to agent file
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @param {string} frontmatterTemplate - Template filename
|
||||
* @returns {Promise<string>} Launcher content
|
||||
*/
|
||||
async createLauncherContent(agentName, agentPath, metadata, frontmatterTemplate) {
|
||||
const title = metadata.title || this.formatTitle(agentName);
|
||||
|
||||
// Base activation content
|
||||
const activationContent = `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>
|
||||
`;
|
||||
|
||||
// Load frontmatter template
|
||||
const { UnifiedInstaller } = require('./shared/unified-installer');
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const templateContent = await installer.loadFrontmatterTemplate(frontmatterTemplate);
|
||||
|
||||
if (!templateContent) {
|
||||
// Fallback to basic YAML
|
||||
return `---
|
||||
name: '${agentName}'
|
||||
description: '${title} agent'
|
||||
---
|
||||
|
||||
${activationContent}`;
|
||||
}
|
||||
|
||||
// Apply template variables
|
||||
const variables = {
|
||||
name: agentName,
|
||||
title,
|
||||
displayName: agentName,
|
||||
description: `Activates the ${title} agent persona.`,
|
||||
icon: '🤖',
|
||||
content: activationContent,
|
||||
tools: JSON.stringify([
|
||||
'changes',
|
||||
'edit',
|
||||
'fetch',
|
||||
'githubRepo',
|
||||
'problems',
|
||||
'runCommands',
|
||||
'runTasks',
|
||||
'runTests',
|
||||
'search',
|
||||
'runSubagent',
|
||||
'testFailure',
|
||||
'todos',
|
||||
'usages',
|
||||
]),
|
||||
};
|
||||
|
||||
let result = templateContent;
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
result = result.replaceAll(`{{${key}}}`, value);
|
||||
}
|
||||
|
||||
// Handle TOML templates specially
|
||||
if (frontmatterTemplate.includes('toml')) {
|
||||
const escapedContent = activationContent.replaceAll('"""', String.raw`\"\"\"`);
|
||||
result = result.replace(
|
||||
/prompt = """/,
|
||||
`prompt = """\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\n${escapedContent}`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
return result + activationContent;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ConfigDrivenIdeSetup,
|
||||
loadPlatformCodes,
|
||||
};
|
||||
@@ -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 workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
if (await fs.pathExists(workflowsDir)) {
|
||||
const bmadFiles = (await fs.readdir(workflowsDir)).filter((f) => f.startsWith('bmad'));
|
||||
for (const f of bmadFiles) {
|
||||
await fs.remove(path.join(workflowsDir, f));
|
||||
}
|
||||
console.log(chalk.dim(` Removed old BMAD workflows from ${this.name}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
// 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(workflowsDir, 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(workflowsDir, workflowArtifacts);
|
||||
|
||||
// Generate task and tool commands using FLAT naming (not nested!)
|
||||
// Use the new generateDashTaskToolCommands method with explicit target directory
|
||||
const taskToolGen = new TaskToolCommandGenerator();
|
||||
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, workflowsDir);
|
||||
|
||||
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, workflowsDir)}`));
|
||||
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 directory structure
|
||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
await fs.ensureDir(workflowsDir);
|
||||
|
||||
// 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(workflowsDir, 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,119 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
|
||||
/**
|
||||
* Auggie CLI setup handler
|
||||
* Installs to project directory (.augment/commands)
|
||||
*/
|
||||
class AuggieSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('auggie', 'Auggie CLI');
|
||||
this.detectionPaths = ['.augment'];
|
||||
this.installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}...`));
|
||||
|
||||
// Use flat file structure in .augment/commands/
|
||||
const targetDir = path.join(projectDir, '.augment', 'commands');
|
||||
|
||||
// Install using UnifiedInstaller
|
||||
const counts = await this.installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir,
|
||||
namingStyle: NamingStyle.FLAT_COLON,
|
||||
templateType: TemplateType.AUGMENT,
|
||||
includeNestedStructure: false,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
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.tools} tools installed`));
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflows installed`));
|
||||
console.log(chalk.dim(` - Location: ${path.relative(projectDir, targetDir)}`));
|
||||
console.log(chalk.yellow(`\n 💡 Tip: Add 'model: gpt-4o' to command frontmatter to specify AI model`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...counts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Auggie configuration
|
||||
* Removes bmad* files from .augment/commands/
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const targetDir = path.join(projectDir, '.augment', 'commands');
|
||||
await this.installer.cleanupBmadFiles(targetDir);
|
||||
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 with flat structure
|
||||
const targetDir = path.join(projectDir, '.augment', 'commands');
|
||||
|
||||
// Create .augment/commands directory if it doesn't exist
|
||||
await fs.ensureDir(targetDir);
|
||||
|
||||
// Create custom agent launcher with flat naming: bmad_custom_agent_{name}.md
|
||||
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
|
||||
`;
|
||||
|
||||
// Use flat naming convention consistent with UnifiedInstaller
|
||||
const fileName = `bmad_custom_agent_${agentName.toLowerCase()}.md`;
|
||||
const launcherPath = path.join(targetDir, 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,401 +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 { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
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
|
||||
*
|
||||
* Uses UnifiedInstaller for standard artifact installation,
|
||||
* plus Claude-specific subagent injection handling.
|
||||
*/
|
||||
console.log(`[DEBUG CLAUDE-CODE] Module loaded!`);
|
||||
class ClaudeCodeSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('claude-code', 'Claude Code', true);
|
||||
this.configDir = '.claude';
|
||||
this.commandsDir = 'commands';
|
||||
this.agentsDir = 'agents';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for subagent installation location
|
||||
*/
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old BMAD installation before reinstalling
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
// Remove ANY bmad folder or files at any level
|
||||
const bmadPath = path.join(commandsDir, 'bmad');
|
||||
if (await fs.pathExists(bmadPath)) {
|
||||
await fs.remove(bmadPath);
|
||||
console.log(chalk.dim(` Removed old bmad folder from ${this.name}`));
|
||||
}
|
||||
|
||||
// Also remove any bmad* files at root level
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Claude Code IDE configuration
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
console.log(`[DEBUG CLAUDE-CODE] setup called! projectDir=${projectDir}`);
|
||||
this.projectDir = projectDir;
|
||||
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
const claudeDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(claudeDir, this.commandsDir);
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Use the unified installer for standard artifacts
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
console.log(`[DEBUG CLAUDE-CODE] About to call installer.install, targetDir=${commandsDir}`);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: commandsDir,
|
||||
namingStyle: NamingStyle.FLAT_COLON,
|
||||
templateType: TemplateType.CLAUDE,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
console.log(`[DEBUG CLAUDE-CODE] installer.install done, counts=`, counts);
|
||||
|
||||
// Process Claude Code specific injections for installed modules
|
||||
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
|
||||
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);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
if (counts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||
}
|
||||
if (counts.tasks + counts.tools > 0) {
|
||||
console.log(
|
||||
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: counts.agents,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {}) {
|
||||
return super.processContent(content, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agents from source modules (not installed location)
|
||||
*/
|
||||
async getAgentsFromSource(sourceDir, selectedModules) {
|
||||
const 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
const modules = options.selectedModules || [];
|
||||
const { subagentChoices, installLocation } = preCollectedConfig;
|
||||
|
||||
await this.processModuleInjectionsInternal({
|
||||
projectDir,
|
||||
modules,
|
||||
handler: 'claude-code',
|
||||
subagentChoices,
|
||||
installLocation,
|
||||
interactive: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Claude Code specific injections for installed modules
|
||||
*/
|
||||
async processModuleInjections(projectDir, bmadDir, options) {
|
||||
const modules = options.selectedModules || [];
|
||||
let subagentChoices = null;
|
||||
let installLocation = null;
|
||||
|
||||
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 }) {
|
||||
console.log(`[DEBUG CLAUDE-CODE] processModuleInjectionsInternal called! modules=${modules.join(',')}`);
|
||||
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) {
|
||||
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') {
|
||||
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;
|
||||
|
||||
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');
|
||||
|
||||
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/`));
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
|
||||
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,175 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
const { customAgentDashName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Cline IDE setup handler
|
||||
*
|
||||
* Uses UnifiedInstaller for all artifact installation.
|
||||
* 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 fs.ensureDir(workflowsDir);
|
||||
|
||||
// Clear old BMAD files
|
||||
await this.clearOldBmadFiles(workflowsDir);
|
||||
|
||||
// Use the unified installer - much simpler!
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: workflowsDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.CLINE,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
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.tools > 0) {
|
||||
console.log(chalk.dim(` - ${counts.tools} tools installed`));
|
||||
}
|
||||
console.log(chalk.dim(` - ${counts.total} 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,
|
||||
...counts,
|
||||
destination: workflowsDir,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ClineSetup };
|
||||
@@ -2,25 +2,85 @@ const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const os = require('node:os');
|
||||
const chalk = require('chalk');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
const { customAgentDashName } = require('./shared/path-utils');
|
||||
const { ConfigDrivenIdeSetup } = require('./_config-driven');
|
||||
const { getSourcePath } = require('../../../lib/project-root');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Codex setup handler (CLI mode)
|
||||
*
|
||||
* Uses UnifiedInstaller for all artifact installation.
|
||||
* Extends config-driven setup with Codex-specific features:
|
||||
* - Install location choice (global vs project-specific)
|
||||
* - Configuration prompts
|
||||
* - Detailed setup instructions
|
||||
*/
|
||||
class CodexSetup extends BaseIdeSetup {
|
||||
class CodexSetup extends ConfigDrivenIdeSetup {
|
||||
constructor() {
|
||||
super('codex', 'Codex', true);
|
||||
// Initialize with codex platform config
|
||||
const platformConfig = {
|
||||
name: 'Codex',
|
||||
preferred: false,
|
||||
installer: {
|
||||
target_dir: '.codex/prompts',
|
||||
frontmatter_template: 'none', // Codex uses no frontmatter
|
||||
},
|
||||
};
|
||||
super('codex', platformConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect configuration choices before installation
|
||||
* Get the Codex agent command activation header from central template
|
||||
* @returns {string} The activation header text
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
async getAgentCommandHeader() {
|
||||
const headerPath = getSourcePath('tools/cli/installers/lib/ide/templates', 'codex-agent-command-template.md');
|
||||
return await fs.readFile(headerPath, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override setup to add install location choice and instructions
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
// Collect install location choice
|
||||
const installLocation = options.preCollectedConfig?.installLocation || (await this.collectInstallLocation());
|
||||
|
||||
// Determine destination directory
|
||||
const destDir = this.getCodexPromptDir(projectDir, installLocation);
|
||||
await fs.ensureDir(destDir);
|
||||
await this.clearOldBmadFiles(destDir);
|
||||
|
||||
// Use unified installer with custom destination
|
||||
const { UnifiedInstaller, NamingStyle } = require('./shared/unified-installer');
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: destDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
frontmatterTemplate: 'none', // Codex uses no frontmatter
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
// Show results and instructions
|
||||
this.printResults(counts, destDir, installLocation);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
mode: 'cli',
|
||||
...counts,
|
||||
destination: destDir,
|
||||
installLocation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect install location choice from user
|
||||
*/
|
||||
async collectInstallLocation() {
|
||||
let confirmed = false;
|
||||
let installLocation = 'global';
|
||||
|
||||
@@ -29,11 +89,11 @@ class CodexSetup extends BaseIdeSetup {
|
||||
message: 'Where would you like to install Codex CLI prompts?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
||||
name: 'Global - Simple for single project (~/codex/prompts, references THIS project only)',
|
||||
value: 'global',
|
||||
},
|
||||
{
|
||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>/.codex)`,
|
||||
value: 'project',
|
||||
},
|
||||
],
|
||||
@@ -61,80 +121,8 @@ class CodexSetup extends BaseIdeSetup {
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Codex configuration
|
||||
* Get Codex prompts directory based on location choice
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
const installLocation = options.preCollectedConfig?.installLocation || 'global';
|
||||
const destDir = this.getCodexPromptDir(projectDir, installLocation);
|
||||
|
||||
await fs.ensureDir(destDir);
|
||||
await this.clearOldBmadFiles(destDir);
|
||||
|
||||
// Use the unified installer - so much simpler!
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: destDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.CODEX,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - Mode: CLI`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
if (counts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||
}
|
||||
if (counts.tasks + counts.tools > 0) {
|
||||
console.log(
|
||||
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - ${counts.total} Codex prompt files written`));
|
||||
console.log(chalk.dim(` - Destination: ${destDir}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
mode: 'cli',
|
||||
...counts,
|
||||
destination: destDir,
|
||||
installLocation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Codex installation by checking for BMAD prompt exports
|
||||
*/
|
||||
async detect(projectDir) {
|
||||
const globalDir = this.getCodexPromptDir(null, 'global');
|
||||
const projectDir_local = projectDir || process.cwd();
|
||||
const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project');
|
||||
|
||||
// Check global location
|
||||
if (await fs.pathExists(globalDir)) {
|
||||
const entries = await fs.readdir(globalDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check project-specific location
|
||||
if (await fs.pathExists(projectSpecificDir)) {
|
||||
const entries = await fs.readdir(projectSpecificDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getCodexPromptDir(projectDir = null, location = 'global') {
|
||||
if (location === 'project' && projectDir) {
|
||||
return path.join(projectDir, '.codex', 'prompts');
|
||||
@@ -142,25 +130,28 @@ class CodexSetup extends BaseIdeSetup {
|
||||
return path.join(os.homedir(), '.codex', 'prompts');
|
||||
}
|
||||
|
||||
async clearOldBmadFiles(destDir) {
|
||||
if (!(await fs.pathExists(destDir))) {
|
||||
return;
|
||||
/**
|
||||
* Print results and instructions
|
||||
*/
|
||||
printResults(counts, destDir, installLocation) {
|
||||
console.log(chalk.green(`✓ Codex configured:`));
|
||||
console.log(chalk.dim(` - Mode: CLI`));
|
||||
console.log(chalk.dim(` - Location: ${installLocation}`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
if (counts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||
}
|
||||
if (counts.tasks + counts.tools > 0) {
|
||||
console.log(chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands (${counts.tasks} tasks, ${counts.tools} tools)`));
|
||||
}
|
||||
console.log(chalk.dim(` - ${counts.total} files written`));
|
||||
console.log(chalk.dim(` - Destination: ${destDir}`));
|
||||
|
||||
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);
|
||||
}
|
||||
// Show setup instructions if project-specific
|
||||
if (installLocation === 'project') {
|
||||
console.log('');
|
||||
console.log(chalk.yellow(' Next steps:'));
|
||||
console.log(chalk.dim(this.getProjectSpecificNextSteps()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,20 +217,73 @@ class CodexSetup extends BaseIdeSetup {
|
||||
chalk.dim(' After adding, run: source ~/.bashrc (or source ~/.zshrc)'),
|
||||
chalk.dim(' (The $PWD uses your current working directory)'),
|
||||
];
|
||||
const closingLines = [
|
||||
'',
|
||||
chalk.dim(' This tells Codex CLI to use prompts from this project instead of ~/.codex'),
|
||||
'',
|
||||
chalk.bold.cyan('═'.repeat(70)),
|
||||
'',
|
||||
];
|
||||
|
||||
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];
|
||||
return lines.join('\n');
|
||||
return [...commonLines, ...(isWindows ? windowsLines : unixLines)].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Codex configuration
|
||||
* Get next steps for project-specific installation
|
||||
*/
|
||||
getProjectSpecificNextSteps() {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
if (isWindows) {
|
||||
return `Create codex.cmd in project root with:\n set CODEX_HOME=%~dp0.codex\n codex %*`;
|
||||
}
|
||||
return `Add to ~/.bashrc or ~/.zshrc:\n alias codex='CODEX_HOME="$PWD/.codex" codex'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old BMAD files from destination
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Codex installation (checks both global and project locations)
|
||||
*/
|
||||
async detect(projectDir) {
|
||||
const globalDir = this.getCodexPromptDir(null, 'global');
|
||||
const projectDir_local = projectDir || process.cwd();
|
||||
const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project');
|
||||
|
||||
// Check global location
|
||||
if (await fs.pathExists(globalDir)) {
|
||||
const entries = await fs.readdir(globalDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check project-specific location
|
||||
if (await fs.pathExists(projectSpecificDir)) {
|
||||
const entries = await fs.readdir(projectSpecificDir);
|
||||
if (entries.some((entry) => entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Codex configuration (both global and project-specific)
|
||||
*/
|
||||
async cleanup(projectDir = null) {
|
||||
const globalDir = this.getCodexPromptDir(null, 'global');
|
||||
@@ -258,26 +302,25 @@ class CodexSetup extends BaseIdeSetup {
|
||||
const destDir = this.getCodexPromptDir(projectDir, 'project');
|
||||
await fs.ensureDir(destDir);
|
||||
|
||||
const launcherContent = `---
|
||||
name: '${agentName}'
|
||||
description: '${agentName} agent'
|
||||
---
|
||||
// Load the custom agent launcher template
|
||||
const templatePath = getSourcePath('tools/cli/installers/lib/ide/templates', 'codex-custom-agent-template.md');
|
||||
let templateContent = await fs.readFile(templatePath, 'utf8');
|
||||
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
// Get activation header
|
||||
const activationHeader = await this.getAgentCommandHeader();
|
||||
|
||||
<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>
|
||||
`;
|
||||
// Replace placeholders
|
||||
const relativePath = `_bmad/${agentPath}`;
|
||||
templateContent = templateContent
|
||||
.replaceAll('{{name}}', agentName)
|
||||
.replaceAll('{{description}}', `${agentName} agent`)
|
||||
.replaceAll('{{activationHeader}}', activationHeader)
|
||||
.replaceAll('{{relativePath}}', relativePath);
|
||||
|
||||
const { customAgentDashName } = require('./shared/path-utils');
|
||||
const fileName = customAgentDashName(agentName);
|
||||
const launcherPath = path.join(destDir, fileName);
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
await fs.writeFile(launcherPath, templateContent, 'utf8');
|
||||
|
||||
return {
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
const { customAgentColonName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Crush IDE setup handler
|
||||
*
|
||||
* Uses the UnifiedInstaller - all the complex artifact collection
|
||||
* and writing logic is now centralized.
|
||||
*/
|
||||
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 commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Use the unified installer
|
||||
// Crush uses flat colon naming (bmad_bmm_pm.md) with no frontmatter (like Codex)
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: commandsDir,
|
||||
namingStyle: NamingStyle.FLAT_COLON,
|
||||
templateType: TemplateType.CODEX,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
if (counts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||
}
|
||||
if (counts.tasks + counts.tools > 0) {
|
||||
console.log(
|
||||
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
|
||||
);
|
||||
}
|
||||
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: counts.agents,
|
||||
tasks: counts.tasks,
|
||||
tools: counts.tools,
|
||||
workflows: counts.workflows,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,131 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
const { customAgentColonName } = require('./shared/path-utils');
|
||||
|
||||
/**
|
||||
* Cursor IDE setup handler
|
||||
*
|
||||
* Uses the UnifiedInstaller - all the complex artifact collection
|
||||
* and writing logic is now centralized.
|
||||
*/
|
||||
class CursorSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('cursor', 'Cursor', true);
|
||||
this.configDir = '.cursor';
|
||||
this.rulesDir = 'rules';
|
||||
this.commandsDir = 'commands';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Cursor IDE configuration
|
||||
*/
|
||||
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
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Use the unified installer
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: commandsDir,
|
||||
namingStyle: NamingStyle.FLAT_COLON,
|
||||
templateType: TemplateType.CURSOR,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
if (counts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||
}
|
||||
if (counts.tasks + counts.tools > 0) {
|
||||
console.log(
|
||||
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
|
||||
);
|
||||
}
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: counts.agents,
|
||||
tasks: counts.tasks,
|
||||
tools: counts.tools,
|
||||
workflows: counts.workflows,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old BMAD installation
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
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}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Cursor
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
|
||||
const launcherName = customAgentColonName(agentName);
|
||||
const launcherPath = path.join(commandsDir, launcherName);
|
||||
await this.writeFile(launcherPath, launcherContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: `/${launcherName.replace('.md', '')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CursorSetup };
|
||||
@@ -1,168 +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 { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Use UnifiedInstaller for agents and workflows
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
|
||||
const config = {
|
||||
targetDir: commandsDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.GEMINI,
|
||||
fileExtension: '.toml',
|
||||
};
|
||||
|
||||
const counts = await installer.install(projectDir, bmadDir, config, options.selectedModules || []);
|
||||
|
||||
// Generate activation names for display
|
||||
const agentActivation = `/bmad_agents_{agent-name}`;
|
||||
const workflowActivation = `/bmad_workflows_{workflow-name}`;
|
||||
const taskActivation = `/bmad_tasks_{task-name}`;
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents configured`));
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflows configured`));
|
||||
console.log(chalk.dim(` - ${counts.tasks} tasks configured`));
|
||||
console.log(chalk.dim(` - ${counts.tools} tools configured`));
|
||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||
console.log(chalk.dim(` - Agent activation: ${agentActivation}`));
|
||||
console.log(chalk.dim(` - Workflow activation: ${workflowActivation}`));
|
||||
console.log(chalk.dim(` - Task activation: ${taskActivation}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...counts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,426 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
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.promptsDir = 'prompts';
|
||||
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 and .github/prompts directories
|
||||
const githubDir = path.join(projectDir, this.configDir);
|
||||
const agentsDir = path.join(githubDir, this.agentsDir);
|
||||
const promptsDir = path.join(githubDir, this.promptsDir);
|
||||
await this.ensureDir(agentsDir);
|
||||
await this.ensureDir(promptsDir);
|
||||
|
||||
// Clean up any existing BMAD files before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// 1. Generate agent launchers (custom .agent.md format - not using UnifiedInstaller)
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Create agent files with bmad- 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 bmad- prefix: bmad-{module}-{name}.agent.md
|
||||
const targetPath = path.join(agentsDir, `bmad-${artifact.module}-${artifact.name}.agent.md`);
|
||||
await this.writeFile(targetPath, agentContent);
|
||||
agentCount++;
|
||||
|
||||
console.log(chalk.green(` ✓ Created agent: bmad-${artifact.module}-${artifact.name}`));
|
||||
}
|
||||
|
||||
// 2. Install prompts using UnifiedInstaller
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const promptCounts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: promptsDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.COPILOT,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents created`));
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` - ${promptCounts.agents} prompts, ${promptCounts.workflows} workflows, ${promptCounts.tasks + promptCounts.tools} tasks/tools`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.dim(` - Agents directory: ${path.relative(projectDir, agentsDir)}`));
|
||||
console.log(chalk.dim(` - Prompts directory: ${path.relative(projectDir, promptsDir)}`));
|
||||
console.log(chalk.dim(` - VS Code settings configured`));
|
||||
console.log(chalk.dim('\n Agents and prompts available in VS Code Chat view'));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
prompts: promptCounts.total,
|
||||
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 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) {
|
||||
// Remove old bmd-* files (typo fix) and current bmad-* files
|
||||
if ((file.startsWith('bmd-') || file.startsWith('bmad-')) && 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`));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up prompts directory
|
||||
const promptsDir = path.join(projectDir, this.configDir, this.promptsDir);
|
||||
if (await fs.pathExists(promptsDir)) {
|
||||
const files = await fs.readdir(promptsDir);
|
||||
let removed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad-') && file.endsWith('.md')) {
|
||||
await fs.remove(path.join(promptsDir, file));
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD prompts`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, `bmad-${agentName}.agent.md`);
|
||||
await this.writeFile(agentFilePath, agentContent);
|
||||
|
||||
return {
|
||||
path: agentFilePath,
|
||||
command: `bmad-${agentName}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GitHubCopilotSetup };
|
||||
@@ -1,176 +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}...`));
|
||||
|
||||
// Clean up old BMAD installation first
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Create .iflow/commands directory structure (flat files, no bmad subfolder)
|
||||
const iflowDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(iflowDir, this.commandsDir);
|
||||
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
|
||||
// Setup agents as commands (flat files with dash naming)
|
||||
const agentCount = await agentGen.writeDashArtifacts(commandsDir, agentArtifacts);
|
||||
|
||||
// Get tasks and workflows (ALL workflows now generate commands)
|
||||
const tasks = await this.getTasks(bmadDir);
|
||||
|
||||
// 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 workflows as commands (flat files with dash naming)
|
||||
const workflowCount = await workflowGenerator.writeDashArtifacts(commandsDir, workflowArtifacts);
|
||||
|
||||
// TODO: tasks not yet implemented with flat naming
|
||||
const taskCount = 0;
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agent commands created`));
|
||||
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 commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
||||
|
||||
// Remove old bmad subfolder if it exists
|
||||
if (await fs.pathExists(bmadFolder)) {
|
||||
await fs.remove(bmadFolder);
|
||||
}
|
||||
|
||||
// Also remove any bmad* files at commands root
|
||||
if (await fs.pathExists(commandsDir)) {
|
||||
const bmadFiles = (await fs.readdir(commandsDir)).filter((f) => f.startsWith('bmad'));
|
||||
for (const f of bmadFiles) {
|
||||
await fs.remove(path.join(commandsDir, f));
|
||||
}
|
||||
console.log(chalk.dim(`Removed BMAD commands from iFlow CLI`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
// Create .iflow/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*`;
|
||||
|
||||
const { customAgentDashName } = require('./shared/path-utils');
|
||||
const fileName = customAgentDashName(agentName);
|
||||
const launcherPath = path.join(commandsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'iflow',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: agentName,
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { IFlowSetup };
|
||||
@@ -115,18 +115,20 @@ class KiloSetup extends BaseIdeSetup {
|
||||
|
||||
// Build mode entry (KiloCode uses same schema as Roo)
|
||||
const slug = `bmad-${artifact.module}-${artifact.name}`;
|
||||
let modeEntry = ` - slug: ${slug}\n`;
|
||||
modeEntry += ` name: '${icon} ${title}'\n`;
|
||||
modeEntry += ` roleDefinition: ${roleDefinition}\n`;
|
||||
modeEntry += ` whenToUse: ${whenToUse}\n`;
|
||||
modeEntry += ` customInstructions: |\n`;
|
||||
modeEntry += ` ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
|
||||
modeEntry += ` groups:\n`;
|
||||
modeEntry += ` - read\n`;
|
||||
modeEntry += ` - edit\n`;
|
||||
modeEntry += ` - browser\n`;
|
||||
modeEntry += ` - command\n`;
|
||||
modeEntry += ` - mcp\n`;
|
||||
const modeEntry = ` - slug: ${slug}
|
||||
name: '${icon} ${title}'
|
||||
roleDefinition: ${roleDefinition}
|
||||
whenToUse: ${whenToUse}
|
||||
customInstructions: |
|
||||
${activationHeader.trim()}
|
||||
Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- browser
|
||||
- command
|
||||
- mcp
|
||||
`;
|
||||
|
||||
return modeEntry;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const chalk = require('chalk');
|
||||
const yaml = require('yaml');
|
||||
const { ConfigDrivenIdeSetup, loadPlatformCodes } = require('./_config-driven');
|
||||
|
||||
/**
|
||||
* IDE Manager - handles IDE-specific setup
|
||||
* Dynamically discovers and loads IDE handlers
|
||||
*
|
||||
* NEW: Loads config-driven handlers from platform-codes.yaml
|
||||
* Custom installer files (like kilo.js, kiro-cli.js) are still supported
|
||||
* for IDEs with truly unique requirements.
|
||||
*/
|
||||
class IdeManager {
|
||||
constructor() {
|
||||
this.handlers = new Map();
|
||||
this.loadHandlers();
|
||||
this.platformConfig = null;
|
||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||
this._initialized = false;
|
||||
// Load custom handlers synchronously
|
||||
this.loadCustomInstallerFiles(__dirname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure handlers are initialized (loads config-driven handlers)
|
||||
* Call this before using handlers if needed
|
||||
*/
|
||||
async ensureInitialized() {
|
||||
if (!this._initialized) {
|
||||
await this.loadConfigDrivenHandlers();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,15 +48,28 @@ class IdeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically load all IDE handlers from directory
|
||||
* Dynamically load all IDE handlers
|
||||
*
|
||||
* Loading order:
|
||||
* 1. Load custom installer files (kilo.js, kiro-cli.js) for IDEs with unique requirements
|
||||
* 2. Load config-driven handlers from platform-codes.yaml for all other IDEs
|
||||
* @deprecated Use ensureInitialized() instead
|
||||
*/
|
||||
loadHandlers() {
|
||||
const ideDir = __dirname;
|
||||
async loadHandlers() {
|
||||
await this.ensureInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom installer files (for IDEs with truly unique requirements)
|
||||
* Synchronous version for constructor
|
||||
* @param {string} ideDir - IDE handlers directory
|
||||
*/
|
||||
loadCustomInstallerFiles(ideDir) {
|
||||
try {
|
||||
// Get all JS files in the IDE directory
|
||||
const files = fs.readdirSync(ideDir).filter((file) => {
|
||||
// Skip base class, manager, utility files (starting with _), and helper modules
|
||||
// Skip base class, manager, config-driven, utility files (starting with _)
|
||||
// Also skip shared directory and generator files
|
||||
return (
|
||||
file.endsWith('.js') &&
|
||||
!file.startsWith('_') &&
|
||||
@@ -74,15 +107,64 @@ class IdeManager {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Failed to load IDE handlers:'), error.message);
|
||||
console.error(chalk.red('Failed to load custom IDE handlers:'), error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config-driven handlers from platform-codes.yaml
|
||||
* Async version called by ensureInitialized()
|
||||
*/
|
||||
async loadConfigDrivenHandlers() {
|
||||
try {
|
||||
// Load platform-codes.yaml configuration
|
||||
this.platformConfig = await loadPlatformCodes();
|
||||
|
||||
// Create config-driven handlers for platforms with installer config
|
||||
if (this.platformConfig.platforms) {
|
||||
for (const [platformCode, platformInfo] of Object.entries(this.platformConfig.platforms)) {
|
||||
// Skip if custom handler already exists
|
||||
if (this.handlers.has(platformCode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if no installer config
|
||||
if (!platformInfo.installer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo);
|
||||
handler.setBmadFolderName(this.bmadFolderName);
|
||||
this.handlers.set(platformCode, handler);
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(` Warning: Could not create config-driven handler for ${platformCode}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log summary
|
||||
const customCount = [...this.handlers.entries()].filter(([key]) => {
|
||||
const handler = this.handlers.get(key);
|
||||
return handler && !(handler instanceof ConfigDrivenIdeSetup);
|
||||
}).length;
|
||||
const configCount = [...this.handlers.entries()].filter(([key]) => {
|
||||
const handler = this.handlers.get(key);
|
||||
return handler && handler instanceof ConfigDrivenIdeSetup;
|
||||
}).length;
|
||||
console.log(chalk.dim(` Loaded ${customCount} custom handlers, ${configCount} config-driven handlers`));
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Failed to load config-driven handlers:'), error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available IDEs with their metadata
|
||||
* @returns {Array} Array of IDE information objects
|
||||
* @returns {Promise<Array>} Array of IDE information objects
|
||||
*/
|
||||
getAvailableIdes() {
|
||||
async getAvailableIdes() {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const ides = [];
|
||||
|
||||
for (const [key, handler] of this.handlers) {
|
||||
@@ -113,18 +195,20 @@ class IdeManager {
|
||||
|
||||
/**
|
||||
* Get preferred IDEs
|
||||
* @returns {Array} Array of preferred IDE information
|
||||
* @returns {Promise<Array>} Array of preferred IDE information
|
||||
*/
|
||||
getPreferredIdes() {
|
||||
return this.getAvailableIdes().filter((ide) => ide.preferred);
|
||||
async getPreferredIdes() {
|
||||
const ides = await this.getAvailableIdes();
|
||||
return ides.filter((ide) => ide.preferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get non-preferred IDEs
|
||||
* @returns {Array} Array of non-preferred IDE information
|
||||
* @returns {Promise<Array>} Array of non-preferred IDE information
|
||||
*/
|
||||
getOtherIdes() {
|
||||
return this.getAvailableIdes().filter((ide) => !ide.preferred);
|
||||
async getOtherIdes() {
|
||||
const ides = await this.getAvailableIdes();
|
||||
return ides.filter((ide) => !ide.preferred);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +219,8 @@ class IdeManager {
|
||||
* @param {Object} options - Setup options
|
||||
*/
|
||||
async setup(ideName, projectDir, bmadDir, options = {}) {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const handler = this.handlers.get(ideName.toLowerCase());
|
||||
|
||||
if (!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 };
|
||||
@@ -5,127 +5,177 @@
|
||||
# the installation system to identify different platforms (IDEs, tools, etc.)
|
||||
#
|
||||
# Format:
|
||||
# code: Platform identifier used internally
|
||||
# code: Platform identifier used internally (key)
|
||||
# name: Display name shown to users
|
||||
# preferred: Whether this platform is shown as a recommended option on install
|
||||
# category: Type of platform (ide, tool, service, etc.)
|
||||
# category: Type of platform (ide, cli, tool, service, etc.)
|
||||
# installer: Installation configuration (optional)
|
||||
# frontmatter_template: Path to frontmatter template file (relative to templates/frontmatter/)
|
||||
# If not specified, uses 'common-yaml.md' default
|
||||
|
||||
platforms:
|
||||
# Recommended Platforms
|
||||
claude-code:
|
||||
name: "Claude Code"
|
||||
preferred: true
|
||||
category: cli
|
||||
description: "Anthropic's official CLI for Claude"
|
||||
|
||||
windsurf:
|
||||
name: "Windsurf"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-powered IDE with cascade flows"
|
||||
|
||||
cursor:
|
||||
name: "Cursor"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-first code editor"
|
||||
|
||||
# Other IDEs and Tools
|
||||
cline:
|
||||
name: "Cline"
|
||||
antigravity:
|
||||
name: "Google Antigravity"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding assistant"
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "OpenCode terminal coding assistant"
|
||||
description: "Google's AI development environment"
|
||||
installer:
|
||||
target_dir: .antigravity/commands
|
||||
frontmatter_template: common-yaml.md
|
||||
|
||||
auggie:
|
||||
name: "Auggie"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "AI development tool"
|
||||
installer:
|
||||
target_dir: .augment/commands
|
||||
frontmatter_template: common-yaml.md
|
||||
|
||||
roo:
|
||||
name: "Roo Cline"
|
||||
cline:
|
||||
name: "Cline"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Enhanced Cline fork"
|
||||
description: "AI coding assistant"
|
||||
installer:
|
||||
target_dir: .cline/commands
|
||||
frontmatter_template: none # No frontmatter, content as-is
|
||||
|
||||
rovo:
|
||||
name: "Rovo"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Atlassian's AI coding assistant"
|
||||
|
||||
rovo-dev:
|
||||
name: "Rovo Dev"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Atlassian's Rovo development environment"
|
||||
|
||||
kiro-cli:
|
||||
name: "Kiro CLI"
|
||||
preferred: false
|
||||
claude-code:
|
||||
name: "Claude Code"
|
||||
preferred: true
|
||||
category: cli
|
||||
description: "Kiro command-line interface"
|
||||
|
||||
github-copilot:
|
||||
name: "GitHub Copilot"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "GitHub's AI pair programmer"
|
||||
|
||||
codex:
|
||||
name: "Codex"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "OpenAI Codex integration"
|
||||
|
||||
qwen:
|
||||
name: "QwenCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Qwen AI coding assistant"
|
||||
|
||||
gemini:
|
||||
name: "Gemini CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Google's CLI for Gemini"
|
||||
|
||||
iflow:
|
||||
name: "iFlow"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI workflow automation"
|
||||
|
||||
kilo:
|
||||
name: "KiloCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding platform"
|
||||
description: "Anthropic's official CLI for Claude"
|
||||
installer:
|
||||
target_dir: .claude/commands
|
||||
frontmatter_template: common-yaml.md
|
||||
|
||||
crush:
|
||||
name: "Crush"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI development assistant"
|
||||
installer:
|
||||
target_dir: .crush/commands
|
||||
frontmatter_template: common-yaml.md
|
||||
|
||||
antigravity:
|
||||
name: "Google Antigravity"
|
||||
cursor:
|
||||
name: "Cursor"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-first code editor"
|
||||
installer:
|
||||
target_dir: .cursor/commands
|
||||
frontmatter_template: common-yaml.md
|
||||
|
||||
gemini:
|
||||
name: "Gemini CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Google's CLI for Gemini"
|
||||
installer:
|
||||
target_dir: .gemini/commands
|
||||
file_extension: .toml
|
||||
frontmatter_template: common-toml.md
|
||||
|
||||
github-copilot:
|
||||
name: "GitHub Copilot"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "GitHub's AI pair programmer"
|
||||
installer:
|
||||
targets:
|
||||
- dir: .github/agents
|
||||
frontmatter_template: copilot-agent.md
|
||||
artifact_types: [agents]
|
||||
- dir: .github/prompts
|
||||
frontmatter_template: copilot.md
|
||||
artifact_types: [workflows, tasks, tools]
|
||||
has_vscode_settings: true
|
||||
|
||||
iflow:
|
||||
name: "iFlow"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Google's AI development environment"
|
||||
description: "AI workflow automation"
|
||||
installer:
|
||||
target_dir: .iflow/commands
|
||||
frontmatter_template: common-yaml.md
|
||||
|
||||
kilo:
|
||||
name: "KiloCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding platform"
|
||||
# Kilo has custom installer (.kilocodemodes YAML format) - not config-driven
|
||||
|
||||
kiro-cli:
|
||||
name: "Kiro CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Kiro command-line interface"
|
||||
# Kiro CLI has custom installer (YAML->JSON conversion) - not config-driven
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "OpenCode terminal coding assistant"
|
||||
installer:
|
||||
targets:
|
||||
- dir: .opencode/agent
|
||||
frontmatter_template: opencode-agent.md
|
||||
artifact_types: [agents]
|
||||
- dir: .opencode/command
|
||||
frontmatter_template: opencode.md
|
||||
artifact_types: [workflows, tasks, tools]
|
||||
|
||||
qwen:
|
||||
name: "QwenCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Qwen AI coding assistant"
|
||||
installer:
|
||||
target_dir: .qwen/commands
|
||||
file_extension: .toml
|
||||
frontmatter_template: common-toml.md
|
||||
|
||||
roo:
|
||||
name: "Roo Code"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Enhanced Cline fork"
|
||||
installer:
|
||||
target_dir: .roo/commands
|
||||
frontmatter_template: roo.md
|
||||
skip_existing: true
|
||||
|
||||
rovo-dev:
|
||||
name: "Rovo Dev"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Atlassian's Rovo development environment"
|
||||
installer:
|
||||
target_dir: .rovo-dev/commands
|
||||
frontmatter_template: common-yaml.md
|
||||
|
||||
trae:
|
||||
name: "Trae"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding tool"
|
||||
installer:
|
||||
target_dir: .trae/rules
|
||||
frontmatter_template: trae.md
|
||||
|
||||
windsurf:
|
||||
name: "Windsurf"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-powered IDE with cascade flows"
|
||||
installer:
|
||||
target_dir: .windsurf/workflows
|
||||
frontmatter_template: windsurf.md
|
||||
|
||||
# Platform categories
|
||||
categories:
|
||||
@@ -155,3 +205,12 @@ conventions:
|
||||
name_format: "Title Case"
|
||||
max_code_length: 20
|
||||
allowed_characters: "a-z0-9-"
|
||||
|
||||
# New universal file naming standard
|
||||
file_naming:
|
||||
agent: "bmad-{module}-{name}.agent.md"
|
||||
workflow: "bmad-{module}-{name}.workflow.md"
|
||||
task: "bmad-{module}-{name}.task.md"
|
||||
tool: "bmad-{module}-{name}.tool.md"
|
||||
example_agent: "bmad-cis-storymaster.agent.md"
|
||||
example_workflow: "bmad-bmm-plan-project.workflow.md"
|
||||
@@ -1,219 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
|
||||
/**
|
||||
* Qwen Code setup handler
|
||||
* Creates TOML command files in .qwen/commands/
|
||||
*/
|
||||
class QwenSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('qwen', 'Qwen Code');
|
||||
this.configDir = '.qwen';
|
||||
this.commandsDir = 'commands';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 directory (flat structure, no bmad subfolder)
|
||||
const qwenDir = path.join(projectDir, this.configDir);
|
||||
const commandsDir = path.join(qwenDir, this.commandsDir);
|
||||
|
||||
await this.ensureDir(commandsDir);
|
||||
|
||||
// Update existing settings.json if present
|
||||
await this.updateSettings(qwenDir);
|
||||
|
||||
// Clean up old configuration
|
||||
await this.cleanupOldConfig(qwenDir);
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Use the unified installer with QWEN template for TOML format
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: commandsDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.QWEN,
|
||||
fileExtension: '.toml',
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents configured`));
|
||||
console.log(chalk.dim(` - ${counts.tasks} tasks configured`));
|
||||
console.log(chalk.dim(` - ${counts.tools} tools configured`));
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflows configured`));
|
||||
console.log(chalk.dim(` - ${counts.total} TOML files written to ${path.relative(projectDir, commandsDir)}`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...counts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings.json to remove old agent references
|
||||
*/
|
||||
async updateSettings(qwenDir) {
|
||||
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 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'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Qwen configuration
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
if (await fs.pathExists(commandsDir)) {
|
||||
// Remove any bmad* files from the commands directory
|
||||
const entries = await fs.readdir(commandsDir);
|
||||
for (const entry of entries) {
|
||||
if (entry.startsWith('bmad')) {
|
||||
await fs.remove(path.join(commandsDir, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also remove legacy bmad subfolder if it exists
|
||||
const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, 'bmad');
|
||||
if (await fs.pathExists(bmadCommandsDir)) {
|
||||
await fs.remove(bmadCommandsDir);
|
||||
console.log(chalk.dim(` Cleaned up existing BMAD configuration from Qwen Code`));
|
||||
}
|
||||
|
||||
const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method');
|
||||
if (await fs.pathExists(oldBmadMethodDir)) {
|
||||
await fs.remove(oldBmadMethodDir);
|
||||
console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`));
|
||||
}
|
||||
|
||||
const oldBMadDir = path.join(projectDir, this.configDir, 'BMad');
|
||||
if (await fs.pathExists(oldBMadDir)) {
|
||||
await fs.remove(oldBMadDir);
|
||||
console.log(chalk.dim(` Removed old BMAD configuration from Qwen Code`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||
|
||||
// Create .qwen/commands directory if it doesn't exist
|
||||
await fs.ensureDir(commandsDir);
|
||||
|
||||
// Create custom agent launcher content
|
||||
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*`;
|
||||
|
||||
// Convert to TOML format using the same method as UnifiedInstaller
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = launcherContent.replace(frontmatterRegex, '').trim();
|
||||
const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`);
|
||||
|
||||
const tomlContent = `description = "BMAD Custom Agent: ${agentName}"
|
||||
prompt = """
|
||||
${escapedContent}
|
||||
"""
|
||||
`;
|
||||
|
||||
// Use flat naming: bmad-custom-agent-agentname.toml
|
||||
const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.toml`;
|
||||
const launcherPath = path.join(commandsDir, fileName);
|
||||
|
||||
// Write the launcher file
|
||||
await fs.writeFile(launcherPath, tomlContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'qwen',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: fileName.replace('.toml', ''),
|
||||
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,187 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
|
||||
/**
|
||||
* Rovo Dev IDE setup handler
|
||||
*
|
||||
* Uses UnifiedInstaller for all artifact installation with flat file structure.
|
||||
* All BMAD artifacts are installed to .rovodev/workflows/ as flat files.
|
||||
*/
|
||||
class RovoDevSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('rovo-dev', 'Atlassian Rovo Dev', false);
|
||||
this.configDir = '.rovodev';
|
||||
this.workflowsDir = 'workflows';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
||||
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
// Use the unified installer - all artifacts go to workflows folder as flat files
|
||||
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||
const counts = await installer.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: workflowsDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.CLAUDE,
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
if (counts.workflows > 0) {
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflows installed`));
|
||||
}
|
||||
if (counts.tasks + counts.tools > 0) {
|
||||
console.log(chalk.dim(` - ${counts.tasks + counts.tools} tasks/tools installed (${counts.tasks} tasks, ${counts.tools} tools)`));
|
||||
}
|
||||
console.log(chalk.dim(` - ${counts.total} files written to ${path.relative(projectDir, workflowsDir)}`));
|
||||
console.log(chalk.yellow(`\n Note: All BMAD items are available in .rovodev/workflows/`));
|
||||
console.log(chalk.dim(` - Access items by typing @ in Rovo Dev to see available files`));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...counts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 files from workflows directory
|
||||
const workflowsDir = path.join(rovoDevDir, this.workflowsDir);
|
||||
if (await fs.pathExists(workflowsDir)) {
|
||||
const entries = await fs.readdir(workflowsDir);
|
||||
const bmadFiles = entries.filter((file) => file.startsWith('bmad') && file.endsWith('.md'));
|
||||
|
||||
for (const file of bmadFiles) {
|
||||
await fs.remove(path.join(workflowsDir, file));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove legacy subagents directory
|
||||
const subagentsDir = path.join(rovoDevDir, 'subagents');
|
||||
if (await fs.pathExists(subagentsDir)) {
|
||||
await fs.remove(subagentsDir);
|
||||
console.log(chalk.dim(` Removed legacy subagents directory`));
|
||||
}
|
||||
|
||||
// Remove legacy references directory
|
||||
const referencesDir = path.join(rovoDevDir, 'references');
|
||||
if (await fs.pathExists(referencesDir)) {
|
||||
await fs.remove(referencesDir);
|
||||
console.log(chalk.dim(` Removed legacy references directory`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 files 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
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Rovo Dev
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Installation result
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
const launcherContent = `---
|
||||
name: ${agentName}
|
||||
description: Custom BMAD agent: ${agentName}
|
||||
---
|
||||
|
||||
# ${agentName} Custom Agent
|
||||
|
||||
**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!
|
||||
|
||||
This is a launcher for the custom BMAD agent "${agentName}".
|
||||
|
||||
## Usage
|
||||
1. First run: \`${agentPath}\` to load the complete agent
|
||||
2. Then use this workflow as ${agentName}
|
||||
|
||||
The agent will follow the persona and instructions from the main agent file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by BMAD Method*`;
|
||||
|
||||
// Use flat naming: bmad-custom-agent-agentname.md
|
||||
const fileName = `bmad-custom-agent-${agentName.toLowerCase()}.md`;
|
||||
const launcherPath = path.join(workflowsDir, fileName);
|
||||
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
|
||||
return {
|
||||
ide: 'rovo-dev',
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: fileName.replace('.md', ''),
|
||||
type: 'custom-agent-launcher',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RovoDevSetup };
|
||||
@@ -32,8 +32,10 @@ class AgentCommandGenerator {
|
||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
||||
artifacts.push({
|
||||
type: 'agent-launcher',
|
||||
module: agent.module,
|
||||
name: agent.name,
|
||||
displayName: agent.displayName || agent.name,
|
||||
description: agent.description,
|
||||
module: agent.module,
|
||||
relativePath: path.join(agent.module, 'agents', agentPathInModule),
|
||||
content: launcherContent,
|
||||
sourcePath: agent.path,
|
||||
|
||||
@@ -44,9 +44,26 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
||||
|
||||
if (content.includes('localskip="true"')) continue;
|
||||
|
||||
// Extract description from YAML frontmatter if present
|
||||
let description = null;
|
||||
let agentName = file.replace('.md', '');
|
||||
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
||||
if (frontmatterMatch) {
|
||||
const descMatch = frontmatterMatch[1].match(/description:\s*"([^"]+)"/);
|
||||
if (descMatch) {
|
||||
description = descMatch[1];
|
||||
}
|
||||
const nameMatch = frontmatterMatch[1].match(/name:\s*"([^"]+)"/);
|
||||
if (nameMatch) {
|
||||
agentName = nameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
agents.push({
|
||||
path: filePath,
|
||||
name: file.replace('.md', ''),
|
||||
name: agentName,
|
||||
displayName: agentName,
|
||||
description: description,
|
||||
module: 'standalone', // Mark as standalone agent
|
||||
});
|
||||
}
|
||||
@@ -114,9 +131,26 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract description from YAML frontmatter if present
|
||||
let description = null;
|
||||
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
||||
if (frontmatterMatch) {
|
||||
const descMatch = frontmatterMatch[1].match(/description:\s*"([^"]+)"/);
|
||||
if (descMatch) {
|
||||
description = descMatch[1];
|
||||
}
|
||||
// Also extract name from frontmatter if available
|
||||
const nameMatch = frontmatterMatch[1].match(/name:\s*"([^"]+)"/);
|
||||
if (nameMatch) {
|
||||
entry.name = `${nameMatch[1]}.md`;
|
||||
}
|
||||
}
|
||||
|
||||
agents.push({
|
||||
path: fullPath,
|
||||
name: entry.name.replace('.md', ''),
|
||||
displayName: entry.name.replace('.md', ''),
|
||||
description: description,
|
||||
module: moduleName,
|
||||
relativePath: newRelativePath, // Keep the .md extension for the full path
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Provides utilities to convert hierarchical paths to flat naming conventions.
|
||||
* - Underscore format (bmad_module_name.md) - Windows-compatible universal format
|
||||
* - Suffix-based format (bmad-module-name.agent.md) - New universal standard
|
||||
*/
|
||||
|
||||
// Default file extension for backward compatibility
|
||||
@@ -12,6 +13,17 @@ const DEFAULT_FILE_EXTENSION = '.md';
|
||||
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
||||
const AGENT_SEGMENT = 'agents';
|
||||
|
||||
/**
|
||||
* Artifact type to suffix mapping
|
||||
* Used for new suffix-based naming convention
|
||||
*/
|
||||
const ARTIFACT_SUFFIXES = {
|
||||
agent: '.agent',
|
||||
workflow: '.workflow',
|
||||
task: '.task',
|
||||
tool: '.tool',
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert hierarchical path to flat underscore-separated name
|
||||
* Converts: 'bmm', 'agents', 'pm' → 'bmad_bmm_agent_pm.md'
|
||||
@@ -193,6 +205,7 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) {
|
||||
|
||||
// Use dash naming style
|
||||
const isAgent = type === AGENT_SEGMENT;
|
||||
// For core module, skip the module prefix
|
||||
if (module === 'core') {
|
||||
return isAgent ? `bmad-agent-${name}${fileExtension}` : `bmad-${name}${fileExtension}`;
|
||||
}
|
||||
@@ -201,6 +214,79 @@ function toDashPath(relativePath, fileExtension = DEFAULT_FILE_EXTENSION) {
|
||||
return isAgent ? `${prefix}${module}-agent-${name}${fileExtension}` : `${prefix}${module}-${name}${fileExtension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative path to suffix-based name (NEW UNIVERSAL STANDARD)
|
||||
* Converts: 'cis/agents/storymaster.md' → 'bmad-cis-storymaster.agent.md'
|
||||
* Converts: 'bmm/workflows/plan-project.md' → 'bmad-bmm-plan-project.workflow.md'
|
||||
* Converts: 'bmm/tasks/create-story.md' → 'bmad-bmm-create-story.task.md'
|
||||
* Converts: 'bmm/tools/file-ops.md' → 'bmad-bmm-file-ops.tool.md'
|
||||
* Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||
*
|
||||
* @param {string} relativePath - Path like 'cis/agents/storymaster.md'
|
||||
* @param {string} artifactType - Type of artifact: 'agent', 'workflow', 'task', 'tool'
|
||||
* @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml')
|
||||
* @returns {string} Suffix-based filename like 'bmad-cis-storymaster.agent.md'
|
||||
*/
|
||||
function toSuffixBasedName(relativePath, artifactType, fileExtension = DEFAULT_FILE_EXTENSION) {
|
||||
const extMatch = relativePath.match(/\.[^.]+$/);
|
||||
const originalExt = extMatch ? extMatch[0] : '';
|
||||
const withoutExt = relativePath.replace(originalExt, '');
|
||||
const parts = withoutExt.split(/[/\\]/);
|
||||
|
||||
const module = parts[0];
|
||||
const type = parts[1]; // agents, workflows, tasks, tools
|
||||
const name = parts.slice(2).join('-');
|
||||
|
||||
const suffix = ARTIFACT_SUFFIXES[artifactType] || '';
|
||||
|
||||
// For core module, skip the module prefix (use 'bmad-name.suffix.md')
|
||||
if (module === 'core') {
|
||||
return `bmad-${name}${suffix}.${fileExtension.replace('.', '')}`;
|
||||
}
|
||||
|
||||
// If module already starts with 'bmad-', don't add another prefix
|
||||
const prefix = module.startsWith('bmad-') ? '' : 'bmad-';
|
||||
return `${prefix}${module}-${name}${suffix}.${fileExtension.replace('.', '')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suffix for artifact type
|
||||
* @param {string} artifactType - Type of artifact: 'agent', 'workflow', 'task', 'tool'
|
||||
* @returns {string} Suffix like '.agent', '.workflow', etc.
|
||||
*/
|
||||
function getArtifactSuffix(artifactType) {
|
||||
return ARTIFACT_SUFFIXES[artifactType] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse artifact type from suffix-based filename
|
||||
* Parses: 'bmad-cis-storymaster.agent.md' → 'agent'
|
||||
* Parses: 'bmad-bmm-plan-project.workflow.md' → 'workflow'
|
||||
*
|
||||
* @param {string} filename - Suffix-based filename
|
||||
* @returns {string|null} Artifact type or null if not found
|
||||
*/
|
||||
function parseArtifactTypeFromFilename(filename) {
|
||||
for (const [type, suffix] of Object.entries(ARTIFACT_SUFFIXES)) {
|
||||
if (filename.includes(`${suffix}.`)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom agent suffix-based name
|
||||
* Creates: 'bmad-custom-fred-commit-poet.agent.md'
|
||||
*
|
||||
* @param {string} agentName - Custom agent name
|
||||
* @param {string} [fileExtension='.md'] - File extension including dot
|
||||
* @returns {string} Suffix-based filename like 'bmad-custom-fred-commit-poet.agent.md'
|
||||
*/
|
||||
function customAgentSuffixName(agentName, fileExtension = DEFAULT_FILE_EXTENSION) {
|
||||
return `bmad-custom-${agentName}.agent.${fileExtension.replace('.', '')}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_FILE_EXTENSION,
|
||||
toUnderscoreName,
|
||||
@@ -221,4 +307,10 @@ module.exports = {
|
||||
parseDashName,
|
||||
TYPE_SEGMENTS,
|
||||
AGENT_SEGMENT,
|
||||
// New suffix-based naming functions (UNIVERSAL STANDARD)
|
||||
ARTIFACT_SUFFIXES,
|
||||
toSuffixBasedName,
|
||||
getArtifactSuffix,
|
||||
parseArtifactTypeFromFilename,
|
||||
customAgentSuffixName,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const csv = require('csv-parse/sync');
|
||||
const chalk = require('chalk');
|
||||
const { toColonName, toColonPath, toDashPath } = require('./path-utils');
|
||||
const { toColonName, toColonPath, toDashPath, toSuffixBasedName } = require('./path-utils');
|
||||
|
||||
/**
|
||||
* Generates command files for standalone tasks and tools
|
||||
@@ -14,46 +14,6 @@ class TaskToolCommandGenerator {
|
||||
* Use generateColonTaskToolCommands() or generateDashTaskToolCommands() instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate command content for a task or tool
|
||||
* @param {Object} item - Task or tool item from manifest
|
||||
* @param {string} type - 'task' or 'tool'
|
||||
* @param {string} [format='yaml'] - Output format: 'yaml' or 'toml'
|
||||
*/
|
||||
generateCommandContent(item, type, format = 'yaml') {
|
||||
const description = item.description || `Execute ${item.displayName || item.name}`;
|
||||
|
||||
// Convert path to use {project-root} placeholder
|
||||
let itemPath = item.path;
|
||||
if (itemPath.startsWith('bmad/')) {
|
||||
itemPath = `{project-root}/${itemPath}`;
|
||||
}
|
||||
|
||||
const content = `# ${item.displayName || item.name}
|
||||
|
||||
LOAD and execute the ${type} at: ${itemPath}
|
||||
|
||||
Follow all instructions in the ${type} file exactly as written.
|
||||
`;
|
||||
|
||||
if (format === 'toml') {
|
||||
// Escape any triple quotes in content
|
||||
const escapedContent = content.replaceAll('"""', String.raw`\"\"\"`);
|
||||
return `description = "${description}"
|
||||
prompt = """
|
||||
${escapedContent}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
|
||||
// Default YAML format
|
||||
return `---
|
||||
description: '${description.replaceAll("'", "''")}'
|
||||
---
|
||||
|
||||
${content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load task manifest CSV
|
||||
*/
|
||||
@@ -257,6 +217,163 @@ ${content}`;
|
||||
|
||||
return writtenCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task and tool commands using suffix-based format (NEW UNIVERSAL STANDARD)
|
||||
* Creates flat files like: bmad-bmm-create-story.task.md
|
||||
*
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml')
|
||||
* @param {string} [templateContent] - Frontmatter template content (from platform-codes.yaml)
|
||||
* @param {string} [frontmatterTemplate] - Frontmatter template filename
|
||||
* @param {boolean} [skipExisting=false] - Skip if file already exists
|
||||
* @returns {Object} Generation results
|
||||
*/
|
||||
async generateSuffixBasedTaskToolCommands(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
baseCommandsDir,
|
||||
fileExtension = '.md',
|
||||
templateContent = null,
|
||||
frontmatterTemplate = 'common-yaml.md',
|
||||
skipExisting = false,
|
||||
) {
|
||||
const tasks = await this.loadTaskManifest(bmadDir);
|
||||
const tools = await this.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) : [];
|
||||
|
||||
let generatedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
// Generate command files for tasks
|
||||
for (const task of standaloneTasks) {
|
||||
const commandContent = this.generateCommandContent(task, 'task', templateContent, frontmatterTemplate);
|
||||
// Use suffix-based format: bmad-bmm-create-story.task.md
|
||||
const relativePath = `${task.module}/tasks/${task.name}.md`;
|
||||
const suffixName = toSuffixBasedName(relativePath, 'task', fileExtension);
|
||||
const commandPath = path.join(baseCommandsDir, suffixName);
|
||||
|
||||
// Skip if already exists
|
||||
if (skipExisting && (await fs.pathExists(commandPath))) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await fs.ensureDir(baseCommandsDir);
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
// Generate command files for tools
|
||||
for (const tool of standaloneTools) {
|
||||
const commandContent = this.generateCommandContent(tool, 'tool', templateContent, frontmatterTemplate);
|
||||
// Use suffix-based format: bmad-bmm-file-ops.tool.md
|
||||
const relativePath = `${tool.module}/tools/${tool.name}.md`;
|
||||
const suffixName = toSuffixBasedName(relativePath, 'tool', fileExtension);
|
||||
const commandPath = path.join(baseCommandsDir, suffixName);
|
||||
|
||||
// Skip if already exists
|
||||
if (skipExisting && (await fs.pathExists(commandPath))) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await fs.ensureDir(baseCommandsDir);
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
if (skippedCount > 0) {
|
||||
console.log(chalk.dim(` Skipped ${skippedCount} existing task/tool files`));
|
||||
}
|
||||
|
||||
return {
|
||||
generated: generatedCount,
|
||||
tasks: standaloneTasks.length,
|
||||
tools: standaloneTools.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate command content for a task or tool
|
||||
* @param {Object} item - Task or tool item from manifest
|
||||
* @param {string} type - 'task' or 'tool'
|
||||
* @param {string|Object|null} [templateOrFormat] - Template content or format string ('yaml'/'toml') for backward compat
|
||||
* @param {string} [frontmatterTemplate] - Template filename (for format detection)
|
||||
*/
|
||||
generateCommandContent(item, type, templateOrFormat = null, frontmatterTemplate = null) {
|
||||
const description = item.description || `Execute ${item.displayName || item.name}`;
|
||||
|
||||
// Convert path to use {project-root} placeholder
|
||||
let itemPath = item.path;
|
||||
if (itemPath.startsWith('bmad/')) {
|
||||
itemPath = `{project-root}/${itemPath}`;
|
||||
}
|
||||
|
||||
const content = `# ${item.displayName || item.name}
|
||||
|
||||
LOAD and execute the ${type} at: ${itemPath}
|
||||
|
||||
Follow all instructions in the ${type} file exactly as written.
|
||||
`;
|
||||
|
||||
// Handle old calling convention: (item, type, format) where format is 'yaml' or 'toml'
|
||||
if (typeof templateOrFormat === 'string' && (templateOrFormat === 'yaml' || templateOrFormat === 'toml')) {
|
||||
if (templateOrFormat === 'toml') {
|
||||
// TOML format
|
||||
const escapedContent = content.replaceAll('"""', String.raw`\"\"\"`);
|
||||
return `description = "${description}"
|
||||
prompt = """
|
||||
${escapedContent}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
// Default YAML format
|
||||
return `---
|
||||
description: '${description.replaceAll("'", "''")}'
|
||||
---
|
||||
|
||||
${content}`;
|
||||
}
|
||||
|
||||
// New calling convention with template content
|
||||
const templateContent = templateOrFormat;
|
||||
if (!templateContent || frontmatterTemplate === 'none' || (templateContent === null && frontmatterTemplate === null)) {
|
||||
// Default YAML
|
||||
return `---
|
||||
description: '${description.replaceAll("'", "''")}'
|
||||
---
|
||||
|
||||
${content}`;
|
||||
}
|
||||
|
||||
// Apply template variables
|
||||
const variables = {
|
||||
name: item.name,
|
||||
displayName: item.displayName || item.name,
|
||||
description,
|
||||
content,
|
||||
icon: '🤖',
|
||||
};
|
||||
|
||||
let result = templateContent;
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
result = result.replaceAll(`{{${key}}}`, value);
|
||||
}
|
||||
|
||||
// Handle TOML templates specially
|
||||
if (frontmatterTemplate && frontmatterTemplate.includes('toml')) {
|
||||
const escapedContent = content.replaceAll('"""', String.raw`\"\"\"`);
|
||||
result = result.replace(/prompt = """/, `prompt = """\n${escapedContent}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TaskToolCommandGenerator };
|
||||
|
||||
@@ -1,47 +1,27 @@
|
||||
/**
|
||||
* Unified BMAD Installer for all IDEs
|
||||
*
|
||||
* Replaces the fractured, duplicated setup logic across all IDE handlers.
|
||||
* All IDEs do the same thing:
|
||||
* 1. Collect agents, workflows, tasks, tools from the same sources
|
||||
* 2. Write them to a target directory
|
||||
* 3. Use a naming convention (flat-colon, flat-dash, or nested)
|
||||
*
|
||||
* The only differences between IDEs are:
|
||||
* - target directory (e.g., .claude/commands/, .cursor/rules/)
|
||||
* - naming style (underscore vs dash vs nested)
|
||||
* - template/frontmatter (some need YAML, some need custom frontmatter)
|
||||
* ALL IDE configuration comes from platform-codes.yaml
|
||||
* NO IDE-specific code in this file - just loads and applies templates
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./task-tool-command-generator');
|
||||
const { toColonPath, toDashPath } = require('./path-utils');
|
||||
const { toColonPath, toDashPath, toSuffixBasedName, getArtifactSuffix } = require('./path-utils');
|
||||
|
||||
/**
|
||||
* Naming styles
|
||||
* @deprecated Use 'suffix-based' for all new installations
|
||||
*/
|
||||
const NamingStyle = {
|
||||
FLAT_COLON: 'flat-colon', // bmad_bmm_agent_pm.md (Windows-compatible)
|
||||
FLAT_DASH: 'flat-dash', // bmad-bmm-agent-pm.md
|
||||
NESTED: 'nested', // bmad/bmm/agents/pm.md (OLD, deprecated)
|
||||
};
|
||||
|
||||
/**
|
||||
* Template types for different IDE frontmatter/formatting
|
||||
*/
|
||||
const TemplateType = {
|
||||
CLAUDE: 'claude', // YAML frontmatter with name/description
|
||||
CURSOR: 'cursor', // Same as Claude
|
||||
CODEX: 'codex', // No frontmatter, direct content
|
||||
CLINE: 'cline', // No frontmatter, direct content
|
||||
WINDSURF: 'windsurf', // YAML with auto_execution_mode
|
||||
AUGMENT: 'augment', // YAML frontmatter
|
||||
GEMINI: 'gemini', // TOML frontmatter with description/prompt
|
||||
QWEN: 'qwen', // TOML frontmatter with description/prompt (same as Gemini)
|
||||
COPILOT: 'copilot', // YAML with tools array for GitHub Copilot
|
||||
FLAT_COLON: 'flat-colon',
|
||||
FLAT_DASH: 'flat-dash',
|
||||
NESTED: 'nested',
|
||||
SUFFIX_BASED: 'suffix-based',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -49,18 +29,22 @@ const TemplateType = {
|
||||
* @typedef {Object} UnifiedInstallConfig
|
||||
* @property {string} targetDir - Full path to target directory
|
||||
* @property {NamingStyle} namingStyle - How to name files
|
||||
* @property {TemplateType} templateType - What template format to use
|
||||
* @property {string} [fileExtension='.md'] - File extension including dot (e.g., '.md', '.toml')
|
||||
* @property {string} [frontmatterTemplate] - Frontmatter template filename (from platform-codes.yaml)
|
||||
* @property {string} [fileExtension='.md'] - File extension including dot
|
||||
* @property {boolean} includeNestedStructure - For NESTED style, create subdirectories
|
||||
* @property {Function} [customTemplateFn] - Optional custom template function
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unified BMAD Installer
|
||||
*
|
||||
* Driven entirely by platform-codes.yaml configuration
|
||||
* Frontmatter templates are loaded from templates/frontmatter/ directory
|
||||
*/
|
||||
class UnifiedInstaller {
|
||||
constructor(bmadFolderName = 'bmad') {
|
||||
this.bmadFolderName = bmadFolderName;
|
||||
this.templateDir = path.join(__dirname, '../templates/frontmatter');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,15 +59,19 @@ class UnifiedInstaller {
|
||||
async install(projectDir, bmadDir, config, selectedModules = []) {
|
||||
const {
|
||||
targetDir,
|
||||
namingStyle = NamingStyle.FLAT_COLON,
|
||||
templateType = TemplateType.CLAUDE,
|
||||
namingStyle = NamingStyle.SUFFIX_BASED,
|
||||
frontmatterTemplate = 'common-yaml.md',
|
||||
fileExtension = '.md',
|
||||
includeNestedStructure = false,
|
||||
customTemplateFn = null,
|
||||
skipExisting = false,
|
||||
artifactTypes = null,
|
||||
} = config;
|
||||
|
||||
// Clean up any existing BMAD files in target directory
|
||||
await this.cleanupBmadFiles(targetDir, fileExtension);
|
||||
// Clean up any existing BMAD files in target directory (unless skipExisting)
|
||||
if (!skipExisting) {
|
||||
await this.cleanupBmadFiles(targetDir, fileExtension);
|
||||
}
|
||||
|
||||
// Ensure target directory exists
|
||||
await fs.ensureDir(targetDir);
|
||||
@@ -97,49 +85,83 @@ class UnifiedInstaller {
|
||||
total: 0,
|
||||
};
|
||||
|
||||
// Check if we should install agents
|
||||
const installAgents = !artifactTypes || artifactTypes.includes('agents');
|
||||
const installWorkflows = !artifactTypes || artifactTypes.includes('workflows');
|
||||
const installTasks = !artifactTypes || artifactTypes.includes('tasks');
|
||||
const installTools = !artifactTypes || artifactTypes.includes('tools');
|
||||
|
||||
// Load frontmatter template once (if not 'none')
|
||||
let templateContent = null;
|
||||
if (frontmatterTemplate && frontmatterTemplate !== 'none') {
|
||||
templateContent = await this.loadFrontmatterTemplate(frontmatterTemplate);
|
||||
}
|
||||
|
||||
// 1. Install Agents
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
||||
counts.agents = await this.writeArtifacts(
|
||||
agentArtifacts,
|
||||
targetDir,
|
||||
namingStyle,
|
||||
templateType,
|
||||
fileExtension,
|
||||
customTemplateFn,
|
||||
'agent',
|
||||
);
|
||||
if (installAgents) {
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
||||
counts.agents = await this.writeArtifacts(
|
||||
agentArtifacts,
|
||||
targetDir,
|
||||
namingStyle,
|
||||
templateContent,
|
||||
frontmatterTemplate,
|
||||
fileExtension,
|
||||
customTemplateFn,
|
||||
'agent',
|
||||
skipExisting,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Install Workflows (filter out README artifacts)
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
const workflowArtifactsFiltered = workflowArtifacts.filter((a) => {
|
||||
const name = path.basename(a.relativePath || '');
|
||||
return name.toLowerCase() !== 'readme.md' && !name.toLowerCase().startsWith('readme-');
|
||||
});
|
||||
counts.workflows = await this.writeArtifacts(
|
||||
workflowArtifactsFiltered,
|
||||
targetDir,
|
||||
namingStyle,
|
||||
templateType,
|
||||
fileExtension,
|
||||
customTemplateFn,
|
||||
'workflow',
|
||||
);
|
||||
if (installWorkflows) {
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
const workflowArtifactsFiltered = workflowArtifacts.filter((a) => {
|
||||
const name = path.basename(a.relativePath || '');
|
||||
return name.toLowerCase() !== 'readme.md' && !name.toLowerCase().startsWith('readme-');
|
||||
});
|
||||
counts.workflows = await this.writeArtifacts(
|
||||
workflowArtifactsFiltered,
|
||||
targetDir,
|
||||
namingStyle,
|
||||
templateContent,
|
||||
frontmatterTemplate,
|
||||
fileExtension,
|
||||
customTemplateFn,
|
||||
'workflow',
|
||||
skipExisting,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Install Tasks and Tools from manifest CSV (standalone items)
|
||||
const ttGen = new TaskToolCommandGenerator();
|
||||
console.log(`[DEBUG] About to call TaskToolCommandGenerator, namingStyle=${namingStyle}, targetDir=${targetDir}`);
|
||||
// 3. Install Tasks and Tools from manifest CSV
|
||||
if (installTasks || installTools) {
|
||||
const ttGen = new TaskToolCommandGenerator();
|
||||
|
||||
// For now, ALWAYS use flat structure - nested is deprecated
|
||||
// TODO: Remove nested branch entirely after verification
|
||||
const taskToolResult =
|
||||
namingStyle === NamingStyle.FLAT_DASH
|
||||
? await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension)
|
||||
: await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension);
|
||||
|
||||
counts.tasks = taskToolResult.tasks || 0;
|
||||
counts.tools = taskToolResult.tools || 0;
|
||||
// Use suffix-based naming if specified
|
||||
if (namingStyle === NamingStyle.SUFFIX_BASED) {
|
||||
const taskToolResult = await ttGen.generateSuffixBasedTaskToolCommands(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
targetDir,
|
||||
fileExtension,
|
||||
templateContent,
|
||||
frontmatterTemplate,
|
||||
skipExisting,
|
||||
);
|
||||
counts.tasks = taskToolResult.tasks || 0;
|
||||
counts.tools = taskToolResult.tools || 0;
|
||||
} else if (namingStyle === NamingStyle.FLAT_DASH) {
|
||||
const taskToolResult = await ttGen.generateDashTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension);
|
||||
counts.tasks = taskToolResult.tasks || 0;
|
||||
counts.tools = taskToolResult.tools || 0;
|
||||
} else {
|
||||
const taskToolResult = await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir, fileExtension);
|
||||
counts.tasks = taskToolResult.tasks || 0;
|
||||
counts.tools = taskToolResult.tools || 0;
|
||||
}
|
||||
}
|
||||
|
||||
counts.total = counts.agents + counts.workflows + counts.tasks + counts.tools;
|
||||
|
||||
@@ -147,206 +169,89 @@ class UnifiedInstaller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any existing BMAD files in target directory
|
||||
* @param {string} targetDir - Target directory to clean
|
||||
* @param {string} [fileExtension='.md'] - File extension to match
|
||||
* Load frontmatter template from file
|
||||
* @param {string} templateFile - Template filename
|
||||
* @returns {Promise<string|null>} Template content or null if not found
|
||||
*/
|
||||
async cleanupBmadFiles(targetDir, fileExtension = '.md') {
|
||||
if (!(await fs.pathExists(targetDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recursively find and remove any bmad* files or directories
|
||||
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
// Only remove files with the matching extension
|
||||
if (entry.name.startsWith('bmad') && entry.name.endsWith(fileExtension)) {
|
||||
const entryPath = path.join(targetDir, entry.name);
|
||||
await fs.remove(entryPath);
|
||||
}
|
||||
async loadFrontmatterTemplate(templateFile) {
|
||||
const templatePath = path.join(this.templateDir, templateFile);
|
||||
try {
|
||||
return await fs.readFile(templatePath, 'utf8');
|
||||
} catch {
|
||||
console.warn(chalk.yellow(`Warning: Could not load template ${templateFile}, using default`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write artifacts with specified naming style and template
|
||||
* @param {Array} artifacts - Artifacts to write
|
||||
* @param {string} targetDir - Target directory
|
||||
* @param {NamingStyle} namingStyle - Naming style to use
|
||||
* @param {TemplateType} templateType - Template type to use
|
||||
* @param {string} fileExtension - File extension including dot
|
||||
* @param {Function} customTemplateFn - Optional custom template function
|
||||
* @param {string} artifactType - Type of artifact for logging
|
||||
* @returns {Promise<number>} Number of artifacts written
|
||||
* Apply frontmatter template to content
|
||||
* @param {Object} artifact - Artifact with metadata
|
||||
* @param {string} content - Original content
|
||||
* @param {string} templateContent - Template content
|
||||
* @param {string} templateFile - Template filename (for special handling)
|
||||
* @returns {string} Content with frontmatter applied
|
||||
*/
|
||||
async writeArtifacts(artifacts, targetDir, namingStyle, templateType, fileExtension, customTemplateFn, artifactType) {
|
||||
console.log(
|
||||
`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}, fileExtension=${fileExtension}`,
|
||||
);
|
||||
let written = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
// Determine target path based on naming style
|
||||
let targetPath;
|
||||
let content = artifact.content;
|
||||
console.log(`[DEBUG] writeArtifacts processing: relativePath=${artifact.relativePath}, name=${artifact.name}`);
|
||||
|
||||
if (namingStyle === NamingStyle.FLAT_COLON) {
|
||||
const flatName = toColonPath(artifact.relativePath, fileExtension);
|
||||
targetPath = path.join(targetDir, flatName);
|
||||
} else if (namingStyle === NamingStyle.FLAT_DASH) {
|
||||
const flatName = toDashPath(artifact.relativePath, fileExtension);
|
||||
targetPath = path.join(targetDir, flatName);
|
||||
} else {
|
||||
// Fallback: treat as flat even if NESTED specified
|
||||
const flatName = toColonPath(artifact.relativePath, fileExtension);
|
||||
targetPath = path.join(targetDir, flatName);
|
||||
}
|
||||
|
||||
// Apply template transformations if needed
|
||||
if (customTemplateFn) {
|
||||
content = customTemplateFn(artifact, content, templateType);
|
||||
} else {
|
||||
content = this.applyTemplate(artifact, content, templateType);
|
||||
}
|
||||
|
||||
// For flat files, just ensure targetDir exists (no nested dirs needed)
|
||||
await fs.ensureDir(targetDir);
|
||||
await fs.writeFile(targetPath, content, 'utf8');
|
||||
written++;
|
||||
applyFrontmatterTemplate(artifact, content, templateContent, templateFile) {
|
||||
if (!templateContent) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply template/frontmatter based on type
|
||||
*/
|
||||
applyTemplate(artifact, content, templateType) {
|
||||
switch (templateType) {
|
||||
case TemplateType.CLAUDE:
|
||||
case TemplateType.CURSOR: {
|
||||
// Already has YAML frontmatter from generator
|
||||
return content;
|
||||
}
|
||||
|
||||
case TemplateType.CODEX:
|
||||
case TemplateType.CLINE: {
|
||||
// No frontmatter needed, content as-is
|
||||
return content;
|
||||
}
|
||||
|
||||
case TemplateType.WINDSURF: {
|
||||
// Add Windsurf-specific frontmatter
|
||||
return this.addWindsurfFrontmatter(artifact, content);
|
||||
}
|
||||
|
||||
case TemplateType.AUGMENT: {
|
||||
// Add Augment frontmatter
|
||||
return this.addAugmentFrontmatter(artifact, content);
|
||||
}
|
||||
|
||||
case TemplateType.GEMINI: {
|
||||
// Add Gemini TOML frontmatter
|
||||
return this.addGeminiFrontmatter(artifact, content);
|
||||
}
|
||||
|
||||
case TemplateType.COPILOT: {
|
||||
// Add Copilot frontmatter with tools array
|
||||
return this.addCopilotFrontmatter(artifact, content);
|
||||
}
|
||||
|
||||
case TemplateType.QWEN: {
|
||||
// Add Qwen TOML frontmatter (same as Gemini)
|
||||
return this.addGeminiFrontmatter(artifact, content);
|
||||
}
|
||||
|
||||
default: {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Windsurf frontmatter with auto_execution_mode
|
||||
*/
|
||||
addWindsurfFrontmatter(artifact, content) {
|
||||
// Remove existing frontmatter if present
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
||||
|
||||
// Determine auto_execution_mode based on type
|
||||
let autoExecMode = '1'; // default for workflows
|
||||
if (artifact.type === 'agent') {
|
||||
autoExecMode = '3';
|
||||
} else if (artifact.type === 'task' || artifact.type === 'tool') {
|
||||
autoExecMode = '2';
|
||||
}
|
||||
|
||||
const name = artifact.name || artifact.displayName || 'workflow';
|
||||
const frontmatter = `---
|
||||
description: ${name}
|
||||
auto_execution_mode: ${autoExecMode}
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
return frontmatter + contentWithoutFrontmatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Augment frontmatter
|
||||
*/
|
||||
addAugmentFrontmatter(artifact, content) {
|
||||
// Augment uses simple YAML frontmatter
|
||||
const name = artifact.name || artifact.displayName || 'workflow';
|
||||
const frontmatter = `---
|
||||
description: ${name}
|
||||
---
|
||||
|
||||
`;
|
||||
// Only add if not already present
|
||||
if (!content.startsWith('---')) {
|
||||
return frontmatter + content;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Gemini TOML frontmatter
|
||||
* Converts content to TOML format with description and prompt fields
|
||||
*/
|
||||
addGeminiFrontmatter(artifact, content) {
|
||||
// Remove existing YAML frontmatter if present
|
||||
// Extract existing frontmatter if present
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '').trim();
|
||||
|
||||
// Extract description from artifact or content
|
||||
let description = artifact.name || artifact.displayName || 'BMAD Command';
|
||||
if (artifact.module) {
|
||||
description = `BMAD ${artifact.module.toUpperCase()} ${artifact.type || 'Command'}: ${description}`;
|
||||
// Get artifact metadata for template substitution
|
||||
const name = artifact.name || artifact.displayName || 'workflow';
|
||||
const title = this.formatTitle(name);
|
||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
||||
const icon = iconMatch ? iconMatch[1] : '🤖';
|
||||
|
||||
// Use artifact's description if available, otherwise generate fallback
|
||||
const description = artifact.description || `Activates the ${name} ${artifact.type || 'workflow'}.`;
|
||||
|
||||
// Template variables
|
||||
const variables = {
|
||||
name,
|
||||
title,
|
||||
displayName: name,
|
||||
description,
|
||||
icon,
|
||||
content: contentWithoutFrontmatter,
|
||||
|
||||
// Special variables for certain templates
|
||||
autoExecMode: this.getAutoExecMode(artifact),
|
||||
tools: JSON.stringify(this.getCopilotTools()),
|
||||
};
|
||||
|
||||
// Apply template substitutions
|
||||
let result = templateContent;
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
result = result.replaceAll(`{{${key}}}`, value);
|
||||
}
|
||||
|
||||
// Escape any triple quotes in content
|
||||
const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`);
|
||||
// Append content after frontmatter (for TOML templates with prompt field)
|
||||
if (templateFile.includes('toml') && !result.includes('{{content}}')) {
|
||||
const escapedContent = contentWithoutFrontmatter.replaceAll('"""', String.raw`\"\"\"`);
|
||||
result = result.replace(/prompt = """/, `prompt = """\n${escapedContent}`);
|
||||
}
|
||||
|
||||
return `description = "${description}"
|
||||
prompt = """
|
||||
${escapedContent}
|
||||
"""
|
||||
`;
|
||||
return result.trim() + '\n\n' + contentWithoutFrontmatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add GitHub Copilot frontmatter with tools array
|
||||
* Get auto_execution_mode for Windsurf based on artifact type
|
||||
*/
|
||||
addCopilotFrontmatter(artifact, content) {
|
||||
// Remove existing frontmatter if present
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
||||
getAutoExecMode(artifact) {
|
||||
if (artifact.type === 'agent') return '3';
|
||||
if (artifact.type === 'task' || artifact.type === 'tool') return '2';
|
||||
return '1'; // default for workflows
|
||||
}
|
||||
|
||||
// GitHub Copilot tools array (as specified)
|
||||
const tools = [
|
||||
/**
|
||||
* Get GitHub Copilot tools array
|
||||
*/
|
||||
getCopilotTools() {
|
||||
return [
|
||||
'changes',
|
||||
'edit',
|
||||
'fetch',
|
||||
@@ -361,75 +266,110 @@ ${escapedContent}
|
||||
'todos',
|
||||
'usages',
|
||||
];
|
||||
|
||||
const name = artifact.name || artifact.displayName || 'prompt';
|
||||
const description = `Activates the ${name} ${artifact.type || 'workflow'}.`;
|
||||
|
||||
const frontmatter = `---
|
||||
description: "${description}"
|
||||
tools: ${JSON.stringify(tools)}
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
return frontmatter + contentWithoutFrontmatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks from manifest CSV
|
||||
* Clean up any existing BMAD files in target directory
|
||||
*/
|
||||
async getTasksFromManifest(bmadDir) {
|
||||
const csv = require('csv-parse/sync');
|
||||
const manifestPath = path.join(bmadDir, '_config', 'task-manifest.csv');
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return [];
|
||||
async cleanupBmadFiles(targetDir, fileExtension = '.md') {
|
||||
if (!(await fs.pathExists(targetDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||
const tasks = csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
||||
|
||||
// Filter for standalone only
|
||||
return tasks
|
||||
.filter((t) => t.standalone === 'true' || t.standalone === true)
|
||||
.map((t) => ({
|
||||
...t,
|
||||
content: null, // Will be read from path when writing
|
||||
}));
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('bmad') && entry.name.endsWith(fileExtension)) {
|
||||
const entryPath = path.join(targetDir, entry.name);
|
||||
await fs.remove(entryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tools from manifest CSV
|
||||
* Write artifacts with specified naming style and template
|
||||
*/
|
||||
async getToolsFromManifest(bmadDir) {
|
||||
const csv = require('csv-parse/sync');
|
||||
const manifestPath = path.join(bmadDir, '_config', 'tool-manifest.csv');
|
||||
async writeArtifacts(
|
||||
artifacts,
|
||||
targetDir,
|
||||
namingStyle,
|
||||
templateContent,
|
||||
templateFile,
|
||||
fileExtension,
|
||||
customTemplateFn,
|
||||
artifactType,
|
||||
skipExisting = false,
|
||||
) {
|
||||
let written = 0;
|
||||
let skipped = 0;
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return [];
|
||||
for (const artifact of artifacts) {
|
||||
// Determine target path based on naming style
|
||||
let targetPath;
|
||||
let content = artifact.content;
|
||||
|
||||
switch (namingStyle) {
|
||||
case NamingStyle.SUFFIX_BASED: {
|
||||
const suffixName = toSuffixBasedName(artifact.relativePath, artifactType, fileExtension);
|
||||
targetPath = path.join(targetDir, suffixName);
|
||||
|
||||
break;
|
||||
}
|
||||
case NamingStyle.FLAT_COLON: {
|
||||
const flatName = toColonPath(artifact.relativePath, fileExtension);
|
||||
targetPath = path.join(targetDir, flatName);
|
||||
|
||||
break;
|
||||
}
|
||||
case NamingStyle.FLAT_DASH: {
|
||||
const flatName = toDashPath(artifact.relativePath, fileExtension);
|
||||
targetPath = path.join(targetDir, flatName);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const flatName = toColonPath(artifact.relativePath, fileExtension);
|
||||
targetPath = path.join(targetDir, flatName);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if file already exists
|
||||
if (skipExisting && (await fs.pathExists(targetPath))) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply template transformations
|
||||
if (customTemplateFn) {
|
||||
content = customTemplateFn(artifact, content, templateFile);
|
||||
} else if (templateFile !== 'none') {
|
||||
content = this.applyFrontmatterTemplate(artifact, content, templateContent, templateFile);
|
||||
}
|
||||
|
||||
await fs.ensureDir(targetDir);
|
||||
await fs.writeFile(targetPath, content, 'utf8');
|
||||
written++;
|
||||
}
|
||||
|
||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||
const tools = csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
if (skipped > 0) {
|
||||
console.log(chalk.dim(` Skipped ${skipped} existing files`));
|
||||
}
|
||||
|
||||
// Filter for standalone only
|
||||
return tools
|
||||
.filter((t) => t.standalone === 'true' || t.standalone === true)
|
||||
.map((t) => ({
|
||||
...t,
|
||||
content: null, // Will be read from path when writing
|
||||
}));
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format name as title
|
||||
*/
|
||||
formatTitle(name) {
|
||||
return name
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UnifiedInstaller,
|
||||
NamingStyle,
|
||||
TemplateType,
|
||||
};
|
||||
|
||||
@@ -35,6 +35,9 @@ class WorkflowCommandGenerator {
|
||||
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
||||
artifacts.push({
|
||||
type: 'workflow-command',
|
||||
name: workflow.name,
|
||||
displayName: workflow.displayName || workflow.name,
|
||||
description: workflow.description,
|
||||
module: workflow.module,
|
||||
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
||||
content: commandContent,
|
||||
|
||||
@@ -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 @_bmad/{{relativePath}}
|
||||
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,8 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
|
||||
{{activationHeader}}
|
||||
|
||||
Run @_bmad/{{relativePath}} to load the full agent.
|
||||
@@ -0,0 +1,4 @@
|
||||
description = "{{description}}"
|
||||
prompt = """
|
||||
{{content}}
|
||||
"""
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: '{{description}}'
|
||||
---
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: "{{description}}"
|
||||
tools: {{tools}}
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
description: "{{description}}"
|
||||
tools: {{tools}}
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: 'BMAD {{name}} agent'
|
||||
mode: 'primary'
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
name: '{{name}}'
|
||||
description: 'BMAD {{name}} command'
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
name: '{{icon}} {{title}}'
|
||||
description: 'Use for {{title}} tasks'
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
description: "{{name}}"
|
||||
always: true
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
description: {{name}}
|
||||
auto_execution_mode: {{autoExecMode}}
|
||||
---
|
||||
@@ -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,244 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
/**
|
||||
* Windsurf IDE setup handler
|
||||
*
|
||||
* Uses UnifiedInstaller for consistent artifact collection and writing.
|
||||
* Windsurf-specific configuration:
|
||||
* - Flat file naming (FLAT_DASH): bmad-bmm-agent-pm.md
|
||||
* - Windsurf frontmatter with auto_execution_mode
|
||||
*/
|
||||
class WindsurfSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('windsurf', 'Windsurf', true); // preferred IDE
|
||||
this.configDir = '.windsurf';
|
||||
this.workflowsDir = 'workflows';
|
||||
this.unifiedInstaller = new UnifiedInstaller(this.bmadFolderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 directory
|
||||
const windsurfDir = path.join(projectDir, this.configDir);
|
||||
const workflowsDir = path.join(windsurfDir, this.workflowsDir);
|
||||
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
// Clean up any existing BMAD workflows before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Use UnifiedInstaller with Windsurf-specific configuration
|
||||
const counts = await this.unifiedInstaller.install(
|
||||
projectDir,
|
||||
bmadDir,
|
||||
{
|
||||
targetDir: workflowsDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.WINDSURF,
|
||||
customTemplateFn: this.windsurfTemplate.bind(this),
|
||||
},
|
||||
options.selectedModules || [],
|
||||
);
|
||||
|
||||
// Post-process tasks and tools to add Windsurf auto_execution_mode
|
||||
// UnifiedInstaller handles agents/workflows correctly, but tasks/tools
|
||||
// need special handling for proper Windsurf frontmatter
|
||||
await this.addWindsurfTaskToolFrontmatter(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.tools} tools installed`));
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflows installed`));
|
||||
console.log(chalk.dim(` - Total: ${counts.total} items`));
|
||||
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,
|
||||
...counts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Windsurf-specific template function
|
||||
* Adds proper Windsurf frontmatter with auto_execution_mode
|
||||
*/
|
||||
windsurfTemplate(artifact, content, templateType) {
|
||||
// Strip existing frontmatter
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
||||
|
||||
// Determine auto_execution_mode based on type
|
||||
let autoExecMode = '1'; // default for workflows
|
||||
let description = artifact.name || artifact.displayName || 'workflow';
|
||||
|
||||
if (artifact.type === 'agent') {
|
||||
autoExecMode = '3';
|
||||
description = artifact.name || 'agent';
|
||||
} else if (artifact.type === 'workflow') {
|
||||
autoExecMode = '1';
|
||||
description = artifact.name || 'workflow';
|
||||
}
|
||||
|
||||
return `---
|
||||
description: ${description}
|
||||
auto_execution_mode: ${autoExecMode}
|
||||
---
|
||||
|
||||
${contentWithoutFrontmatter}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Windsurf auto_execution_mode to task and tool files
|
||||
* These are generated by TaskToolCommandGenerator with basic YAML
|
||||
* but need the Windsurf-specific auto_execution_mode field
|
||||
*/
|
||||
async addWindsurfTaskToolFrontmatter(workflowsDir) {
|
||||
if (!(await fs.pathExists(workflowsDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
|
||||
let updatedCount = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.name.startsWith('bmad-') || !entry.name.endsWith('.md')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(workflowsDir, entry.name);
|
||||
let content = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
// Check if this is a task or tool file
|
||||
// They have pattern: bmad-module-task-name.md or bmad-module-tool-name.md
|
||||
const parts = entry.name.replace('bmad-', '').replace('.md', '').split('-');
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const type = parts.at(-2); // second to last part should be 'task' or 'tool'
|
||||
|
||||
if (type === 'task' || type === 'tool') {
|
||||
// Check if auto_execution_mode is already present
|
||||
if (content.includes('auto_execution_mode')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract existing description if present
|
||||
const descMatch = content.match(/description: '(.+?)'/);
|
||||
const description = descMatch ? descMatch[1] : entry.name.replace('.md', '');
|
||||
|
||||
// Strip existing frontmatter and add Windsurf-specific frontmatter
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
||||
|
||||
content = `---
|
||||
description: '${description}'
|
||||
auto_execution_mode: 2
|
||||
---
|
||||
|
||||
${contentWithoutFrontmatter}`;
|
||||
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
console.log(chalk.dim(` Updated ${updatedCount} task/tool files with Windsurf frontmatter`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Windsurf configuration - remove only BMAD files
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
if (await fs.pathExists(workflowsDir)) {
|
||||
// Remove all bmad* files from workflows directory
|
||||
const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
|
||||
let removedCount = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('bmad')) {
|
||||
const entryPath = path.join(workflowsDir, entry.name);
|
||||
await fs.remove(entryPath);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removedCount} existing BMAD workflow files`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null; // IDE not configured for this project
|
||||
}
|
||||
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
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 - flat naming
|
||||
const workflowContent = `---
|
||||
description: ${metadata.title || agentName}
|
||||
auto_execution_mode: 3
|
||||
---
|
||||
|
||||
${launcherContent}`;
|
||||
|
||||
// Use flat naming: bmad-custom-agent-agentname.md
|
||||
const flatName = `bmad-custom-agent-${agentName}.md`;
|
||||
const launcherPath = path.join(workflowsDir, flatName);
|
||||
await fs.writeFile(launcherPath, workflowContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: flatName.replace('.md', ''),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { WindsurfSetup };
|
||||
@@ -9,7 +9,7 @@ const { getProjectRoot } = require('./project-root');
|
||||
*/
|
||||
class PlatformCodes {
|
||||
constructor() {
|
||||
this.configPath = path.join(getProjectRoot(), 'tools', 'platform-codes.yaml');
|
||||
this.configPath = path.join(getProjectRoot(), 'tools/cli/installers/lib/ide/platform-codes.yaml');
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
|
||||
@@ -363,8 +363,8 @@ class UI {
|
||||
const { IdeManager } = require('../installers/lib/ide/manager');
|
||||
const ideManager = new IdeManager();
|
||||
|
||||
const preferredIdes = ideManager.getPreferredIdes();
|
||||
const otherIdes = ideManager.getOtherIdes();
|
||||
const preferredIdes = await ideManager.getPreferredIdes();
|
||||
const otherIdes = await ideManager.getOtherIdes();
|
||||
|
||||
// Build grouped options object for groupMultiselect
|
||||
const groupedOptions = {};
|
||||
|
||||
Reference in New Issue
Block a user