const path = require('node:path'); const fs = require('fs-extra'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { loadModuleInjectionConfig, shouldApplyInjection, filterAgentInstructions, resolveSubagentFiles, } = require('./shared/module-injections'); const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts'); const { customAgentColonName } = require('./shared/path-utils'); const prompts = require('../../../lib/prompts'); /** * Claude Code IDE setup handler */ class ClaudeCodeSetup extends BaseIdeSetup { constructor() { super('claude-code', 'Claude Code', true); // preferred IDE this.configDir = '.claude'; this.commandsDir = 'commands'; this.agentsDir = 'agents'; } /** * Prompt for subagent installation location * @returns {Promise} Selected location ('project' or 'user') */ async promptInstallLocation() { return prompts.select({ message: 'Where would you like to install Claude Code subagents?', choices: [ { name: 'Project level (.claude/agents/)', value: 'project' }, { name: 'User level (~/.claude/agents/)', value: 'user' }, ], default: 'project', }); } // /** // * Collect configuration choices before installation // * @param {Object} options - Configuration options // * @returns {Object} Collected configuration // */ // async collectConfiguration(options = {}) { // const config = { // subagentChoices: null, // installLocation: null, // }; // const sourceModulesPath = getSourcePath('modules'); // const modules = options.selectedModules || []; // for (const moduleName of modules) { // // Check for Claude Code sub-module injection config in SOURCE directory // const injectionConfigPath = path.join(sourceModulesPath, moduleName, 'sub-modules', 'claude-code', 'injections.yaml'); // if (await this.exists(injectionConfigPath)) { // const yaml = require('yaml'); // try { // // Load injection configuration // const configContent = await fs.readFile(injectionConfigPath, 'utf8'); // const injectionConfig = yaml.parse(configContent); // // Ask about subagents if they exist and we haven't asked yet // if (injectionConfig.subagents && !config.subagentChoices) { // config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents); // if (config.subagentChoices.install !== 'none') { // config.installLocation = await this.promptInstallLocation(); // } // } // } catch (error) { // console.log(chalk.yellow(` Warning: Failed to process ${moduleName} features: ${error.message}`)); // } // } // } // return config; // } /** * Cleanup old BMAD installation before reinstalling * @param {string} projectDir - Project directory */ async cleanup(projectDir) { const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); // Remove any bmad:* files from the commands directory if (await fs.pathExists(commandsDir)) { const entries = await fs.readdir(commandsDir); let removedCount = 0; for (const entry of entries) { if (entry.startsWith('bmad:')) { await fs.remove(path.join(commandsDir, entry)); removedCount++; } } // Also remove legacy bmad folder if it exists const bmadFolder = path.join(commandsDir, 'bmad'); if (await fs.pathExists(bmadFolder)) { await fs.remove(bmadFolder); console.log(chalk.dim(` Removed old BMAD commands from ${this.name}`)); } } } /** * Clean up legacy folder structure (module/type/name.md) if it exists * This can be called after migration to remove old nested directories * @param {string} projectDir - Project directory */ async cleanupLegacyFolders(projectDir) { const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); if (!(await fs.pathExists(commandsDir))) { return; } // Remove legacy bmad folder if it exists const bmadFolder = path.join(commandsDir, 'bmad'); if (await fs.pathExists(bmadFolder)) { await fs.remove(bmadFolder); console.log(chalk.dim(` Removed legacy bmad folder from ${this.name}`)); } } /** * Setup Claude Code IDE configuration * @param {string} projectDir - Project directory * @param {string} bmadDir - BMAD installation directory * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { // Store project directory for use in processContent this.projectDir = projectDir; console.log(chalk.cyan(`Setting up ${this.name}...`)); // Clean up old BMAD installation first await this.cleanup(projectDir); // Create .claude/commands directory structure const claudeDir = path.join(projectDir, this.configDir); const commandsDir = path.join(claudeDir, this.commandsDir); await this.ensureDir(commandsDir); // Use colon format: files written directly to commands dir (no bmad subfolder) // Creates: .claude/commands/bmad:bmm:pm.md // Generate agent launchers using AgentCommandGenerator // This creates small launcher files that reference the actual agents in _bmad/ const agentGen = new AgentCommandGenerator(this.bmadFolderName); const { artifacts: agentArtifacts, counts: agentCounts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); // Write agent launcher files using flat colon naming // Creates files like: bmad:bmm:pm.md const agentCount = await agentGen.writeColonArtifacts(commandsDir, agentArtifacts); // Process Claude Code specific injections for installed modules // Use pre-collected configuration if available, or skip if already configured if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) { // IDE is already configured from previous installation, skip prompting // Just process with default/existing configuration await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {}); } else if (options.preCollectedConfig) { await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig); } else { await this.processModuleInjections(projectDir, bmadDir, options); } // Skip CLAUDE.md creation - let user manage their own CLAUDE.md file // await this.createClaudeConfig(projectDir, modules); // Generate workflow commands from manifest (if it exists) const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); const { artifacts: workflowArtifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); // Write workflow-command artifacts using flat colon naming // Creates files like: bmad:bmm:correct-course.md const workflowCommandCount = await workflowGen.writeColonArtifacts(commandsDir, workflowArtifacts); // Generate task and tool commands from manifests (if they exist) const taskToolGen = new TaskToolCommandGenerator(); const taskToolResult = await taskToolGen.generateColonTaskToolCommands(projectDir, bmadDir, commandsDir); console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents installed`)); if (workflowCommandCount > 0) { console.log(chalk.dim(` - ${workflowCommandCount} workflow commands generated`)); } if (taskToolResult.generated > 0) { console.log( chalk.dim( ` - ${taskToolResult.generated} task/tool commands generated (${taskToolResult.tasks} tasks, ${taskToolResult.tools} tools)`, ), ); } console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); return { success: true, agents: agentCount, }; } // Method removed - CLAUDE.md file management left to user /** * Read and process file content */ async readAndProcess(filePath, metadata) { const content = await fs.readFile(filePath, 'utf8'); return this.processContent(content, metadata); } /** * Override processContent to keep {project-root} placeholder */ processContent(content, metadata = {}) { // Use the base class method WITHOUT projectDir to preserve {project-root} placeholder return super.processContent(content, metadata); } /** * Get agents from source modules (not installed location) */ async getAgentsFromSource(sourceDir, selectedModules) { const agents = []; // Add core agents const corePath = getModulePath('core'); if (await fs.pathExists(path.join(corePath, 'agents'))) { const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core'); agents.push(...coreAgents); } // Add module agents for (const moduleName of selectedModules) { const modulePath = path.join(sourceDir, moduleName); const agentsPath = path.join(modulePath, 'agents'); if (await fs.pathExists(agentsPath)) { const moduleAgents = await getAgentsFromDir(agentsPath, moduleName); agents.push(...moduleAgents); } } return agents; } /** * Process module injections with pre-collected configuration */ async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) { // Get list of installed modules const modules = options.selectedModules || []; const { subagentChoices, installLocation } = preCollectedConfig; // Get the actual source directory (not the installation directory) await this.processModuleInjectionsInternal({ projectDir, modules, handler: 'claude-code', subagentChoices, installLocation, interactive: false, }); } /** * Process Claude Code specific injections for installed modules * Looks for injections.yaml in each module's claude-code sub-module */ async processModuleInjections(projectDir, bmadDir, options) { // Get list of installed modules const modules = options.selectedModules || []; let subagentChoices = null; let installLocation = null; // Get the actual source directory (not the installation directory) const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ projectDir, modules, handler: 'claude-code', subagentChoices, installLocation, interactive: true, }); if (updatedChoices) { subagentChoices = updatedChoices; } if (updatedLocation) { installLocation = updatedLocation; } } async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) { let choices = subagentChoices; let location = installLocation; for (const moduleName of modules) { const configData = await loadModuleInjectionConfig(handler, moduleName); if (!configData) { continue; } const { config, handlerBaseDir } = configData; if (interactive) { console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler.replace('-', ' ')} features...`)); } if (interactive && config.subagents && !choices) { // choices = await this.promptSubagentInstallation(config.subagents); // if (choices.install !== 'none') { // location = await this.promptInstallLocation(); // } } if (config.injections && choices && choices.install !== 'none') { for (const injection of config.injections) { if (shouldApplyInjection(injection, choices)) { await this.injectContent(projectDir, injection, choices); } } } if (config.subagents && choices && choices.install !== 'none') { await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project'); } } return { subagentChoices: choices, installLocation: location }; } /** * Prompt user for subagent installation preferences */ async promptSubagentInstallation(subagentConfig) { // First ask if they want to install subagents const install = await prompts.select({ message: 'Would you like to install Claude Code subagents for enhanced functionality?', choices: [ { name: 'Yes, install all subagents', value: 'all' }, { name: 'Yes, let me choose specific subagents', value: 'selective' }, { name: 'No, skip subagent installation', value: 'none' }, ], default: 'all', }); if (install === 'selective') { // Show list of available subagents with descriptions const subagentInfo = { 'market-researcher.md': 'Market research and competitive analysis', 'requirements-analyst.md': 'Requirements extraction and validation', 'technical-evaluator.md': 'Technology stack evaluation', 'epic-optimizer.md': 'Epic and story breakdown optimization', 'document-reviewer.md': 'Document quality review', }; const selected = await prompts.multiselect({ message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, options: subagentConfig.files.map((file) => ({ label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, value: file, })), initialValues: subagentConfig.files, }); return { install: 'selective', selected }; } return { install }; } /** * Inject content at specified point in file */ async injectContent(projectDir, injection, subagentChoices = null) { const targetPath = path.join(projectDir, injection.file); if (await this.exists(targetPath)) { let content = await fs.readFile(targetPath, 'utf8'); const marker = ``; if (content.includes(marker)) { let injectionContent = injection.content; // Filter content if selective subagents chosen if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') { injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected); } content = content.replace(marker, injectionContent); await fs.writeFile(targetPath, content); console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`)); } } } /** * Copy selected subagents to appropriate Claude agents directory */ async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) { const os = require('node:os'); // Determine target directory based on user choice let targetDir; if (location === 'user') { targetDir = path.join(os.homedir(), '.claude', 'agents'); console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`)); } else { targetDir = path.join(projectDir, '.claude', 'agents'); console.log(chalk.dim(` Installing subagents to project: .claude/agents/`)); } // Ensure target directory exists await this.ensureDir(targetDir); const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); let copiedCount = 0; for (const resolved of resolvedFiles) { try { const sourcePath = resolved.absolutePath; const subFolder = path.dirname(resolved.relativePath); let targetPath; if (subFolder && subFolder !== '.') { const targetSubDir = path.join(targetDir, subFolder); await this.ensureDir(targetSubDir); targetPath = path.join(targetSubDir, path.basename(resolved.file)); } else { targetPath = path.join(targetDir, path.basename(resolved.file)); } await fs.copyFile(sourcePath, targetPath); console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`)); copiedCount++; } catch (error) { console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`)); } } if (copiedCount > 0) { console.log(chalk.dim(` Total subagents installed: ${copiedCount}`)); } } /** * Install a custom agent launcher for Claude Code * @param {string} projectDir - Project directory * @param {string} agentName - Agent name (e.g., "fred-commit-poet") * @param {string} agentPath - Path to compiled agent (relative to project root) * @param {Object} metadata - Agent metadata * @returns {Object|null} Info about created command */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); if (!(await this.exists(path.join(projectDir, this.configDir)))) { return null; // IDE not configured for this project } await this.ensureDir(commandsDir); const launcherContent = `--- name: '${agentName}' description: '${agentName} agent' --- You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. 1. LOAD the FULL agent file from @${agentPath} 2. READ its entire contents - this contains the complete agent persona, menu, and instructions 3. FOLLOW every step in the section precisely 4. DISPLAY the welcome/greeting as instructed 5. PRESENT the numbered menu 6. WAIT for user input before proceeding `; // Use colon format: bmad:custom:agents:fred-commit-poet.md // Written directly to commands dir (no bmad subfolder) const launcherName = customAgentColonName(agentName); const launcherPath = path.join(commandsDir, launcherName); await this.writeFile(launcherPath, launcherContent); return { path: launcherPath, command: `/${launcherName.replace('.md', '')}`, }; } } module.exports = { ClaudeCodeSetup };