Files
BMAD-METHOD/tools/installer/lib/installer.js

655 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const path = require("node:path");
const fileManager = require("./file-manager");
const configLoader = require("./config-loader");
const ideSetup = require("./ide-setup");
// Dynamic imports for ES modules
let chalk, ora, inquirer;
// Initialize ES modules
async function initializeModules() {
if (!chalk) {
chalk = (await import("chalk")).default;
ora = (await import("ora")).default;
inquirer = (await import("inquirer")).default;
}
}
class Installer {
async install(config) {
// Initialize ES modules
await initializeModules();
const spinner = ora("Analyzing installation directory...").start();
try {
// Resolve installation directory
let installDir = path.resolve(config.directory);
if (path.basename(installDir) === '.bmad-core') {
// If user points directly to .bmad-core, treat its parent as the project root
installDir = path.dirname(installDir);
}
// Check if directory exists and handle non-existent directories
if (!(await fileManager.pathExists(installDir))) {
spinner.stop();
console.log(chalk.yellow(`\nThe directory ${chalk.bold(installDir)} does not exist.`));
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{
name: 'Create the directory and continue',
value: 'create'
},
{
name: 'Choose a different directory',
value: 'change'
},
{
name: 'Cancel installation',
value: 'cancel'
}
]
}
]);
if (action === 'cancel') {
console.log(chalk.red('Installation cancelled.'));
process.exit(0);
} else if (action === 'change') {
const { newDirectory } = await inquirer.prompt([
{
type: 'input',
name: 'newDirectory',
message: 'Enter the new directory path:',
validate: (input) => {
if (!input.trim()) {
return 'Please enter a valid directory path';
}
return true;
}
}
]);
config.directory = newDirectory;
return await this.install(config); // Recursive call with new directory
} else if (action === 'create') {
try {
await fileManager.ensureDirectory(installDir);
console.log(chalk.green(`✓ Created directory: ${installDir}`));
} catch (error) {
console.error(chalk.red(`Failed to create directory: ${error.message}`));
console.error(chalk.yellow('You may need to check permissions or use a different path.'));
process.exit(1);
}
}
spinner.start("Analyzing installation directory...");
}
// Detect current state
const state = await this.detectInstallationState(installDir);
// Handle different states
switch (state.type) {
case "clean":
return await this.performFreshInstall(config, installDir, spinner);
case "v4_existing":
return await this.handleExistingV4Installation(
config,
installDir,
state,
spinner
);
case "v3_existing":
return await this.handleV3Installation(
config,
installDir,
state,
spinner
);
case "unknown_existing":
return await this.handleUnknownInstallation(
config,
installDir,
state,
spinner
);
}
} catch (error) {
spinner.fail("Installation failed");
throw error;
}
}
async detectInstallationState(installDir) {
// Ensure modules are initialized
await initializeModules();
const state = {
type: "clean",
hasV4Manifest: false,
hasV3Structure: false,
hasBmadCore: false,
hasOtherFiles: false,
manifest: null,
};
// Check if directory exists
if (!(await fileManager.pathExists(installDir))) {
return state; // clean install
}
// Check for V4 installation (has .bmad-core with manifest)
const bmadCorePath = path.join(installDir, ".bmad-core");
const manifestPath = path.join(bmadCorePath, "install-manifest.yml");
if (await fileManager.pathExists(manifestPath)) {
state.type = "v4_existing";
state.hasV4Manifest = true;
state.hasBmadCore = true;
state.manifest = await fileManager.readManifest(installDir);
return state;
}
// Check for V3 installation (has bmad-agent directory)
const bmadAgentPath = path.join(installDir, "bmad-agent");
if (await fileManager.pathExists(bmadAgentPath)) {
state.type = "v3_existing";
state.hasV3Structure = true;
return state;
}
// Check for .bmad-core without manifest (broken V4 or manual copy)
if (await fileManager.pathExists(bmadCorePath)) {
state.type = "unknown_existing";
state.hasBmadCore = true;
return state;
}
// Check if directory has other files
const glob = require("glob");
const files = glob.sync("**/*", {
cwd: installDir,
nodir: true,
ignore: ["**/.git/**", "**/node_modules/**"],
});
if (files.length > 0) {
// Directory has other files, but no BMAD installation.
// Treat as clean install but record that it isn't empty.
state.hasOtherFiles = true;
}
return state; // clean install
}
async performFreshInstall(config, installDir, spinner) {
// Ensure modules are initialized
await initializeModules();
spinner.text = "Installing BMAD Method...";
let files = [];
if (config.installType === "full") {
// Full installation - copy entire .bmad-core folder as a subdirectory
spinner.text = "Copying complete .bmad-core folder...";
const sourceDir = configLoader.getBmadCorePath();
const bmadCoreDestDir = path.join(installDir, ".bmad-core");
await fileManager.copyDirectory(sourceDir, bmadCoreDestDir);
// Get list of all files for manifest
const glob = require("glob");
files = glob
.sync("**/*", {
cwd: bmadCoreDestDir,
nodir: true,
ignore: ["**/.git/**", "**/node_modules/**"],
})
.map((file) => path.join(".bmad-core", file));
} else if (config.installType === "single-agent") {
// Single agent installation
spinner.text = `Installing ${config.agent} agent...`;
// Copy agent file
const agentPath = configLoader.getAgentPath(config.agent);
const destAgentPath = path.join(
installDir,
"agents",
`${config.agent}.md`
);
await fileManager.copyFile(agentPath, destAgentPath);
files.push(`agents/${config.agent}.md`);
// Copy dependencies
const dependencies = await configLoader.getAgentDependencies(
config.agent
);
const sourceBase = configLoader.getBmadCorePath();
for (const dep of dependencies) {
spinner.text = `Copying dependency: ${dep}`;
if (dep.includes("*")) {
// Handle glob patterns
const copiedFiles = await fileManager.copyGlobPattern(
dep.replace(".bmad-core/", ""),
sourceBase,
installDir
);
files.push(...copiedFiles);
} else {
// Handle single files
const sourcePath = path.join(
sourceBase,
dep.replace(".bmad-core/", "")
);
const destPath = path.join(
installDir,
dep.replace(".bmad-core/", "")
);
if (await fileManager.copyFile(sourcePath, destPath)) {
files.push(dep.replace(".bmad-core/", ""));
}
}
}
}
// Set up IDE integration if requested
const ides = config.ides || (config.ide ? [config.ide] : []);
if (ides.length > 0) {
for (const ide of ides) {
spinner.text = `Setting up ${ide} integration...`;
await ideSetup.setup(ide, installDir, config.agent);
}
}
// Create manifest
spinner.text = "Creating installation manifest...";
await fileManager.createManifest(installDir, config, files);
spinner.succeed("Installation complete!");
this.showSuccessMessage(config, installDir);
}
async handleExistingV4Installation(config, installDir, state, spinner) {
// Ensure modules are initialized
await initializeModules();
spinner.stop();
console.log(chalk.yellow("\n🔍 Found existing BMAD v4 installation"));
console.log(` Directory: ${installDir}`);
console.log(` Version: ${state.manifest.version}`);
console.log(
` Installed: ${new Date(
state.manifest.installed_at
).toLocaleDateString()}`
);
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "What would you like to do?",
choices: [
{ name: "Update existing installation", value: "update" },
{ name: "Reinstall (overwrite)", value: "reinstall" },
{ name: "Cancel", value: "cancel" },
],
},
]);
switch (action) {
case "update":
return await this.performUpdate(config, installDir, state.manifest, spinner);
case "reinstall":
return await this.performReinstall(config, installDir, spinner);
case "cancel":
console.log("Installation cancelled.");
return;
}
}
async handleV3Installation(config, installDir, state, spinner) {
// Ensure modules are initialized
await initializeModules();
spinner.stop();
console.log(
chalk.yellow("\n🔍 Found BMAD v3 installation (bmad-agent/ directory)")
);
console.log(` Directory: ${installDir}`);
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "What would you like to do?",
choices: [
{ name: "Upgrade from v3 to v4 (recommended)", value: "upgrade" },
{ name: "Install v4 alongside v3", value: "alongside" },
{ name: "Cancel", value: "cancel" },
],
},
]);
switch (action) {
case "upgrade": {
console.log(chalk.cyan("\n📦 Starting v3 to v4 upgrade process..."));
const V3ToV4Upgrader = require("../../upgraders/v3-to-v4-upgrader");
const upgrader = new V3ToV4Upgrader();
return await upgrader.upgrade({ projectPath: installDir });
}
case "alongside":
return await this.performFreshInstall(config, installDir, spinner);
case "cancel":
console.log("Installation cancelled.");
return;
}
}
async handleUnknownInstallation(config, installDir, state, spinner) {
// Ensure modules are initialized
await initializeModules();
spinner.stop();
console.log(chalk.yellow("\n⚠ Directory contains existing files"));
console.log(` Directory: ${installDir}`);
if (state.hasBmadCore) {
console.log(" Found: .bmad-core directory (but no manifest)");
}
if (state.hasOtherFiles) {
console.log(" Found: Other files in directory");
}
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "What would you like to do?",
choices: [
{ name: "Install anyway (may overwrite files)", value: "force" },
{ name: "Choose different directory", value: "different" },
{ name: "Cancel", value: "cancel" },
],
},
]);
switch (action) {
case "force":
return await this.performFreshInstall(config, installDir, spinner);
case "different": {
const { newDir } = await inquirer.prompt([
{
type: "input",
name: "newDir",
message: "Enter new installation directory:",
default: path.join(path.dirname(installDir), "bmad-project"),
},
]);
config.directory = newDir;
return await this.install(config);
}
case "cancel":
console.log("Installation cancelled.");
return;
}
}
async performUpdate(newConfig, installDir, manifest, spinner) {
spinner.start("Checking for updates...");
try {
// Check for modified files
spinner.text = "Checking for modified files...";
const modifiedFiles = await fileManager.checkModifiedFiles(
installDir,
manifest
);
if (modifiedFiles.length > 0) {
spinner.warn("Found modified files");
console.log(chalk.yellow("\nThe following files have been modified:"));
for (const file of modifiedFiles) {
console.log(` - ${file}`);
}
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "How would you like to proceed?",
choices: [
{ name: "Backup and overwrite modified files", value: "backup" },
{ name: "Skip modified files", value: "skip" },
{ name: "Cancel update", value: "cancel" },
],
},
]);
if (action === "cancel") {
console.log("Update cancelled.");
return;
}
if (action === "backup") {
spinner.start("Backing up modified files...");
for (const file of modifiedFiles) {
const filePath = path.join(installDir, file);
const backupPath = await fileManager.backupFile(filePath);
console.log(
chalk.dim(` Backed up: ${file}${path.basename(backupPath)}`)
);
}
}
}
// Perform update by re-running installation
spinner.text = "Updating files...";
const config = {
installType: manifest.install_type,
agent: manifest.agent,
directory: installDir,
ide: newConfig.ide || manifest.ide_setup, // Use new IDE choice if provided
};
await this.performFreshInstall(config, installDir, spinner);
} catch (error) {
spinner.fail("Update failed");
throw error;
}
}
async performReinstall(config, installDir, spinner) {
spinner.start("Reinstalling BMAD Method...");
// Remove existing .bmad-core
const bmadCorePath = path.join(installDir, ".bmad-core");
if (await fileManager.pathExists(bmadCorePath)) {
await fileManager.removeDirectory(bmadCorePath);
}
return await this.performFreshInstall(config, installDir, spinner);
}
showSuccessMessage(config, installDir) {
console.log(chalk.green("\n✓ BMAD Method installed successfully!\n"));
const ides = config.ides || (config.ide ? [config.ide] : []);
if (ides.length > 0) {
for (const ide of ides) {
const ideConfig = configLoader.getIdeConfiguration(ide);
if (ideConfig?.instructions) {
console.log(
chalk.bold(`To use BMAD agents in ${ideConfig.name}:`)
);
console.log(ideConfig.instructions);
}
}
} else {
console.log(chalk.yellow("No IDE configuration was set up."));
console.log(
"You can manually configure your IDE using the agent files in:",
installDir
);
}
// Information about installation components
console.log(chalk.bold("\n🎯 Installation Summary:"));
console.log(chalk.green("✓ .bmad-core framework installed with all agents and workflows"));
if (ides.length > 0) {
const ideNames = ides.map(ide => {
const ideConfig = configLoader.getIdeConfiguration(ide);
return ideConfig?.name || ide;
}).join(", ");
console.log(chalk.green(`✓ IDE rules and configurations set up for: ${ideNames}`));
}
// Information about web bundles
console.log(chalk.bold("\n📦 Web Bundles Available:"));
console.log("Self-contained web bundles have been included in your installation:");
console.log(chalk.cyan(` ${installDir}/.bmad-core/web-bundles/`));
console.log("These bundles work independently without this installation and can be");
console.log("shared, moved, or used in other projects as standalone files.");
if (config.installType === "single-agent") {
console.log(
chalk.dim(
"\nNeed other agents? Run: npx bmad-method install --agent=<name>"
)
);
console.log(
chalk.dim("Need everything? Run: npx bmad-method install --full")
);
}
}
// Legacy method for backward compatibility
async update() {
// Initialize ES modules
await initializeModules();
console.log(chalk.yellow('The "update" command is deprecated.'));
console.log(
'Please use "install" instead - it will detect and offer to update existing installations.'
);
const installDir = await this.findInstallation();
if (installDir) {
const config = {
installType: "full",
directory: path.dirname(installDir),
ide: null,
};
return await this.install(config);
}
console.log(chalk.red("No BMAD installation found."));
}
async listAgents() {
// Initialize ES modules
await initializeModules();
const agents = await configLoader.getAvailableAgents();
console.log(chalk.bold("\nAvailable BMAD Agents:\n"));
for (const agent of agents) {
console.log(chalk.cyan(` ${agent.id.padEnd(20)}`), agent.description);
}
console.log(
chalk.dim("\nInstall with: npx bmad-method install --agent=<id>\n")
);
}
async showStatus() {
// Initialize ES modules
await initializeModules();
const installDir = await this.findInstallation();
if (!installDir) {
console.log(
chalk.yellow("No BMAD installation found in current directory tree")
);
return;
}
const manifest = await fileManager.readManifest(installDir);
if (!manifest) {
console.log(chalk.red("Invalid installation - manifest not found"));
return;
}
console.log(chalk.bold("\nBMAD Installation Status:\n"));
console.log(` Directory: ${installDir}`);
console.log(` Version: ${manifest.version}`);
console.log(
` Installed: ${new Date(
manifest.installed_at
).toLocaleDateString()}`
);
console.log(` Type: ${manifest.install_type}`);
if (manifest.agent) {
console.log(` Agent: ${manifest.agent}`);
}
if (manifest.ide_setup) {
console.log(` IDE Setup: ${manifest.ide_setup}`);
}
console.log(` Total Files: ${manifest.files.length}`);
// Check for modifications
const modifiedFiles = await fileManager.checkModifiedFiles(
installDir,
manifest
);
if (modifiedFiles.length > 0) {
console.log(chalk.yellow(` Modified Files: ${modifiedFiles.length}`));
}
console.log("");
}
async getAvailableAgents() {
return configLoader.getAvailableAgents();
}
async findInstallation() {
// Look for .bmad-core in current directory or parent directories
let currentDir = process.cwd();
while (currentDir !== path.dirname(currentDir)) {
const bmadDir = path.join(currentDir, ".bmad-core");
const manifestPath = path.join(bmadDir, "install-manifest.yml");
if (await fileManager.pathExists(manifestPath)) {
return bmadDir;
}
currentDir = path.dirname(currentDir);
}
// Also check if we're inside a .bmad-core directory
if (path.basename(process.cwd()) === ".bmad-core") {
const manifestPath = path.join(process.cwd(), "install-manifest.yml");
if (await fileManager.pathExists(manifestPath)) {
return process.cwd();
}
}
return null;
}
}
module.exports = new Installer();