diff --git a/.gitignore b/.gitignore index f700eb24..c57b48e0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ test-project-install/* sample-project/* flattened-codebase.xml *.stats.md +dist/ diff --git a/tools/installer/bin/bmad.js b/tools/installer/bin/bmad.js index 39816407..f8bc8a9a 100755 --- a/tools/installer/bin/bmad.js +++ b/tools/installer/bin/bmad.js @@ -1,201 +1,219 @@ #!/usr/bin/env node -const { program } = require('commander'); -const path = require('node:path'); -const fs = require('node:fs').promises; -const yaml = require('js-yaml'); -const chalk = require('chalk').default || require('chalk'); -const inquirer = require('inquirer').default || require('inquirer'); -const semver = require('semver'); -const https = require('node:https'); +const { program } = require("commander"); +const path = require("node:path"); +const fs = require("node:fs").promises; +const yaml = require("js-yaml"); +const chalk = require("chalk").default || require("chalk"); +const inquirer = require("inquirer").default || require("inquirer"); +const semver = require("semver"); +const https = require("node:https"); // Handle both execution contexts (from root via npx or from installer directory) let version; let installer; let packageName; try { - // Try installer context first (when run from tools/installer/) - version = require('../package.json').version; - packageName = require('../package.json').name; - installer = require('../lib/installer'); + // Try installer context first (when run from tools/installer/) + version = require("../package.json").version; + packageName = require("../package.json").name; + installer = require("../lib/installer"); } catch (error) { - // Fall back to root context (when run via npx from GitHub) - console.log(`Installer context not found (${error.message}), trying root context...`); - try { - version = require('../../../package.json').version; - installer = require('../../../tools/installer/lib/installer'); - } catch (error) { - console.error( - 'Error: Could not load required modules. Please ensure you are running from the correct directory.', - ); - console.error('Debug info:', { - __dirname, - cwd: process.cwd(), - error: error.message, - }); - process.exit(1); - } + // Fall back to root context (when run via npx from GitHub) + console.log( + `Installer context not found (${error.message}), trying root context...`, + ); + try { + version = require("../../../package.json").version; + installer = require("../../../tools/installer/lib/installer"); + } catch (error) { + console.error( + "Error: Could not load required modules. Please ensure you are running from the correct directory.", + ); + console.error("Debug info:", { + __dirname, + cwd: process.cwd(), + error: error.message, + }); + process.exit(1); + } } program - .version(version) - .description('BMad Method installer - Universal AI agent framework for any domain'); + .version(version) + .description( + "BMad Method installer - Universal AI agent framework for any domain", + ); program - .command('install') - .description('Install BMad Method agents and tools') - .option('-f, --full', 'Install complete BMad Method') - .option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)') - .option('-d, --directory ', 'Installation directory') - .option( - '-i, --ide ', - 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, codex, codex-web, auggie-cli, iflow-cli, other)', - ) - .option( - '-e, --expansion-packs ', - 'Install specific expansion packs (can specify multiple)', - ) - .action(async (options) => { - try { - if (!options.full && !options.expansionOnly) { - // Interactive mode - const answers = await promptInstallation(); - if (!answers._alreadyInstalled) { - await installer.install(answers); - process.exit(0); - } - } else { - // Direct mode - let installType = 'full'; - if (options.expansionOnly) installType = 'expansion-only'; + .command("install") + .description("Install BMad Method agents and tools") + .option("-f, --full", "Install complete BMad Method") + .option("-x, --expansion-only", "Install only expansion packs (no bmad-core)") + .option("-d, --directory ", "Installation directory") + .option( + "-i, --ide ", + "Configure for specific IDE(s) - can specify multiple (opencode, cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, codex, codex-web, auggie-cli, iflow-cli, other)", + ) + .option( + "-e, --expansion-packs ", + "Install specific expansion packs (can specify multiple)", + ) + .action(async (options) => { + try { + if (!options.full && !options.expansionOnly) { + // Interactive mode + const answers = await promptInstallation(); + if (!answers._alreadyInstalled) { + await installer.install(answers); + process.exit(0); + } + } else { + // Direct mode + let installType = "full"; + if (options.expansionOnly) installType = "expansion-only"; - const config = { - installType, - directory: options.directory || '.', - ides: (options.ide || []).filter((ide) => ide !== 'other'), - expansionPacks: options.expansionPacks || [], - }; - await installer.install(config); - process.exit(0); - } - } catch (error) { - console.error(chalk.red('Installation failed:'), error.message); - process.exit(1); - } - }); + const config = { + installType, + directory: options.directory || ".", + ides: (options.ide || []).filter((ide) => ide !== "other"), + expansionPacks: options.expansionPacks || [], + }; + await installer.install(config); + process.exit(0); + } + } catch (error) { + console.error(chalk.red("Installation failed:"), error.message); + process.exit(1); + } + }); program - .command('update') - .description('Update existing BMad installation') - .option('--force', 'Force update, overwriting modified files') - .option('--dry-run', 'Show what would be updated without making changes') - .action(async () => { - try { - await installer.update(); - } catch (error) { - console.error(chalk.red('Update failed:'), error.message); - process.exit(1); - } - }); + .command("update") + .description("Update existing BMad installation") + .option("--force", "Force update, overwriting modified files") + .option("--dry-run", "Show what would be updated without making changes") + .action(async () => { + try { + await installer.update(); + } catch (error) { + console.error(chalk.red("Update failed:"), error.message); + process.exit(1); + } + }); // Command to check if updates are available program - .command('update-check') - .description('Check for BMad Update') - .action(async () => { - console.log('Checking for updates...'); + .command("update-check") + .description("Check for BMad Update") + .action(async () => { + console.log("Checking for updates..."); - // Make HTTP request to npm registry for latest version info - const req = https.get(`https://registry.npmjs.org/${packageName}/latest`, (res) => { - // Check for HTTP errors (non-200 status codes) - if (res.statusCode !== 200) { - console.error(chalk.red(`Update check failed: Received status code ${res.statusCode}`)); - return; - } + // Make HTTP request to npm registry for latest version info + const req = https.get( + `https://registry.npmjs.org/${packageName}/latest`, + (res) => { + // Check for HTTP errors (non-200 status codes) + if (res.statusCode !== 200) { + console.error( + chalk.red( + `Update check failed: Received status code ${res.statusCode}`, + ), + ); + return; + } - // Accumulate response data chunks - let data = ''; - res.on('data', (chunk) => (data += chunk)); + // Accumulate response data chunks + let data = ""; + res.on("data", (chunk) => (data += chunk)); - // Process complete response - res.on('end', () => { - try { - // Parse npm registry response and extract version - const latest = JSON.parse(data).version; + // Process complete response + res.on("end", () => { + try { + // Parse npm registry response and extract version + const latest = JSON.parse(data).version; - // Compare versions using semver - if (semver.gt(latest, version)) { - console.log( - chalk.bold.blue(`⚠️ ${packageName} update available: ${version} → ${latest}`), - ); - console.log(chalk.bold.blue('\nInstall latest by running:')); - console.log(chalk.bold.magenta(` npm install ${packageName}@latest`)); - console.log(chalk.dim(' or')); - console.log(chalk.bold.magenta(` npx ${packageName}@latest`)); - } else { - console.log(chalk.bold.blue(`✨ ${packageName} is up to date`)); - } - } catch (error) { - // Handle JSON parsing errors - console.error(chalk.red('Failed to parse npm registry data:'), error.message); - } - }); - }); + // Compare versions using semver + if (semver.gt(latest, version)) { + console.log( + chalk.bold.blue( + `! ${packageName} update available: ${version} → ${latest}`, + ), + ); + console.log(chalk.bold.blue("\nInstall latest by running:")); + console.log( + chalk.bold.magenta(` npm install ${packageName}@latest`), + ); + console.log(chalk.dim(" or")); + console.log(chalk.bold.magenta(` npx ${packageName}@latest`)); + } else { + console.log(chalk.bold.blue(`✨ ${packageName} is up to date`)); + } + } catch (error) { + // Handle JSON parsing errors + console.error( + chalk.red("Failed to parse npm registry data:"), + error.message, + ); + } + }); + }, + ); - // Handle network/connection errors - req.on('error', (error) => { - console.error(chalk.red('Update check failed:'), error.message); - }); + // Handle network/connection errors + req.on("error", (error) => { + console.error(chalk.red("Update check failed:"), error.message); + }); - // Set 30 second timeout to prevent hanging - req.setTimeout(30_000, () => { - req.destroy(); - console.error(chalk.red('Update check timed out')); - }); - }); + // Set 30 second timeout to prevent hanging + req.setTimeout(30_000, () => { + req.destroy(); + console.error(chalk.red("Update check timed out")); + }); + }); program - .command('list:expansions') - .description('List available expansion packs') - .action(async () => { - try { - await installer.listExpansionPacks(); - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); + .command("list:expansions") + .description("List available expansion packs") + .action(async () => { + try { + await installer.listExpansionPacks(); + } catch (error) { + console.error(chalk.red("Error:"), error.message); + process.exit(1); + } + }); program - .command('status') - .description('Show installation status') - .action(async () => { - try { - await installer.showStatus(); - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); + .command("status") + .description("Show installation status") + .action(async () => { + try { + await installer.showStatus(); + } catch (error) { + console.error(chalk.red("Error:"), error.message); + process.exit(1); + } + }); program - .command('flatten') - .description('Flatten codebase to XML format') - .option('-i, --input ', 'Input directory to flatten', process.cwd()) - .option('-o, --output ', 'Output file path', 'flattened-codebase.xml') - .action(async (options) => { - try { - await installer.flatten(options); - } catch (error) { - console.error(chalk.red('Flatten failed:'), error.message); - process.exit(1); - } - }); + .command("flatten") + .description("Flatten codebase to XML format") + .option("-i, --input ", "Input directory to flatten", process.cwd()) + .option("-o, --output ", "Output file path", "flattened-codebase.xml") + .action(async (options) => { + try { + await installer.flatten(options); + } catch (error) { + console.error(chalk.red("Flatten failed:"), error.message); + process.exit(1); + } + }); async function promptInstallation() { - // Display ASCII logo - console.log( - chalk.bold.cyan(` + // Display ASCII logo + console.log( + chalk.bold.cyan(` ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗ ██████╔╝██╔████╔██║███████║██║ ██║█████╗██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║ @@ -203,420 +221,456 @@ async function promptInstallation() { ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ `), - ); + ); - console.log(chalk.bold.magenta('🚀 Universal AI Agent Framework for Any Domain')); - console.log(chalk.bold.blue(`✨ Installer v${version}\n`)); + console.log( + chalk.bold.magenta("🚀 Universal AI Agent Framework for Any Domain"), + ); + console.log(chalk.bold.blue(`✨ Installer v${version}\n`)); - const answers = {}; + const answers = {}; - // Ask for installation directory first - const { directory } = await inquirer.prompt([ - { - type: 'input', - name: 'directory', - message: 'Enter the full path to your project directory where BMad should be installed:', - default: path.resolve('.'), - validate: (input) => { - if (!input.trim()) { - return 'Please enter a valid project path'; - } - return true; - }, - }, - ]); - answers.directory = directory; + // Ask for installation directory first + const { directory } = await inquirer.prompt([ + { + type: "input", + name: "directory", + message: + "Enter the full path to your project directory where BMad should be installed:", + default: path.resolve("."), + validate: (input) => { + if (!input.trim()) { + return "Please enter a valid project path"; + } + return true; + }, + }, + ]); + answers.directory = directory; - // Detect existing installations - const installDir = path.resolve(directory); - const state = await installer.detectInstallationState(installDir); + // Detect existing installations + const installDir = path.resolve(directory); + const state = await installer.detectInstallationState(installDir); - // Check for existing expansion packs - const existingExpansionPacks = state.expansionPacks || {}; + // Check for existing expansion packs + const existingExpansionPacks = state.expansionPacks || {}; - // Get available expansion packs - const availableExpansionPacks = await installer.getAvailableExpansionPacks(); + // Get available expansion packs + const availableExpansionPacks = await installer.getAvailableExpansionPacks(); - // Build choices list - const choices = []; + // Build choices list + const choices = []; - // Load core config to get short-title - const coreConfigPath = path.join(__dirname, '..', '..', '..', 'bmad-core', 'core-config.yaml'); - const coreConfig = yaml.load(await fs.readFile(coreConfigPath, 'utf8')); - const coreShortTitle = coreConfig['short-title'] || 'BMad Agile Core System'; + // Load core config to get short-title + const coreConfigPath = path.join( + __dirname, + "..", + "..", + "..", + "bmad-core", + "core-config.yaml", + ); + const coreConfig = yaml.load(await fs.readFile(coreConfigPath, "utf8")); + const coreShortTitle = coreConfig["short-title"] || "BMad Agile Core System"; - // Add BMad core option - let bmadOptionText; - if (state.type === 'v4_existing') { - const currentVersion = state.manifest?.version || 'unknown'; - const newVersion = version; // Always use package.json version - const versionInfo = - currentVersion === newVersion - ? `(v${currentVersion} - reinstall)` - : `(v${currentVersion} → v${newVersion})`; - bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`; - } else { - bmadOptionText = `${coreShortTitle} (v${version}) .bmad-core`; - } + // Add BMad core option + let bmadOptionText; + if (state.type === "v4_existing") { + const currentVersion = state.manifest?.version || "unknown"; + const newVersion = version; // Always use package.json version + const versionInfo = + currentVersion === newVersion + ? `(v${currentVersion} - reinstall)` + : `(v${currentVersion} → v${newVersion})`; + bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`; + } else { + bmadOptionText = `${coreShortTitle} (v${version}) .bmad-core`; + } - choices.push({ - name: bmadOptionText, - value: 'bmad-core', - checked: true, - }); + choices.push({ + name: bmadOptionText, + value: "bmad-core", + checked: true, + }); - // Add expansion pack options - for (const pack of availableExpansionPacks) { - const existing = existingExpansionPacks[pack.id]; - let packOptionText; + // Add expansion pack options + for (const pack of availableExpansionPacks) { + const existing = existingExpansionPacks[pack.id]; + let packOptionText; - if (existing) { - const currentVersion = existing.manifest?.version || 'unknown'; - const newVersion = pack.version; - const versionInfo = - currentVersion === newVersion - ? `(v${currentVersion} - reinstall)` - : `(v${currentVersion} → v${newVersion})`; - packOptionText = `Update ${pack.shortTitle} ${versionInfo} .${pack.id}`; - } else { - packOptionText = `${pack.shortTitle} (v${pack.version}) .${pack.id}`; - } + if (existing) { + const currentVersion = existing.manifest?.version || "unknown"; + const newVersion = pack.version; + const versionInfo = + currentVersion === newVersion + ? `(v${currentVersion} - reinstall)` + : `(v${currentVersion} → v${newVersion})`; + packOptionText = `Update ${pack.shortTitle} ${versionInfo} .${pack.id}`; + } else { + packOptionText = `${pack.shortTitle} (v${pack.version}) .${pack.id}`; + } - choices.push({ - name: packOptionText, - value: pack.id, - checked: false, - }); - } + choices.push({ + name: packOptionText, + value: pack.id, + checked: false, + }); + } - // Ask what to install - const { selectedItems } = await inquirer.prompt([ - { - type: 'checkbox', - name: 'selectedItems', - message: 'Select what to install/update (use space to select, enter to continue):', - choices: choices, - validate: (selected) => { - if (selected.length === 0) { - return 'Please select at least one item to install'; - } - return true; - }, - }, - ]); + // Ask what to install + const { selectedItems } = await inquirer.prompt([ + { + type: "checkbox", + name: "selectedItems", + message: + "Select what to install/update (use space to select, enter to continue):", + choices: choices, + validate: (selected) => { + if (selected.length === 0) { + return "Please select at least one item to install"; + } + return true; + }, + }, + ]); - // Process selections - answers.installType = selectedItems.includes('bmad-core') ? 'full' : 'expansion-only'; - answers.expansionPacks = selectedItems.filter((item) => item !== 'bmad-core'); + // Process selections + answers.installType = selectedItems.includes("bmad-core") + ? "full" + : "expansion-only"; + answers.expansionPacks = selectedItems.filter((item) => item !== "bmad-core"); - // Ask sharding questions if installing BMad core - if (selectedItems.includes('bmad-core')) { - console.log(chalk.cyan('\n📋 Document Organization Settings')); - console.log(chalk.dim('Configure how your project documentation should be organized.\n')); + // Ask sharding questions if installing BMad core + if (selectedItems.includes("bmad-core")) { + console.log(chalk.cyan("\n📋 Document Organization Settings")); + console.log( + chalk.dim( + "Configure how your project documentation should be organized.\n", + ), + ); - // Ask about PRD sharding - const { prdSharded } = await inquirer.prompt([ - { - type: 'confirm', - name: 'prdSharded', - message: 'Will the PRD (Product Requirements Document) be sharded into multiple files?', - default: true, - }, - ]); - answers.prdSharded = prdSharded; + // Ask about PRD sharding + const { prdSharded } = await inquirer.prompt([ + { + type: "confirm", + name: "prdSharded", + message: + "Will the PRD (Product Requirements Document) be sharded into multiple files?", + default: true, + }, + ]); + answers.prdSharded = prdSharded; - // Ask about architecture sharding - const { architectureSharded } = await inquirer.prompt([ - { - type: 'confirm', - name: 'architectureSharded', - message: 'Will the architecture documentation be sharded into multiple files?', - default: true, - }, - ]); - answers.architectureSharded = architectureSharded; + // Ask about architecture sharding + const { architectureSharded } = await inquirer.prompt([ + { + type: "confirm", + name: "architectureSharded", + message: + "Will the architecture documentation be sharded into multiple files?", + default: true, + }, + ]); + answers.architectureSharded = architectureSharded; - // Show warning if architecture sharding is disabled - if (!architectureSharded) { - console.log(chalk.yellow.bold('\n⚠️ IMPORTANT: Architecture Sharding Disabled')); - console.log( - chalk.yellow( - 'With architecture sharding disabled, you should still create the files listed', - ), - ); - console.log( - chalk.yellow( - 'in devLoadAlwaysFiles (like coding-standards.md, tech-stack.md, source-tree.md)', - ), - ); - console.log(chalk.yellow('as these are used by the dev agent at runtime.')); - console.log( - chalk.yellow( - '\nAlternatively, you can remove these files from the devLoadAlwaysFiles list', - ), - ); - console.log(chalk.yellow('in your core-config.yaml after installation.')); + // Show warning if architecture sharding is disabled + if (!architectureSharded) { + console.log( + chalk.yellow.bold("\n! IMPORTANT: Architecture Sharding Disabled"), + ); + console.log( + chalk.yellow( + "With architecture sharding disabled, you should still create the files listed", + ), + ); + console.log( + chalk.yellow( + "in devLoadAlwaysFiles (like coding-standards.md, tech-stack.md, source-tree.md)", + ), + ); + console.log( + chalk.yellow("as these are used by the dev agent at runtime."), + ); + console.log( + chalk.yellow( + "\nAlternatively, you can remove these files from the devLoadAlwaysFiles list", + ), + ); + console.log(chalk.yellow("in your core-config.yaml after installation.")); - const { acknowledge } = await inquirer.prompt([ - { - type: 'confirm', - name: 'acknowledge', - message: 'Do you acknowledge this requirement and want to proceed?', - default: false, - }, - ]); + const { acknowledge } = await inquirer.prompt([ + { + type: "confirm", + name: "acknowledge", + message: "Do you acknowledge this requirement and want to proceed?", + default: false, + }, + ]); - if (!acknowledge) { - console.log(chalk.red('Installation cancelled.')); - process.exit(0); - } - } - } + if (!acknowledge) { + console.log(chalk.red("Installation cancelled.")); + process.exit(0); + } + } + } - // Ask for IDE configuration - let ides = []; - let ideSelectionComplete = false; + // Ask for IDE configuration + let ides = []; + let ideSelectionComplete = false; - while (!ideSelectionComplete) { - console.log(chalk.cyan('\n🛠️ IDE Configuration')); - console.log( - chalk.bold.yellow.bgRed( - ' ⚠️ IMPORTANT: This is a MULTISELECT! Use SPACEBAR to toggle each IDE! ', - ), - ); - console.log(chalk.bold.magenta('🔸 Use arrow keys to navigate')); - console.log(chalk.bold.magenta('🔸 Use SPACEBAR to select/deselect IDEs')); - console.log(chalk.bold.magenta('🔸 Press ENTER when finished selecting\n')); + while (!ideSelectionComplete) { + console.log(chalk.cyan("\n🛠 IDE Configuration")); + console.log( + chalk.bold.yellow.bgRed( + " ! IMPORTANT: This is a MULTISELECT! Use SPACEBAR to toggle each IDE! ", + ), + ); + console.log(chalk.bold.magenta("🔸 Use arrow keys to navigate")); + console.log(chalk.bold.magenta("🔸 Use SPACEBAR to select/deselect IDEs")); + console.log(chalk.bold.magenta("🔸 Press ENTER when finished selecting\n")); - const ideResponse = await inquirer.prompt([ - { - type: 'checkbox', - name: 'ides', - message: - 'Which IDE(s) do you want to configure? (Select with SPACEBAR, confirm with ENTER):', - choices: [ - { name: 'Cursor', value: 'cursor' }, - { name: 'Claude Code', value: 'claude-code' }, - { name: 'iFlow CLI', value: 'iflow-cli' }, - { name: 'Windsurf', value: 'windsurf' }, - { name: 'Trae', value: 'trae' }, // { name: 'Trae', value: 'trae'} - { name: 'Roo Code', value: 'roo' }, - { name: 'Kilo Code', value: 'kilo' }, - { name: 'Cline', value: 'cline' }, - { name: 'Gemini CLI', value: 'gemini' }, - { name: 'Qwen Code', value: 'qwen-code' }, - { name: 'Crush', value: 'crush' }, - { name: 'Github Copilot', value: 'github-copilot' }, - { name: 'Auggie CLI (Augment Code)', value: 'auggie-cli' }, - { name: 'Codex CLI', value: 'codex' }, - { name: 'Codex Web', value: 'codex-web' }, - ], - }, - ]); + const ideResponse = await inquirer.prompt([ + { + type: "checkbox", + name: "ides", + message: + "Which IDE(s) do you want to configure? (Select with SPACEBAR, confirm with ENTER):", + choices: [ + { name: "Open Code", value: "opencode" }, + { name: "Cursor", value: "cursor" }, + { name: "Claude Code", value: "claude-code" }, + { name: "iFlow CLI", value: "iflow-cli" }, + { name: "Windsurf", value: "windsurf" }, + { name: "Trae", value: "trae" }, // { name: 'Trae', value: 'trae'} + { name: "Roo Code", value: "roo" }, + { name: "Kilo Code", value: "kilo" }, + { name: "Cline", value: "cline" }, + { name: "Gemini CLI", value: "gemini" }, + { name: "Qwen Code", value: "qwen-code" }, + { name: "Crush", value: "crush" }, + { name: "Github Copilot", value: "github-copilot" }, + { name: "Auggie CLI (Augment Code)", value: "auggie-cli" }, + { name: "Codex CLI", value: "codex" }, + { name: "Codex Web", value: "codex-web" }, + ], + }, + ]); - ides = ideResponse.ides; + ides = ideResponse.ides; - // Confirm no IDE selection if none selected - if (ides.length === 0) { - const { confirmNoIde } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmNoIde', - message: chalk.red( - '⚠️ You have NOT selected any IDEs. This means NO IDE integration will be set up. Is this correct?', - ), - default: false, - }, - ]); + // Confirm no IDE selection if none selected + if (ides.length === 0) { + const { confirmNoIde } = await inquirer.prompt([ + { + type: "confirm", + name: "confirmNoIde", + message: chalk.red( + "! You have NOT selected any IDEs. This means NO IDE integration will be set up. Is this correct?", + ), + default: false, + }, + ]); - if (!confirmNoIde) { - console.log( - chalk.bold.red( - '\n🔄 Returning to IDE selection. Remember to use SPACEBAR to select IDEs!\n', - ), - ); - continue; // Go back to IDE selection only - } - } + if (!confirmNoIde) { + console.log( + chalk.bold.red( + "\n🔄 Returning to IDE selection. Remember to use SPACEBAR to select IDEs!\n", + ), + ); + continue; // Go back to IDE selection only + } + } - ideSelectionComplete = true; - } + ideSelectionComplete = true; + } - // Use selected IDEs directly - answers.ides = ides; + // Use selected IDEs directly + answers.ides = ides; - // Configure GitHub Copilot immediately if selected - if (ides.includes('github-copilot')) { - console.log(chalk.cyan('\n🔧 GitHub Copilot Configuration')); - console.log( - chalk.dim('BMad works best with specific VS Code settings for optimal agent experience.\n'), - ); + // Configure GitHub Copilot immediately if selected + if (ides.includes("github-copilot")) { + console.log(chalk.cyan("\n🔧 GitHub Copilot Configuration")); + console.log( + chalk.dim( + "BMad works best with specific VS Code settings for optimal agent experience.\n", + ), + ); - const { configChoice } = await inquirer.prompt([ - { - type: 'list', - name: 'configChoice', - message: chalk.yellow('How would you like to configure GitHub Copilot settings?'), - choices: [ - { - name: 'Use recommended defaults (fastest setup)', - value: 'defaults', - }, - { - name: 'Configure each setting manually (customize to your preferences)', - value: 'manual', - }, - { - name: "Skip settings configuration (I'll configure manually later)", - value: 'skip', - }, - ], - default: 'defaults', - }, - ]); + const { configChoice } = await inquirer.prompt([ + { + type: "list", + name: "configChoice", + message: chalk.yellow( + "How would you like to configure GitHub Copilot settings?", + ), + choices: [ + { + name: "Use recommended defaults (fastest setup)", + value: "defaults", + }, + { + name: "Configure each setting manually (customize to your preferences)", + value: "manual", + }, + { + name: "Skip settings configuration (I'll configure manually later)", + value: "skip", + }, + ], + default: "defaults", + }, + ]); - answers.githubCopilotConfig = { configChoice }; - } + answers.githubCopilotConfig = { configChoice }; + } - // Configure Auggie CLI (Augment Code) immediately if selected - if (ides.includes('auggie-cli')) { - console.log(chalk.cyan('\n📍 Auggie CLI Location Configuration')); - console.log(chalk.dim('Choose where to install BMad agents for Auggie CLI access.\n')); + // Configure Auggie CLI (Augment Code) immediately if selected + if (ides.includes("auggie-cli")) { + console.log(chalk.cyan("\n📍 Auggie CLI Location Configuration")); + console.log( + chalk.dim("Choose where to install BMad agents for Auggie CLI access.\n"), + ); - const { selectedLocations } = await inquirer.prompt([ - { - type: 'checkbox', - name: 'selectedLocations', - message: 'Select Auggie CLI command locations:', - choices: [ - { - name: 'User Commands (Global): Available across all your projects (user-wide)', - value: 'user', - }, - { - name: 'Workspace Commands (Project): Stored in repository, shared with team', - value: 'workspace', - }, - ], - validate: (selected) => { - if (selected.length === 0) { - return 'Please select at least one location'; - } - return true; - }, - }, - ]); + const { selectedLocations } = await inquirer.prompt([ + { + type: "checkbox", + name: "selectedLocations", + message: "Select Auggie CLI command locations:", + choices: [ + { + name: "User Commands (Global): Available across all your projects (user-wide)", + value: "user", + }, + { + name: "Workspace Commands (Project): Stored in repository, shared with team", + value: "workspace", + }, + ], + validate: (selected) => { + if (selected.length === 0) { + return "Please select at least one location"; + } + return true; + }, + }, + ]); - answers.augmentCodeConfig = { selectedLocations }; - } + answers.augmentCodeConfig = { selectedLocations }; + } - // Ask for web bundles installation - const { includeWebBundles } = await inquirer.prompt([ - { - type: 'confirm', - name: 'includeWebBundles', - message: - 'Would you like to include pre-built web bundles? (standalone files for ChatGPT, Claude, Gemini)', - default: false, - }, - ]); + // Ask for web bundles installation + const { includeWebBundles } = await inquirer.prompt([ + { + type: "confirm", + name: "includeWebBundles", + message: + "Would you like to include pre-built web bundles? (standalone files for ChatGPT, Claude, Gemini)", + default: false, + }, + ]); - if (includeWebBundles) { - console.log(chalk.cyan('\n📦 Web bundles are standalone files perfect for web AI platforms.')); - console.log( - chalk.dim(' You can choose different teams/agents than your IDE installation.\n'), - ); + if (includeWebBundles) { + console.log( + chalk.cyan( + "\n📦 Web bundles are standalone files perfect for web AI platforms.", + ), + ); + console.log( + chalk.dim( + " You can choose different teams/agents than your IDE installation.\n", + ), + ); - const { webBundleType } = await inquirer.prompt([ - { - type: 'list', - name: 'webBundleType', - message: 'What web bundles would you like to include?', - choices: [ - { - name: 'All available bundles (agents, teams, expansion packs)', - value: 'all', - }, - { - name: 'Specific teams only', - value: 'teams', - }, - { - name: 'Individual agents only', - value: 'agents', - }, - { - name: 'Custom selection', - value: 'custom', - }, - ], - }, - ]); + const { webBundleType } = await inquirer.prompt([ + { + type: "list", + name: "webBundleType", + message: "What web bundles would you like to include?", + choices: [ + { + name: "All available bundles (agents, teams, expansion packs)", + value: "all", + }, + { + name: "Specific teams only", + value: "teams", + }, + { + name: "Individual agents only", + value: "agents", + }, + { + name: "Custom selection", + value: "custom", + }, + ], + }, + ]); - answers.webBundleType = webBundleType; + answers.webBundleType = webBundleType; - // If specific teams, let them choose which teams - if (webBundleType === 'teams' || webBundleType === 'custom') { - const teams = await installer.getAvailableTeams(); - const { selectedTeams } = await inquirer.prompt([ - { - type: 'checkbox', - name: 'selectedTeams', - message: 'Select team bundles to include:', - choices: teams.map((t) => ({ - name: `${t.icon || '📋'} ${t.name}: ${t.description}`, - value: t.id, - checked: webBundleType === 'teams', // Check all if teams-only mode - })), - validate: (answer) => { - if (answer.length === 0) { - return 'You must select at least one team.'; - } - return true; - }, - }, - ]); - answers.selectedWebBundleTeams = selectedTeams; - } + // If specific teams, let them choose which teams + if (webBundleType === "teams" || webBundleType === "custom") { + const teams = await installer.getAvailableTeams(); + const { selectedTeams } = await inquirer.prompt([ + { + type: "checkbox", + name: "selectedTeams", + message: "Select team bundles to include:", + choices: teams.map((t) => ({ + name: `${t.icon || "📋"} ${t.name}: ${t.description}`, + value: t.id, + checked: webBundleType === "teams", // Check all if teams-only mode + })), + validate: (answer) => { + if (answer.length === 0) { + return "You must select at least one team."; + } + return true; + }, + }, + ]); + answers.selectedWebBundleTeams = selectedTeams; + } - // If custom selection, also ask about individual agents - if (webBundleType === 'custom') { - const { includeIndividualAgents } = await inquirer.prompt([ - { - type: 'confirm', - name: 'includeIndividualAgents', - message: 'Also include individual agent bundles?', - default: true, - }, - ]); - answers.includeIndividualAgents = includeIndividualAgents; - } + // If custom selection, also ask about individual agents + if (webBundleType === "custom") { + const { includeIndividualAgents } = await inquirer.prompt([ + { + type: "confirm", + name: "includeIndividualAgents", + message: "Also include individual agent bundles?", + default: true, + }, + ]); + answers.includeIndividualAgents = includeIndividualAgents; + } - const { webBundlesDirectory } = await inquirer.prompt([ - { - type: 'input', - name: 'webBundlesDirectory', - message: 'Enter directory for web bundles:', - default: `${answers.directory}/web-bundles`, - validate: (input) => { - if (!input.trim()) { - return 'Please enter a valid directory path'; - } - return true; - }, - }, - ]); - answers.webBundlesDirectory = webBundlesDirectory; - } + const { webBundlesDirectory } = await inquirer.prompt([ + { + type: "input", + name: "webBundlesDirectory", + message: "Enter directory for web bundles:", + default: `${answers.directory}/web-bundles`, + validate: (input) => { + if (!input.trim()) { + return "Please enter a valid directory path"; + } + return true; + }, + }, + ]); + answers.webBundlesDirectory = webBundlesDirectory; + } - answers.includeWebBundles = includeWebBundles; + answers.includeWebBundles = includeWebBundles; - return answers; + return answers; } program.parse(process.argv); // Show help if no command provided if (process.argv.slice(2).length === 0) { - program.outputHelp(); + program.outputHelp(); } diff --git a/tools/installer/config/install.config.yaml b/tools/installer/config/install.config.yaml index f7010838..55e7ea4b 100644 --- a/tools/installer/config/install.config.yaml +++ b/tools/installer/config/install.config.yaml @@ -85,6 +85,17 @@ ide-configurations: # 2. Type @agent-name (e.g., "@dev", "@pm", "@architect") # 3. The agent will adopt that persona for the conversation # 4. Rules are stored in .clinerules/ directory in your project + opencode: + name: Opencode CLI + rule-dir: .opencode/ + format: multi-file + command-suffix: .md + instructions: | + # To use BMad agents with the Opencode CLI: + # 1. The installer creates a `agent` and `command` folder in `.opencode/`. + # 2. This adds custom commands for each agent and task. + # 3. Type /BMad:agents: (e.g., "/BMad:agents:dev", "/BMad:agents:pm") or /BMad:tasks: (e.g., "/BMad:tasks:create-doc"). + # 4. The agent will adopt that persona for the conversation or preform the task. gemini: name: Gemini CLI rule-dir: .gemini/commands/BMad/ diff --git a/tools/installer/lib/ide-setup.js b/tools/installer/lib/ide-setup.js index 4de928bd..68760d6f 100644 --- a/tools/installer/lib/ide-setup.js +++ b/tools/installer/lib/ide-setup.js @@ -1,1821 +1,2255 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const yaml = require('js-yaml'); -const chalk = require('chalk'); -const inquirer = require('inquirer'); -const fileManager = require('./file-manager'); -const configLoader = require('./config-loader'); -const { extractYamlFromAgent } = require('../../lib/yaml-utils'); -const BaseIdeSetup = require('./ide-base-setup'); -const resourceLocator = require('./resource-locator'); +const path = require("node:path"); +const fs = require("fs-extra"); +const yaml = require("js-yaml"); +const chalk = require("chalk"); +const inquirer = require("inquirer"); +const fileManager = require("./file-manager"); +const configLoader = require("./config-loader"); +const { extractYamlFromAgent } = require("../../lib/yaml-utils"); +const BaseIdeSetup = require("./ide-base-setup"); +const resourceLocator = require("./resource-locator"); class IdeSetup extends BaseIdeSetup { - constructor() { - super(); - this.ideAgentConfig = null; - } - - 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 { - console.warn('Failed to load IDE agent configuration, using defaults'); - return { - 'roo-permissions': {}, - 'cline-order': {}, - }; - } - } - - async setup(ide, installDir, selectedAgent = null, spinner = null, preConfiguredSettings = null) { - const ideConfig = await configLoader.getIdeConfiguration(ide); - - if (!ideConfig) { - console.log(chalk.yellow(`\nNo configuration available for ${ide}`)); - return false; - } - - switch (ide) { - case 'cursor': { - return this.setupCursor(installDir, selectedAgent); - } - case 'claude-code': { - return this.setupClaudeCode(installDir, selectedAgent); - } - case 'iflow-cli': { - return this.setupIFlowCli(installDir, selectedAgent); - } - case 'crush': { - return this.setupCrush(installDir, selectedAgent); - } - case 'windsurf': { - return this.setupWindsurf(installDir, selectedAgent); - } - case 'trae': { - return this.setupTrae(installDir, selectedAgent); - } - case 'roo': { - return this.setupRoo(installDir, selectedAgent); - } - case 'cline': { - return this.setupCline(installDir, selectedAgent); - } - case 'kilo': { - return this.setupKilocode(installDir, selectedAgent); - } - case 'gemini': { - return this.setupGeminiCli(installDir, selectedAgent); - } - case 'github-copilot': { - return this.setupGitHubCopilot(installDir, selectedAgent, spinner, preConfiguredSettings); - } - case 'qwen-code': { - return this.setupQwenCode(installDir, selectedAgent); - } - case 'auggie-cli': { - return this.setupAuggieCLI(installDir, selectedAgent, spinner, preConfiguredSettings); - } - case 'codex': { - return this.setupCodex(installDir, selectedAgent, { webEnabled: false }); - } - case 'codex-web': { - return this.setupCodex(installDir, selectedAgent, { webEnabled: true }); - } - default: { - console.log(chalk.yellow(`\nIDE ${ide} not yet supported`)); - return false; - } - } - } - - async setupCodex(installDir, selectedAgent, options) { - options = options ?? { webEnabled: false }; - // Codex reads AGENTS.md at the project root as project memory (CLI & Web). - // Inject/update a BMAD section with guidance, directory, and details. - const filePath = path.join(installDir, 'AGENTS.md'); - const startMarker = ''; - const endMarker = ''; - - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); - const tasks = await this.getAllTaskIds(installDir); - - // Build BMAD section content - let section = ''; - section += `${startMarker}\n`; - section += `# BMAD-METHOD Agents and Tasks\n\n`; - section += `This section is auto-generated by BMAD-METHOD for Codex. Codex merges this AGENTS.md into context.\n\n`; - section += `## How To Use With Codex\n\n`; - section += `- Codex CLI: run \`codex\` in this project. Reference an agent naturally, e.g., "As dev, implement ...".\n`; - section += `- Codex Web: open this repo and reference roles the same way; Codex reads \`AGENTS.md\`.\n`; - section += `- Commit \`.bmad-core\` and this \`AGENTS.md\` file to your repo so Codex (Web/CLI) can read full agent definitions.\n`; - section += `- Refresh this section after agent updates: \`npx bmad-method install -f -i codex\`.\n\n`; - - section += `### Helpful Commands\n\n`; - section += `- List agents: \`npx bmad-method list:agents\`\n`; - section += `- Reinstall BMAD core and regenerate AGENTS.md: \`npx bmad-method install -f -i codex\`\n`; - section += `- Validate configuration: \`npx bmad-method validate\`\n\n`; - - // Agents directory table - section += `## Agents\n\n`; - section += `### Directory\n\n`; - section += `| Title | ID | When To Use |\n|---|---|---|\n`; - const agentSummaries = []; - for (const agentId of agents) { - const agentPath = await this.findAgentPath(agentId, installDir); - if (!agentPath) continue; - const raw = await fileManager.readFile(agentPath); - const yamlMatch = raw.match(/```ya?ml\r?\n([\s\S]*?)```/); - const yamlBlock = yamlMatch ? yamlMatch[1].trim() : null; - const title = await this.getAgentTitle(agentId, installDir); - const whenToUse = yamlBlock?.match(/whenToUse:\s*"?([^\n"]+)"?/i)?.[1]?.trim() || ''; - agentSummaries.push({ agentId, title, whenToUse, yamlBlock, raw, path: agentPath }); - section += `| ${title} | ${agentId} | ${whenToUse || '—'} |\n`; - } - section += `\n`; - - // Detailed agent sections - for (const { agentId, title, whenToUse, yamlBlock, raw, path: agentPath } of agentSummaries) { - const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/'); - section += `### ${title} (id: ${agentId})\n`; - section += `Source: ${relativePath}\n\n`; - if (whenToUse) section += `- When to use: ${whenToUse}\n`; - section += `- How to activate: Mention "As ${agentId}, ..." or "Use ${title} to ..."\n\n`; - if (yamlBlock) { - section += '```yaml\n' + yamlBlock + '\n```\n\n'; - } else { - section += '```md\n' + raw.trim() + '\n```\n\n'; - } - } - - // Tasks - if (tasks && tasks.length > 0) { - section += `## Tasks\n\n`; - section += `These are reusable task briefs you can reference directly in Codex.\n\n`; - for (const taskId of tasks) { - const taskPath = await this.findTaskPath(taskId, installDir); - if (!taskPath) continue; - const raw = await fileManager.readFile(taskPath); - const relativePath = path.relative(installDir, taskPath).replaceAll('\\', '/'); - section += `### Task: ${taskId}\n`; - section += `Source: ${relativePath}\n`; - section += `- How to use: "Use task ${taskId} with the appropriate agent" and paste relevant parts as needed.\n\n`; - section += '```md\n' + raw.trim() + '\n```\n\n'; - } - } - - section += `${endMarker}\n`; - - // Write or update AGENTS.md - let finalContent = ''; - if (await fileManager.pathExists(filePath)) { - const existing = await fileManager.readFile(filePath); - if (existing.includes(startMarker) && existing.includes(endMarker)) { - // Replace existing BMAD block - const pattern = String.raw`${startMarker}[\s\S]*?${endMarker}`; - const replaced = existing.replace(new RegExp(pattern, 'm'), section); - finalContent = replaced; - } else { - // Append BMAD block to existing file - finalContent = existing.trimEnd() + `\n\n` + section; - } - } else { - // Create fresh AGENTS.md with a small header and BMAD block - finalContent += '# Project Agents\n\n'; - finalContent += 'This file provides guidance and memory for Codex CLI.\n\n'; - finalContent += section; - } - - await fileManager.writeFile(filePath, finalContent); - console.log(chalk.green('✓ Created/updated AGENTS.md for Codex CLI integration')); - console.log( - chalk.dim( - 'Codex reads AGENTS.md automatically. Run `codex` in this project to use BMAD agents.', - ), - ); - - // Optionally add helpful npm scripts if a package.json exists - try { - const pkgPath = path.join(installDir, 'package.json'); - if (await fileManager.pathExists(pkgPath)) { - const pkgRaw = await fileManager.readFile(pkgPath); - const pkg = JSON.parse(pkgRaw); - pkg.scripts = pkg.scripts || {}; - const updated = { ...pkg.scripts }; - if (!updated['bmad:refresh']) updated['bmad:refresh'] = 'bmad-method install -f -i codex'; - if (!updated['bmad:list']) updated['bmad:list'] = 'bmad-method list:agents'; - if (!updated['bmad:validate']) updated['bmad:validate'] = 'bmad-method validate'; - const changed = JSON.stringify(updated) !== JSON.stringify(pkg.scripts); - if (changed) { - const newPkg = { ...pkg, scripts: updated }; - await fileManager.writeFile(pkgPath, JSON.stringify(newPkg, null, 2) + '\n'); - console.log(chalk.green('✓ Added npm scripts: bmad:refresh, bmad:list, bmad:validate')); - } - } - } catch { - console.log( - chalk.yellow('⚠︎ Skipped adding npm scripts (package.json not writable or invalid)'), - ); - } - - // Adjust .gitignore behavior depending on Codex mode - try { - const gitignorePath = path.join(installDir, '.gitignore'); - const ignoreLines = ['# BMAD (local only)', '.bmad-core/', '.bmad-*/']; - const exists = await fileManager.pathExists(gitignorePath); - if (options.webEnabled) { - if (exists) { - let gi = await fileManager.readFile(gitignorePath); - // Remove lines that ignore BMAD dot-folders - const updated = gi - .split(/\r?\n/) - .filter((l) => !/^\s*\.bmad-core\/?\s*$/.test(l) && !/^\s*\.bmad-\*\/?\s*$/.test(l)) - .join('\n'); - if (updated !== gi) { - await fileManager.writeFile(gitignorePath, updated.trimEnd() + '\n'); - console.log(chalk.green('✓ Updated .gitignore to include .bmad-core in commits')); - } - } - } else { - // Local-only: add ignores if missing - let base = exists ? await fileManager.readFile(gitignorePath) : ''; - const haveCore = base.includes('.bmad-core/'); - const haveStar = base.includes('.bmad-*/'); - if (!haveCore || !haveStar) { - const sep = base.endsWith('\n') || base.length === 0 ? '' : '\n'; - const add = [!haveCore || !haveStar ? ignoreLines.join('\n') : ''] - .filter(Boolean) - .join('\n'); - const out = base + sep + add + '\n'; - await fileManager.writeFile(gitignorePath, out); - console.log(chalk.green('✓ Added .bmad-core/* to .gitignore for local-only Codex setup')); - } - } - } catch { - console.log(chalk.yellow('⚠︎ Could not update .gitignore (skipping)')); - } - - return true; - } - - async setupCursor(installDir, selectedAgent) { - const cursorRulesDir = path.join(installDir, '.cursor', 'rules', 'bmad'); - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); - - await fileManager.ensureDirectory(cursorRulesDir); - - for (const agentId of agents) { - const agentPath = await this.findAgentPath(agentId, installDir); - - if (agentPath) { - const mdcContent = await this.createAgentRuleContent(agentId, agentPath, installDir, 'mdc'); - const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`); - 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 setupCrush(installDir, selectedAgent) { - // Setup bmad-core commands - const coreSlashPrefix = await this.getCoreSlashPrefix(installDir); - const coreAgents = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir); - const coreTasks = await this.getCoreTaskIds(installDir); - await this.setupCrushForPackage( - installDir, - 'core', - coreSlashPrefix, - coreAgents, - coreTasks, - '.bmad-core', - ); - - // Setup expansion pack commands - const expansionPacks = await this.getInstalledExpansionPacks(installDir); - for (const packInfo of expansionPacks) { - const packSlashPrefix = await this.getExpansionPackSlashPrefix(packInfo.path); - const packAgents = await this.getExpansionPackAgents(packInfo.path); - const packTasks = await this.getExpansionPackTasks(packInfo.path); - - if (packAgents.length > 0 || packTasks.length > 0) { - // Use the actual directory name where the expansion pack is installed - const rootPath = path.relative(installDir, packInfo.path); - await this.setupCrushForPackage( - installDir, - packInfo.name, - packSlashPrefix, - packAgents, - packTasks, - rootPath, - ); - } - } - - return true; - } - - async setupClaudeCode(installDir, selectedAgent) { - // Setup bmad-core commands - const coreSlashPrefix = await this.getCoreSlashPrefix(installDir); - const coreAgents = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir); - const coreTasks = await this.getCoreTaskIds(installDir); - await this.setupClaudeCodeForPackage( - installDir, - 'core', - coreSlashPrefix, - coreAgents, - coreTasks, - '.bmad-core', - ); - - // Setup expansion pack commands - const expansionPacks = await this.getInstalledExpansionPacks(installDir); - for (const packInfo of expansionPacks) { - const packSlashPrefix = await this.getExpansionPackSlashPrefix(packInfo.path); - const packAgents = await this.getExpansionPackAgents(packInfo.path); - const packTasks = await this.getExpansionPackTasks(packInfo.path); - - if (packAgents.length > 0 || packTasks.length > 0) { - // Use the actual directory name where the expansion pack is installed - const rootPath = path.relative(installDir, packInfo.path); - await this.setupClaudeCodeForPackage( - installDir, - packInfo.name, - packSlashPrefix, - packAgents, - packTasks, - rootPath, - ); - } - } - - return true; - } - - async setupClaudeCodeForPackage( - installDir, - packageName, - slashPrefix, - agentIds, - taskIds, - rootPath, - ) { - const commandsBaseDir = path.join(installDir, '.claude', 'commands', slashPrefix); - const agentsDir = path.join(commandsBaseDir, 'agents'); - const tasksDir = path.join(commandsBaseDir, 'tasks'); - - // Ensure directories exist - await fileManager.ensureDirectory(agentsDir); - await fileManager.ensureDirectory(tasksDir); - - // Setup agents - for (const agentId of agentIds) { - // Find the agent file - for expansion packs, prefer the expansion pack version - let agentPath; - if (packageName === 'core') { - // For core, use the normal search - agentPath = await this.findAgentPath(agentId, installDir); - } else { - // For expansion packs, first try to find the agent in the expansion pack directory - const expansionPackPath = path.join(installDir, rootPath, 'agents', `${agentId}.md`); - if (await fileManager.pathExists(expansionPackPath)) { - agentPath = expansionPackPath; - } else { - // Fall back to core if not found in expansion pack - agentPath = await this.findAgentPath(agentId, installDir); - } - } - - const commandPath = path.join(agentsDir, `${agentId}.md`); - - if (agentPath) { - // Create command file with agent content - let agentContent = await fileManager.readFile(agentPath); - - // Replace {root} placeholder with the appropriate root path for this context - agentContent = agentContent.replaceAll('{root}', rootPath); - - // 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 agent command: /${agentId}`)); - } - } - - // Setup tasks - for (const taskId of taskIds) { - // Find the task file - for expansion packs, prefer the expansion pack version - let taskPath; - if (packageName === 'core') { - // For core, use the normal search - taskPath = await this.findTaskPath(taskId, installDir); - } else { - // For expansion packs, first try to find the task in the expansion pack directory - const expansionPackPath = path.join(installDir, rootPath, 'tasks', `${taskId}.md`); - if (await fileManager.pathExists(expansionPackPath)) { - taskPath = expansionPackPath; - } else { - // Fall back to core if not found in expansion pack - taskPath = await this.findTaskPath(taskId, installDir); - } - } - - const commandPath = path.join(tasksDir, `${taskId}.md`); - - if (taskPath) { - // Create command file with task content - let taskContent = await fileManager.readFile(taskPath); - - // Replace {root} placeholder with the appropriate root path for this context - taskContent = taskContent.replaceAll('{root}', rootPath); - - // Add command header - let commandContent = `# /${taskId} Task\n\n`; - commandContent += `When this command is used, execute the following task:\n\n`; - commandContent += taskContent; - - await fileManager.writeFile(commandPath, commandContent); - console.log(chalk.green(`✓ Created task command: /${taskId}`)); - } - } - - console.log( - chalk.green(`\n✓ Created Claude Code commands for ${packageName} in ${commandsBaseDir}`), - ); - console.log(chalk.dim(` - Agents in: ${agentsDir}`)); - console.log(chalk.dim(` - Tasks in: ${tasksDir}`)); - } - - async setupIFlowCli(installDir, selectedAgent) { - // Setup bmad-core commands - const coreSlashPrefix = await this.getCoreSlashPrefix(installDir); - const coreAgents = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir); - const coreTasks = await this.getCoreTaskIds(installDir); - await this.setupIFlowCliForPackage( - installDir, - 'core', - coreSlashPrefix, - coreAgents, - coreTasks, - '.bmad-core', - ); - - // Setup expansion pack commands - const expansionPacks = await this.getInstalledExpansionPacks(installDir); - for (const packInfo of expansionPacks) { - const packSlashPrefix = await this.getExpansionPackSlashPrefix(packInfo.path); - const packAgents = await this.getExpansionPackAgents(packInfo.path); - const packTasks = await this.getExpansionPackTasks(packInfo.path); - - if (packAgents.length > 0 || packTasks.length > 0) { - // Use the actual directory name where the expansion pack is installed - const rootPath = path.relative(installDir, packInfo.path); - await this.setupIFlowCliForPackage( - installDir, - packInfo.name, - packSlashPrefix, - packAgents, - packTasks, - rootPath, - ); - } - } - - return true; - } - - async setupIFlowCliForPackage(installDir, packageName, slashPrefix, agentIds, taskIds, rootPath) { - const commandsBaseDir = path.join(installDir, '.iflow', 'commands', slashPrefix); - const agentsDir = path.join(commandsBaseDir, 'agents'); - const tasksDir = path.join(commandsBaseDir, 'tasks'); - - // Ensure directories exist - await fileManager.ensureDirectory(agentsDir); - await fileManager.ensureDirectory(tasksDir); - - // Setup agents - for (const agentId of agentIds) { - // Find the agent file - for expansion packs, prefer the expansion pack version - let agentPath; - if (packageName === 'core') { - // For core, use the normal search - agentPath = await this.findAgentPath(agentId, installDir); - } else { - // For expansion packs, first try to find the agent in the expansion pack directory - const expansionPackPath = path.join(installDir, rootPath, 'agents', `${agentId}.md`); - if (await fileManager.pathExists(expansionPackPath)) { - agentPath = expansionPackPath; - } else { - // Fall back to core if not found in expansion pack - agentPath = await this.findAgentPath(agentId, installDir); - } - } - - const commandPath = path.join(agentsDir, `${agentId}.md`); - - if (agentPath) { - // Create command file with agent content - let agentContent = await fileManager.readFile(agentPath); - - // Replace {root} placeholder with the appropriate root path for this context - agentContent = agentContent.replaceAll('{root}', rootPath); - - // 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 agent command: /${agentId}`)); - } - } - - // Setup tasks - for (const taskId of taskIds) { - // Find the task file - for expansion packs, prefer the expansion pack version - let taskPath; - if (packageName === 'core') { - // For core, use the normal search - taskPath = await this.findTaskPath(taskId, installDir); - } else { - // For expansion packs, first try to find the task in the expansion pack directory - const expansionPackPath = path.join(installDir, rootPath, 'tasks', `${taskId}.md`); - if (await fileManager.pathExists(expansionPackPath)) { - taskPath = expansionPackPath; - } else { - // Fall back to core if not found in expansion pack - taskPath = await this.findTaskPath(taskId, installDir); - } - } - - const commandPath = path.join(tasksDir, `${taskId}.md`); - - if (taskPath) { - // Create command file with task content - let taskContent = await fileManager.readFile(taskPath); - - // Replace {root} placeholder with the appropriate root path for this context - taskContent = taskContent.replaceAll('{root}', rootPath); - - // Add command header - let commandContent = `# /${taskId} Task\n\n`; - commandContent += `When this command is used, execute the following task:\n\n`; - commandContent += taskContent; - - await fileManager.writeFile(commandPath, commandContent); - console.log(chalk.green(`✓ Created task command: /${taskId}`)); - } - } - - console.log( - chalk.green(`\n✓ Created iFlow CLI commands for ${packageName} in ${commandsBaseDir}`), - ); - console.log(chalk.dim(` - Agents in: ${agentsDir}`)); - console.log(chalk.dim(` - Tasks in: ${tasksDir}`)); - } - - async setupCrushForPackage(installDir, packageName, slashPrefix, agentIds, taskIds, rootPath) { - const commandsBaseDir = path.join(installDir, '.crush', 'commands', slashPrefix); - const agentsDir = path.join(commandsBaseDir, 'agents'); - const tasksDir = path.join(commandsBaseDir, 'tasks'); - - // Ensure directories exist - await fileManager.ensureDirectory(agentsDir); - await fileManager.ensureDirectory(tasksDir); - - // Setup agents - for (const agentId of agentIds) { - // Find the agent file - for expansion packs, prefer the expansion pack version - let agentPath; - if (packageName === 'core') { - // For core, use the normal search - agentPath = await this.findAgentPath(agentId, installDir); - } else { - // For expansion packs, first try to find the agent in the expansion pack directory - const expansionPackPath = path.join(installDir, rootPath, 'agents', `${agentId}.md`); - if (await fileManager.pathExists(expansionPackPath)) { - agentPath = expansionPackPath; - } else { - // Fall back to core if not found in expansion pack - agentPath = await this.findAgentPath(agentId, installDir); - } - } - - const commandPath = path.join(agentsDir, `${agentId}.md`); - - if (agentPath) { - // Create command file with agent content - let agentContent = await fileManager.readFile(agentPath); - - // Replace {root} placeholder with the appropriate root path for this context - agentContent = agentContent.replaceAll('{root}', rootPath); - - // 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 agent command: /${agentId}`)); - } - } - - // Setup tasks - for (const taskId of taskIds) { - // Find the task file - for expansion packs, prefer the expansion pack version - let taskPath; - if (packageName === 'core') { - // For core, use the normal search - taskPath = await this.findTaskPath(taskId, installDir); - } else { - // For expansion packs, first try to find the task in the expansion pack directory - const expansionPackPath = path.join(installDir, rootPath, 'tasks', `${taskId}.md`); - if (await fileManager.pathExists(expansionPackPath)) { - taskPath = expansionPackPath; - } else { - // Fall back to core if not found in expansion pack - taskPath = await this.findTaskPath(taskId, installDir); - } - } - - const commandPath = path.join(tasksDir, `${taskId}.md`); - - if (taskPath) { - // Create command file with task content - let taskContent = await fileManager.readFile(taskPath); - - // Replace {root} placeholder with the appropriate root path for this context - taskContent = taskContent.replaceAll('{root}', rootPath); - - // Add command header - let commandContent = `# /${taskId} Task\n\n`; - commandContent += `When this command is used, execute the following task:\n\n`; - commandContent += taskContent; - - await fileManager.writeFile(commandPath, commandContent); - console.log(chalk.green(`✓ Created task command: /${taskId}`)); - } - } - - console.log(chalk.green(`\n✓ Created Crush commands for ${packageName} in ${commandsBaseDir}`)); - console.log(chalk.dim(` - Agents in: ${agentsDir}`)); - console.log(chalk.dim(` - Tasks in: ${tasksDir}`)); - } - - async setupWindsurf(installDir, selectedAgent) { - const windsurfWorkflowDir = path.join(installDir, '.windsurf', 'workflows'); - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); - - await fileManager.ensureDirectory(windsurfWorkflowDir); - - for (const agentId of agents) { - // Find the agent file - const agentPath = await this.findAgentPath(agentId, installDir); - - if (agentPath) { - const agentContent = await fileManager.readFile(agentPath); - const mdPath = path.join(windsurfWorkflowDir, `${agentId}.md`); - - // Write the agent file contents prefixed with Windsurf frontmatter - let mdContent = `---\n`; - mdContent += `description: ${agentId}\n`; - mdContent += `auto_execution_mode: 3\n`; - mdContent += `---\n\n`; - mdContent += agentContent; - - await fileManager.writeFile(mdPath, mdContent); - console.log(chalk.green(`✓ Created workflow: ${agentId}.md`)); - } - } - - console.log(chalk.green(`\n✓ Created Windsurf workflows in ${windsurfWorkflowDir}`)); - - return true; - } - - async setupTrae(installDir, selectedAgent) { - const traeRulesDir = path.join(installDir, '.trae', 'rules'); - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); - - await fileManager.ensureDirectory(traeRulesDir); - - for (const agentId of agents) { - // Find the agent file - const agentPath = await this.findAgentPath(agentId, installDir); - - if (agentPath) { - const agentContent = await fileManager.readFile(agentPath); - const mdPath = path.join(traeRulesDir, `${agentId}.md`); - - // Create MD content (similar to Cursor but without frontmatter) - let mdContent = `# ${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 YAML, 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 += '```yaml\n'; - // Extract just the YAML content from the agent file - const yamlContent = extractYamlFromAgent(agentContent); - if (yamlContent) { - mdContent += yamlContent; - } 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'; - const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/'); - mdContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\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 YAML configuration above.\n`; - - await fileManager.writeFile(mdPath, mdContent); - console.log(chalk.green(`✓ Created rule: ${agentId}.md`)); - } - } - } - - async findAgentPath(agentId, installDir) { - // Try to find the agent file in various locations - const possiblePaths = [ - path.join(installDir, '.bmad-core', 'agents', `${agentId}.md`), - path.join(installDir, 'agents', `${agentId}.md`), - ]; - - // Also check expansion pack directories - const glob = require('glob'); - const expansionDirectories = glob.sync('.*/agents', { cwd: installDir }); - for (const expDir of expansionDirectories) { - possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`)); - } - - for (const agentPath of possiblePaths) { - if (await fileManager.pathExists(agentPath)) { - return agentPath; - } - } - - return null; - } - - async getAllAgentIds(installDir) { - const glob = require('glob'); - const allAgentIds = []; - - // Check core agents in .bmad-core or root - let agentsDir = path.join(installDir, '.bmad-core', 'agents'); - if (!(await fileManager.pathExists(agentsDir))) { - agentsDir = path.join(installDir, 'agents'); - } - - if (await fileManager.pathExists(agentsDir)) { - const agentFiles = glob.sync('*.md', { cwd: agentsDir }); - allAgentIds.push(...agentFiles.map((file) => path.basename(file, '.md'))); - } - - // Also check for expansion pack agents in dot folders - const expansionDirectories = glob.sync('.*/agents', { cwd: installDir }); - for (const expDir of expansionDirectories) { - const fullExpDir = path.join(installDir, expDir); - const expAgentFiles = glob.sync('*.md', { cwd: fullExpDir }); - allAgentIds.push(...expAgentFiles.map((file) => path.basename(file, '.md'))); - } - - // Remove duplicates - return [...new Set(allAgentIds)]; - } - - async getCoreAgentIds(installDir) { - const allAgentIds = []; - - // Check core agents in .bmad-core or root only - let agentsDir = path.join(installDir, '.bmad-core', 'agents'); - if (!(await fileManager.pathExists(agentsDir))) { - agentsDir = path.join(installDir, 'bmad-core', 'agents'); - } - - if (await fileManager.pathExists(agentsDir)) { - const glob = require('glob'); - const agentFiles = glob.sync('*.md', { cwd: agentsDir }); - allAgentIds.push(...agentFiles.map((file) => path.basename(file, '.md'))); - } - - return [...new Set(allAgentIds)]; - } - - async getCoreTaskIds(installDir) { - const allTaskIds = []; - const glob = require('glob'); - - // Check core tasks in .bmad-core or root only - let tasksDir = path.join(installDir, '.bmad-core', 'tasks'); - if (!(await fileManager.pathExists(tasksDir))) { - tasksDir = path.join(installDir, 'bmad-core', 'tasks'); - } - - if (await fileManager.pathExists(tasksDir)) { - const taskFiles = glob.sync('*.md', { cwd: tasksDir }); - allTaskIds.push(...taskFiles.map((file) => path.basename(file, '.md'))); - } - - // Check common tasks - const commonTasksDir = path.join(installDir, 'common', 'tasks'); - if (await fileManager.pathExists(commonTasksDir)) { - const glob = require('glob'); - const commonTaskFiles = glob.sync('*.md', { cwd: commonTasksDir }); - allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, '.md'))); - } - - return [...new Set(allTaskIds)]; - } - - async getAgentTitle(agentId, installDir) { - // Try to find the agent file in various locations - const possiblePaths = [ - path.join(installDir, '.bmad-core', 'agents', `${agentId}.md`), - path.join(installDir, 'agents', `${agentId}.md`), - ]; - - // Also check expansion pack directories - const glob = require('glob'); - const expansionDirectories = glob.sync('.*/agents', { cwd: installDir }); - for (const expDir of expansionDirectories) { - possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`)); - } - - for (const agentPath of possiblePaths) { - if (await fileManager.pathExists(agentPath)) { - try { - const agentContent = await fileManager.readFile(agentPath); - const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); - - if (yamlMatch) { - const yaml = yamlMatch[1]; - const titleMatch = yaml.match(/title:\s*(.+)/); - if (titleMatch) { - return titleMatch[1].trim(); - } - } - } catch (error) { - console.warn(`Failed to read agent title for ${agentId}: ${error.message}`); - } - } - } - - // Fallback to formatted agent ID - return agentId - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - async getAllTaskIds(installDir) { - const glob = require('glob'); - const allTaskIds = []; - - // Check core tasks in .bmad-core or root - let tasksDir = path.join(installDir, '.bmad-core', 'tasks'); - if (!(await fileManager.pathExists(tasksDir))) { - tasksDir = path.join(installDir, 'bmad-core', 'tasks'); - } - - if (await fileManager.pathExists(tasksDir)) { - const taskFiles = glob.sync('*.md', { cwd: tasksDir }); - allTaskIds.push(...taskFiles.map((file) => path.basename(file, '.md'))); - } - - // Check common tasks - const commonTasksDir = path.join(installDir, 'common', 'tasks'); - if (await fileManager.pathExists(commonTasksDir)) { - const commonTaskFiles = glob.sync('*.md', { cwd: commonTasksDir }); - allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, '.md'))); - } - - // Also check for expansion pack tasks in dot folders - const expansionDirectories = glob.sync('.*/tasks', { cwd: installDir }); - for (const expDir of expansionDirectories) { - const fullExpDir = path.join(installDir, expDir); - const expTaskFiles = glob.sync('*.md', { cwd: fullExpDir }); - allTaskIds.push(...expTaskFiles.map((file) => path.basename(file, '.md'))); - } - - // Check expansion-packs folder tasks - const expansionPacksDir = path.join(installDir, 'expansion-packs'); - if (await fileManager.pathExists(expansionPacksDir)) { - const expPackDirectories = glob.sync('*/tasks', { cwd: expansionPacksDir }); - for (const expDir of expPackDirectories) { - const fullExpDir = path.join(expansionPacksDir, expDir); - const expTaskFiles = glob.sync('*.md', { cwd: fullExpDir }); - allTaskIds.push(...expTaskFiles.map((file) => path.basename(file, '.md'))); - } - } - - // Remove duplicates - return [...new Set(allTaskIds)]; - } - - async findTaskPath(taskId, installDir) { - // Try to find the task file in various locations - const possiblePaths = [ - path.join(installDir, '.bmad-core', 'tasks', `${taskId}.md`), - path.join(installDir, 'bmad-core', 'tasks', `${taskId}.md`), - path.join(installDir, 'common', 'tasks', `${taskId}.md`), - ]; - - // Also check expansion pack directories - const glob = require('glob'); - - // Check dot folder expansion packs - const expansionDirectories = glob.sync('.*/tasks', { cwd: installDir }); - for (const expDir of expansionDirectories) { - possiblePaths.push(path.join(installDir, expDir, `${taskId}.md`)); - } - - // Check expansion-packs folder - const expansionPacksDir = path.join(installDir, 'expansion-packs'); - if (await fileManager.pathExists(expansionPacksDir)) { - const expPackDirectories = glob.sync('*/tasks', { cwd: expansionPacksDir }); - for (const expDir of expPackDirectories) { - possiblePaths.push(path.join(expansionPacksDir, expDir, `${taskId}.md`)); - } - } - - for (const taskPath of possiblePaths) { - if (await fileManager.pathExists(taskPath)) { - return taskPath; - } - } - - return null; - } - - async getCoreSlashPrefix(installDir) { - try { - const coreConfigPath = path.join(installDir, '.bmad-core', 'core-config.yaml'); - if (!(await fileManager.pathExists(coreConfigPath))) { - // Try bmad-core directory - const altConfigPath = path.join(installDir, 'bmad-core', 'core-config.yaml'); - if (await fileManager.pathExists(altConfigPath)) { - const configContent = await fileManager.readFile(altConfigPath); - const config = yaml.load(configContent); - return config.slashPrefix || 'BMad'; - } - return 'BMad'; // fallback - } - - const configContent = await fileManager.readFile(coreConfigPath); - const config = yaml.load(configContent); - return config.slashPrefix || 'BMad'; - } catch (error) { - console.warn(`Failed to read core slashPrefix, using default 'BMad': ${error.message}`); - return 'BMad'; - } - } - - async getInstalledExpansionPacks(installDir) { - const expansionPacks = []; - - // Check for dot-prefixed expansion packs in install directory - const glob = require('glob'); - const dotExpansions = glob.sync('.bmad-*', { cwd: installDir }); - - for (const dotExpansion of dotExpansions) { - if (dotExpansion !== '.bmad-core') { - const packPath = path.join(installDir, dotExpansion); - const packName = dotExpansion.slice(1); // remove the dot - expansionPacks.push({ - name: packName, - path: packPath, - }); - } - } - - // Check for expansion-packs directory style - const expansionPacksDir = path.join(installDir, 'expansion-packs'); - if (await fileManager.pathExists(expansionPacksDir)) { - const packDirectories = glob.sync('*', { cwd: expansionPacksDir }); - - for (const packDir of packDirectories) { - const packPath = path.join(expansionPacksDir, packDir); - if ( - (await fileManager.pathExists(packPath)) && - (await fileManager.pathExists(path.join(packPath, 'config.yaml'))) - ) { - expansionPacks.push({ - name: packDir, - path: packPath, - }); - } - } - } - - return expansionPacks; - } - - async getExpansionPackSlashPrefix(packPath) { - try { - const configPath = path.join(packPath, 'config.yaml'); - if (await fileManager.pathExists(configPath)) { - const configContent = await fileManager.readFile(configPath); - const config = yaml.load(configContent); - return config.slashPrefix || path.basename(packPath); - } - } catch (error) { - console.warn(`Failed to read expansion pack slashPrefix from ${packPath}: ${error.message}`); - } - - return path.basename(packPath); // fallback to directory name - } - - async getExpansionPackAgents(packPath) { - const agentsDir = path.join(packPath, 'agents'); - if (!(await fileManager.pathExists(agentsDir))) { - return []; - } - - try { - const glob = require('glob'); - const agentFiles = glob.sync('*.md', { cwd: agentsDir }); - return agentFiles.map((file) => path.basename(file, '.md')); - } catch (error) { - console.warn(`Failed to read expansion pack agents from ${packPath}: ${error.message}`); - return []; - } - } - - async getExpansionPackTasks(packPath) { - const tasksDir = path.join(packPath, 'tasks'); - if (!(await fileManager.pathExists(tasksDir))) { - return []; - } - - try { - const glob = require('glob'); - const taskFiles = glob.sync('*.md', { cwd: tasksDir }); - return taskFiles.map((file) => path.basename(file, '.md')); - } catch (error) { - console.warn(`Failed to read expansion pack tasks from ${packPath}: ${error.message}`); - return []; - } - } - - async setupRoo(installDir, selectedAgent) { - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); - - // Check for existing .roomodes file in project root - const roomodesPath = path.join(installDir, '.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 = ''; - - // Load dynamic agent permissions from configuration - const config = await this.loadIdeAgentConfig(); - const agentPermissions = config['roo-permissions'] || {}; - - for (const agentId of agents) { - // Skip if already exists - // Check both with and without bmad- prefix to handle both cases - const checkSlug = agentId.startsWith('bmad-') ? agentId : `bmad-${agentId}`; - if (existingModes.includes(checkSlug)) { - console.log(chalk.dim(`Skipping ${agentId} - already exists in .roomodes`)); - continue; - } - - // Read agent file to extract all information - const agentPath = await this.findAgentPath(agentId, installDir); - - if (agentPath) { - const agentContent = await fileManager.readFile(agentPath); - - // Extract YAML content - const yamlMatch = agentContent.match(/```ya?ml\r?\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.`; - - // Add permissions based on agent type - const permissions = agentPermissions[agentId]; - // Build mode entry with proper formatting (matching exact indentation) - // Avoid double "bmad-" prefix for agents that already have it - const slug = agentId.startsWith('bmad-') ? agentId : `bmad-${agentId}`; - newModesContent += ` - slug: ${slug}\n`; - newModesContent += ` name: '${icon} ${title}'\n`; - if (permissions) { - newModesContent += ` description: '${permissions.description}'\n`; - } - newModesContent += ` roleDefinition: ${roleDefinition}\n`; - newModesContent += ` whenToUse: ${whenToUse}\n`; - // Get relative path from installDir to agent file - const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/'); - newModesContent += ` 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\n`; - newModesContent += ` groups:\n`; - newModesContent += ` - read\n`; - - 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 .roomodes file in project root')); - - 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 setupKilocode(installDir, selectedAgent) { - 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).replaceAll('\\', '/'); - 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; - } - - async setupCline(installDir, selectedAgent) { - 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 += '```yaml\n'; - // Extract just the YAML content from the agent file - const yamlContent = extractYamlFromAgent(agentContent); - if (yamlContent) { - mdContent += yamlContent; - } 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).replaceAll('\\', '/'); - 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 ideConfig = await configLoader.getIdeConfiguration('gemini'); - const bmadCommandsDir = path.join(installDir, ideConfig['rule-dir']); - - const agentCommandsDir = path.join(bmadCommandsDir, 'agents'); - const taskCommandsDir = path.join(bmadCommandsDir, 'tasks'); - await fileManager.ensureDirectory(agentCommandsDir); - await fileManager.ensureDirectory(taskCommandsDir); - - // Process Agents - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); - for (const agentId of agents) { - const agentPath = await this.findAgentPath(agentId, installDir); - if (!agentPath) { - console.log(chalk.yellow(`✗ Agent file not found for ${agentId}, skipping.`)); - continue; - } - - const agentTitle = await this.getAgentTitle(agentId, installDir); - const commandPath = path.join(agentCommandsDir, `${agentId}.toml`); - - // Get relative path from installDir to agent file for @{file} reference - const relativeAgentPath = path.relative(installDir, agentPath).replaceAll('\\', '/'); - - const tomlContent = `description = "Activates the ${agentTitle} agent from the BMad Method." -prompt = """ + constructor() { + super(); + this.ideAgentConfig = null; + } + + 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 { + console.warn("Failed to load IDE agent configuration, using defaults"); + return { + "roo-permissions": {}, + "cline-order": {}, + }; + } + } + + async setup( + ide, + installDir, + selectedAgent = null, + spinner = null, + preConfiguredSettings = null, + ) { + const ideConfig = await configLoader.getIdeConfiguration(ide); + + if (!ideConfig) { + console.log(chalk.yellow(`\nNo configuration available for ${ide}`)); + return false; + } + + switch (ide) { + case "cursor": { + return this.setupCursor(installDir, selectedAgent); + } + case "claude-code": { + return this.setupClaudeCode(installDir, selectedAgent); + } + case "iflow-cli": { + return this.setupIFlowCli(installDir, selectedAgent); + } + case "crush": { + return this.setupCrush(installDir, selectedAgent); + } + case "windsurf": { + return this.setupWindsurf(installDir, selectedAgent); + } + case "trae": { + return this.setupTrae(installDir, selectedAgent); + } + case "roo": { + return this.setupRoo(installDir, selectedAgent); + } + case "cline": { + return this.setupCline(installDir, selectedAgent); + } + case "kilo": { + return this.setupKilocode(installDir, selectedAgent); + } + case "gemini": { + return this.setupGeminiCli(installDir, selectedAgent); + } + case "opencode": { + return this.setupGeminiCli(installDir, selectedAgent); + } + case "github-copilot": { + return this.setupGitHubCopilot( + installDir, + selectedAgent, + spinner, + preConfiguredSettings, + ); + } + case "qwen-code": { + return this.setupQwenCode(installDir, selectedAgent); + } + case "auggie-cli": { + return this.setupAuggieCLI( + installDir, + selectedAgent, + spinner, + preConfiguredSettings, + ); + } + case "codex": { + return this.setupCodex(installDir, selectedAgent, { + webEnabled: false, + }); + } + case "codex-web": { + return this.setupCodex(installDir, selectedAgent, { webEnabled: true }); + } + default: { + console.log(chalk.yellow(`\nIDE ${ide} not yet supported`)); + return false; + } + } + } + + async setupCodex(installDir, selectedAgent, options) { + options = options ?? { webEnabled: false }; + // Codex reads AGENTS.md at the project root as project memory (CLI & Web). + // Inject/update a BMAD section with guidance, directory, and details. + const filePath = path.join(installDir, "AGENTS.md"); + const startMarker = ""; + const endMarker = ""; + + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + const tasks = await this.getAllTaskIds(installDir); + + // Build BMAD section content + let section = ""; + section += `${startMarker}\n`; + section += `# BMAD-METHOD Agents and Tasks\n\n`; + section += `This section is auto-generated by BMAD-METHOD for Codex. Codex merges this AGENTS.md into context.\n\n`; + section += `## How To Use With Codex\n\n`; + section += `- Codex CLI: run \`codex\` in this project. Reference an agent naturally, e.g., "As dev, implement ...".\n`; + section += `- Codex Web: open this repo and reference roles the same way; Codex reads \`AGENTS.md\`.\n`; + section += `- Commit \`.bmad-core\` and this \`AGENTS.md\` file to your repo so Codex (Web/CLI) can read full agent definitions.\n`; + section += `- Refresh this section after agent updates: \`npx bmad-method install -f -i codex\`.\n\n`; + + section += `### Helpful Commands\n\n`; + section += `- List agents: \`npx bmad-method list:agents\`\n`; + section += `- Reinstall BMAD core and regenerate AGENTS.md: \`npx bmad-method install -f -i codex\`\n`; + section += `- Validate configuration: \`npx bmad-method validate\`\n\n`; + + // Agents directory table + section += `## Agents\n\n`; + section += `### Directory\n\n`; + section += `| Title | ID | When To Use |\n|---|---|---|\n`; + const agentSummaries = []; + for (const agentId of agents) { + const agentPath = await this.findAgentPath(agentId, installDir); + if (!agentPath) continue; + const raw = await fileManager.readFile(agentPath); + const yamlMatch = raw.match(/```ya?ml\r?\n([\s\S]*?)```/); + const yamlBlock = yamlMatch ? yamlMatch[1].trim() : null; + const title = await this.getAgentTitle(agentId, installDir); + const whenToUse = + yamlBlock?.match(/whenToUse:\s*"?([^\n"]+)"?/i)?.[1]?.trim() || ""; + agentSummaries.push({ + agentId, + title, + whenToUse, + yamlBlock, + raw, + path: agentPath, + }); + section += `| ${title} | ${agentId} | ${whenToUse || "—"} |\n`; + } + section += `\n`; + + // Detailed agent sections + for (const { + agentId, + title, + whenToUse, + yamlBlock, + raw, + path: agentPath, + } of agentSummaries) { + const relativePath = path + .relative(installDir, agentPath) + .replaceAll("\\", "/"); + section += `### ${title} (id: ${agentId})\n`; + section += `Source: ${relativePath}\n\n`; + if (whenToUse) section += `- When to use: ${whenToUse}\n`; + section += `- How to activate: Mention "As ${agentId}, ..." or "Use ${title} to ..."\n\n`; + if (yamlBlock) { + section += "```yaml\n" + yamlBlock + "\n```\n\n"; + } else { + section += "```md\n" + raw.trim() + "\n```\n\n"; + } + } + + // Tasks + if (tasks && tasks.length > 0) { + section += `## Tasks\n\n`; + section += `These are reusable task briefs you can reference directly in Codex.\n\n`; + for (const taskId of tasks) { + const taskPath = await this.findTaskPath(taskId, installDir); + if (!taskPath) continue; + const raw = await fileManager.readFile(taskPath); + const relativePath = path + .relative(installDir, taskPath) + .replaceAll("\\", "/"); + section += `### Task: ${taskId}\n`; + section += `Source: ${relativePath}\n`; + section += `- How to use: "Use task ${taskId} with the appropriate agent" and paste relevant parts as needed.\n\n`; + section += "```md\n" + raw.trim() + "\n```\n\n"; + } + } + + section += `${endMarker}\n`; + + // Write or update AGENTS.md + let finalContent = ""; + if (await fileManager.pathExists(filePath)) { + const existing = await fileManager.readFile(filePath); + if (existing.includes(startMarker) && existing.includes(endMarker)) { + // Replace existing BMAD block + const pattern = String.raw`${startMarker}[\s\S]*?${endMarker}`; + const replaced = existing.replace(new RegExp(pattern, "m"), section); + finalContent = replaced; + } else { + // Append BMAD block to existing file + finalContent = existing.trimEnd() + `\n\n` + section; + } + } else { + // Create fresh AGENTS.md with a small header and BMAD block + finalContent += "# Project Agents\n\n"; + finalContent += + "This file provides guidance and memory for Codex CLI.\n\n"; + finalContent += section; + } + + await fileManager.writeFile(filePath, finalContent); + console.log( + chalk.green("✓ Created/updated AGENTS.md for Codex CLI integration"), + ); + console.log( + chalk.dim( + "Codex reads AGENTS.md automatically. Run `codex` in this project to use BMAD agents.", + ), + ); + + // Optionally add helpful npm scripts if a package.json exists + try { + const pkgPath = path.join(installDir, "package.json"); + if (await fileManager.pathExists(pkgPath)) { + const pkgRaw = await fileManager.readFile(pkgPath); + const pkg = JSON.parse(pkgRaw); + pkg.scripts = pkg.scripts || {}; + const updated = { ...pkg.scripts }; + if (!updated["bmad:refresh"]) + updated["bmad:refresh"] = "bmad-method install -f -i codex"; + if (!updated["bmad:list"]) + updated["bmad:list"] = "bmad-method list:agents"; + if (!updated["bmad:validate"]) + updated["bmad:validate"] = "bmad-method validate"; + const changed = JSON.stringify(updated) !== JSON.stringify(pkg.scripts); + if (changed) { + const newPkg = { ...pkg, scripts: updated }; + await fileManager.writeFile( + pkgPath, + JSON.stringify(newPkg, null, 2) + "\n", + ); + console.log( + chalk.green( + "✓ Added npm scripts: bmad:refresh, bmad:list, bmad:validate", + ), + ); + } + } + } catch { + console.log( + chalk.yellow( + "! Skipped adding npm scripts (package.json not writable or invalid)", + ), + ); + } + + // Adjust .gitignore behavior depending on Codex mode + try { + const gitignorePath = path.join(installDir, ".gitignore"); + const ignoreLines = ["# BMAD (local only)", ".bmad-core/", ".bmad-*/"]; + const exists = await fileManager.pathExists(gitignorePath); + if (options.webEnabled) { + if (exists) { + let gi = await fileManager.readFile(gitignorePath); + // Remove lines that ignore BMAD dot-folders + const updated = gi + .split(/\r?\n/) + .filter( + (l) => + !/^\s*\.bmad-core\/?\s*$/.test(l) && + !/^\s*\.bmad-\*\/?\s*$/.test(l), + ) + .join("\n"); + if (updated !== gi) { + await fileManager.writeFile( + gitignorePath, + updated.trimEnd() + "\n", + ); + console.log( + chalk.green( + "✓ Updated .gitignore to include .bmad-core in commits", + ), + ); + } + } + } else { + // Local-only: add ignores if missing + let base = exists ? await fileManager.readFile(gitignorePath) : ""; + const haveCore = base.includes(".bmad-core/"); + const haveStar = base.includes(".bmad-*/"); + if (!haveCore || !haveStar) { + const sep = base.endsWith("\n") || base.length === 0 ? "" : "\n"; + const add = [!haveCore || !haveStar ? ignoreLines.join("\n") : ""] + .filter(Boolean) + .join("\n"); + const out = base + sep + add + "\n"; + await fileManager.writeFile(gitignorePath, out); + console.log( + chalk.green( + "✓ Added .bmad-core/* to .gitignore for local-only Codex setup", + ), + ); + } + } + } catch { + console.log(chalk.yellow("! Could not update .gitignore (skipping)")); + } + + return true; + } + + async setupCursor(installDir, selectedAgent) { + const cursorRulesDir = path.join(installDir, ".cursor", "rules", "bmad"); + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + + await fileManager.ensureDirectory(cursorRulesDir); + + for (const agentId of agents) { + const agentPath = await this.findAgentPath(agentId, installDir); + + if (agentPath) { + const mdcContent = await this.createAgentRuleContent( + agentId, + agentPath, + installDir, + "mdc", + ); + const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`); + 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 setupCrush(installDir, selectedAgent) { + // Setup bmad-core commands + const coreSlashPrefix = await this.getCoreSlashPrefix(installDir); + const coreAgents = selectedAgent + ? [selectedAgent] + : await this.getCoreAgentIds(installDir); + const coreTasks = await this.getCoreTaskIds(installDir); + await this.setupCrushForPackage( + installDir, + "core", + coreSlashPrefix, + coreAgents, + coreTasks, + ".bmad-core", + ); + + // Setup expansion pack commands + const expansionPacks = await this.getInstalledExpansionPacks(installDir); + for (const packInfo of expansionPacks) { + const packSlashPrefix = await this.getExpansionPackSlashPrefix( + packInfo.path, + ); + const packAgents = await this.getExpansionPackAgents(packInfo.path); + const packTasks = await this.getExpansionPackTasks(packInfo.path); + + if (packAgents.length > 0 || packTasks.length > 0) { + // Use the actual directory name where the expansion pack is installed + const rootPath = path.relative(installDir, packInfo.path); + await this.setupCrushForPackage( + installDir, + packInfo.name, + packSlashPrefix, + packAgents, + packTasks, + rootPath, + ); + } + } + + return true; + } + + async setupClaudeCode(installDir, selectedAgent) { + // Setup bmad-core commands + const coreSlashPrefix = await this.getCoreSlashPrefix(installDir); + const coreAgents = selectedAgent + ? [selectedAgent] + : await this.getCoreAgentIds(installDir); + const coreTasks = await this.getCoreTaskIds(installDir); + await this.setupClaudeCodeForPackage( + installDir, + "core", + coreSlashPrefix, + coreAgents, + coreTasks, + ".bmad-core", + ); + + // Setup expansion pack commands + const expansionPacks = await this.getInstalledExpansionPacks(installDir); + for (const packInfo of expansionPacks) { + const packSlashPrefix = await this.getExpansionPackSlashPrefix( + packInfo.path, + ); + const packAgents = await this.getExpansionPackAgents(packInfo.path); + const packTasks = await this.getExpansionPackTasks(packInfo.path); + + if (packAgents.length > 0 || packTasks.length > 0) { + // Use the actual directory name where the expansion pack is installed + const rootPath = path.relative(installDir, packInfo.path); + await this.setupClaudeCodeForPackage( + installDir, + packInfo.name, + packSlashPrefix, + packAgents, + packTasks, + rootPath, + ); + } + } + + return true; + } + + async setupClaudeCodeForPackage( + installDir, + packageName, + slashPrefix, + agentIds, + taskIds, + rootPath, + ) { + const commandsBaseDir = path.join( + installDir, + ".claude", + "commands", + slashPrefix, + ); + const agentsDir = path.join(commandsBaseDir, "agents"); + const tasksDir = path.join(commandsBaseDir, "tasks"); + + // Ensure directories exist + await fileManager.ensureDirectory(agentsDir); + await fileManager.ensureDirectory(tasksDir); + + // Setup agents + for (const agentId of agentIds) { + // Find the agent file - for expansion packs, prefer the expansion pack version + let agentPath; + if (packageName === "core") { + // For core, use the normal search + agentPath = await this.findAgentPath(agentId, installDir); + } else { + // For expansion packs, first try to find the agent in the expansion pack directory + const expansionPackPath = path.join( + installDir, + rootPath, + "agents", + `${agentId}.md`, + ); + if (await fileManager.pathExists(expansionPackPath)) { + agentPath = expansionPackPath; + } else { + // Fall back to core if not found in expansion pack + agentPath = await this.findAgentPath(agentId, installDir); + } + } + + const commandPath = path.join(agentsDir, `${agentId}.md`); + + if (agentPath) { + // Create command file with agent content + let agentContent = await fileManager.readFile(agentPath); + + // Replace {root} placeholder with the appropriate root path for this context + agentContent = agentContent.replaceAll("{root}", rootPath); + + // 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 agent command: /${agentId}`)); + } + } + + // Setup tasks + for (const taskId of taskIds) { + // Find the task file - for expansion packs, prefer the expansion pack version + let taskPath; + if (packageName === "core") { + // For core, use the normal search + taskPath = await this.findTaskPath(taskId, installDir); + } else { + // For expansion packs, first try to find the task in the expansion pack directory + const expansionPackPath = path.join( + installDir, + rootPath, + "tasks", + `${taskId}.md`, + ); + if (await fileManager.pathExists(expansionPackPath)) { + taskPath = expansionPackPath; + } else { + // Fall back to core if not found in expansion pack + taskPath = await this.findTaskPath(taskId, installDir); + } + } + + const commandPath = path.join(tasksDir, `${taskId}.md`); + + if (taskPath) { + // Create command file with task content + let taskContent = await fileManager.readFile(taskPath); + + // Replace {root} placeholder with the appropriate root path for this context + taskContent = taskContent.replaceAll("{root}", rootPath); + + // Add command header + let commandContent = `# /${taskId} Task\n\n`; + commandContent += `When this command is used, execute the following task:\n\n`; + commandContent += taskContent; + + await fileManager.writeFile(commandPath, commandContent); + console.log(chalk.green(`✓ Created task command: /${taskId}`)); + } + } + + console.log( + chalk.green( + `\n✓ Created Claude Code commands for ${packageName} in ${commandsBaseDir}`, + ), + ); + console.log(chalk.dim(` - Agents in: ${agentsDir}`)); + console.log(chalk.dim(` - Tasks in: ${tasksDir}`)); + } + + async setupIFlowCli(installDir, selectedAgent) { + // Setup bmad-core commands + const coreSlashPrefix = await this.getCoreSlashPrefix(installDir); + const coreAgents = selectedAgent + ? [selectedAgent] + : await this.getCoreAgentIds(installDir); + const coreTasks = await this.getCoreTaskIds(installDir); + await this.setupIFlowCliForPackage( + installDir, + "core", + coreSlashPrefix, + coreAgents, + coreTasks, + ".bmad-core", + ); + + // Setup expansion pack commands + const expansionPacks = await this.getInstalledExpansionPacks(installDir); + for (const packInfo of expansionPacks) { + const packSlashPrefix = await this.getExpansionPackSlashPrefix( + packInfo.path, + ); + const packAgents = await this.getExpansionPackAgents(packInfo.path); + const packTasks = await this.getExpansionPackTasks(packInfo.path); + + if (packAgents.length > 0 || packTasks.length > 0) { + // Use the actual directory name where the expansion pack is installed + const rootPath = path.relative(installDir, packInfo.path); + await this.setupIFlowCliForPackage( + installDir, + packInfo.name, + packSlashPrefix, + packAgents, + packTasks, + rootPath, + ); + } + } + + return true; + } + + async setupIFlowCliForPackage( + installDir, + packageName, + slashPrefix, + agentIds, + taskIds, + rootPath, + ) { + const commandsBaseDir = path.join( + installDir, + ".iflow", + "commands", + slashPrefix, + ); + const agentsDir = path.join(commandsBaseDir, "agents"); + const tasksDir = path.join(commandsBaseDir, "tasks"); + + // Ensure directories exist + await fileManager.ensureDirectory(agentsDir); + await fileManager.ensureDirectory(tasksDir); + + // Setup agents + for (const agentId of agentIds) { + // Find the agent file - for expansion packs, prefer the expansion pack version + let agentPath; + if (packageName === "core") { + // For core, use the normal search + agentPath = await this.findAgentPath(agentId, installDir); + } else { + // For expansion packs, first try to find the agent in the expansion pack directory + const expansionPackPath = path.join( + installDir, + rootPath, + "agents", + `${agentId}.md`, + ); + if (await fileManager.pathExists(expansionPackPath)) { + agentPath = expansionPackPath; + } else { + // Fall back to core if not found in expansion pack + agentPath = await this.findAgentPath(agentId, installDir); + } + } + + const commandPath = path.join(agentsDir, `${agentId}.md`); + + if (agentPath) { + // Create command file with agent content + let agentContent = await fileManager.readFile(agentPath); + + // Replace {root} placeholder with the appropriate root path for this context + agentContent = agentContent.replaceAll("{root}", rootPath); + + // 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 agent command: /${agentId}`)); + } + } + + // Setup tasks + for (const taskId of taskIds) { + // Find the task file - for expansion packs, prefer the expansion pack version + let taskPath; + if (packageName === "core") { + // For core, use the normal search + taskPath = await this.findTaskPath(taskId, installDir); + } else { + // For expansion packs, first try to find the task in the expansion pack directory + const expansionPackPath = path.join( + installDir, + rootPath, + "tasks", + `${taskId}.md`, + ); + if (await fileManager.pathExists(expansionPackPath)) { + taskPath = expansionPackPath; + } else { + // Fall back to core if not found in expansion pack + taskPath = await this.findTaskPath(taskId, installDir); + } + } + + const commandPath = path.join(tasksDir, `${taskId}.md`); + + if (taskPath) { + // Create command file with task content + let taskContent = await fileManager.readFile(taskPath); + + // Replace {root} placeholder with the appropriate root path for this context + taskContent = taskContent.replaceAll("{root}", rootPath); + + // Add command header + let commandContent = `# /${taskId} Task\n\n`; + commandContent += `When this command is used, execute the following task:\n\n`; + commandContent += taskContent; + + await fileManager.writeFile(commandPath, commandContent); + console.log(chalk.green(`✓ Created task command: /${taskId}`)); + } + } + + console.log( + chalk.green( + `\n✓ Created iFlow CLI commands for ${packageName} in ${commandsBaseDir}`, + ), + ); + console.log(chalk.dim(` - Agents in: ${agentsDir}`)); + console.log(chalk.dim(` - Tasks in: ${tasksDir}`)); + } + + async setupCrushForPackage( + installDir, + packageName, + slashPrefix, + agentIds, + taskIds, + rootPath, + ) { + const commandsBaseDir = path.join( + installDir, + ".crush", + "commands", + slashPrefix, + ); + const agentsDir = path.join(commandsBaseDir, "agents"); + const tasksDir = path.join(commandsBaseDir, "tasks"); + + // Ensure directories exist + await fileManager.ensureDirectory(agentsDir); + await fileManager.ensureDirectory(tasksDir); + + // Setup agents + for (const agentId of agentIds) { + // Find the agent file - for expansion packs, prefer the expansion pack version + let agentPath; + if (packageName === "core") { + // For core, use the normal search + agentPath = await this.findAgentPath(agentId, installDir); + } else { + // For expansion packs, first try to find the agent in the expansion pack directory + const expansionPackPath = path.join( + installDir, + rootPath, + "agents", + `${agentId}.md`, + ); + if (await fileManager.pathExists(expansionPackPath)) { + agentPath = expansionPackPath; + } else { + // Fall back to core if not found in expansion pack + agentPath = await this.findAgentPath(agentId, installDir); + } + } + + const commandPath = path.join(agentsDir, `${agentId}.md`); + + if (agentPath) { + // Create command file with agent content + let agentContent = await fileManager.readFile(agentPath); + + // Replace {root} placeholder with the appropriate root path for this context + agentContent = agentContent.replaceAll("{root}", rootPath); + + // 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 agent command: /${agentId}`)); + } + } + + // Setup tasks + for (const taskId of taskIds) { + // Find the task file - for expansion packs, prefer the expansion pack version + let taskPath; + if (packageName === "core") { + // For core, use the normal search + taskPath = await this.findTaskPath(taskId, installDir); + } else { + // For expansion packs, first try to find the task in the expansion pack directory + const expansionPackPath = path.join( + installDir, + rootPath, + "tasks", + `${taskId}.md`, + ); + if (await fileManager.pathExists(expansionPackPath)) { + taskPath = expansionPackPath; + } else { + // Fall back to core if not found in expansion pack + taskPath = await this.findTaskPath(taskId, installDir); + } + } + + const commandPath = path.join(tasksDir, `${taskId}.md`); + + if (taskPath) { + // Create command file with task content + let taskContent = await fileManager.readFile(taskPath); + + // Replace {root} placeholder with the appropriate root path for this context + taskContent = taskContent.replaceAll("{root}", rootPath); + + // Add command header + let commandContent = `# /${taskId} Task\n\n`; + commandContent += `When this command is used, execute the following task:\n\n`; + commandContent += taskContent; + + await fileManager.writeFile(commandPath, commandContent); + console.log(chalk.green(`✓ Created task command: /${taskId}`)); + } + } + + console.log( + chalk.green( + `\n✓ Created Crush commands for ${packageName} in ${commandsBaseDir}`, + ), + ); + console.log(chalk.dim(` - Agents in: ${agentsDir}`)); + console.log(chalk.dim(` - Tasks in: ${tasksDir}`)); + } + + async setupWindsurf(installDir, selectedAgent) { + const windsurfWorkflowDir = path.join(installDir, ".windsurf", "workflows"); + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + + await fileManager.ensureDirectory(windsurfWorkflowDir); + + for (const agentId of agents) { + // Find the agent file + const agentPath = await this.findAgentPath(agentId, installDir); + + if (agentPath) { + const agentContent = await fileManager.readFile(agentPath); + const mdPath = path.join(windsurfWorkflowDir, `${agentId}.md`); + + // Write the agent file contents prefixed with Windsurf frontmatter + let mdContent = `---\n`; + mdContent += `description: ${agentId}\n`; + mdContent += `auto_execution_mode: 3\n`; + mdContent += `---\n\n`; + mdContent += agentContent; + + await fileManager.writeFile(mdPath, mdContent); + console.log(chalk.green(`✓ Created workflow: ${agentId}.md`)); + } + } + + console.log( + chalk.green(`\n✓ Created Windsurf workflows in ${windsurfWorkflowDir}`), + ); + + return true; + } + + async setupTrae(installDir, selectedAgent) { + const traeRulesDir = path.join(installDir, ".trae", "rules"); + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + + await fileManager.ensureDirectory(traeRulesDir); + + for (const agentId of agents) { + // Find the agent file + const agentPath = await this.findAgentPath(agentId, installDir); + + if (agentPath) { + const agentContent = await fileManager.readFile(agentPath); + const mdPath = path.join(traeRulesDir, `${agentId}.md`); + + // Create MD content (similar to Cursor but without frontmatter) + let mdContent = `# ${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 YAML, 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 += "```yaml\n"; + // Extract just the YAML content from the agent file + const yamlContent = extractYamlFromAgent(agentContent); + if (yamlContent) { + mdContent += yamlContent; + } 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"; + const relativePath = path + .relative(installDir, agentPath) + .replaceAll("\\", "/"); + mdContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\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 YAML configuration above.\n`; + + await fileManager.writeFile(mdPath, mdContent); + console.log(chalk.green(`✓ Created rule: ${agentId}.md`)); + } + } + } + + async findAgentPath(agentId, installDir) { + // Try to find the agent file in various locations + const possiblePaths = [ + path.join(installDir, ".bmad-core", "agents", `${agentId}.md`), + path.join(installDir, "agents", `${agentId}.md`), + ]; + + // Also check expansion pack directories + const glob = require("glob"); + const expansionDirectories = glob.sync(".*/agents", { cwd: installDir }); + for (const expDir of expansionDirectories) { + possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`)); + } + + for (const agentPath of possiblePaths) { + if (await fileManager.pathExists(agentPath)) { + return agentPath; + } + } + + return null; + } + + async getAllAgentIds(installDir) { + const glob = require("glob"); + const allAgentIds = []; + + // Check core agents in .bmad-core or root + let agentsDir = path.join(installDir, ".bmad-core", "agents"); + if (!(await fileManager.pathExists(agentsDir))) { + agentsDir = path.join(installDir, "agents"); + } + + if (await fileManager.pathExists(agentsDir)) { + const agentFiles = glob.sync("*.md", { cwd: agentsDir }); + allAgentIds.push(...agentFiles.map((file) => path.basename(file, ".md"))); + } + + // Also check for expansion pack agents in dot folders + const expansionDirectories = glob.sync(".*/agents", { cwd: installDir }); + for (const expDir of expansionDirectories) { + const fullExpDir = path.join(installDir, expDir); + const expAgentFiles = glob.sync("*.md", { cwd: fullExpDir }); + allAgentIds.push( + ...expAgentFiles.map((file) => path.basename(file, ".md")), + ); + } + + // Remove duplicates + return [...new Set(allAgentIds)]; + } + + async getCoreAgentIds(installDir) { + const allAgentIds = []; + + // Check core agents in .bmad-core or root only + let agentsDir = path.join(installDir, ".bmad-core", "agents"); + if (!(await fileManager.pathExists(agentsDir))) { + agentsDir = path.join(installDir, "bmad-core", "agents"); + } + + if (await fileManager.pathExists(agentsDir)) { + const glob = require("glob"); + const agentFiles = glob.sync("*.md", { cwd: agentsDir }); + allAgentIds.push(...agentFiles.map((file) => path.basename(file, ".md"))); + } + + return [...new Set(allAgentIds)]; + } + + async getCoreTaskIds(installDir) { + const allTaskIds = []; + const glob = require("glob"); + + // Check core tasks in .bmad-core or root only + let tasksDir = path.join(installDir, ".bmad-core", "tasks"); + if (!(await fileManager.pathExists(tasksDir))) { + tasksDir = path.join(installDir, "bmad-core", "tasks"); + } + + if (await fileManager.pathExists(tasksDir)) { + const taskFiles = glob.sync("*.md", { cwd: tasksDir }); + allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md"))); + } + + // Check common tasks + const commonTasksDir = path.join(installDir, "common", "tasks"); + if (await fileManager.pathExists(commonTasksDir)) { + const glob = require("glob"); + const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir }); + allTaskIds.push( + ...commonTaskFiles.map((file) => path.basename(file, ".md")), + ); + } + + return [...new Set(allTaskIds)]; + } + + async getAgentTitle(agentId, installDir) { + // Try to find the agent file in various locations + const possiblePaths = [ + path.join(installDir, ".bmad-core", "agents", `${agentId}.md`), + path.join(installDir, "agents", `${agentId}.md`), + ]; + + // Also check expansion pack directories + const glob = require("glob"); + const expansionDirectories = glob.sync(".*/agents", { cwd: installDir }); + for (const expDir of expansionDirectories) { + possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`)); + } + + for (const agentPath of possiblePaths) { + if (await fileManager.pathExists(agentPath)) { + try { + const agentContent = await fileManager.readFile(agentPath); + const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); + + if (yamlMatch) { + const yaml = yamlMatch[1]; + const titleMatch = yaml.match(/title:\s*(.+)/); + if (titleMatch) { + return titleMatch[1].trim(); + } + } + } catch (error) { + console.warn( + `Failed to read agent title for ${agentId}: ${error.message}`, + ); + } + } + } + + // Fallback to formatted agent ID + return agentId + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + } + + async getAllTaskIds(installDir) { + const glob = require("glob"); + const allTaskIds = []; + + // Check core tasks in .bmad-core or root + let tasksDir = path.join(installDir, ".bmad-core", "tasks"); + if (!(await fileManager.pathExists(tasksDir))) { + tasksDir = path.join(installDir, "bmad-core", "tasks"); + } + + if (await fileManager.pathExists(tasksDir)) { + const taskFiles = glob.sync("*.md", { cwd: tasksDir }); + allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md"))); + } + + // Check common tasks + const commonTasksDir = path.join(installDir, "common", "tasks"); + if (await fileManager.pathExists(commonTasksDir)) { + const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir }); + allTaskIds.push( + ...commonTaskFiles.map((file) => path.basename(file, ".md")), + ); + } + + // Also check for expansion pack tasks in dot folders + const expansionDirectories = glob.sync(".*/tasks", { cwd: installDir }); + for (const expDir of expansionDirectories) { + const fullExpDir = path.join(installDir, expDir); + const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir }); + allTaskIds.push( + ...expTaskFiles.map((file) => path.basename(file, ".md")), + ); + } + + // Check expansion-packs folder tasks + const expansionPacksDir = path.join(installDir, "expansion-packs"); + if (await fileManager.pathExists(expansionPacksDir)) { + const expPackDirectories = glob.sync("*/tasks", { + cwd: expansionPacksDir, + }); + for (const expDir of expPackDirectories) { + const fullExpDir = path.join(expansionPacksDir, expDir); + const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir }); + allTaskIds.push( + ...expTaskFiles.map((file) => path.basename(file, ".md")), + ); + } + } + + // Remove duplicates + return [...new Set(allTaskIds)]; + } + + async findTaskPath(taskId, installDir) { + // Try to find the task file in various locations + const possiblePaths = [ + path.join(installDir, ".bmad-core", "tasks", `${taskId}.md`), + path.join(installDir, "bmad-core", "tasks", `${taskId}.md`), + path.join(installDir, "common", "tasks", `${taskId}.md`), + ]; + + // Also check expansion pack directories + const glob = require("glob"); + + // Check dot folder expansion packs + const expansionDirectories = glob.sync(".*/tasks", { cwd: installDir }); + for (const expDir of expansionDirectories) { + possiblePaths.push(path.join(installDir, expDir, `${taskId}.md`)); + } + + // Check expansion-packs folder + const expansionPacksDir = path.join(installDir, "expansion-packs"); + if (await fileManager.pathExists(expansionPacksDir)) { + const expPackDirectories = glob.sync("*/tasks", { + cwd: expansionPacksDir, + }); + for (const expDir of expPackDirectories) { + possiblePaths.push( + path.join(expansionPacksDir, expDir, `${taskId}.md`), + ); + } + } + + for (const taskPath of possiblePaths) { + if (await fileManager.pathExists(taskPath)) { + return taskPath; + } + } + + return null; + } + + async getCoreSlashPrefix(installDir) { + try { + const coreConfigPath = path.join( + installDir, + ".bmad-core", + "core-config.yaml", + ); + if (!(await fileManager.pathExists(coreConfigPath))) { + // Try bmad-core directory + const altConfigPath = path.join( + installDir, + "bmad-core", + "core-config.yaml", + ); + if (await fileManager.pathExists(altConfigPath)) { + const configContent = await fileManager.readFile(altConfigPath); + const config = yaml.load(configContent); + return config.slashPrefix || "BMad"; + } + return "BMad"; // fallback + } + + const configContent = await fileManager.readFile(coreConfigPath); + const config = yaml.load(configContent); + return config.slashPrefix || "BMad"; + } catch (error) { + console.warn( + `Failed to read core slashPrefix, using default 'BMad': ${error.message}`, + ); + return "BMad"; + } + } + + async getInstalledExpansionPacks(installDir) { + const expansionPacks = []; + + // Check for dot-prefixed expansion packs in install directory + const glob = require("glob"); + const dotExpansions = glob.sync(".bmad-*", { cwd: installDir }); + + for (const dotExpansion of dotExpansions) { + if (dotExpansion !== ".bmad-core") { + const packPath = path.join(installDir, dotExpansion); + const packName = dotExpansion.slice(1); // remove the dot + expansionPacks.push({ + name: packName, + path: packPath, + }); + } + } + + // Check for expansion-packs directory style + const expansionPacksDir = path.join(installDir, "expansion-packs"); + if (await fileManager.pathExists(expansionPacksDir)) { + const packDirectories = glob.sync("*", { cwd: expansionPacksDir }); + + for (const packDir of packDirectories) { + const packPath = path.join(expansionPacksDir, packDir); + if ( + (await fileManager.pathExists(packPath)) && + (await fileManager.pathExists(path.join(packPath, "config.yaml"))) + ) { + expansionPacks.push({ + name: packDir, + path: packPath, + }); + } + } + } + + return expansionPacks; + } + + async getExpansionPackSlashPrefix(packPath) { + try { + const configPath = path.join(packPath, "config.yaml"); + if (await fileManager.pathExists(configPath)) { + const configContent = await fileManager.readFile(configPath); + const config = yaml.load(configContent); + return config.slashPrefix || path.basename(packPath); + } + } catch (error) { + console.warn( + `Failed to read expansion pack slashPrefix from ${packPath}: ${error.message}`, + ); + } + + return path.basename(packPath); // fallback to directory name + } + + async getExpansionPackAgents(packPath) { + const agentsDir = path.join(packPath, "agents"); + if (!(await fileManager.pathExists(agentsDir))) { + return []; + } + + try { + const glob = require("glob"); + const agentFiles = glob.sync("*.md", { cwd: agentsDir }); + return agentFiles.map((file) => path.basename(file, ".md")); + } catch (error) { + console.warn( + `Failed to read expansion pack agents from ${packPath}: ${error.message}`, + ); + return []; + } + } + + async getExpansionPackTasks(packPath) { + const tasksDir = path.join(packPath, "tasks"); + if (!(await fileManager.pathExists(tasksDir))) { + return []; + } + + try { + const glob = require("glob"); + const taskFiles = glob.sync("*.md", { cwd: tasksDir }); + return taskFiles.map((file) => path.basename(file, ".md")); + } catch (error) { + console.warn( + `Failed to read expansion pack tasks from ${packPath}: ${error.message}`, + ); + return []; + } + } + + async setupRoo(installDir, selectedAgent) { + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + + // Check for existing .roomodes file in project root + const roomodesPath = path.join(installDir, ".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 = ""; + + // Load dynamic agent permissions from configuration + const config = await this.loadIdeAgentConfig(); + const agentPermissions = config["roo-permissions"] || {}; + + for (const agentId of agents) { + // Skip if already exists + // Check both with and without bmad- prefix to handle both cases + const checkSlug = agentId.startsWith("bmad-") + ? agentId + : `bmad-${agentId}`; + if (existingModes.includes(checkSlug)) { + console.log( + chalk.dim(`Skipping ${agentId} - already exists in .roomodes`), + ); + continue; + } + + // Read agent file to extract all information + const agentPath = await this.findAgentPath(agentId, installDir); + + if (agentPath) { + const agentContent = await fileManager.readFile(agentPath); + + // Extract YAML content + const yamlMatch = agentContent.match(/```ya?ml\r?\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.`; + + // Add permissions based on agent type + const permissions = agentPermissions[agentId]; + // Build mode entry with proper formatting (matching exact indentation) + // Avoid double "bmad-" prefix for agents that already have it + const slug = agentId.startsWith("bmad-") + ? agentId + : `bmad-${agentId}`; + newModesContent += ` - slug: ${slug}\n`; + newModesContent += ` name: '${icon} ${title}'\n`; + if (permissions) { + newModesContent += ` description: '${permissions.description}'\n`; + } + newModesContent += ` roleDefinition: ${roleDefinition}\n`; + newModesContent += ` whenToUse: ${whenToUse}\n`; + // Get relative path from installDir to agent file + const relativePath = path + .relative(installDir, agentPath) + .replaceAll("\\", "/"); + newModesContent += ` 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\n`; + newModesContent += ` groups:\n`; + newModesContent += ` - read\n`; + + 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 .roomodes file in project root")); + + 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 setupKilocode(installDir, selectedAgent) { + 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) + .replaceAll("\\", "/"); + 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; + } + + async setupCline(installDir, selectedAgent) { + 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 += "```yaml\n"; + // Extract just the YAML content from the agent file + const yamlContent = extractYamlFromAgent(agentContent); + if (yamlContent) { + mdContent += yamlContent; + } 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) + .replaceAll("\\", "/"); + 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 setupOpencodeCli(installDir, selectedAgent) { + const ideConfig = await configLoader.getIdeConfiguration("opencode"); + const bmadCommandsDir = path.join(installDir, ideConfig["rule-dir"]); + + const agentCommandsDir = path.join(bmadCommandsDir, "agent"); + const taskCommandsDir = path.join(bmadCommandsDir, "command"); + await fileManager.ensureDirectory(agentCommandsDir); + await fileManager.ensureDirectory(taskCommandsDir); + + // Process Agents + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + for (const agentId of agents) { + const agentPath = await this.findAgentPath(agentId, installDir); + if (!agentPath) { + console.log( + chalk.yellow(`✗ Agent file not found for ${agentId}, skipping.`), + ); + continue; + } + + const agentTitle = await this.getAgentTitle(agentId, installDir); + const commandPath = path.join(agentCommandsDir, `${agentId}.toml`); + + // Get relative path from installDir to agent file for @{file} reference + const relativeAgentPath = path + .relative(installDir, agentPath) + .replaceAll("\\", "/"); + + const tomlContent = `--- +description = "Activates the ${agentTitle} agent from the BMad Method." +--- CRITICAL: You are now the BMad '${agentTitle}' agent. Adopt its persona, follow its instructions, and use its capabilities. The full agent definition is below. @{${relativeAgentPath}} -"""`; +`; - await fileManager.writeFile(commandPath, tomlContent); - console.log(chalk.green(`✓ Created agent command: /bmad:agents:${agentId}`)); - } + await fileManager.writeFile(commandPath, tomlContent); + console.log( + chalk.green(`✓ Created agent command: /bmad:agents:${agentId}`), + ); + } - // Process Tasks - const tasks = await this.getAllTaskIds(installDir); - for (const taskId of tasks) { - const taskPath = await this.findTaskPath(taskId, installDir); - if (!taskPath) { - console.log(chalk.yellow(`✗ Task file not found for ${taskId}, skipping.`)); - continue; - } + // Process Tasks + const tasks = await this.getAllTaskIds(installDir); + for (const taskId of tasks) { + const taskPath = await this.findTaskPath(taskId, installDir); + if (!taskPath) { + console.log( + chalk.yellow(`✗ Task file not found for ${taskId}, skipping.`), + ); + continue; + } - const taskTitle = taskId - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - const commandPath = path.join(taskCommandsDir, `${taskId}.toml`); + const taskTitle = taskId + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + const commandPath = path.join(taskCommandsDir, `${taskId}.toml`); - // Get relative path from installDir to task file for @{file} reference - const relativeTaskPath = path.relative(installDir, taskPath).replaceAll('\\', '/'); + // Get relative path from installDir to task file for @{file} reference + const relativeTaskPath = path + .relative(installDir, taskPath) + .replaceAll("\\", "/"); - const tomlContent = `description = "Executes the BMad Task: ${taskTitle}" + const tomlContent = `description = "Executes the BMad Task: ${taskTitle}" prompt = """ CRITICAL: You are to execute the BMad Task defined below. @{${relativeTaskPath}} """`; - await fileManager.writeFile(commandPath, tomlContent); - console.log(chalk.green(`✓ Created task command: /bmad:tasks:${taskId}`)); - } + await fileManager.writeFile(commandPath, tomlContent); + console.log(chalk.green(`✓ Created task command: /bmad:tasks:${taskId}`)); + } - console.log( - chalk.green(` + console.log( + chalk.green(` +✓ Created Opencode CLI extension in ${bmadCommandsDir}`), + ); + console.log( + chalk.dim( + "You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.", + ), + ); + + return true; + } + + async setupGeminiCli(installDir, selectedAgent) { + const ideConfig = await configLoader.getIdeConfiguration("gemini"); + const bmadCommandsDir = path.join(installDir, ideConfig["rule-dir"]); + + const agentCommandsDir = path.join(bmadCommandsDir, "agents"); + const taskCommandsDir = path.join(bmadCommandsDir, "tasks"); + await fileManager.ensureDirectory(agentCommandsDir); + await fileManager.ensureDirectory(taskCommandsDir); + + // Process Agents + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + for (const agentId of agents) { + const agentPath = await this.findAgentPath(agentId, installDir); + if (!agentPath) { + console.log( + chalk.yellow(`✗ Agent file not found for ${agentId}, skipping.`), + ); + continue; + } + + const agentTitle = await this.getAgentTitle(agentId, installDir); + const commandPath = path.join(agentCommandsDir, `${agentId}.toml`); + + // Get relative path from installDir to agent file for @{file} reference + const relativeAgentPath = path + .relative(installDir, agentPath) + .replaceAll("\\", "/"); + + const tomlContent = `description = "Activates the ${agentTitle} agent from the BMad Method." +prompt = """ +CRITICAL: You are now the BMad '${agentTitle}' agent. Adopt its persona, follow its instructions, and use its capabilities. The full agent definition is below. + +@{${relativeAgentPath}} +"""`; + + await fileManager.writeFile(commandPath, tomlContent); + console.log( + chalk.green(`✓ Created agent command: /bmad:agents:${agentId}`), + ); + } + + // Process Tasks + const tasks = await this.getAllTaskIds(installDir); + for (const taskId of tasks) { + const taskPath = await this.findTaskPath(taskId, installDir); + if (!taskPath) { + console.log( + chalk.yellow(`✗ Task file not found for ${taskId}, skipping.`), + ); + continue; + } + + const taskTitle = taskId + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + const commandPath = path.join(taskCommandsDir, `${taskId}.toml`); + + // Get relative path from installDir to task file for @{file} reference + const relativeTaskPath = path + .relative(installDir, taskPath) + .replaceAll("\\", "/"); + + const tomlContent = `description = "Executes the BMad Task: ${taskTitle}" +prompt = """ +CRITICAL: You are to execute the BMad Task defined below. + +@{${relativeTaskPath}} +"""`; + + await fileManager.writeFile(commandPath, tomlContent); + console.log(chalk.green(`✓ Created task command: /bmad:tasks:${taskId}`)); + } + + console.log( + chalk.green(` ✓ Created Gemini CLI extension in ${bmadCommandsDir}`), - ); - console.log( - chalk.dim('You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.'), - ); + ); + console.log( + chalk.dim( + "You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.", + ), + ); - return true; - } + return true; + } - async setupQwenCode(installDir, selectedAgent) { - const qwenDir = path.join(installDir, '.qwen'); - const bmadMethodDir = path.join(qwenDir, 'bmad-method'); - await fileManager.ensureDirectory(bmadMethodDir); + async setupQwenCode(installDir, selectedAgent) { + const qwenDir = path.join(installDir, ".qwen"); + const bmadMethodDir = path.join(qwenDir, "bmad-method"); + await fileManager.ensureDirectory(bmadMethodDir); - // Update logic for existing settings.json - const settingsPath = path.join(qwenDir, 'settings.json'); - if (await fileManager.pathExists(settingsPath)) { - try { - const settingsContent = await fileManager.readFile(settingsPath); - const settings = JSON.parse(settingsContent); - let updated = false; + // Update logic for existing settings.json + const settingsPath = path.join(qwenDir, "settings.json"); + if (await fileManager.pathExists(settingsPath)) { + try { + const settingsContent = await fileManager.readFile(settingsPath); + const settings = JSON.parse(settingsContent); + let updated = false; - // Handle contextFileName property - if (settings.contextFileName && Array.isArray(settings.contextFileName)) { - const originalLength = settings.contextFileName.length; - settings.contextFileName = settings.contextFileName.filter( - (fileName) => !fileName.startsWith('agents/'), - ); - if (settings.contextFileName.length !== originalLength) { - updated = true; - } - } + // Handle contextFileName property + if ( + settings.contextFileName && + Array.isArray(settings.contextFileName) + ) { + const originalLength = settings.contextFileName.length; + settings.contextFileName = settings.contextFileName.filter( + (fileName) => !fileName.startsWith("agents/"), + ); + if (settings.contextFileName.length !== originalLength) { + updated = true; + } + } - if (updated) { - await fileManager.writeFile(settingsPath, JSON.stringify(settings, null, 2)); - console.log(chalk.green('✓ Updated .qwen/settings.json - removed agent file references')); - } - } catch (error) { - console.warn(chalk.yellow('Could not update .qwen/settings.json'), error); - } - } + if (updated) { + await fileManager.writeFile( + settingsPath, + JSON.stringify(settings, null, 2), + ); + console.log( + chalk.green( + "✓ Updated .qwen/settings.json - removed agent file references", + ), + ); + } + } catch (error) { + console.warn( + chalk.yellow("Could not update .qwen/settings.json"), + error, + ); + } + } - // Remove old agents directory - const agentsDir = path.join(qwenDir, 'agents'); - if (await fileManager.pathExists(agentsDir)) { - await fileManager.removeDirectory(agentsDir); - console.log(chalk.green('✓ Removed old .qwen/agents directory')); - } + // Remove old agents directory + const agentsDir = path.join(qwenDir, "agents"); + if (await fileManager.pathExists(agentsDir)) { + await fileManager.removeDirectory(agentsDir); + console.log(chalk.green("✓ Removed old .qwen/agents directory")); + } - // Get all available agents - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); - let concatenatedContent = ''; + // Get all available agents + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); + let concatenatedContent = ""; - for (const agentId of agents) { - // Find the source agent file - const agentPath = await this.findAgentPath(agentId, installDir); + 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); + if (agentPath) { + const agentContent = await fileManager.readFile(agentPath); - // Create properly formatted agent rule content (similar to gemini) - let agentRuleContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`; - agentRuleContent += `This rule is triggered when the user types \`*${agentId}\` and activates the ${await this.getAgentTitle( - agentId, - installDir, - )} agent persona.\n\n`; - agentRuleContent += '## Agent Activation\n\n'; - agentRuleContent += - 'CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n'; - agentRuleContent += '```yaml\n'; - // Extract just the YAML content from the agent file - const yamlContent = extractYamlFromAgent(agentContent); - if (yamlContent) { - agentRuleContent += yamlContent; - } else { - // If no YAML found, include the whole content minus the header - agentRuleContent += agentContent.replace(/^#.*$/m, '').trim(); - } - agentRuleContent += '\n```\n\n'; - agentRuleContent += '## File Reference\n\n'; - const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/'); - agentRuleContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`; - agentRuleContent += '## Usage\n\n'; - agentRuleContent += `When the user types \`*${agentId}\`, activate this ${await this.getAgentTitle( - agentId, - installDir, - )} persona and follow all instructions defined in the YAML configuration above.\n`; + // Create properly formatted agent rule content (similar to gemini) + let agentRuleContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`; + agentRuleContent += `This rule is triggered when the user types \`*${agentId}\` and activates the ${await this.getAgentTitle( + agentId, + installDir, + )} agent persona.\n\n`; + agentRuleContent += "## Agent Activation\n\n"; + agentRuleContent += + "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; + agentRuleContent += "```yaml\n"; + // Extract just the YAML content from the agent file + const yamlContent = extractYamlFromAgent(agentContent); + if (yamlContent) { + agentRuleContent += yamlContent; + } else { + // If no YAML found, include the whole content minus the header + agentRuleContent += agentContent.replace(/^#.*$/m, "").trim(); + } + agentRuleContent += "\n```\n\n"; + agentRuleContent += "## File Reference\n\n"; + const relativePath = path + .relative(installDir, agentPath) + .replaceAll("\\", "/"); + agentRuleContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`; + agentRuleContent += "## Usage\n\n"; + agentRuleContent += `When the user types \`*${agentId}\`, activate this ${await this.getAgentTitle( + agentId, + installDir, + )} persona and follow all instructions defined in the YAML configuration above.\n`; - // Add to concatenated content with separator - concatenatedContent += agentRuleContent + '\n\n---\n\n'; - console.log(chalk.green(`✓ Added context for *${agentId}`)); - } - } + // Add to concatenated content with separator + concatenatedContent += agentRuleContent + "\n\n---\n\n"; + console.log(chalk.green(`✓ Added context for *${agentId}`)); + } + } - // Write the concatenated content to QWEN.md - const qwenMdPath = path.join(bmadMethodDir, 'QWEN.md'); - await fileManager.writeFile(qwenMdPath, concatenatedContent); - console.log(chalk.green(`\n✓ Created QWEN.md in ${bmadMethodDir}`)); + // Write the concatenated content to QWEN.md + const qwenMdPath = path.join(bmadMethodDir, "QWEN.md"); + await fileManager.writeFile(qwenMdPath, concatenatedContent); + console.log(chalk.green(`\n✓ Created QWEN.md in ${bmadMethodDir}`)); - return true; - } + return true; + } - async setupGitHubCopilot( - installDir, - selectedAgent, - spinner = null, - preConfiguredSettings = null, - ) { - // Configure VS Code workspace settings first to avoid UI conflicts with loading spinners - await this.configureVsCodeSettings(installDir, spinner, preConfiguredSettings); + async setupGitHubCopilot( + installDir, + selectedAgent, + spinner = null, + preConfiguredSettings = null, + ) { + // Configure VS Code workspace settings first to avoid UI conflicts with loading spinners + await this.configureVsCodeSettings( + installDir, + spinner, + preConfiguredSettings, + ); - const chatmodesDir = path.join(installDir, '.github', 'chatmodes'); - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); + const chatmodesDir = path.join(installDir, ".github", "chatmodes"); + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); - await fileManager.ensureDirectory(chatmodesDir); + await fileManager.ensureDirectory(chatmodesDir); - for (const agentId of agents) { - // Find the agent file - const agentPath = await this.findAgentPath(agentId, installDir); - const chatmodePath = path.join(chatmodesDir, `${agentId}.chatmode.md`); + for (const agentId of agents) { + // Find the agent file + const agentPath = await this.findAgentPath(agentId, installDir); + const chatmodePath = path.join(chatmodesDir, `${agentId}.chatmode.md`); - if (agentPath) { - // Create chat mode file with agent content - const agentContent = await fileManager.readFile(agentPath); - const agentTitle = await this.getAgentTitle(agentId, installDir); + if (agentPath) { + // Create chat mode file with agent content + const agentContent = await fileManager.readFile(agentPath); + const agentTitle = await this.getAgentTitle(agentId, installDir); - // Extract whenToUse for the description - const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); - let description = `Activates the ${agentTitle} agent persona.`; - if (yamlMatch) { - const whenToUseMatch = yamlMatch[1].match(/whenToUse:\s*"(.*?)"/); - if (whenToUseMatch && whenToUseMatch[1]) { - description = whenToUseMatch[1]; - } - } + // Extract whenToUse for the description + const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); + let description = `Activates the ${agentTitle} agent persona.`; + if (yamlMatch) { + const whenToUseMatch = yamlMatch[1].match(/whenToUse:\s*"(.*?)"/); + if (whenToUseMatch && whenToUseMatch[1]) { + description = whenToUseMatch[1]; + } + } - let chatmodeContent = `--- + let chatmodeContent = `--- description: "${description.replaceAll('"', String.raw`\"`)}" tools: ['changes', 'codebase', 'fetch', 'findTestFiles', 'githubRepo', 'problems', 'usages', 'editFiles', 'runCommands', 'runTasks', 'runTests', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure'] --- `; - chatmodeContent += agentContent; + chatmodeContent += agentContent; - await fileManager.writeFile(chatmodePath, chatmodeContent); - console.log(chalk.green(`✓ Created chat mode: ${agentId}.chatmode.md`)); - } - } + await fileManager.writeFile(chatmodePath, chatmodeContent); + console.log(chalk.green(`✓ Created chat mode: ${agentId}.chatmode.md`)); + } + } - console.log(chalk.green(`\n✓ Github Copilot setup complete!`)); - console.log(chalk.dim(`You can now find the BMad agents in the Chat view's mode selector.`)); + console.log(chalk.green(`\n✓ Github Copilot setup complete!`)); + console.log( + chalk.dim( + `You can now find the BMad agents in the Chat view's mode selector.`, + ), + ); - return true; - } + return true; + } - async configureVsCodeSettings(installDir, spinner, preConfiguredSettings = null) { - const vscodeDir = path.join(installDir, '.vscode'); - const settingsPath = path.join(vscodeDir, 'settings.json'); + async configureVsCodeSettings( + installDir, + spinner, + preConfiguredSettings = null, + ) { + const vscodeDir = path.join(installDir, ".vscode"); + const settingsPath = path.join(vscodeDir, "settings.json"); - await fileManager.ensureDirectory(vscodeDir); + await fileManager.ensureDirectory(vscodeDir); - // Read existing settings if they exist - let existingSettings = {}; - if (await fileManager.pathExists(settingsPath)) { - try { - const existingContent = await fileManager.readFile(settingsPath); - existingSettings = JSON.parse(existingContent); - console.log(chalk.yellow('Found existing .vscode/settings.json. Merging BMad settings...')); - } catch { - console.warn(chalk.yellow('Could not parse existing settings.json. Creating new one.')); - existingSettings = {}; - } - } + // Read existing settings if they exist + let existingSettings = {}; + if (await fileManager.pathExists(settingsPath)) { + try { + const existingContent = await fileManager.readFile(settingsPath); + existingSettings = JSON.parse(existingContent); + console.log( + chalk.yellow( + "Found existing .vscode/settings.json. Merging BMad settings...", + ), + ); + } catch { + console.warn( + chalk.yellow( + "Could not parse existing settings.json. Creating new one.", + ), + ); + existingSettings = {}; + } + } - // Use pre-configured settings if provided, otherwise prompt - let configChoice; - if (preConfiguredSettings && preConfiguredSettings.configChoice) { - configChoice = preConfiguredSettings.configChoice; - console.log(chalk.dim(`Using pre-configured GitHub Copilot settings: ${configChoice}`)); - } else { - // Clear any previous output and add spacing to avoid conflicts with loaders - console.log('\n'.repeat(2)); - console.log(chalk.blue('🔧 Github Copilot Agent Settings Configuration')); - console.log( - chalk.dim('BMad works best with specific VS Code settings for optimal agent experience.'), - ); - console.log(''); // Add extra spacing + // Use pre-configured settings if provided, otherwise prompt + let configChoice; + if (preConfiguredSettings && preConfiguredSettings.configChoice) { + configChoice = preConfiguredSettings.configChoice; + console.log( + chalk.dim( + `Using pre-configured GitHub Copilot settings: ${configChoice}`, + ), + ); + } else { + // Clear any previous output and add spacing to avoid conflicts with loaders + console.log("\n".repeat(2)); + console.log(chalk.blue("🔧 Github Copilot Agent Settings Configuration")); + console.log( + chalk.dim( + "BMad works best with specific VS Code settings for optimal agent experience.", + ), + ); + console.log(""); // Add extra spacing - const response = await inquirer.prompt([ - { - type: 'list', - name: 'configChoice', - message: chalk.yellow('How would you like to configure GitHub Copilot settings?'), - choices: [ - { - name: 'Use recommended defaults (fastest setup)', - value: 'defaults', - }, - { - name: 'Configure each setting manually (customize to your preferences)', - value: 'manual', - }, - { - name: "Skip settings configuration (I'll configure manually later)", - value: 'skip', - }, - ], - default: 'defaults', - }, - ]); - configChoice = response.configChoice; - } + const response = await inquirer.prompt([ + { + type: "list", + name: "configChoice", + message: chalk.yellow( + "How would you like to configure GitHub Copilot settings?", + ), + choices: [ + { + name: "Use recommended defaults (fastest setup)", + value: "defaults", + }, + { + name: "Configure each setting manually (customize to your preferences)", + value: "manual", + }, + { + name: "Skip settings configuration (I'll configure manually later)", + value: "skip", + }, + ], + default: "defaults", + }, + ]); + configChoice = response.configChoice; + } - let bmadSettings = {}; + let bmadSettings = {}; - if (configChoice === 'skip') { - console.log(chalk.yellow('⚠️ Skipping VS Code settings configuration.')); - console.log(chalk.dim('You can manually configure these settings in .vscode/settings.json:')); - console.log(chalk.dim(' • chat.agent.enabled: true')); - console.log(chalk.dim(' • chat.agent.maxRequests: 15')); - console.log(chalk.dim(' • github.copilot.chat.agent.runTasks: true')); - console.log(chalk.dim(' • chat.mcp.discovery.enabled: true')); - console.log(chalk.dim(' • github.copilot.chat.agent.autoFix: true')); - console.log(chalk.dim(' • chat.tools.autoApprove: false')); - return true; - } + if (configChoice === "skip") { + console.log(chalk.yellow("! Skipping VS Code settings configuration.")); + console.log( + chalk.dim( + "You can manually configure these settings in .vscode/settings.json:", + ), + ); + console.log(chalk.dim(" • chat.agent.enabled: true")); + console.log(chalk.dim(" • chat.agent.maxRequests: 15")); + console.log(chalk.dim(" • github.copilot.chat.agent.runTasks: true")); + console.log(chalk.dim(" • chat.mcp.discovery.enabled: true")); + console.log(chalk.dim(" • github.copilot.chat.agent.autoFix: true")); + console.log(chalk.dim(" • chat.tools.autoApprove: false")); + return true; + } - if (configChoice === 'defaults') { - // Use recommended defaults - bmadSettings = { - 'chat.agent.enabled': true, - 'chat.agent.maxRequests': 15, - 'github.copilot.chat.agent.runTasks': true, - 'chat.mcp.discovery.enabled': true, - 'github.copilot.chat.agent.autoFix': true, - 'chat.tools.autoApprove': false, - }; - console.log(chalk.green('✓ Using recommended BMad defaults for Github Copilot settings')); - } else { - // Manual configuration - console.log(chalk.blue("\n📋 Let's configure each setting for your preferences:")); + if (configChoice === "defaults") { + // Use recommended defaults + bmadSettings = { + "chat.agent.enabled": true, + "chat.agent.maxRequests": 15, + "github.copilot.chat.agent.runTasks": true, + "chat.mcp.discovery.enabled": true, + "github.copilot.chat.agent.autoFix": true, + "chat.tools.autoApprove": false, + }; + console.log( + chalk.green( + "✓ Using recommended BMad defaults for Github Copilot settings", + ), + ); + } else { + // Manual configuration + console.log( + chalk.blue("\n📋 Let's configure each setting for your preferences:"), + ); - // Pause spinner during manual configuration prompts - let spinnerWasActive = false; - if (spinner && spinner.isSpinning) { - spinner.stop(); - spinnerWasActive = true; - } + // Pause spinner during manual configuration prompts + let spinnerWasActive = false; + if (spinner && spinner.isSpinning) { + spinner.stop(); + spinnerWasActive = true; + } - const manualSettings = await inquirer.prompt([ - { - type: 'input', - name: 'maxRequests', - message: 'Maximum requests per agent session (recommended: 15)?', - default: '15', - validate: (input) => { - const number_ = Number.parseInt(input); - if (isNaN(number_) || number_ < 1 || number_ > 50) { - return 'Please enter a number between 1 and 50'; - } - return true; - }, - }, - { - type: 'confirm', - name: 'runTasks', - message: 'Allow agents to run workspace tasks (package.json scripts, etc.)?', - default: true, - }, - { - type: 'confirm', - name: 'mcpDiscovery', - message: 'Enable MCP (Model Context Protocol) server discovery?', - default: true, - }, - { - type: 'confirm', - name: 'autoFix', - message: 'Enable automatic error detection and fixing in generated code?', - default: true, - }, - { - type: 'confirm', - name: 'autoApprove', - message: 'Auto-approve ALL tools without confirmation? (⚠️ EXPERIMENTAL - less secure)', - default: false, - }, - ]); + const manualSettings = await inquirer.prompt([ + { + type: "input", + name: "maxRequests", + message: "Maximum requests per agent session (recommended: 15)?", + default: "15", + validate: (input) => { + const number_ = Number.parseInt(input); + if (isNaN(number_) || number_ < 1 || number_ > 50) { + return "Please enter a number between 1 and 50"; + } + return true; + }, + }, + { + type: "confirm", + name: "runTasks", + message: + "Allow agents to run workspace tasks (package.json scripts, etc.)?", + default: true, + }, + { + type: "confirm", + name: "mcpDiscovery", + message: "Enable MCP (Model Context Protocol) server discovery?", + default: true, + }, + { + type: "confirm", + name: "autoFix", + message: + "Enable automatic error detection and fixing in generated code?", + default: true, + }, + { + type: "confirm", + name: "autoApprove", + message: + "Auto-approve ALL tools without confirmation? (! EXPERIMENTAL - less secure)", + default: false, + }, + ]); - // Restart spinner if it was active before prompts - if (spinner && spinnerWasActive) { - spinner.start(); - } + // Restart spinner if it was active before prompts + if (spinner && spinnerWasActive) { + spinner.start(); + } - bmadSettings = { - 'chat.agent.enabled': true, // Always enabled - required for BMad agents - 'chat.agent.maxRequests': Number.parseInt(manualSettings.maxRequests), - 'github.copilot.chat.agent.runTasks': manualSettings.runTasks, - 'chat.mcp.discovery.enabled': manualSettings.mcpDiscovery, - 'github.copilot.chat.agent.autoFix': manualSettings.autoFix, - 'chat.tools.autoApprove': manualSettings.autoApprove, - }; + bmadSettings = { + "chat.agent.enabled": true, // Always enabled - required for BMad agents + "chat.agent.maxRequests": Number.parseInt(manualSettings.maxRequests), + "github.copilot.chat.agent.runTasks": manualSettings.runTasks, + "chat.mcp.discovery.enabled": manualSettings.mcpDiscovery, + "github.copilot.chat.agent.autoFix": manualSettings.autoFix, + "chat.tools.autoApprove": manualSettings.autoApprove, + }; - console.log(chalk.green('✓ Custom settings configured')); - } + console.log(chalk.green("✓ Custom settings configured")); + } - // Merge settings (existing settings take precedence to avoid overriding user preferences) - const mergedSettings = { ...bmadSettings, ...existingSettings }; + // Merge settings (existing settings take precedence to avoid overriding user preferences) + const mergedSettings = { ...bmadSettings, ...existingSettings }; - // Write the updated settings - await fileManager.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2)); + // Write the updated settings + await fileManager.writeFile( + settingsPath, + JSON.stringify(mergedSettings, null, 2), + ); - console.log(chalk.green('✓ VS Code workspace settings configured successfully')); - console.log(chalk.dim(' Settings written to .vscode/settings.json:')); - for (const [key, value] of Object.entries(bmadSettings)) { - console.log(chalk.dim(` • ${key}: ${value}`)); - } - console.log(chalk.dim('')); - console.log(chalk.dim('You can modify these settings anytime in .vscode/settings.json')); - } + console.log( + chalk.green("✓ VS Code workspace settings configured successfully"), + ); + console.log(chalk.dim(" Settings written to .vscode/settings.json:")); + for (const [key, value] of Object.entries(bmadSettings)) { + console.log(chalk.dim(` • ${key}: ${value}`)); + } + console.log(chalk.dim("")); + console.log( + chalk.dim( + "You can modify these settings anytime in .vscode/settings.json", + ), + ); + } - async setupAuggieCLI(installDir, selectedAgent, spinner = null, preConfiguredSettings = null) { - const os = require('node:os'); - const inquirer = require('inquirer'); - const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); + async setupAuggieCLI( + installDir, + selectedAgent, + spinner = null, + preConfiguredSettings = null, + ) { + const os = require("node:os"); + const inquirer = require("inquirer"); + const agents = selectedAgent + ? [selectedAgent] + : await this.getAllAgentIds(installDir); - // Get the IDE configuration to access location options - const ideConfig = await configLoader.getIdeConfiguration('auggie-cli'); - const locations = ideConfig.locations; + // Get the IDE configuration to access location options + const ideConfig = await configLoader.getIdeConfiguration("auggie-cli"); + const locations = ideConfig.locations; - // Use pre-configured settings if provided, otherwise prompt - let selectedLocations; - if (preConfiguredSettings && preConfiguredSettings.selectedLocations) { - selectedLocations = preConfiguredSettings.selectedLocations; - console.log( - chalk.dim( - `Using pre-configured Auggie CLI (Augment Code) locations: ${selectedLocations.join(', ')}`, - ), - ); - } else { - // Pause spinner during location selection to avoid UI conflicts - let spinnerWasActive = false; - if (spinner && spinner.isSpinning) { - spinner.stop(); - spinnerWasActive = true; - } + // Use pre-configured settings if provided, otherwise prompt + let selectedLocations; + if (preConfiguredSettings && preConfiguredSettings.selectedLocations) { + selectedLocations = preConfiguredSettings.selectedLocations; + console.log( + chalk.dim( + `Using pre-configured Auggie CLI (Augment Code) locations: ${selectedLocations.join(", ")}`, + ), + ); + } else { + // Pause spinner during location selection to avoid UI conflicts + let spinnerWasActive = false; + if (spinner && spinner.isSpinning) { + spinner.stop(); + spinnerWasActive = true; + } - // Clear any previous output and add spacing to avoid conflicts with loaders - console.log('\n'.repeat(2)); - console.log(chalk.blue('📍 Auggie CLI Location Configuration')); - console.log(chalk.dim('Choose where to install BMad agents for Auggie CLI access.')); - console.log(''); // Add extra spacing + // Clear any previous output and add spacing to avoid conflicts with loaders + console.log("\n".repeat(2)); + console.log(chalk.blue("📍 Auggie CLI Location Configuration")); + console.log( + chalk.dim("Choose where to install BMad agents for Auggie CLI access."), + ); + console.log(""); // Add extra spacing - const response = await inquirer.prompt([ - { - type: 'checkbox', - name: 'selectedLocations', - message: 'Select Auggie CLI command locations:', - choices: Object.entries(locations).map(([key, location]) => ({ - name: `${location.name}: ${location.description}`, - value: key, - })), - validate: (selected) => { - if (selected.length === 0) { - return 'Please select at least one location'; - } - return true; - }, - }, - ]); - selectedLocations = response.selectedLocations; + const response = await inquirer.prompt([ + { + type: "checkbox", + name: "selectedLocations", + message: "Select Auggie CLI command locations:", + choices: Object.entries(locations).map(([key, location]) => ({ + name: `${location.name}: ${location.description}`, + value: key, + })), + validate: (selected) => { + if (selected.length === 0) { + return "Please select at least one location"; + } + return true; + }, + }, + ]); + selectedLocations = response.selectedLocations; - // Restart spinner if it was active before prompts - if (spinner && spinnerWasActive) { - spinner.start(); - } - } + // Restart spinner if it was active before prompts + if (spinner && spinnerWasActive) { + spinner.start(); + } + } - // Install to each selected location - for (const locationKey of selectedLocations) { - const location = locations[locationKey]; - let commandsDir = location['rule-dir']; + // Install to each selected location + for (const locationKey of selectedLocations) { + const location = locations[locationKey]; + let commandsDir = location["rule-dir"]; - // Handle tilde expansion for user directory - if (commandsDir.startsWith('~/')) { - commandsDir = path.join(os.homedir(), commandsDir.slice(2)); - } else if (commandsDir.startsWith('./')) { - commandsDir = path.join(installDir, commandsDir.slice(2)); - } + // Handle tilde expansion for user directory + if (commandsDir.startsWith("~/")) { + commandsDir = path.join(os.homedir(), commandsDir.slice(2)); + } else if (commandsDir.startsWith("./")) { + commandsDir = path.join(installDir, commandsDir.slice(2)); + } - await fileManager.ensureDirectory(commandsDir); + await fileManager.ensureDirectory(commandsDir); - for (const agentId of agents) { - // Find the agent file - const agentPath = await this.findAgentPath(agentId, installDir); + for (const agentId of agents) { + // Find the agent file + const agentPath = await this.findAgentPath(agentId, installDir); - if (agentPath) { - const agentContent = await fileManager.readFile(agentPath); - const mdPath = path.join(commandsDir, `${agentId}.md`); - await fileManager.writeFile(mdPath, agentContent); - console.log(chalk.green(`✓ Created command: ${agentId}.md in ${location.name}`)); - } - } + if (agentPath) { + const agentContent = await fileManager.readFile(agentPath); + const mdPath = path.join(commandsDir, `${agentId}.md`); + await fileManager.writeFile(mdPath, agentContent); + console.log( + chalk.green(`✓ Created command: ${agentId}.md in ${location.name}`), + ); + } + } - console.log(chalk.green(`\n✓ Created Auggie CLI commands in ${commandsDir}`)); - console.log(chalk.dim(` Location: ${location.name} - ${location.description}`)); - } + console.log( + chalk.green(`\n✓ Created Auggie CLI commands in ${commandsDir}`), + ); + console.log( + chalk.dim(` Location: ${location.name} - ${location.description}`), + ); + } - return true; - } + return true; + } } module.exports = new IdeSetup();