#!/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"); // 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"); } 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); } } program .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 (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); } }); 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 to check if updates are available program .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; } // 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; // 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); }); // 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); } }); 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); } }); 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); } }); async function promptInstallation() { // Display ASCII logo console.log( chalk.bold.cyan(` ██████╗ ███╗ ███╗ █████╗ ██████╗ ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ██╔══██╗████╗ ████║██╔══██╗██╔══██╗ ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗ ██████╔╝██╔████╔██║███████║██║ ██║█████╗██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║ ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║╚════╝██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║ ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ `), ); console.log( chalk.bold.magenta("🚀 Universal AI Agent Framework for Any Domain"), ); console.log(chalk.bold.blue(`✨ Installer v${version}\n`)); 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; // Detect existing installations const installDir = path.resolve(directory); const state = await installer.detectInstallationState(installDir); // Check for existing expansion packs const existingExpansionPacks = state.expansionPacks || {}; // Get available expansion packs const availableExpansionPacks = await installer.getAvailableExpansionPacks(); // 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"; // 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, }); // 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}`; } 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; }, }, ]); // 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 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; // 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, }, ]); if (!acknowledge) { console.log(chalk.red("Installation cancelled.")); process.exit(0); } } } // 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")); 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; // 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 } } ideSelectionComplete = true; } // 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", ), ); 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 }; } // 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; }, }, ]); 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, }, ]); 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", }, ], }, ]); 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 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; } answers.includeWebBundles = includeWebBundles; return answers; } program.parse(process.argv); // Show help if no command provided if (process.argv.slice(2).length === 0) { program.outputHelp(); }