677 lines
19 KiB
JavaScript
Executable File
677 lines
19 KiB
JavaScript
Executable File
#!/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 <path>", "Installation directory")
|
|
.option(
|
|
"-i, --ide <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 <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 <path>", "Input directory to flatten", process.cwd())
|
|
.option("-o, --output <path>", "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();
|
|
}
|