UNPKG

@cloudkinetix/bmad-enhanced

Version:

Cloud-Kinetix enhanced fork of BMAD-METHOD - Breakthrough Method of Agile AI-driven Development with robust versioning and unified validation.

708 lines (588 loc) 27.5 kB
const path = require("path"); const { CK_AGENTS } = require('../config/agent-names'); const fs = require("fs-extra"); const yaml = require("js-yaml"); const fileManager = require("./file-manager"); const configLoader = require("./config-loader"); // Import ES module loader const { getChalk } = require("./es-module-loader"); class IdeSetup { constructor() { this.ideAgentConfig = null; } async setup(ide, installDir, selectedAgent = null, options = {}) { const chalk = await getChalk(); switch (ide) { case "cursor": return this.setupCursor(installDir, selectedAgent); case "claude-code": return this.setupClaudeCode(installDir, selectedAgent); case "windsurf": return this.setupWindsurf(installDir, selectedAgent); case "roo": return this.setupRoo(installDir, selectedAgent); case "cline": return this.setupCline(installDir, selectedAgent); case "gemini": return this.setupGeminiCli(installDir, selectedAgent); case "kilo": return this.setupKilocode(installDir, selectedAgent); default: console.log(chalk.yellow(`\nIDE ${ide} not yet supported`)); return false; } } async setupCursor(installDir, selectedAgent) { const chalk = await getChalk(); const cursorRulesDir = path.join(installDir, ".cursor", "rules"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(cursorRulesDir); for (const agentId of agents) { // Find the agent file (handles core, expansion packs, and single agent installs) const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`); // Create MDC content with proper format let mdcContent = "---\n"; mdcContent += "description: \n"; mdcContent += "globs: []\n"; mdcContent += "alwaysApply: false\n"; mdcContent += "---\n\n"; mdcContent += `# ${agentId.toUpperCase()} Agent Rule\n\n`; mdcContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle( agentId, installDir )} agent persona.\n\n`; mdcContent += "## Agent Activation\n\n"; mdcContent += "CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; mdcContent += "```yml\n"; // Extract just the YAML content from the agent file const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/); if (yamlMatch) { mdcContent += yamlMatch[1].trim(); } else { // If no YAML found, include the whole content minus the header mdcContent += agentContent.replace(/^#.*$/m, "").trim(); } mdcContent += "\n```\n\n"; mdcContent += "## File Reference\n\n"; mdcContent += `The complete agent definition is available in [.bmad-core/agents/${agentId}.md](mdc:.bmad-core/agents/${agentId}.md).\n\n`; mdcContent += "## Usage\n\n"; mdcContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle( agentId, installDir )} persona and follow all instructions defined in the YML configuration above.\n`; await fileManager.writeFile(mdcPath, mdcContent); console.log(chalk.green(`✓ Created rule: ${agentId}.mdc`)); } } console.log(chalk.green(`\n✓ Created Cursor rules in ${cursorRulesDir}`)); return true; } async setupClaudeCode(installDir, selectedAgent) { const chalk = await getChalk(); const commandsDir = path.join(installDir, ".claude", "commands"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(commandsDir); for (const agentId of agents) { // Find the agent file (handles core, expansion packs, and single agent installs) const agentPath = await this.findAgentPath(agentId, installDir); const commandPath = path.join(commandsDir, `${agentId}.md`); if (agentPath) { // Create command file with agent content const agentContent = await fileManager.readFile(agentPath); // Add command header let commandContent = `# /${agentId} Command\n\n`; commandContent += `When this command is used, adopt the following agent persona:\n\n`; commandContent += agentContent; await fileManager.writeFile(commandPath, commandContent); console.log(chalk.green(`✓ Created command: /${agentId}`)); } } // NOTE: As of v1.10.5, expansion pack commands are handled by tools/installer/lib/ide-setup.js // which properly creates the organized structure (.claude/commands/{prefix}/...) // This prevents duplicate commands and maintains consistency with BMAD expansion packs. console.log(chalk.green(`\n✓ Created Claude Code commands in ${commandsDir}`)); return true; } async setupWindsurf(installDir, selectedAgent) { const chalk = await getChalk(); const windsurfRulesDir = path.join(installDir, ".windsurf", "rules"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(windsurfRulesDir); for (const agentId of agents) { // Find the agent file (handles core, expansion packs, and single agent installs) const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); const mdPath = path.join(windsurfRulesDir, `${agentId}.md`); // Create MD content with YAML frontmatter for Windsurf recognition let mdContent = `---\ntrigger: manual\n---\n\n# ${agentId.toUpperCase()} Agent Rule\n\n`; mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle( agentId, installDir )} agent persona.\n\n`; mdContent += "## Agent Activation\n\n"; mdContent += "CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; mdContent += "```yml\n"; // Extract just the YAML content from the agent file const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/); if (yamlMatch) { mdContent += yamlMatch[1].trim(); } else { // If no YAML found, include the whole content minus the header mdContent += agentContent.replace(/^#.*$/m, "").trim(); } mdContent += "\n```\n\n"; mdContent += "## File Reference\n\n"; mdContent += `The complete agent definition is available in [.bmad-core/agents/${agentId}.md](.bmad-core/agents/${agentId}.md).\n\n`; mdContent += "## Usage\n\n"; mdContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle( agentId, installDir )} persona and follow all instructions defined in the YML configuration above.\n`; await fileManager.writeFile(mdPath, mdContent); console.log(chalk.green(`✓ Created rule: ${agentId}.md`)); } } console.log(chalk.green(`\n✓ Created Windsurf rules in ${windsurfRulesDir}`)); return true; } async getAllAgentIds(installDir) { const allAgentIds = new Set(); const agentDirs = await this.getAgentDirectories(installDir); for (const dir of agentDirs) { const agentFiles = await fs.readdir(dir).catch(() => []); agentFiles .filter(file => file.endsWith('.md')) .forEach(file => allAgentIds.add(path.basename(file, '.md'))); } return Array.from(allAgentIds); } async getAgentDirectories(installDir) { const glob = require("glob"); const dirs = []; // Core agents directory const coreDirs = [ path.join(installDir, ".bmad-core", "agents"), path.join(installDir, "agents") ]; for (const dir of coreDirs) { if (await fileManager.pathExists(dir)) { dirs.push(dir); break; // Only use the first one that exists } } // Expansion pack directories const patterns = [".bmad-*/agents", ".ck-*/agents", ".bmad-ck-*/agents"]; for (const pattern of patterns) { const matches = glob.sync(pattern, { cwd: installDir }); dirs.push(...matches.map(m => path.join(installDir, m))); } return dirs; } getAgentTitle(agentId) { const agentTitles = { analyst: "Business Analyst", architect: "Solution Architect", "bmad-master": "BMAD Master", "bmad-orchestrator": "BMAD Orchestrator", dev: "Developer", pm: "Product Manager", po: "Product Owner", qa: "QA Specialist", sm: "Scrum Master", "ux-expert": "UX Expert", }; return agentTitles[agentId] || agentId; } async setupRoo(installDir, selectedAgent) { const chalk = await getChalk(); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); // Create .roo directory first const rooDir = path.join(installDir, ".roo"); await fileManager.ensureDirectory(rooDir); // Check for existing .roomodes file inside .roo directory const roomodesPath = path.join(rooDir, ".roomodes"); let existingModes = []; let existingContent = ""; if (await fileManager.pathExists(roomodesPath)) { existingContent = await fileManager.readFile(roomodesPath); // Parse existing modes to avoid duplicates const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g); for (const match of modeMatches) { existingModes.push(match[1]); } console.log(chalk.yellow(`Found existing .roomodes file with ${existingModes.length} modes`)); } // Create new modes content let newModesContent = ""; // Define file permissions for each agent type const agentPermissions = { analyst: { fileRegex: "\\.(md|txt)$", description: "Documentation and text files", }, pm: { fileRegex: "\\.(md|txt)$", description: "Product documentation", }, architect: { fileRegex: "\\.(md|txt|yml|yaml|json)$", description: "Architecture docs and configs", }, dev: null, // Full edit access qa: { fileRegex: "\\.(test|spec)\\.(js|ts|jsx|tsx)$|\\.md$", description: "Test files and documentation", }, "ux-expert": { fileRegex: "\\.(md|css|scss|html|jsx|tsx)$", description: "Design-related files", }, po: { fileRegex: "\\.(md|txt)$", description: "Story and requirement docs", }, sm: { fileRegex: "\\.(md|txt)$", description: "Process and planning docs", }, "bmad-orchestrator": null, // Full edit access "bmad-master": null, // Full edit access }; for (const agentId of agents) { // Skip if already exists if (existingModes.includes(`bmad-${agentId}`)) { console.log(chalk.dim(`Skipping ${agentId} - already exists in .roomodes`)); continue; } // Read agent file to extract all information let agentPath = path.join(installDir, ".bmad-core", "agents", `${agentId}.md`); if (!(await fileManager.pathExists(agentPath))) { agentPath = path.join(installDir, "agents", `${agentId}.md`); } if (await fileManager.pathExists(agentPath)) { const agentContent = await fileManager.readFile(agentPath); // Extract YAML content const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/); if (yamlMatch) { const yaml = yamlMatch[1]; // Extract agent info from YAML const titleMatch = yaml.match(/title:\s*(.+)/); const iconMatch = yaml.match(/icon:\s*(.+)/); const whenToUseMatch = yaml.match(/whenToUse:\s*"(.+)"/); const roleDefinitionMatch = yaml.match(/roleDefinition:\s*"(.+)"/); const title = titleMatch ? titleMatch[1].trim() : await this.getAgentTitle(agentId, installDir); const icon = iconMatch ? iconMatch[1].trim() : "🤖"; const whenToUse = whenToUseMatch ? whenToUseMatch[1].trim() : `Use for ${title} tasks`; const roleDefinition = roleDefinitionMatch ? roleDefinitionMatch[1].trim() : `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`; // Build mode entry with proper formatting (matching exact indentation) newModesContent += ` - slug: bmad-${agentId}\n`; newModesContent += ` name: '${icon} ${title}'\n`; newModesContent += ` roleDefinition: ${roleDefinition}\n`; newModesContent += ` whenToUse: ${whenToUse}\n`; newModesContent += ` customInstructions: CRITICAL Read the full YML from .bmad-core/agents/${agentId}.md start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`; newModesContent += ` groups:\n`; newModesContent += ` - read\n`; // Add permissions based on agent type const permissions = agentPermissions[agentId]; if (permissions) { newModesContent += ` - - edit\n`; newModesContent += ` - fileRegex: ${permissions.fileRegex}\n`; newModesContent += ` description: ${permissions.description}\n`; } else { newModesContent += ` - edit\n`; } console.log(chalk.green(`✓ Added mode: bmad-${agentId} (${icon} ${title})`)); } } } // Build final roomodes content let roomodesContent = ""; if (existingContent) { // If there's existing content, append new modes to it roomodesContent = existingContent.trim() + "\n" + newModesContent; } else { // Create new .roomodes file with proper YAML structure roomodesContent = "customModes:\n" + newModesContent; } // Write .roomodes file await fileManager.writeFile(roomodesPath, roomodesContent); console.log(chalk.green("✓ Created .roo/.roomodes file")); // Create README in .roo directory const agentList = []; for (const id of agents) { const title = await this.getAgentTitle(id, installDir); agentList.push(`- **bmad-${id}** - ${title}`); } const rooReadme = `# Roo Code Custom Modes for BMAD-METHOD This directory contains custom mode configurations for Roo Code to enable BMAD agent personalities. ## Setup The \`.roomodes\` file defines all BMAD agents as custom modes using the proper \`customModes:\` structure. Modes are automatically available in Roo Code when you open this project. ## Available Modes ${agentList.join("\n")} ## Usage In Roo Code: 1. Open the mode selector (usually in the status bar) 2. Select any BMAD agent mode 3. The AI will adopt that agent's personality and expertise ## File Permissions Each agent has specific file access permissions: - **Analysts, PM, PO, SM**: Limited to documentation files (.md, .txt) - **Architect**: Architecture docs and configs (.md, .txt, .yml, .yaml, .json) - **QA**: Test files and documentation - **UX Expert**: Design-related files (.md, .css, .scss, .html, .jsx, .tsx) - **Developer, Orchestrator, Master**: Full edit access to all files `; const readmePath = path.join(rooDir, "README.md"); await fileManager.writeFile(readmePath, rooReadme); console.log(chalk.green("✓ Created .roo/README.md")); console.log(chalk.green(`\n✓ Roo Code setup complete!`)); console.log(chalk.dim("Custom modes will be available when you open this project in Roo Code")); return true; } async loadIdeAgentConfig() { if (this.ideAgentConfig) return this.ideAgentConfig; try { const configPath = path.join(__dirname, "..", "config", "ide-agent-config.yaml"); const configContent = await fs.readFile(configPath, "utf8"); this.ideAgentConfig = yaml.load(configContent); return this.ideAgentConfig; } catch (error) { console.warn("Failed to load IDE agent configuration, using defaults"); return { "roo-permissions": {}, "cline-order": {}, }; } } async findAgentPath(agentId, installDir) { const agentDirs = await this.getAgentDirectories(installDir); for (const dir of agentDirs) { const agentPath = path.join(dir, `${agentId}.md`); if (await fileManager.pathExists(agentPath)) { return agentPath; } } return null; } async getAgentTitle(agentId, installDir) { // Try to get title from agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { try { const agentContent = await fileManager.readFile(agentPath); const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/); if (yamlMatch) { const titleMatch = yamlMatch[1].match(/title:\s*(.+)/); if (titleMatch) { return titleMatch[1].trim(); } } } catch (error) { // Fall back to default titles } } // Fallback to predefined titles const agentTitles = { analyst: "Business Analyst", architect: "Solution Architect", "bmad-master": "BMAD Master", "bmad-orchestrator": "BMAD Orchestrator", dev: "Developer", pm: "Product Manager", po: "Product Owner", qa: "QA Specialist", sm: "Scrum Master", "ux-expert": "UX Expert", // CK agents from central configuration [CK_AGENTS.jira]: "JIRA Integration Assistant", [CK_AGENTS.llmArchitect]: "LLM Architect", [CK_AGENTS.llmEngineer]: "LLM Engineer", [CK_AGENTS.llmOrchestrator]: "LLM Orchestrator", [CK_AGENTS.llmSafetyGovernance]: "LLM Safety & Governance", [CK_AGENTS.llmWizard]: "LLM Development Wizard", [CK_AGENTS.glab]: "GitLab CI/CD", [CK_AGENTS.parallel]: "Parallel Development Orchestrator", "claude-coordinator": "Claude Coordinator", }; return agentTitles[agentId] || agentId; } async setupCline(installDir, selectedAgent) { const chalk = await getChalk(); const clineRulesDir = path.join(installDir, ".clinerules"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(clineRulesDir); // Load dynamic agent ordering from configuration const config = await this.loadIdeAgentConfig(); const agentOrder = config["cline-order"] || {}; for (const agentId of agents) { // Find the agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); // Get numeric prefix for ordering const order = agentOrder[agentId] || 99; const prefix = order.toString().padStart(2, "0"); const mdPath = path.join(clineRulesDir, `${prefix}-${agentId}.md`); // Create MD content for Cline (focused on project standards and role) let mdContent = `# ${await this.getAgentTitle(agentId, installDir)} Agent\n\n`; mdContent += `This rule defines the ${await this.getAgentTitle(agentId, installDir)} persona and project standards.\n\n`; mdContent += "## Role Definition\n\n"; mdContent += "When the user types `@" + agentId + "`, adopt this persona and follow these guidelines:\n\n"; mdContent += "```yml\n"; // Extract just the YAML content from the agent file const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/); if (yamlMatch) { mdContent += yamlMatch[1].trim(); } else { // If no YAML found, include the whole content minus the header mdContent += agentContent.replace(/^#.*$/m, "").trim(); } mdContent += "\n```\n\n"; mdContent += "## Project Standards\n\n"; mdContent += `- Always maintain consistency with project documentation in .bmad-core/\n`; mdContent += `- Follow the agent's specific guidelines and constraints\n`; mdContent += `- Update relevant project files when making changes\n`; const relativePath = path.relative(installDir, agentPath).replace(/\\/g, "/"); mdContent += `- Reference the complete agent definition in [${relativePath}](${relativePath})\n\n`; mdContent += "## Usage\n\n"; mdContent += `Type \`@${agentId}\` to activate this ${await this.getAgentTitle(agentId, installDir)} persona.\n`; await fileManager.writeFile(mdPath, mdContent); console.log(chalk.green(`✓ Created rule: ${prefix}-${agentId}.md`)); } } console.log(chalk.green(`\n✓ Created Cline rules in ${clineRulesDir}`)); return true; } async setupGeminiCli(installDir, selectedAgent) { const chalk = await getChalk(); const geminiDir = path.join(installDir, ".gemini"); const agentsContextDir = path.join(geminiDir, "agents"); await fileManager.ensureDirectory(agentsContextDir); // Get all available agents const agents = await this.getAllAgentIds(installDir); const agentContextFiles = []; for (const agentId of agents) { // Find the source agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); const contextFilePath = path.join(agentsContextDir, `${agentId}.md`); // Copy the agent content directly into its own context file await fileManager.writeFile(contextFilePath, agentContent); // Store the relative path for settings.json const relativePath = path.relative(geminiDir, contextFilePath); agentContextFiles.push(relativePath.replace(/\\/g, "/")); // Ensure forward slashes for consistency console.log(chalk.green(`✓ Created context file for @${agentId}`)); } } console.log(chalk.green(`\n✓ Created individual agent context files in ${agentsContextDir}`)); // Create or update settings.json const settingsPath = path.join(geminiDir, "settings.json"); let settings = {}; if (await fileManager.pathExists(settingsPath)) { try { const existingSettings = await fileManager.readFile(settingsPath); settings = JSON.parse(existingSettings); console.log(chalk.yellow("Found existing .gemini/settings.json. Merging settings...")); } catch (e) { console.error( chalk.red("Error parsing existing settings.json. It will be overwritten."), e ); settings = {}; } } // Set contextFileName to our new array of files settings.contextFileName = agentContextFiles; await fileManager.writeFile(settingsPath, JSON.stringify(settings, null, 2)); console.log(chalk.green(`✓ Configured .gemini/settings.json to load all agent context files.`)); return true; } async setupKilocode(installDir, selectedAgent) { const chalk = await getChalk(); const filePath = path.join(installDir, ".kilocodemodes"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); let existingModes = [], existingContent = ""; if (await fileManager.pathExists(filePath)) { existingContent = await fileManager.readFile(filePath); for (const match of existingContent.matchAll(/- slug: ([\w-]+)/g)) { existingModes.push(match[1]); } console.log(chalk.yellow(`Found existing .kilocodemodes file with ${existingModes.length} modes`)); } const config = await this.loadIdeAgentConfig(); const permissions = config['roo-permissions'] || {}; // reuse same roo permissions block (Kilo Code understands same mode schema) let newContent = ""; for (const agentId of agents) { const slug = agentId.startsWith('bmad-') ? agentId : `bmad-${agentId}`; if (existingModes.includes(slug)) { console.log(chalk.dim(`Skipping ${agentId} - already exists in .kilocodemodes`)); continue; } const agentPath = await this.findAgentPath(agentId, installDir); if (!agentPath) { console.log(chalk.red(`✗ Could not find agent file for ${agentId}`)); continue; } const agentContent = await fileManager.readFile(agentPath); const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); if (!yamlMatch) { console.log(chalk.red(`✗ Could not extract YAML block for ${agentId}`)); continue; } const yaml = yamlMatch[1]; // Robust fallback for title and icon const title = (yaml.match(/title:\s*(.+)/)?.[1]?.trim()) || await this.getAgentTitle(agentId, installDir); const icon = (yaml.match(/icon:\s*(.+)/)?.[1]?.trim()) || '🤖'; const whenToUse = (yaml.match(/whenToUse:\s*"(.+)"/)?.[1]?.trim()) || `Use for ${title} tasks`; const roleDefinition = (yaml.match(/roleDefinition:\s*"(.+)"/)?.[1]?.trim()) || `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`; const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); const customInstructions = `CRITICAL 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`; // Add permissions from config if they exist const agentPermission = permissions[agentId]; // Begin .kilocodemodes block newContent += ` - slug: ${slug}\n`; newContent += ` name: '${icon} ${title}'\n`; if (agentPermission) { newContent += ` description: '${agentPermission.description}'\n`; } newContent += ` roleDefinition: ${roleDefinition}\n`; newContent += ` whenToUse: ${whenToUse}\n`; newContent += ` customInstructions: ${customInstructions}\n`; newContent += ` groups:\n`; newContent += ` - read\n`; if (agentPermission) { newContent += ` - - edit\n`; newContent += ` - fileRegex: ${agentPermission.fileRegex}\n`; newContent += ` description: ${agentPermission.description}\n`; } else { // Fallback to generic edit newContent += ` - edit\n`; } console.log(chalk.green(`✓ Added Kilo mode: ${slug} (${icon} ${title})`)); } const finalContent = existingContent ? existingContent.trim() + "\n" + newContent : "customModes:\n" + newContent; await fileManager.writeFile(filePath, finalContent); console.log(chalk.green("✓ Created .kilocodemodes file in project root")); console.log(chalk.green(`✓ KiloCode setup complete!`)); console.log(chalk.dim("Custom modes will be available when you open this project in KiloCode")); return true; } } module.exports = new IdeSetup();