const path = require('node:path'); const fs = require('fs-extra'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer'); const { loadModuleInjectionConfig, shouldApplyInjection, filterAgentInstructions, resolveSubagentFiles, } = require('./shared/module-injections'); const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts'); const { customAgentColonName } = require('./shared/path-utils'); const prompts = require('../../../lib/prompts'); /** * Claude Code IDE setup handler * * Uses UnifiedInstaller for standard artifact installation, * plus Claude-specific subagent injection handling. */ console.log(`[DEBUG CLAUDE-CODE] Module loaded!`); class ClaudeCodeSetup extends BaseIdeSetup { constructor() { super('claude-code', 'Claude Code', true); this.configDir = '.claude'; this.commandsDir = 'commands'; this.agentsDir = 'agents'; } /** * Prompt for subagent installation location */ async promptInstallLocation() { return prompts.select({ message: 'Where would you like to install Claude Code subagents?', choices: [ { name: 'Project level (.claude/agents/)', value: 'project' }, { name: 'User level (~/.claude/agents/)', value: 'user' }, ], default: 'project', }); } /** * Cleanup old BMAD installation before reinstalling */ async cleanup(projectDir) { const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); // Remove ANY bmad folder or files at any level const bmadPath = path.join(commandsDir, 'bmad'); if (await fs.pathExists(bmadPath)) { await fs.remove(bmadPath); console.log(chalk.dim(` Removed old bmad folder from ${this.name}`)); } // Also remove any bmad* files at root level if (await fs.pathExists(commandsDir)) { const entries = await fs.readdir(commandsDir); let removedCount = 0; for (const entry of entries) { if (entry.startsWith('bmad')) { await fs.remove(path.join(commandsDir, entry)); removedCount++; } } } } /** * Setup Claude Code IDE configuration */ async setup(projectDir, bmadDir, options = {}) { console.log(`[DEBUG CLAUDE-CODE] setup called! projectDir=${projectDir}`); this.projectDir = projectDir; console.log(chalk.cyan(`Setting up ${this.name}...`)); await this.cleanup(projectDir); const claudeDir = path.join(projectDir, this.configDir); const commandsDir = path.join(claudeDir, this.commandsDir); await this.ensureDir(commandsDir); // Use the unified installer for standard artifacts const installer = new UnifiedInstaller(this.bmadFolderName); console.log(`[DEBUG CLAUDE-CODE] About to call installer.install, targetDir=${commandsDir}`); const counts = await installer.install( projectDir, bmadDir, { targetDir: commandsDir, namingStyle: NamingStyle.FLAT_COLON, templateType: TemplateType.CLAUDE, }, options.selectedModules || [], ); console.log(`[DEBUG CLAUDE-CODE] installer.install done, counts=`, counts); // Process Claude Code specific injections for installed modules if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) { await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {}); } else if (options.preCollectedConfig) { await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig); } else { await this.processModuleInjections(projectDir, bmadDir, options); } console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${counts.agents} agents installed`)); if (counts.workflows > 0) { console.log(chalk.dim(` - ${counts.workflows} workflow commands generated`)); } if (counts.tasks + counts.tools > 0) { console.log( chalk.dim(` - ${counts.tasks + counts.tools} task/tool commands generated (${counts.tasks} tasks, ${counts.tools} tools)`), ); } console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, commandsDir)}`)); return { success: true, agents: counts.agents, }; } /** * Read and process file content */ async readAndProcess(filePath, metadata) { const content = await fs.readFile(filePath, 'utf8'); return this.processContent(content, metadata); } /** * Override processContent to keep {project-root} placeholder */ processContent(content, metadata = {}) { return super.processContent(content, metadata); } /** * Get agents from source modules (not installed location) */ async getAgentsFromSource(sourceDir, selectedModules) { const agents = []; const corePath = getModulePath('core'); if (await fs.pathExists(path.join(corePath, 'agents'))) { const coreAgents = await getAgentsFromDir(path.join(corePath, 'agents'), 'core'); agents.push(...coreAgents); } for (const moduleName of selectedModules) { const modulePath = path.join(sourceDir, moduleName); const agentsPath = path.join(modulePath, 'agents'); if (await fs.pathExists(agentsPath)) { const moduleAgents = await getAgentsFromDir(agentsPath, moduleName); agents.push(...moduleAgents); } } return agents; } /** * Process module injections with pre-collected configuration */ async processModuleInjectionsWithConfig(projectDir, bmadDir, options, preCollectedConfig) { const modules = options.selectedModules || []; const { subagentChoices, installLocation } = preCollectedConfig; await this.processModuleInjectionsInternal({ projectDir, modules, handler: 'claude-code', subagentChoices, installLocation, interactive: false, }); } /** * Process Claude Code specific injections for installed modules */ async processModuleInjections(projectDir, bmadDir, options) { const modules = options.selectedModules || []; let subagentChoices = null; let installLocation = null; const { subagentChoices: updatedChoices, installLocation: updatedLocation } = await this.processModuleInjectionsInternal({ projectDir, modules, handler: 'claude-code', subagentChoices, installLocation, interactive: true, }); if (updatedChoices) { subagentChoices = updatedChoices; } if (updatedLocation) { installLocation = updatedLocation; } } async processModuleInjectionsInternal({ projectDir, modules, handler, subagentChoices, installLocation, interactive = false }) { console.log(`[DEBUG CLAUDE-CODE] processModuleInjectionsInternal called! modules=${modules.join(',')}`); let choices = subagentChoices; let location = installLocation; for (const moduleName of modules) { const configData = await loadModuleInjectionConfig(handler, moduleName); if (!configData) { continue; } const { config, handlerBaseDir } = configData; if (interactive) { console.log(chalk.cyan(`\nConfiguring ${moduleName} ${handler.replace('-', ' ')} features...`)); } if (interactive && config.subagents && !choices) { // choices = await this.promptSubagentInstallation(config.subagents); // if (choices.install !== 'none') { // location = await this.promptInstallLocation(); // } } if (config.injections && choices && choices.install !== 'none') { for (const injection of config.injections) { if (shouldApplyInjection(injection, choices)) { await this.injectContent(projectDir, injection, choices); } } } if (config.subagents && choices && choices.install !== 'none') { await this.copySelectedSubagents(projectDir, handlerBaseDir, config.subagents, choices, location || 'project'); } } return { subagentChoices: choices, installLocation: location }; } /** * Prompt user for subagent installation preferences */ async promptSubagentInstallation(subagentConfig) { const install = await prompts.select({ message: 'Would you like to install Claude Code subagents for enhanced functionality?', choices: [ { name: 'Yes, install all subagents', value: 'all' }, { name: 'Yes, let me choose specific subagents', value: 'selective' }, { name: 'No, skip subagent installation', value: 'none' }, ], default: 'all', }); if (install === 'selective') { const subagentInfo = { 'market-researcher.md': 'Market research and competitive analysis', 'requirements-analyst.md': 'Requirements extraction and validation', 'technical-evaluator.md': 'Technology stack evaluation', 'epic-optimizer.md': 'Epic and story breakdown optimization', 'document-reviewer.md': 'Document quality review', }; const selected = await prompts.multiselect({ message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`, options: subagentConfig.files.map((file) => ({ label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, value: file, })), initialValues: subagentConfig.files, }); return { install: 'selective', selected }; } return { install }; } /** * Inject content at specified point in file */ async injectContent(projectDir, injection, subagentChoices = null) { const targetPath = path.join(projectDir, injection.file); if (await this.exists(targetPath)) { let content = await fs.readFile(targetPath, 'utf8'); const marker = ``; if (content.includes(marker)) { let injectionContent = injection.content; if (subagentChoices && subagentChoices.install === 'selective' && injection.point === 'pm-agent-instructions') { injectionContent = filterAgentInstructions(injection.content, subagentChoices.selected); } content = content.replace(marker, injectionContent); await fs.writeFile(targetPath, content); console.log(chalk.dim(` Injected: ${injection.point} → ${injection.file}`)); } } } /** * Copy selected subagents to appropriate Claude agents directory */ async copySelectedSubagents(projectDir, handlerBaseDir, subagentConfig, choices, location) { const os = require('node:os'); let targetDir; if (location === 'user') { targetDir = path.join(os.homedir(), '.claude', 'agents'); console.log(chalk.dim(` Installing subagents globally to: ~/.claude/agents/`)); } else { targetDir = path.join(projectDir, '.claude', 'agents'); console.log(chalk.dim(` Installing subagents to project: .claude/agents/`)); } await this.ensureDir(targetDir); const resolvedFiles = await resolveSubagentFiles(handlerBaseDir, subagentConfig, choices); let copiedCount = 0; for (const resolved of resolvedFiles) { try { const sourcePath = resolved.absolutePath; const subFolder = path.dirname(resolved.relativePath); let targetPath; if (subFolder && subFolder !== '.') { const targetSubDir = path.join(targetDir, subFolder); await this.ensureDir(targetSubDir); targetPath = path.join(targetSubDir, path.basename(resolved.file)); } else { targetPath = path.join(targetDir, path.basename(resolved.file)); } await fs.copyFile(sourcePath, targetPath); console.log(chalk.green(` ✓ Installed: ${subFolder === '.' ? '' : `${subFolder}/`}${path.basename(resolved.file, '.md')}`)); copiedCount++; } catch (error) { console.log(chalk.yellow(` ⚠ Error copying ${resolved.file}: ${error.message}`)); } } if (copiedCount > 0) { console.log(chalk.dim(` Total subagents installed: ${copiedCount}`)); } } /** * Install a custom agent launcher for Claude Code */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { const commandsDir = path.join(projectDir, this.configDir, this.commandsDir); if (!(await this.exists(path.join(projectDir, this.configDir)))) { return null; } await this.ensureDir(commandsDir); const launcherContent = `--- name: '${agentName}' description: '${agentName} agent' --- You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. 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 `; 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 };