mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
claude cline codex installers use central function
This commit is contained in:
@@ -150,9 +150,10 @@ class AntigravitySetup extends BaseIdeSetup {
|
|||||||
// Write workflow-command artifacts with FLATTENED naming using shared utility
|
// Write workflow-command artifacts with FLATTENED naming using shared utility
|
||||||
const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts);
|
const workflowCommandCount = await workflowGen.writeDashArtifacts(bmadWorkflowsDir, workflowArtifacts);
|
||||||
|
|
||||||
// Generate task and tool commands from manifests (if they exist)
|
// Generate task and tool commands using FLAT naming (not nested!)
|
||||||
|
// Use the new generateDashTaskToolCommands method with explicit target directory
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
const taskToolGen = new TaskToolCommandGenerator();
|
||||||
const taskToolResult = await taskToolGen.generateTaskToolCommands(projectDir, bmadDir);
|
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, bmadWorkflowsDir);
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ const fs = require('fs-extra');
|
|||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const {
|
const {
|
||||||
loadModuleInjectionConfig,
|
loadModuleInjectionConfig,
|
||||||
shouldApplyInjection,
|
shouldApplyInjection,
|
||||||
@@ -18,10 +16,14 @@ const prompts = require('../../../lib/prompts');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Code IDE setup handler
|
* 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 {
|
class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('claude-code', 'Claude Code', true); // preferred IDE
|
super('claude-code', 'Claude Code', true);
|
||||||
this.configDir = '.claude';
|
this.configDir = '.claude';
|
||||||
this.commandsDir = 'commands';
|
this.commandsDir = 'commands';
|
||||||
this.agentsDir = 'agents';
|
this.agentsDir = 'agents';
|
||||||
@@ -29,7 +31,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt for subagent installation location
|
* Prompt for subagent installation location
|
||||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
|
||||||
*/
|
*/
|
||||||
async promptInstallLocation() {
|
async promptInstallLocation() {
|
||||||
return prompts.select({
|
return prompts.select({
|
||||||
@@ -42,57 +43,20 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Collect configuration choices before installation
|
|
||||||
// * @param {Object} options - Configuration options
|
|
||||||
// * @returns {Object} Collected configuration
|
|
||||||
// */
|
|
||||||
// async collectConfiguration(options = {}) {
|
|
||||||
// const config = {
|
|
||||||
// subagentChoices: null,
|
|
||||||
// installLocation: null,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const sourceModulesPath = getSourcePath('modules');
|
|
||||||
// const modules = options.selectedModules || [];
|
|
||||||
|
|
||||||
// for (const moduleName of modules) {
|
|
||||||
// // Check for Claude Code sub-module injection config in SOURCE directory
|
|
||||||
// const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml');
|
|
||||||
|
|
||||||
// if (await this.exists(injectionConfigPath)) {
|
|
||||||
// const yaml = require('yaml');
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Load injection configuration
|
|
||||||
// const configContent = await fs.readFile(injectionConfigPath, 'utf8');
|
|
||||||
// const injectionConfig = yaml.parse(configContent);
|
|
||||||
|
|
||||||
// // Ask about subagents if they exist and we haven't asked yet
|
|
||||||
// if (injectionConfig.subagents && !config.subagentChoices) {
|
|
||||||
// config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
|
||||||
|
|
||||||
// if (config.subagentChoices.install !== 'none') {
|
|
||||||
// config.installLocation = await this.promptInstallLocation();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return config;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup old BMAD installation before reinstalling
|
* Cleanup old BMAD installation before reinstalling
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
*/
|
*/
|
||||||
async cleanup(projectDir) {
|
async cleanup(projectDir) {
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||||
|
|
||||||
// Remove any bmad* files from the commands directory (cleans up old bmad: and bmad- formats)
|
// 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)) {
|
if (await fs.pathExists(commandsDir)) {
|
||||||
const entries = await fs.readdir(commandsDir);
|
const entries = await fs.readdir(commandsDir);
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
@@ -102,72 +66,41 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
removedCount++;
|
removedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Also remove legacy bmad folder if it exists
|
|
||||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
|
||||||
if (await fs.pathExists(bmadFolder)) {
|
|
||||||
await fs.remove(bmadFolder);
|
|
||||||
console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up legacy folder structure (module/type/name.md) if it exists
|
|
||||||
* This can be called after migration to remove old nested directories
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
*/
|
|
||||||
async cleanupLegacyFolders(projectDir) {
|
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(commandsDir))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove legacy bmad folder if it exists
|
|
||||||
const bmadFolder = path.join(commandsDir, 'bmad');
|
|
||||||
if (await fs.pathExists(bmadFolder)) {
|
|
||||||
await fs.remove(bmadFolder);
|
|
||||||
console.log(chalk.dim(` Removed legacy bmad folder from ${this.name}`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup Claude Code IDE configuration
|
* Setup Claude Code IDE configuration
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
*/
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
async setup(projectDir, bmadDir, options = {}) {
|
||||||
// Store project directory for use in processContent
|
console.log(`[DEBUG CLAUDE-CODE] setup called! projectDir=${projectDir}`);
|
||||||
this.projectDir = projectDir;
|
this.projectDir = projectDir;
|
||||||
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
await this.cleanup(projectDir);
|
||||||
|
|
||||||
// Create .claude/commands directory structure
|
|
||||||
const claudeDir = path.join(projectDir, this.configDir);
|
const claudeDir = path.join(projectDir, this.configDir);
|
||||||
const commandsDir = path.join(claudeDir, this.commandsDir);
|
const commandsDir = path.join(claudeDir, this.commandsDir);
|
||||||
await this.ensureDir(commandsDir);
|
await this.ensureDir(commandsDir);
|
||||||
|
|
||||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
// Use the unified installer for standard artifacts
|
||||||
// Creates: .claude/commands/bmad_bmm_pm.md
|
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||||
|
console.log(`[DEBUG CLAUDE-CODE] About to call installer.install, targetDir=${commandsDir}`);
|
||||||
// Generate agent launchers using AgentCommandGenerator
|
const counts = await installer.install(
|
||||||
// This creates small launcher files that reference the actual agents in _bmad/
|
projectDir,
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
bmadDir,
|
||||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
{
|
||||||
|
targetDir: commandsDir,
|
||||||
// Write agent launcher files using flat underscore naming
|
namingStyle: NamingStyle.FLAT_COLON,
|
||||||
// Creates files like: bmad_bmm_pm.md
|
templateType: TemplateType.CLAUDE,
|
||||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
},
|
||||||
|
options.selectedModules || [],
|
||||||
|
);
|
||||||
|
console.log(`[DEBUG CLAUDE-CODE] installer.install done, counts=`, counts);
|
||||||
|
|
||||||
// Process Claude Code specific injections for installed modules
|
// Process Claude Code specific injections for installed modules
|
||||||
// Use pre-collected configuration if available, or skip if already configured
|
|
||||||
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
|
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, {});
|
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {});
|
||||||
} else if (options.preCollectedConfig) {
|
} else if (options.preCollectedConfig) {
|
||||||
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
|
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
|
||||||
@@ -175,43 +108,24 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
await this.processModuleInjections(projectDir, bmadDir, options);
|
await this.processModuleInjections(projectDir, bmadDir, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip CLAUDE.md creation - let user manage their own CLAUDE.md file
|
|
||||||
// await this.createClaudeConfig(projectDir, modules);
|
|
||||||
|
|
||||||
// Generate workflow commands from manifest (if it exists)
|
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Write workflow-command artifacts using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_correct-course.md
|
|
||||||
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
|
|
||||||
|
|
||||||
// Generate task and tool commands from manifests (if they exist)
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||||
if (workflowCommandCount > 0) {
|
if (counts.workflows > 0) {
|
||||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||||
}
|
}
|
||||||
if (taskToolResult.generated > 0) {
|
if (counts.tasks + counts.tools > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
chalk.dim(
|
chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`),
|
||||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
agents: agentCount,
|
agents: counts.agents,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method removed - CLAUDE.md file management left to user
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read and process file content
|
* Read and process file content
|
||||||
*/
|
*/
|
||||||
@@ -224,7 +138,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
* Override processContent to keep {project-root} placeholder
|
* Override processContent to keep {project-root} placeholder
|
||||||
*/
|
*/
|
||||||
processContent(content, metadata = {}) {
|
processContent(content, metadata = {}) {
|
||||||
// Use the base class method WITHOUT projectDir to preserve {project-root} placeholder
|
|
||||||
return super.processContent(content, metadata);
|
return super.processContent(content, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,14 +147,12 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
async getAgentsFromSource(sourceDir, selectedModules) {
|
async getAgentsFromSource(sourceDir, selectedModules) {
|
||||||
const agents = [];
|
const agents = [];
|
||||||
|
|
||||||
// Add core agents
|
|
||||||
const corePath = getModulePath('core');
|
const corePath = getModulePath('core');
|
||||||
if (await fs.pathExists(path.join(corePath, 'agents'))) {
|
if (await fs.pathExists(path.join(corePath, 'agents'))) {
|
||||||
const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core');
|
const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core');
|
||||||
agents.push(...coreAgents);
|
agents.push(...coreAgents);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add module agents
|
|
||||||
for (const moduleName of selectedModules) {
|
for (const moduleName of selectedModules) {
|
||||||
const modulePath = path.join(sourceDir, moduleName);
|
const modulePath = path.join(sourceDir, moduleName);
|
||||||
const agentsPath = path.join(modulePath, 'agents');
|
const agentsPath = path.join(modulePath, 'agents');
|
||||||
@@ -259,11 +170,9 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
* Process module injections with pre-collected configuration
|
* Process module injections with pre-collected configuration
|
||||||
*/
|
*/
|
||||||
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
|
async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) {
|
||||||
// Get list of installed modules
|
|
||||||
const modules = options.selectedModules || [];
|
const modules = options.selectedModules || [];
|
||||||
const { subagentChoices, installLocation } = preCollectedConfig;
|
const { subagentChoices, installLocation } = preCollectedConfig;
|
||||||
|
|
||||||
// Get the actual source directory (not the installation directory)
|
|
||||||
await this.processModuleInjectionsInternal({
|
await this.processModuleInjectionsInternal({
|
||||||
projectDir,
|
projectDir,
|
||||||
modules,
|
modules,
|
||||||
@@ -276,15 +185,12 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Process Claude Code specific injections for installed modules
|
* Process Claude Code specific injections for installed modules
|
||||||
* Looks for injections.yaml in each module's claude-code sub-module
|
|
||||||
*/
|
*/
|
||||||
async processModuleInjections(projectDir, bmadDir, options) {
|
async processModuleInjections(projectDir, bmadDir, options) {
|
||||||
// Get list of installed modules
|
|
||||||
const modules = options.selectedModules || [];
|
const modules = options.selectedModules || [];
|
||||||
let subagentChoices = null;
|
let subagentChoices = null;
|
||||||
let installLocation = null;
|
let installLocation = null;
|
||||||
|
|
||||||
// Get the actual source directory (not the installation directory)
|
|
||||||
const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({
|
const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({
|
||||||
projectDir,
|
projectDir,
|
||||||
modules,
|
modules,
|
||||||
@@ -303,6 +209,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) {
|
async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) {
|
||||||
|
console.log(`[DEBUG CLAUDE-CODE] processModuleInjectionsInternal called! modules=${modules.join(',')}`);
|
||||||
let choices = subagentChoices;
|
let choices = subagentChoices;
|
||||||
let location = installLocation;
|
let location = installLocation;
|
||||||
|
|
||||||
@@ -346,7 +253,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
// First ask if they want to install subagents
|
|
||||||
const install = await prompts.select({
|
const install = await prompts.select({
|
||||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
||||||
choices: [
|
choices: [
|
||||||
@@ -358,7 +264,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (install === 'selective') {
|
if (install === 'selective') {
|
||||||
// Show list of available subagents with descriptions
|
|
||||||
const subagentInfo = {
|
const subagentInfo = {
|
||||||
'market-researcher.md': 'Market research and competitive analysis',
|
'market-researcher.md': 'Market research and competitive analysis',
|
||||||
'requirements-analyst.md': 'Requirements extraction and validation',
|
'requirements-analyst.md': 'Requirements extraction and validation',
|
||||||
@@ -395,7 +300,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
if (content.includes(marker)) {
|
if (content.includes(marker)) {
|
||||||
let injectionContent = injection.content;
|
let injectionContent = injection.content;
|
||||||
|
|
||||||
// Filter content if selective subagents chosen
|
|
||||||
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
|
if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') {
|
||||||
injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected);
|
injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected);
|
||||||
}
|
}
|
||||||
@@ -413,7 +317,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) {
|
async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) {
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
|
|
||||||
// Determine target directory based on user choice
|
|
||||||
let targetDir;
|
let targetDir;
|
||||||
if (location === 'user') {
|
if (location === 'user') {
|
||||||
targetDir = path.join(os.homedir(), '.claude', 'agents');
|
targetDir = path.join(os.homedir(), '.claude', 'agents');
|
||||||
@@ -423,7 +326,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
console.log(chalk.dim(` Installing subagents to project: .claude/agents/`));
|
console.log(chalk.dim(` Installing subagents to project: .claude/agents/`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure target directory exists
|
|
||||||
await this.ensureDir(targetDir);
|
await this.ensureDir(targetDir);
|
||||||
|
|
||||||
const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices);
|
const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices);
|
||||||
@@ -458,17 +360,12 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a custom agent launcher for Claude Code
|
* Install a custom agent launcher for Claude Code
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object|null} Info about created command
|
|
||||||
*/
|
*/
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||||
return null; // IDE not configured for this project
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.ensureDir(commandsDir);
|
await this.ensureDir(commandsDir);
|
||||||
@@ -490,8 +387,6 @@ You must fully embody this agent's persona and follow all activation instruction
|
|||||||
</agent-activation>
|
</agent-activation>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
// Written directly to commands dir (no bmad subfolder)
|
|
||||||
const launcherName = customAgentColonName(agentName);
|
const launcherName = customAgentColonName(agentName);
|
||||||
const launcherPath = path.join(commandsDir, launcherName);
|
const launcherPath = path.join(commandsDir, launcherName);
|
||||||
await this.writeFile(launcherPath, launcherContent);
|
await this.writeFile(launcherPath, launcherContent);
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class ClineSetup extends BaseIdeSetup {
|
|||||||
artifacts.push({
|
artifacts.push({
|
||||||
type: 'task',
|
type: 'task',
|
||||||
module: task.module,
|
module: task.module,
|
||||||
|
path: task.path,
|
||||||
sourcePath: task.path,
|
sourcePath: task.path,
|
||||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -3,25 +3,22 @@ const fs = require('fs-extra');
|
|||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { customAgentDashName } = require('./shared/path-utils');
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
|
||||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex setup handler (CLI mode)
|
* Codex setup handler (CLI mode)
|
||||||
|
*
|
||||||
|
* Uses UnifiedInstaller for all artifact installation.
|
||||||
*/
|
*/
|
||||||
class CodexSetup extends BaseIdeSetup {
|
class CodexSetup extends BaseIdeSetup {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('codex', 'Codex', true); // preferred IDE
|
super('codex', 'Codex', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect configuration choices before installation
|
* Collect configuration choices before installation
|
||||||
* @param {Object} options - Configuration options
|
|
||||||
* @returns {Object} Collected configuration
|
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
@@ -43,7 +40,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
default: 'global',
|
default: 'global',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display detailed instructions for the chosen option
|
|
||||||
console.log('');
|
console.log('');
|
||||||
if (installLocation === 'project') {
|
if (installLocation === 'project') {
|
||||||
console.log(this.getProjectSpecificInstructions());
|
console.log(this.getProjectSpecificInstructions());
|
||||||
@@ -51,7 +47,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
console.log(this.getGlobalInstructions());
|
console.log(this.getGlobalInstructions());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm the choice
|
|
||||||
confirmed = await prompts.confirm({
|
confirmed = await prompts.confirm({
|
||||||
message: 'Proceed with this installation option?',
|
message: 'Proceed with this installation option?',
|
||||||
default: true,
|
default: true,
|
||||||
@@ -67,78 +62,48 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup Codex configuration
|
* Setup Codex configuration
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
*/
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
async setup(projectDir, bmadDir, options = {}) {
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||||
|
|
||||||
// Always use CLI mode
|
|
||||||
const mode = 'cli';
|
|
||||||
|
|
||||||
// Get installation location from pre-collected config or default to global
|
|
||||||
const installLocation = options.preCollectedConfig?.installLocation || 'global';
|
const installLocation = options.preCollectedConfig?.installLocation || 'global';
|
||||||
|
|
||||||
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
|
|
||||||
|
|
||||||
const destDir = this.getCodexPromptDir(projectDir, installLocation);
|
const destDir = this.getCodexPromptDir(projectDir, installLocation);
|
||||||
|
|
||||||
await fs.ensureDir(destDir);
|
await fs.ensureDir(destDir);
|
||||||
await this.clearOldBmadFiles(destDir);
|
await this.clearOldBmadFiles(destDir);
|
||||||
|
|
||||||
// Collect artifacts and write using underscore format
|
// Use the unified installer - so much simpler!
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
const installer = new UnifiedInstaller(this.bmadFolderName);
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
const counts = await installer.install(
|
||||||
const agentCount = await agentGen.writeDashArtifacts(destDir, agentArtifacts);
|
projectDir,
|
||||||
|
bmadDir,
|
||||||
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []);
|
{
|
||||||
const taskArtifacts = [];
|
targetDir: destDir,
|
||||||
for (const task of tasks) {
|
namingStyle: NamingStyle.FLAT_DASH,
|
||||||
const content = await this.readAndProcessWithProject(
|
templateType: TemplateType.CODEX,
|
||||||
task.path,
|
},
|
||||||
{
|
options.selectedModules || [],
|
||||||
module: task.module,
|
);
|
||||||
name: task.name,
|
|
||||||
},
|
|
||||||
projectDir,
|
|
||||||
);
|
|
||||||
taskArtifacts.push({
|
|
||||||
type: 'task',
|
|
||||||
module: task.module,
|
|
||||||
sourcePath: task.path,
|
|
||||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
|
|
||||||
|
|
||||||
// Also write tasks using underscore format
|
|
||||||
const ttGen = new TaskToolCommandGenerator();
|
|
||||||
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
|
|
||||||
|
|
||||||
const written = agentCount + workflowCount + tasksWritten;
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||||
console.log(chalk.dim(` - Mode: CLI`));
|
console.log(chalk.dim(` - Mode: CLI`));
|
||||||
console.log(chalk.dim(` - ${counts.agents} agents exported`));
|
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||||
console.log(chalk.dim(` - ${counts.tasks} tasks exported`));
|
if (counts.workflows > 0) {
|
||||||
console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`));
|
console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`));
|
||||||
if (counts.workflowLaunchers > 0) {
|
|
||||||
console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`));
|
|
||||||
}
|
}
|
||||||
console.log(chalk.dim(` - ${written} Codex prompt files written`));
|
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}`));
|
console.log(chalk.dim(` - Destination: ${destDir}`));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
mode,
|
mode: 'cli',
|
||||||
artifacts,
|
...counts,
|
||||||
counts,
|
|
||||||
destination: destDir,
|
destination: destDir,
|
||||||
written,
|
|
||||||
installLocation,
|
installLocation,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -147,7 +112,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
* Detect Codex installation by checking for BMAD prompt exports
|
* Detect Codex installation by checking for BMAD prompt exports
|
||||||
*/
|
*/
|
||||||
async detect(projectDir) {
|
async detect(projectDir) {
|
||||||
// Check both global and project-specific locations
|
|
||||||
const globalDir = this.getCodexPromptDir(null, 'global');
|
const globalDir = this.getCodexPromptDir(null, 'global');
|
||||||
const projectDir_local = projectDir || process.cwd();
|
const projectDir_local = projectDir || process.cwd();
|
||||||
const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project');
|
const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project');
|
||||||
@@ -171,63 +135,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect Claude-style artifacts for Codex export.
|
|
||||||
* Returns the normalized artifact list for further processing.
|
|
||||||
*/
|
|
||||||
async collectClaudeArtifacts(projectDir, bmadDir, options = {}) {
|
|
||||||
const selectedModules = options.selectedModules || [];
|
|
||||||
const artifacts = [];
|
|
||||||
|
|
||||||
// Generate agent launchers
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
|
||||||
|
|
||||||
for (const artifact of agentArtifacts) {
|
|
||||||
artifacts.push({
|
|
||||||
type: 'agent',
|
|
||||||
module: artifact.module,
|
|
||||||
sourcePath: artifact.sourcePath,
|
|
||||||
relativePath: artifact.relativePath,
|
|
||||||
content: artifact.content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tasks = await getTasksFromBmad(bmadDir, selectedModules);
|
|
||||||
for (const task of tasks) {
|
|
||||||
const content = await this.readAndProcessWithProject(
|
|
||||||
task.path,
|
|
||||||
{
|
|
||||||
module: task.module,
|
|
||||||
name: task.name,
|
|
||||||
},
|
|
||||||
projectDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
artifacts.push({
|
|
||||||
type: 'task',
|
|
||||||
module: task.module,
|
|
||||||
sourcePath: task.path,
|
|
||||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
|
||||||
artifacts.push(...workflowArtifacts);
|
|
||||||
|
|
||||||
return {
|
|
||||||
artifacts,
|
|
||||||
counts: {
|
|
||||||
agents: agentArtifacts.length,
|
|
||||||
tasks: tasks.length,
|
|
||||||
workflows: workflowCounts.commands,
|
|
||||||
workflowLaunchers: workflowCounts.launchers,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getCodexPromptDir(projectDir = null, location = 'global') {
|
getCodexPromptDir(projectDir = null, location = 'global') {
|
||||||
if (location === 'project' && projectDir) {
|
if (location === 'project' && projectDir) {
|
||||||
return path.join(projectDir, '.codex', 'prompts');
|
return path.join(projectDir, '.codex', 'prompts');
|
||||||
@@ -235,19 +142,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
return path.join(os.homedir(), '.codex', 'prompts');
|
return path.join(os.homedir(), '.codex', 'prompts');
|
||||||
}
|
}
|
||||||
|
|
||||||
async flattenAndWriteArtifacts(artifacts, destDir) {
|
|
||||||
let written = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
const flattenedName = this.flattenFilename(artifact.relativePath);
|
|
||||||
const targetPath = path.join(destDir, flattenedName);
|
|
||||||
await fs.writeFile(targetPath, artifact.content);
|
|
||||||
written++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearOldBmadFiles(destDir) {
|
async clearOldBmadFiles(destDir) {
|
||||||
if (!(await fs.pathExists(destDir))) {
|
if (!(await fs.pathExists(destDir))) {
|
||||||
return;
|
return;
|
||||||
@@ -270,16 +164,10 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readAndProcessWithProject(filePath, metadata, projectDir) {
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
return super.processContent(content, metadata, projectDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get instructions for global installation
|
* Get instructions for global installation
|
||||||
* @returns {string} Instructions text
|
|
||||||
*/
|
*/
|
||||||
getGlobalInstructions(destDir) {
|
getGlobalInstructions() {
|
||||||
const lines = [
|
const lines = [
|
||||||
'',
|
'',
|
||||||
chalk.bold.cyan('═'.repeat(70)),
|
chalk.bold.cyan('═'.repeat(70)),
|
||||||
@@ -292,7 +180,7 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"),
|
chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"),
|
||||||
'',
|
'',
|
||||||
chalk.green(' ✓ You can now use /commands in Codex CLI'),
|
chalk.green(' ✓ You can now use /commands in Codex CLI'),
|
||||||
chalk.dim(' Example: /bmad_bmm_pm'),
|
chalk.dim(' Example: /bmad-bmm-pm'),
|
||||||
chalk.dim(' Type / to see all available commands'),
|
chalk.dim(' Type / to see all available commands'),
|
||||||
'',
|
'',
|
||||||
chalk.bold.cyan('═'.repeat(70)),
|
chalk.bold.cyan('═'.repeat(70)),
|
||||||
@@ -303,11 +191,8 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get instructions for project-specific installation
|
* Get instructions for project-specific installation
|
||||||
* @param {string} projectDir - Optional project directory
|
|
||||||
* @param {string} destDir - Optional destination directory
|
|
||||||
* @returns {string} Instructions text
|
|
||||||
*/
|
*/
|
||||||
getProjectSpecificInstructions(projectDir = null, destDir = null) {
|
getProjectSpecificInstructions() {
|
||||||
const isWindows = os.platform() === 'win32';
|
const isWindows = os.platform() === 'win32';
|
||||||
|
|
||||||
const commonLines = [
|
const commonLines = [
|
||||||
@@ -316,7 +201,7 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
chalk.bold.yellow(' Project-Specific Codex Configuration'),
|
chalk.bold.yellow(' Project-Specific Codex Configuration'),
|
||||||
chalk.bold.cyan('═'.repeat(70)),
|
chalk.bold.cyan('═'.repeat(70)),
|
||||||
'',
|
'',
|
||||||
chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '<project>/.codex/prompts'),
|
chalk.white(' Prompts will be installed to: ') + chalk.cyan('<project>/.codex/prompts'),
|
||||||
'',
|
'',
|
||||||
chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'),
|
chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'),
|
||||||
'',
|
'',
|
||||||
@@ -350,7 +235,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];
|
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];
|
||||||
|
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +242,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
* Cleanup Codex configuration
|
* Cleanup Codex configuration
|
||||||
*/
|
*/
|
||||||
async cleanup(projectDir = null) {
|
async cleanup(projectDir = null) {
|
||||||
// Clean both global and project-specific locations
|
|
||||||
const globalDir = this.getCodexPromptDir(null, 'global');
|
const globalDir = this.getCodexPromptDir(null, 'global');
|
||||||
await this.clearOldBmadFiles(globalDir);
|
await this.clearOldBmadFiles(globalDir);
|
||||||
|
|
||||||
@@ -370,11 +253,6 @@ class CodexSetup extends BaseIdeSetup {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a custom agent launcher for Codex
|
* Install a custom agent launcher for Codex
|
||||||
* @param {string} projectDir - Project directory (not used, Codex installs to home)
|
|
||||||
* @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) {
|
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||||
const destDir = this.getCodexPromptDir(projectDir, 'project');
|
const destDir = this.getCodexPromptDir(projectDir, 'project');
|
||||||
@@ -397,7 +275,6 @@ You must fully embody this agent's persona and follow all activation instruction
|
|||||||
</agent-activation>
|
</agent-activation>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
const fileName = customAgentDashName(agentName);
|
const fileName = customAgentDashName(agentName);
|
||||||
const launcherPath = path.join(destDir, fileName);
|
const launcherPath = path.join(destDir, fileName);
|
||||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||||
|
|||||||
@@ -1,31 +1,77 @@
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const { customAgentColonName } = require('./shared/path-utils');
|
const { customAgentColonName } = require('./shared/path-utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cursor IDE setup handler
|
* Cursor IDE setup handler
|
||||||
|
*
|
||||||
|
* Uses the UnifiedInstaller - all the complex artifact collection
|
||||||
|
* and writing logic is now centralized.
|
||||||
*/
|
*/
|
||||||
class CursorSetup extends BaseIdeSetup {
|
class CursorSetup extends BaseIdeSetup {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('cursor', 'Cursor', true); // preferred IDE
|
super('cursor', 'Cursor', true);
|
||||||
this.configDir = '.cursor';
|
this.configDir = '.cursor';
|
||||||
this.rulesDir = 'rules';
|
this.rulesDir = 'rules';
|
||||||
this.commandsDir = 'commands';
|
this.commandsDir = 'commands';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup old BMAD installation before reinstalling
|
* Setup Cursor IDE configuration
|
||||||
* @param {string} projectDir - Project directory
|
*/
|
||||||
|
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) {
|
async cleanup(projectDir) {
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
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)) {
|
if (await fs.pathExists(commandsDir)) {
|
||||||
const entries = await fs.readdir(commandsDir);
|
const entries = await fs.readdir(commandsDir);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -42,88 +88,24 @@ class CursorSetup extends BaseIdeSetup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup Cursor IDE configuration
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Object} options - Setup options
|
|
||||||
*/
|
|
||||||
async setup(projectDir, bmadDir, options = {}) {
|
|
||||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
|
||||||
|
|
||||||
// Clean up old BMAD installation first
|
|
||||||
await this.cleanup(projectDir);
|
|
||||||
|
|
||||||
// Create .cursor/commands directory structure
|
|
||||||
const cursorDir = path.join(projectDir, this.configDir);
|
|
||||||
const commandsDir = path.join(cursorDir, this.commandsDir);
|
|
||||||
await this.ensureDir(commandsDir);
|
|
||||||
|
|
||||||
// Use underscore format: files written directly to commands dir (no bmad subfolder)
|
|
||||||
// Creates: .cursor/commands/bmad_bmm_pm.md
|
|
||||||
|
|
||||||
// Generate agent launchers using AgentCommandGenerator
|
|
||||||
// This creates small launcher files that reference the actual agents in _bmad/
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
|
||||||
|
|
||||||
// Write agent launcher files using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_pm.md
|
|
||||||
const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts);
|
|
||||||
|
|
||||||
// Generate workflow commands from manifest (if it exists)
|
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
||||||
|
|
||||||
// Write workflow-command artifacts using flat underscore naming
|
|
||||||
// Creates files like: bmad_bmm_correct-course.md
|
|
||||||
const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts);
|
|
||||||
|
|
||||||
// Generate task and tool commands from manifests (if they exist)
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator();
|
|
||||||
const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir);
|
|
||||||
|
|
||||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
|
||||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
|
||||||
if (workflowCommandCount > 0) {
|
|
||||||
console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`));
|
|
||||||
}
|
|
||||||
if (taskToolResult.generated > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.dim(
|
|
||||||
` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
agents: agentCount,
|
|
||||||
tasks: taskToolResult.tasks || 0,
|
|
||||||
tools: taskToolResult.tools || 0,
|
|
||||||
workflows: workflowCommandCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a custom agent launcher for Cursor
|
* Install a custom agent launcher for Cursor
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
|
||||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
|
||||||
* @param {Object} metadata - Agent metadata
|
|
||||||
* @returns {Object|null} Info about created command
|
|
||||||
*/
|
*/
|
||||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||||
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
const commandsDir = path.join(projectDir, this.configDir, this.commandsDir);
|
||||||
|
|
||||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||||
return null; // IDE not configured for this project
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.ensureDir(commandsDir);
|
await this.ensureDir(commandsDir);
|
||||||
|
|
||||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
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">
|
<agent-activation CRITICAL="TRUE">
|
||||||
1. LOAD the FULL agent file from @${agentPath}
|
1. LOAD the FULL agent file from @${agentPath}
|
||||||
@@ -135,20 +117,9 @@ class CursorSetup extends BaseIdeSetup {
|
|||||||
</agent-activation>
|
</agent-activation>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Cursor uses YAML frontmatter matching Claude Code format
|
|
||||||
const commandContent = `---
|
|
||||||
name: '${agentName}'
|
|
||||||
description: '${agentName} agent'
|
|
||||||
---
|
|
||||||
|
|
||||||
${launcherContent}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
|
||||||
// Written directly to commands dir (no bmad subfolder)
|
|
||||||
const launcherName = customAgentColonName(agentName);
|
const launcherName = customAgentColonName(agentName);
|
||||||
const launcherPath = path.join(commandsDir, launcherName);
|
const launcherPath = path.join(commandsDir, launcherName);
|
||||||
await this.writeFile(launcherPath, commandContent);
|
await this.writeFile(launcherPath, launcherContent);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: launcherPath,
|
path: launcherPath,
|
||||||
|
|||||||
@@ -9,54 +9,10 @@ const { toColonName, toColonPath, toDashPath } = require('./path-utils');
|
|||||||
*/
|
*/
|
||||||
class TaskToolCommandGenerator {
|
class TaskToolCommandGenerator {
|
||||||
/**
|
/**
|
||||||
* Generate task and tool commands from manifest CSVs
|
* REMOVED: Old generateTaskToolCommands method that created nested structure.
|
||||||
* @param {string} projectDir - Project directory
|
* This was causing bugs where files were written to wrong directories.
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
* Use generateColonTaskToolCommands() or generateDashTaskToolCommands() instead.
|
||||||
* @param {string} baseCommandsDir - Optional base commands directory (defaults to .claude/commands/bmad)
|
|
||||||
*/
|
*/
|
||||||
async generateTaskToolCommands(projectDir, bmadDir, baseCommandsDir = null) {
|
|
||||||
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) : [];
|
|
||||||
|
|
||||||
// Base commands directory - use provided or default to Claude Code structure
|
|
||||||
const commandsDir = baseCommandsDir || path.join(projectDir, '.claude', 'commands', 'bmad');
|
|
||||||
|
|
||||||
let generatedCount = 0;
|
|
||||||
|
|
||||||
// Generate command files for tasks
|
|
||||||
for (const task of standaloneTasks) {
|
|
||||||
const moduleTasksDir = path.join(commandsDir, task.module, 'tasks');
|
|
||||||
await fs.ensureDir(moduleTasksDir);
|
|
||||||
|
|
||||||
const commandContent = this.generateCommandContent(task, 'task');
|
|
||||||
const commandPath = path.join(moduleTasksDir, `${task.name}.md`);
|
|
||||||
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate command files for tools
|
|
||||||
for (const tool of standaloneTools) {
|
|
||||||
const moduleToolsDir = path.join(commandsDir, tool.module, 'tools');
|
|
||||||
await fs.ensureDir(moduleToolsDir);
|
|
||||||
|
|
||||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
|
||||||
const commandPath = path.join(moduleToolsDir, `${tool.name}.md`);
|
|
||||||
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
generated: generatedCount,
|
|
||||||
tasks: standaloneTasks.length,
|
|
||||||
tools: standaloneTools.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate command content for a task or tool
|
* Generate command content for a task or tool
|
||||||
@@ -93,10 +49,16 @@ Follow all instructions in the ${type} file exactly as written.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||||
return csv.parse(csvContent, {
|
const tasks = csv.parse(csvContent, {
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filter out README files
|
||||||
|
return tasks.filter((task) => {
|
||||||
|
const nameLower = task.name.toLowerCase();
|
||||||
|
return !nameLower.includes('readme') && task.name !== 'README';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,10 +72,16 @@ Follow all instructions in the ${type} file exactly as written.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||||
return csv.parse(csvContent, {
|
const tools = csv.parse(csvContent, {
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filter out README files
|
||||||
|
return tools.filter((tool) => {
|
||||||
|
const nameLower = tool.name.toLowerCase();
|
||||||
|
return !nameLower.includes('readme') && tool.name !== 'README';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,12 +103,16 @@ Follow all instructions in the ${type} file exactly as written.
|
|||||||
|
|
||||||
let generatedCount = 0;
|
let generatedCount = 0;
|
||||||
|
|
||||||
|
// DEBUG: Log parameters
|
||||||
|
console.log(`[DEBUG generateColonTaskToolCommands] baseCommandsDir: ${baseCommandsDir}`);
|
||||||
|
|
||||||
// Generate command files for tasks
|
// Generate command files for tasks
|
||||||
for (const task of standaloneTasks) {
|
for (const task of standaloneTasks) {
|
||||||
const commandContent = this.generateCommandContent(task, 'task');
|
const commandContent = this.generateCommandContent(task, 'task');
|
||||||
// Use underscore format: bmad_bmm_name.md
|
// Use underscore format: bmad_bmm_name.md
|
||||||
const flatName = toColonName(task.module, 'tasks', task.name);
|
const flatName = toColonName(task.module, 'tasks', task.name);
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
const commandPath = path.join(baseCommandsDir, flatName);
|
||||||
|
console.log(`[DEBUG generateColonTaskToolCommands] Writing task ${task.name} to: ${commandPath}`);
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
await fs.ensureDir(path.dirname(commandPath));
|
||||||
await fs.writeFile(commandPath, commandContent);
|
await fs.writeFile(commandPath, commandContent);
|
||||||
generatedCount++;
|
generatedCount++;
|
||||||
@@ -186,7 +158,7 @@ Follow all instructions in the ${type} file exactly as written.
|
|||||||
// Generate command files for tasks
|
// Generate command files for tasks
|
||||||
for (const task of standaloneTasks) {
|
for (const task of standaloneTasks) {
|
||||||
const commandContent = this.generateCommandContent(task, 'task');
|
const commandContent = this.generateCommandContent(task, 'task');
|
||||||
// Use underscore format: bmad_bmm_name.md
|
// Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath)
|
||||||
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
|
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
const commandPath = path.join(baseCommandsDir, flatName);
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
await fs.ensureDir(path.dirname(commandPath));
|
||||||
@@ -197,7 +169,7 @@ Follow all instructions in the ${type} file exactly as written.
|
|||||||
// Generate command files for tools
|
// Generate command files for tools
|
||||||
for (const tool of standaloneTools) {
|
for (const tool of standaloneTools) {
|
||||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
const commandContent = this.generateCommandContent(tool, 'tool');
|
||||||
// Use underscore format: bmad_bmm_name.md
|
// Use underscore format: bmad_bmm_name.md (toDashPath aliases toColonPath)
|
||||||
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
|
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
const commandPath = path.join(baseCommandsDir, flatName);
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
await fs.ensureDir(path.dirname(commandPath));
|
||||||
|
|||||||
329
tools/cli/installers/lib/ide/shared/unified-installer.js
Normal file
329
tools/cli/installers/lib/ide/shared/unified-installer.js
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
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');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Naming styles
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified installer configuration
|
||||||
|
* @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 {boolean} includeNestedStructure - For NESTED style, create subdirectories
|
||||||
|
* @property {Function} [customTemplateFn] - Optional custom template function
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified BMAD Installer
|
||||||
|
*/
|
||||||
|
class UnifiedInstaller {
|
||||||
|
constructor(bmadFolderName = 'bmad') {
|
||||||
|
this.bmadFolderName = bmadFolderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install BMAD artifacts for an IDE
|
||||||
|
*
|
||||||
|
* @param {string} projectDir - Project root directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory (_bmad)
|
||||||
|
* @param {UnifiedInstallConfig} config - Installation configuration
|
||||||
|
* @param {Array<string>} selectedModules - Modules to install
|
||||||
|
* @returns {Promise<Object>} Installation result with counts
|
||||||
|
*/
|
||||||
|
async install(projectDir, bmadDir, config, selectedModules = []) {
|
||||||
|
const {
|
||||||
|
targetDir,
|
||||||
|
namingStyle = NamingStyle.FLAT_COLON,
|
||||||
|
templateType = TemplateType.CLAUDE,
|
||||||
|
includeNestedStructure = false,
|
||||||
|
customTemplateFn = null,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
// Clean up any existing BMAD files in target directory
|
||||||
|
await this.cleanupBmadFiles(targetDir);
|
||||||
|
|
||||||
|
// Ensure target directory exists
|
||||||
|
await fs.ensureDir(targetDir);
|
||||||
|
|
||||||
|
// Count results
|
||||||
|
const counts = {
|
||||||
|
agents: 0,
|
||||||
|
workflows: 0,
|
||||||
|
tasks: 0,
|
||||||
|
tools: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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, customTemplateFn, 'agent');
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
customTemplateFn,
|
||||||
|
'workflow',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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}`);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
: await ttGen.generateColonTaskToolCommands(projectDir, bmadDir, targetDir);
|
||||||
|
|
||||||
|
counts.tasks = taskToolResult.tasks || 0;
|
||||||
|
counts.tools = taskToolResult.tools || 0;
|
||||||
|
|
||||||
|
counts.total = counts.agents + counts.workflows + counts.tasks + counts.tools;
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up any existing BMAD files in target directory
|
||||||
|
*/
|
||||||
|
async cleanupBmadFiles(targetDir) {
|
||||||
|
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) {
|
||||||
|
if (entry.name.startsWith('bmad')) {
|
||||||
|
const entryPath = path.join(targetDir, entry.name);
|
||||||
|
await fs.remove(entryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write artifacts with specified naming style and template
|
||||||
|
*/
|
||||||
|
async writeArtifacts(artifacts, targetDir, namingStyle, templateType, customTemplateFn, artifactType) {
|
||||||
|
console.log(`[DEBUG] writeArtifacts: artifactType=${artifactType}, count=${artifacts.length}, targetDir=${targetDir}`);
|
||||||
|
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);
|
||||||
|
targetPath = path.join(targetDir, flatName);
|
||||||
|
} else if (namingStyle === NamingStyle.FLAT_DASH) {
|
||||||
|
const flatName = toDashPath(artifact.relativePath);
|
||||||
|
targetPath = path.join(targetDir, flatName);
|
||||||
|
} else {
|
||||||
|
// Fallback: treat as flat even if NESTED specified
|
||||||
|
const flatName = toColonPath(artifact.relativePath);
|
||||||
|
targetPath = path.join(targetDir, flatName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply template transformations if needed
|
||||||
|
if (customTemplateFn) {
|
||||||
|
content = customTemplateFn(artifact, content, templateType);
|
||||||
|
} else {
|
||||||
|
content = this.applyTemplate(artifact, content, templateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.ensureDir(path.dirname(targetPath));
|
||||||
|
await fs.writeFile(targetPath, content, 'utf8');
|
||||||
|
written++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tasks from manifest CSV
|
||||||
|
*/
|
||||||
|
async getTasksFromManifest(bmadDir) {
|
||||||
|
const csv = require('csv-parse/sync');
|
||||||
|
const manifestPath = path.join(bmadDir, '_config', 'task-manifest.csv');
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(manifestPath))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||||
|
const tasks = csv.parse(csvContent, {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: 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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tools from manifest CSV
|
||||||
|
*/
|
||||||
|
async getToolsFromManifest(bmadDir) {
|
||||||
|
const csv = require('csv-parse/sync');
|
||||||
|
const manifestPath = path.join(bmadDir, '_config', 'tool-manifest.csv');
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(manifestPath))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||||
|
const tools = csv.parse(csvContent, {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
UnifiedInstaller,
|
||||||
|
NamingStyle,
|
||||||
|
TemplateType,
|
||||||
|
};
|
||||||
@@ -14,44 +14,10 @@ class WorkflowCommandGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate workflow commands from the manifest CSV
|
* REMOVED: Old generateWorkflowCommands method that created nested structure.
|
||||||
* @param {string} projectDir - Project directory
|
* This was hardcoded to .claude/commands/bmad and caused bugs.
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
* Use collectWorkflowArtifacts() + writeColonArtifacts/writeDashArtifacts() instead.
|
||||||
*/
|
*/
|
||||||
async generateWorkflowCommands(projectDir, bmadDir) {
|
|
||||||
const workflows = await this.loadWorkflowManifest(bmadDir);
|
|
||||||
|
|
||||||
if (!workflows) {
|
|
||||||
console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.'));
|
|
||||||
return { generated: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ALL workflows now generate commands - no standalone filtering
|
|
||||||
const allWorkflows = workflows;
|
|
||||||
|
|
||||||
// Base commands directory
|
|
||||||
const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad');
|
|
||||||
|
|
||||||
let generatedCount = 0;
|
|
||||||
|
|
||||||
// Generate a command file for each workflow, organized by module
|
|
||||||
for (const workflow of allWorkflows) {
|
|
||||||
const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
|
|
||||||
await fs.ensureDir(moduleWorkflowsDir);
|
|
||||||
|
|
||||||
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
|
||||||
const commandPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`);
|
|
||||||
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also create a workflow launcher README in each module
|
|
||||||
const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows);
|
|
||||||
await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows);
|
|
||||||
|
|
||||||
return { generated: generatedCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
async collectWorkflowArtifacts(bmadDir) {
|
async collectWorkflowArtifacts(bmadDir) {
|
||||||
const workflows = await this.loadWorkflowManifest(bmadDir);
|
const workflows = await this.loadWorkflowManifest(bmadDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user