1229 lines
43 KiB
JavaScript
1229 lines
43 KiB
JavaScript
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 {
|
||
// Store the original CWD where npx was executed
|
||
const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
|
||
|
||
// Resolve installation directory relative to where the user ran the command
|
||
let installDir = path.isAbsolute(config.directory)
|
||
? config.directory
|
||
: path.resolve(originalCwd, 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);
|
||
}
|
||
|
||
// Log resolved path for clarity
|
||
if (!path.isAbsolute(config.directory)) {
|
||
spinner.text = `Resolving "${config.directory}" to: ${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;
|
||
}
|
||
}
|
||
]);
|
||
// Preserve the original CWD for the recursive call
|
||
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...");
|
||
}
|
||
|
||
// If this is an update request from early detection, handle it directly
|
||
if (config.installType === 'update') {
|
||
const state = await this.detectInstallationState(installDir);
|
||
if (state.type === 'v4_existing') {
|
||
return await this.performUpdate(config, installDir, state.manifest, spinner);
|
||
} else {
|
||
spinner.fail('No existing v4 installation found to update');
|
||
throw new Error('No existing v4 installation found');
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
|
||
// Copy common/ items to .bmad-core
|
||
spinner.text = "Copying common utilities...";
|
||
await this.copyCommonItems(installDir, ".bmad-core", spinner);
|
||
|
||
// 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,
|
||
".bmad-core",
|
||
"agents",
|
||
`${config.agent}.md`
|
||
);
|
||
await fileManager.copyFile(agentPath, destAgentPath);
|
||
files.push(`.bmad-core/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,
|
||
path.join(installDir, ".bmad-core")
|
||
);
|
||
files.push(...copiedFiles.map(f => `.bmad-core/${f}`));
|
||
} else {
|
||
// Handle single files
|
||
const sourcePath = path.join(
|
||
sourceBase,
|
||
dep.replace(".bmad-core/", "")
|
||
);
|
||
const destPath = path.join(
|
||
installDir,
|
||
dep
|
||
);
|
||
|
||
if (await fileManager.copyFile(sourcePath, destPath)) {
|
||
files.push(dep);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Copy common/ items to .bmad-core
|
||
spinner.text = "Copying common utilities...";
|
||
const commonFiles = await this.copyCommonItems(installDir, ".bmad-core", spinner);
|
||
files.push(...commonFiles);
|
||
} else if (config.installType === "team") {
|
||
// Team installation
|
||
spinner.text = `Installing ${config.team} team...`;
|
||
|
||
// Get team dependencies
|
||
const teamDependencies = await configLoader.getTeamDependencies(config.team);
|
||
const sourceBase = configLoader.getBmadCorePath();
|
||
|
||
// Install all team dependencies
|
||
for (const dep of teamDependencies) {
|
||
spinner.text = `Copying team dependency: ${dep}`;
|
||
|
||
if (dep.includes("*")) {
|
||
// Handle glob patterns
|
||
const copiedFiles = await fileManager.copyGlobPattern(
|
||
dep.replace(".bmad-core/", ""),
|
||
sourceBase,
|
||
path.join(installDir, ".bmad-core")
|
||
);
|
||
files.push(...copiedFiles.map(f => `.bmad-core/${f}`));
|
||
} else {
|
||
// Handle single files
|
||
const sourcePath = path.join(sourceBase, dep.replace(".bmad-core/", ""));
|
||
const destPath = path.join(installDir, dep);
|
||
|
||
if (await fileManager.copyFile(sourcePath, destPath)) {
|
||
files.push(dep);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Copy common/ items to .bmad-core
|
||
spinner.text = "Copying common utilities...";
|
||
const commonFiles = await this.copyCommonItems(installDir, ".bmad-core", spinner);
|
||
files.push(...commonFiles);
|
||
} else if (config.installType === "expansion-only") {
|
||
// Expansion-only installation - create minimal .bmad-core structure
|
||
spinner.text = "Creating minimal .bmad-core structure for expansion packs...";
|
||
|
||
const bmadCoreDestDir = path.join(installDir, ".bmad-core");
|
||
await fileManager.ensureDirectory(bmadCoreDestDir);
|
||
|
||
// Create basic directory structure
|
||
const dirs = ['agents', 'agent-teams', 'templates', 'tasks', 'checklists', 'workflows', 'data', 'utils', 'schemas'];
|
||
for (const dir of dirs) {
|
||
await fileManager.ensureDirectory(path.join(bmadCoreDestDir, dir));
|
||
}
|
||
|
||
// Copy minimal required files (schemas, utils, etc.)
|
||
const sourceBase = configLoader.getBmadCorePath();
|
||
const essentialFiles = [
|
||
'schemas/**/*',
|
||
'utils/**/*'
|
||
];
|
||
|
||
for (const pattern of essentialFiles) {
|
||
const copiedFiles = await fileManager.copyGlobPattern(
|
||
pattern,
|
||
sourceBase,
|
||
bmadCoreDestDir
|
||
);
|
||
files.push(...copiedFiles.map(f => `.bmad-core/${f}`));
|
||
}
|
||
|
||
// Copy common/ items to .bmad-core
|
||
spinner.text = "Copying common utilities...";
|
||
await this.copyCommonItems(installDir, ".bmad-core", spinner);
|
||
}
|
||
|
||
// Install expansion packs if requested
|
||
const expansionFiles = await this.installExpansionPacks(installDir, config.expansionPacks, spinner);
|
||
files.push(...expansionFiles);
|
||
|
||
// Install web bundles if requested
|
||
if (config.includeWebBundles && config.webBundlesDirectory) {
|
||
spinner.text = "Installing web bundles...";
|
||
// Resolve web bundles directory using the same logic as the main installation directory
|
||
const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
|
||
let resolvedWebBundlesDir = path.isAbsolute(config.webBundlesDirectory)
|
||
? config.webBundlesDirectory
|
||
: path.resolve(originalCwd, config.webBundlesDirectory);
|
||
await this.installWebBundles(resolvedWebBundlesDir, config, spinner);
|
||
}
|
||
|
||
// 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,
|
||
ides: config.ides || [] // Pass IDE selections from initial config
|
||
});
|
||
}
|
||
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
|
||
ides: newConfig?.ides || manifest.ides_setup || [],
|
||
};
|
||
|
||
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 (config.expansionPacks && config.expansionPacks.length > 0) {
|
||
console.log(chalk.green(`✓ Expansion packs installed:`));
|
||
for (const packId of config.expansionPacks) {
|
||
console.log(chalk.green(` - ${packId} → .${packId}/`));
|
||
}
|
||
}
|
||
|
||
if (config.includeWebBundles && config.webBundlesDirectory) {
|
||
const bundleInfo = this.getWebBundleInfo(config);
|
||
// Resolve the web bundles directory for display
|
||
const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
|
||
const resolvedWebBundlesDir = path.isAbsolute(config.webBundlesDirectory)
|
||
? config.webBundlesDirectory
|
||
: path.resolve(originalCwd, config.webBundlesDirectory);
|
||
console.log(chalk.green(`✓ Web bundles (${bundleInfo}) installed to: ${resolvedWebBundlesDir}`));
|
||
}
|
||
|
||
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
|
||
if (!config.includeWebBundles) {
|
||
console.log(chalk.bold("\n📦 Web Bundles Available:"));
|
||
console.log("Pre-built web bundles are available and can be added later:");
|
||
console.log(chalk.cyan(" Run the installer again to add them to your project"));
|
||
console.log("These bundles work independently and can be shared, moved, or used");
|
||
console.log("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 listExpansionPacks() {
|
||
// Initialize ES modules
|
||
await initializeModules();
|
||
const expansionPacks = await this.getAvailableExpansionPacks();
|
||
|
||
console.log(chalk.bold("\nAvailable BMAD Expansion Packs:\n"));
|
||
|
||
if (expansionPacks.length === 0) {
|
||
console.log(chalk.yellow("No expansion packs found."));
|
||
return;
|
||
}
|
||
|
||
for (const pack of expansionPacks) {
|
||
console.log(chalk.cyan(` ${pack.id.padEnd(20)}`),
|
||
`${pack.name} v${pack.version}`);
|
||
console.log(chalk.dim(` ${' '.repeat(22)}${pack.description}`));
|
||
if (pack.author && pack.author !== 'Unknown') {
|
||
console.log(chalk.dim(` ${' '.repeat(22)}by ${pack.author}`));
|
||
}
|
||
console.log();
|
||
}
|
||
|
||
console.log(
|
||
chalk.dim("Install with: npx bmad-method install --full --expansion-packs <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 getAvailableExpansionPacks() {
|
||
return configLoader.getAvailableExpansionPacks();
|
||
}
|
||
|
||
async getAvailableTeams() {
|
||
return configLoader.getAvailableTeams();
|
||
}
|
||
|
||
async installExpansionPacks(installDir, selectedPacks, spinner) {
|
||
if (!selectedPacks || selectedPacks.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const installedFiles = [];
|
||
const glob = require('glob');
|
||
|
||
for (const packId of selectedPacks) {
|
||
spinner.text = `Installing expansion pack: ${packId}...`;
|
||
|
||
try {
|
||
const expansionPacks = await this.getAvailableExpansionPacks();
|
||
const pack = expansionPacks.find(p => p.id === packId);
|
||
|
||
if (!pack) {
|
||
console.warn(`Expansion pack ${packId} not found, skipping...`);
|
||
continue;
|
||
}
|
||
|
||
const expansionPackDir = pack.packPath;
|
||
|
||
// Create dedicated dot folder for this expansion pack
|
||
const expansionDotFolder = path.join(installDir, `.${packId}`);
|
||
await fileManager.ensureDirectory(expansionDotFolder);
|
||
|
||
// Define the folders to copy from expansion packs
|
||
const foldersToSync = [
|
||
'agents',
|
||
'agent-teams',
|
||
'templates',
|
||
'tasks',
|
||
'checklists',
|
||
'workflows',
|
||
'data',
|
||
'utils',
|
||
'schemas'
|
||
];
|
||
|
||
// Copy each folder if it exists
|
||
for (const folder of foldersToSync) {
|
||
const sourceFolder = path.join(expansionPackDir, folder);
|
||
|
||
// Check if folder exists in expansion pack
|
||
if (await fileManager.pathExists(sourceFolder)) {
|
||
// Get all files in this folder
|
||
const files = glob.sync('**/*', {
|
||
cwd: sourceFolder,
|
||
nodir: true
|
||
});
|
||
|
||
// Copy each file to the expansion pack's dot folder
|
||
for (const file of files) {
|
||
const sourcePath = path.join(sourceFolder, file);
|
||
const destPath = path.join(expansionDotFolder, folder, file);
|
||
|
||
if (await fileManager.copyFile(sourcePath, destPath)) {
|
||
installedFiles.push(path.join(`.${packId}`, folder, file));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Copy config.yml
|
||
const configPath = path.join(expansionPackDir, 'config.yml');
|
||
if (await fileManager.pathExists(configPath)) {
|
||
const configDestPath = path.join(expansionDotFolder, 'config.yml');
|
||
if (await fileManager.copyFile(configPath, configDestPath)) {
|
||
installedFiles.push(path.join(`.${packId}`, 'config.yml'));
|
||
}
|
||
}
|
||
|
||
// Copy README if it exists
|
||
const readmePath = path.join(expansionPackDir, 'README.md');
|
||
if (await fileManager.pathExists(readmePath)) {
|
||
const readmeDestPath = path.join(expansionDotFolder, 'README.md');
|
||
if (await fileManager.copyFile(readmePath, readmeDestPath)) {
|
||
installedFiles.push(path.join(`.${packId}`, 'README.md'));
|
||
}
|
||
}
|
||
|
||
// Copy common/ items to expansion pack folder
|
||
spinner.text = `Copying common utilities to ${packId}...`;
|
||
await this.copyCommonItems(installDir, `.${packId}`, spinner);
|
||
|
||
// Check and resolve core dependencies
|
||
await this.resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, spinner);
|
||
|
||
// Check and resolve core agents referenced by teams
|
||
await this.resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner);
|
||
|
||
console.log(chalk.green(`✓ Installed expansion pack: ${pack.name} to ${`.${packId}`}`));
|
||
} catch (error) {
|
||
console.error(chalk.red(`Failed to install expansion pack ${packId}: ${error.message}`));
|
||
}
|
||
}
|
||
|
||
return installedFiles;
|
||
}
|
||
|
||
async resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, spinner) {
|
||
const glob = require('glob');
|
||
const yaml = require('yaml');
|
||
const fs = require('fs').promises;
|
||
|
||
// Find all agent files in the expansion pack
|
||
const agentFiles = glob.sync('agents/*.md', {
|
||
cwd: expansionDotFolder
|
||
});
|
||
|
||
for (const agentFile of agentFiles) {
|
||
const agentPath = path.join(expansionDotFolder, agentFile);
|
||
const agentContent = await fs.readFile(agentPath, 'utf8');
|
||
|
||
// Extract YAML frontmatter to check dependencies
|
||
const yamlMatch = agentContent.match(/```yaml\n([\s\S]*?)```/);
|
||
if (yamlMatch) {
|
||
try {
|
||
const agentConfig = yaml.parse(yamlMatch[1]);
|
||
const dependencies = agentConfig.dependencies || {};
|
||
|
||
// Check for core dependencies (those that don't exist in the expansion pack)
|
||
for (const depType of ['tasks', 'templates', 'checklists', 'workflows', 'utils', 'data']) {
|
||
const deps = dependencies[depType] || [];
|
||
|
||
for (const dep of deps) {
|
||
const depFileName = dep.endsWith('.md') ? dep : `${dep}.md`;
|
||
const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||
|
||
// Check if dependency exists in expansion pack
|
||
if (!(await fileManager.pathExists(expansionDepPath))) {
|
||
// Try to find it in core
|
||
const coreDepPath = path.join(configLoader.getBmadCorePath(), depType, depFileName);
|
||
|
||
if (await fileManager.pathExists(coreDepPath)) {
|
||
spinner.text = `Copying core dependency ${dep} for ${packId}...`;
|
||
|
||
// Copy from core to expansion pack dot folder
|
||
const destPath = path.join(expansionDotFolder, depType, depFileName);
|
||
await fileManager.copyFile(coreDepPath, destPath);
|
||
|
||
console.log(chalk.dim(` Added core dependency: ${depType}/${depFileName}`));
|
||
} else {
|
||
console.warn(chalk.yellow(` Warning: Dependency ${depType}/${dep} not found in core or expansion pack`));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn(chalk.yellow(` Warning: Could not parse agent dependencies: ${error.message}`));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner) {
|
||
const glob = require('glob');
|
||
const yaml = require('yaml');
|
||
const fs = require('fs').promises;
|
||
|
||
// Find all team files in the expansion pack
|
||
const teamFiles = glob.sync('agent-teams/*.yml', {
|
||
cwd: expansionDotFolder
|
||
});
|
||
|
||
// Also get existing agents in the expansion pack
|
||
const existingAgents = new Set();
|
||
const agentFiles = glob.sync('agents/*.md', {
|
||
cwd: expansionDotFolder
|
||
});
|
||
for (const agentFile of agentFiles) {
|
||
const agentName = path.basename(agentFile, '.md');
|
||
existingAgents.add(agentName);
|
||
}
|
||
|
||
// Process each team file
|
||
for (const teamFile of teamFiles) {
|
||
const teamPath = path.join(expansionDotFolder, teamFile);
|
||
const teamContent = await fs.readFile(teamPath, 'utf8');
|
||
|
||
try {
|
||
const teamConfig = yaml.parse(teamContent);
|
||
const agents = teamConfig.agents || [];
|
||
|
||
// Add bmad-orchestrator if not present (required for all teams)
|
||
if (!agents.includes('bmad-orchestrator')) {
|
||
agents.unshift('bmad-orchestrator');
|
||
}
|
||
|
||
// Check each agent in the team
|
||
for (const agentId of agents) {
|
||
if (!existingAgents.has(agentId)) {
|
||
// Agent not in expansion pack, try to get from core
|
||
const coreAgentPath = path.join(configLoader.getBmadCorePath(), 'agents', `${agentId}.md`);
|
||
|
||
if (await fileManager.pathExists(coreAgentPath)) {
|
||
spinner.text = `Copying core agent ${agentId} for ${packId}...`;
|
||
|
||
// Copy agent file
|
||
const destPath = path.join(expansionDotFolder, 'agents', `${agentId}.md`);
|
||
await fileManager.copyFile(coreAgentPath, destPath);
|
||
existingAgents.add(agentId);
|
||
|
||
console.log(chalk.dim(` Added core agent: ${agentId}`));
|
||
|
||
// Now resolve this agent's dependencies too
|
||
const agentContent = await fs.readFile(coreAgentPath, 'utf8');
|
||
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
|
||
|
||
if (yamlMatch) {
|
||
try {
|
||
// Clean up the YAML to handle command descriptions
|
||
let yamlContent = yamlMatch[1];
|
||
yamlContent = yamlContent.replace(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, '$1$2');
|
||
|
||
const agentConfig = yaml.parse(yamlContent);
|
||
const dependencies = agentConfig.dependencies || {};
|
||
|
||
// Copy all dependencies for this agent
|
||
for (const depType of ['tasks', 'templates', 'checklists', 'workflows', 'utils', 'data']) {
|
||
const deps = dependencies[depType] || [];
|
||
|
||
for (const dep of deps) {
|
||
const depFileName = dep.endsWith('.md') || dep.endsWith('.yml') ? dep : `${dep}.md`;
|
||
const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||
|
||
// Check if dependency exists in expansion pack
|
||
if (!(await fileManager.pathExists(expansionDepPath))) {
|
||
// Try to find it in core
|
||
const coreDepPath = path.join(configLoader.getBmadCorePath(), depType, depFileName);
|
||
|
||
if (await fileManager.pathExists(coreDepPath)) {
|
||
const destDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||
await fileManager.copyFile(coreDepPath, destDepPath);
|
||
console.log(chalk.dim(` Added agent dependency: ${depType}/${depFileName}`));
|
||
} else {
|
||
// Try common folder
|
||
const commonDepPath = path.join(this.rootDir, 'common', depType, depFileName);
|
||
if (await fileManager.pathExists(commonDepPath)) {
|
||
const destDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||
await fileManager.copyFile(commonDepPath, destDepPath);
|
||
console.log(chalk.dim(` Added agent dependency from common: ${depType}/${depFileName}`));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn(chalk.yellow(` Warning: Could not parse agent ${agentId} dependencies: ${error.message}`));
|
||
}
|
||
}
|
||
} else {
|
||
console.warn(chalk.yellow(` Warning: Core agent ${agentId} not found for team ${path.basename(teamFile, '.yml')}`));
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn(chalk.yellow(` Warning: Could not parse team file ${teamFile}: ${error.message}`));
|
||
}
|
||
}
|
||
}
|
||
|
||
getWebBundleInfo(config) {
|
||
const webBundleType = config.webBundleType || 'all';
|
||
|
||
switch (webBundleType) {
|
||
case 'all':
|
||
return 'all bundles';
|
||
case 'agents':
|
||
return 'individual agents only';
|
||
case 'teams':
|
||
return config.selectedWebBundleTeams ?
|
||
`teams: ${config.selectedWebBundleTeams.join(', ')}` :
|
||
'selected teams';
|
||
case 'custom':
|
||
const parts = [];
|
||
if (config.selectedWebBundleTeams && config.selectedWebBundleTeams.length > 0) {
|
||
parts.push(`teams: ${config.selectedWebBundleTeams.join(', ')}`);
|
||
}
|
||
if (config.includeIndividualAgents) {
|
||
parts.push('individual agents');
|
||
}
|
||
return parts.length > 0 ? parts.join(' + ') : 'custom selection';
|
||
default:
|
||
return 'selected bundles';
|
||
}
|
||
}
|
||
|
||
async installWebBundles(webBundlesDirectory, config, spinner) {
|
||
// Ensure modules are initialized
|
||
await initializeModules();
|
||
|
||
try {
|
||
// Find the dist directory in the BMAD installation
|
||
const distDir = configLoader.getDistPath();
|
||
|
||
if (!(await fileManager.pathExists(distDir))) {
|
||
console.warn(chalk.yellow('Web bundles not found. Run "npm run build" to generate them.'));
|
||
return;
|
||
}
|
||
|
||
// Ensure web bundles directory exists
|
||
await fileManager.ensureDirectory(webBundlesDirectory);
|
||
|
||
const webBundleType = config.webBundleType || 'all';
|
||
|
||
if (webBundleType === 'all') {
|
||
// Copy the entire dist directory structure
|
||
await fileManager.copyDirectory(distDir, webBundlesDirectory);
|
||
console.log(chalk.green(`✓ Installed all web bundles to: ${webBundlesDirectory}`));
|
||
} else {
|
||
let copiedCount = 0;
|
||
|
||
// Copy specific selections based on type
|
||
if (webBundleType === 'agents' || (webBundleType === 'custom' && config.includeIndividualAgents)) {
|
||
const agentsSource = path.join(distDir, 'agents');
|
||
const agentsTarget = path.join(webBundlesDirectory, 'agents');
|
||
if (await fileManager.pathExists(agentsSource)) {
|
||
await fileManager.copyDirectory(agentsSource, agentsTarget);
|
||
console.log(chalk.green(`✓ Copied individual agent bundles`));
|
||
copiedCount += 10; // Approximate count for agents
|
||
}
|
||
}
|
||
|
||
if (webBundleType === 'teams' || webBundleType === 'custom') {
|
||
if (config.selectedWebBundleTeams && config.selectedWebBundleTeams.length > 0) {
|
||
const teamsSource = path.join(distDir, 'teams');
|
||
const teamsTarget = path.join(webBundlesDirectory, 'teams');
|
||
await fileManager.ensureDirectory(teamsTarget);
|
||
|
||
for (const teamId of config.selectedWebBundleTeams) {
|
||
const teamFile = `${teamId}.txt`;
|
||
const sourcePath = path.join(teamsSource, teamFile);
|
||
const targetPath = path.join(teamsTarget, teamFile);
|
||
|
||
if (await fileManager.pathExists(sourcePath)) {
|
||
await fileManager.copyFile(sourcePath, targetPath);
|
||
copiedCount++;
|
||
console.log(chalk.green(`✓ Copied team bundle: ${teamId}`));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Always copy expansion packs if they exist
|
||
const expansionSource = path.join(distDir, 'expansion-packs');
|
||
const expansionTarget = path.join(webBundlesDirectory, 'expansion-packs');
|
||
if (await fileManager.pathExists(expansionSource)) {
|
||
await fileManager.copyDirectory(expansionSource, expansionTarget);
|
||
console.log(chalk.green(`✓ Copied expansion pack bundles`));
|
||
}
|
||
|
||
console.log(chalk.green(`✓ Installed ${copiedCount} selected web bundles to: ${webBundlesDirectory}`));
|
||
}
|
||
} catch (error) {
|
||
console.error(chalk.red(`Failed to install web bundles: ${error.message}`));
|
||
}
|
||
}
|
||
|
||
async copyCommonItems(installDir, targetSubdir, spinner) {
|
||
const glob = require('glob');
|
||
const fs = require('fs').promises;
|
||
const sourceBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename)))); // Go up to project root
|
||
const commonPath = path.join(sourceBase, 'common');
|
||
const targetPath = path.join(installDir, targetSubdir);
|
||
const copiedFiles = [];
|
||
|
||
// Check if common/ exists
|
||
if (!(await fileManager.pathExists(commonPath))) {
|
||
console.warn(chalk.yellow('Warning: common/ folder not found'));
|
||
return copiedFiles;
|
||
}
|
||
|
||
// Copy all items from common/ to target
|
||
const commonItems = glob.sync('**/*', {
|
||
cwd: commonPath,
|
||
nodir: true
|
||
});
|
||
|
||
for (const item of commonItems) {
|
||
const sourcePath = path.join(commonPath, item);
|
||
const destPath = path.join(targetPath, item);
|
||
|
||
// Read the file content
|
||
const content = await fs.readFile(sourcePath, 'utf8');
|
||
|
||
// Replace {root} with the target subdirectory
|
||
const updatedContent = content.replace(/\{root\}/g, targetSubdir);
|
||
|
||
// Ensure directory exists
|
||
await fileManager.ensureDirectory(path.dirname(destPath));
|
||
|
||
// Write the updated content
|
||
await fs.writeFile(destPath, updatedContent, 'utf8');
|
||
copiedFiles.push(path.join(targetSubdir, item));
|
||
}
|
||
|
||
console.log(chalk.dim(` Added ${commonItems.length} common utilities`));
|
||
return copiedFiles;
|
||
}
|
||
|
||
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();
|