@cloudkinetix/bmad-enhanced
Version:
Cloud-Kinetix enhanced fork of BMAD-METHOD - Breakthrough Method of Agile AI-driven Development with robust versioning and unified validation.
692 lines (603 loc) • 26.1 kB
JavaScript
/**
* BMAD Installer Wrapper
*
* This wrapper acts as a transparent proxy to the upstream bmad-method installer
* while handling dependency issues and ensuring seamless integration.
*
* Key features:
* - Automatically picks up any upstream installer changes
* - Handles missing dependencies gracefully
* - Falls back to npx when dependencies are fixed upstream
* - Minimal intervention - only fixes what's broken
*/
const { execSync, spawn } = require("child_process");
const path = require("path");
const fs = require("fs-extra");
const os = require("os");
// Chalk compatibility
let chalk;
try {
const chalkModule = require("chalk");
if (typeof chalkModule.red === "function") {
chalk = chalkModule;
} else {
throw new Error("Chalk API mismatch");
}
} catch (error) {
// Fallback for chalk issues
chalk = {
green: (text) => text,
red: (text) => text,
blue: (text) => text,
yellow: (text) => text,
cyan: (text) => text,
gray: (text) => text,
dim: (text) => text,
};
}
class BMADInstallerWrapper {
constructor() {
this.tempDir = null;
// Get version from metadata to stay in sync with upstream
const packageJson = require("../../package.json");
this.upstreamVersion = packageJson.metadata?.basedOnUpstream || "latest";
}
/**
* Main entry point - intelligently decides how to run upstream installer
*/
async runUpstreamInstaller(targetDir, options = {}) {
// First, try the fastest method - direct npx
if (await this.canUseNpx()) {
console.log(chalk.dim("Using direct npx installation (upstream dependencies fixed)"));
return await this.runNpxInstall(targetDir, options);
}
// If npx fails, use our wrapper approach
console.log(chalk.dim("Using wrapper installation (handling missing dependencies)"));
return await this.runWrappedInstall(targetDir, options);
}
/**
* Generic method to run any upstream command (install, update, etc.)
*/
async runUpstreamCommand(command, targetDir, options = {}) {
// Override the command in options
const cmdOptions = { ...options, command };
// First, try the fastest method - direct npx
if (await this.canUseNpx()) {
console.log(chalk.dim(`Using direct npx for '${command}' (upstream dependencies fixed)`));
return await this.runNpxCommand(command, targetDir, cmdOptions);
}
// If npx fails, use our wrapper approach
console.log(chalk.dim(`Using wrapper for '${command}' (handling missing dependencies)`));
return await this.runWrappedCommand(command, targetDir, cmdOptions);
}
/**
* Run installation directly via npx (preferred when upstream is fixed)
*/
async runNpxInstall(targetDir, options = {}) {
const args = this.buildInstallArgs(options);
const npxCommand = `npx --yes bmad-method@${this.upstreamVersion} ${args.join(" ")}`;
console.log(chalk.dim(`Running: ${npxCommand}`));
return new Promise((resolve, reject) => {
// Use execSync instead of spawn for better PATH handling
try {
const npxCommand = `npx --yes bmad-method@${this.upstreamVersion} ${args.join(' ')}`;
console.log(chalk.dim(`Running: ${npxCommand}`));
execSync(npxCommand, {
cwd: targetDir,
stdio: "inherit",
env: {
...process.env,
PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}`,
// Force npx to not use cache
npm_config_cache: path.join(os.tmpdir(), `npm-cache-${Date.now()}`),
npm_config_prefer_online: 'true'
}
});
resolve({ success: true });
} catch (error) {
if (error.status === 0) {
resolve({ success: true });
} else {
reject(new Error(`BMAD installation failed: ${error.message}`));
}
}
});
}
/**
* Run any command directly via npx
*/
async runNpxCommand(command, targetDir, options = {}) {
const args = this.buildCommandArgs(command, options);
const npxCommand = `npx --yes bmad-method@${this.upstreamVersion} ${args.join(" ")}`;
console.log(chalk.dim(`Running: ${npxCommand}`));
return new Promise((resolve, reject) => {
// Use execSync instead of spawn for better PATH handling
try {
const npxCommand = `npx --yes bmad-method@${this.upstreamVersion} ${args.join(' ')}`;
console.log(chalk.dim(`Running: ${npxCommand}`));
execSync(npxCommand, {
cwd: targetDir,
stdio: "inherit",
env: {
...process.env,
PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}`,
// Force npx to not use cache
npm_config_cache: path.join(os.tmpdir(), `npm-cache-${Date.now()}`),
npm_config_prefer_online: 'true'
}
});
resolve({ success: true });
} catch (error) {
if (error.status === 0) {
resolve({ success: true });
} else {
reject(new Error(`BMAD ${command} failed: ${error.message}`));
}
}
});
}
/**
* Run installation with dependency workaround
*/
async runWrappedInstall(targetDir, options = {}) {
try {
// Create a temporary directory for installation
this.tempDir = path.join(os.tmpdir(), `bmad-install-${Date.now()}`);
await fs.ensureDir(this.tempDir);
// Create a minimal package.json
const tempPackageJson = {
name: "bmad-temp-install",
version: "1.0.0",
description: "Temporary installation for BMAD",
private: true
};
await fs.writeJSON(path.join(this.tempDir, "package.json"), tempPackageJson, { spaces: 2 });
// Install bmad-method and its dependencies
console.log(chalk.dim("Installing BMAD foundation and dependencies..."));
try {
// Get the list of missing dependencies from upstream package
const missingDeps = await this.detectMissingDependencies();
// Install missing dependencies first
for (const dep of missingDeps) {
console.log(chalk.dim(`Installing missing dependency: ${dep.name}@${dep.version}`));
execSync(`npm install ${dep.name}@${dep.version}`, {
cwd: this.tempDir,
stdio: options.verbose ? "inherit" : "pipe"
});
}
// Then install bmad-method
execSync(`npm install bmad-method@${this.upstreamVersion}`, {
cwd: this.tempDir,
stdio: options.verbose ? "inherit" : "pipe"
});
} catch (error) {
console.error(chalk.red("Failed to install BMAD dependencies"));
throw error;
}
// Find the bmad-method binary - check multiple possible locations
// Prioritize the actual installer binary over the wrapper
const possibleBinaries = [
path.join(this.tempDir, "node_modules", "bmad-method", "tools", "installer", "bin", "bmad.js"),
path.join(this.tempDir, "node_modules", ".bin", "bmad-method"),
path.join(this.tempDir, "node_modules", ".bin", "bmad"),
path.join(this.tempDir, "node_modules", "bmad-method", "tools", "bmad-npx-wrapper.js"),
path.join(this.tempDir, "node_modules", "bmad-method", "bin", "bmad.js"),
path.join(this.tempDir, "node_modules", "bmad-method", "bin", "bmad-method.js"),
path.join(this.tempDir, "node_modules", "bmad-method", "cli.js"),
];
let bmadPath = null;
console.log(chalk.dim("Searching for bmad-method binary..."));
for (const binPath of possibleBinaries) {
console.log(chalk.dim(` Checking: ${binPath}`));
if (fs.existsSync(binPath)) {
console.log(chalk.green(` ✓ Found: ${binPath}`));
bmadPath = binPath;
break;
}
}
if (!bmadPath) {
// List what's actually in the directories to help debug
console.error(chalk.red("Could not find bmad-method binary. Checking installed structure:"));
const nodeModulesPath = path.join(this.tempDir, "node_modules");
if (fs.existsSync(nodeModulesPath)) {
const bmadMethodPath = path.join(nodeModulesPath, "bmad-method");
if (fs.existsSync(bmadMethodPath)) {
console.error(chalk.yellow("Contents of bmad-method package:"));
const files = fs.readdirSync(bmadMethodPath);
files.forEach(file => console.error(` - ${file}`));
// Check tools directory specifically
const toolsPath = path.join(bmadMethodPath, "tools");
if (fs.existsSync(toolsPath)) {
console.error(chalk.yellow("Contents of tools directory:"));
const toolFiles = fs.readdirSync(toolsPath);
toolFiles.forEach(file => console.error(` - tools/${file}`));
}
}
// Check .bin directory
const binPath = path.join(nodeModulesPath, ".bin");
if (fs.existsSync(binPath)) {
console.error(chalk.yellow("Contents of .bin directory:"));
const binFiles = fs.readdirSync(binPath);
binFiles.forEach(file => console.error(` - .bin/${file}`));
}
}
throw new Error("Could not find bmad-method binary after installation");
}
// Build arguments array
const args = this.buildInstallArgs(options);
console.log(chalk.dim(`Running: node ${bmadPath} ${args.join(" ")}`));
// Run the installation
return new Promise((resolve, reject) => {
// If we found a symlink in .bin, read where it points to
let actualBinaryPath = bmadPath;
if (bmadPath.includes('.bin') && fs.lstatSync(bmadPath).isSymbolicLink()) {
try {
actualBinaryPath = fs.readlinkSync(bmadPath);
// If it's a relative path, resolve it relative to the .bin directory
if (!path.isAbsolute(actualBinaryPath)) {
actualBinaryPath = path.resolve(path.dirname(bmadPath), actualBinaryPath);
}
console.log(chalk.dim(` Symlink points to: ${actualBinaryPath}`));
} catch (error) {
console.warn(chalk.yellow(` Warning: Could not resolve symlink: ${error.message}`));
}
}
// Check if the actual binary exists
if (!fs.existsSync(actualBinaryPath)) {
console.error(chalk.red(`Binary not found at: ${actualBinaryPath}`));
}
// For the installer binary, always use the npx fallback to avoid module resolution issues
const isInstallerBinary = actualBinaryPath.includes('tools/installer/bin/bmad.js');
if (isInstallerBinary || !fs.existsSync(actualBinaryPath)) {
// Try to use npx directly instead
console.log(chalk.dim("Using npx execution for better compatibility..."));
// Use execSync instead of spawn for better PATH handling
try {
const npxCommand = `npx --yes bmad-method@${this.upstreamVersion} ${args.join(' ')}`;
console.log(chalk.dim(`Running: ${npxCommand}`));
execSync(npxCommand, {
cwd: targetDir,
stdio: "inherit",
env: {
...process.env,
PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}`,
// Force npx to not use cache
npm_config_cache: path.join(this.tempDir, '.npm-cache'),
npm_config_prefer_online: 'true'
}
});
resolve({ success: true });
} catch (error) {
if (error.status === 0) {
resolve({ success: true });
} else {
reject(new Error(`BMAD installation failed: ${error.message}`));
}
}
return;
}
// Check if we're running the npx wrapper
const isNpxWrapper = actualBinaryPath.endsWith('bmad-npx-wrapper.js');
// Build the command args based on whether it's the wrapper or direct binary
let commandArgs;
let commandEnv = {
...process.env,
// Ensure proper module resolution
NODE_PATH: path.join(this.tempDir, "node_modules")
};
if (isNpxWrapper) {
// The npx wrapper expects to be run with specific args
// We need to make sure dependencies are available
commandArgs = [actualBinaryPath, ...args];
// Add the temp node_modules to NODE_PATH for dependency resolution
const existingNodePath = process.env.NODE_PATH || '';
commandEnv.NODE_PATH = existingNodePath ?
`${path.join(this.tempDir, "node_modules")}:${existingNodePath}` :
path.join(this.tempDir, "node_modules");
} else {
commandArgs = [actualBinaryPath, ...args];
}
const child = spawn("node", commandArgs, {
cwd: targetDir,
stdio: "inherit",
env: commandEnv
});
child.on("close", (code) => {
if (code === 0) {
resolve({ success: true });
} else {
reject(new Error(`BMAD installation failed with exit code ${code}`));
}
});
child.on("error", (error) => {
reject(error);
});
});
} finally {
// Clean up temporary directory
if (this.tempDir && fs.existsSync(this.tempDir)) {
try {
await fs.remove(this.tempDir);
} catch (error) {
console.warn(chalk.yellow("Could not clean up temporary directory:", this.tempDir));
}
}
}
}
/**
* Run any command with dependency workaround
*/
async runWrappedCommand(command, targetDir, options = {}) {
try {
// Create a temporary directory for installation
this.tempDir = path.join(os.tmpdir(), `bmad-${command}-${Date.now()}`);
await fs.ensureDir(this.tempDir);
// Create a minimal package.json
const tempPackageJson = {
name: `bmad-temp-${command}`,
version: "1.0.0",
description: `Temporary installation for BMAD ${command}`,
private: true
};
await fs.writeJSON(path.join(this.tempDir, "package.json"), tempPackageJson, { spaces: 2 });
// Install bmad-method and its dependencies
console.log(chalk.dim(`Installing BMAD foundation for ${command} command...`));
try {
// Get the list of missing dependencies from upstream package
const missingDeps = await this.detectMissingDependencies();
// Install missing dependencies first
for (const dep of missingDeps) {
console.log(chalk.dim(`Installing missing dependency: ${dep.name}@${dep.version}`));
execSync(`npm install ${dep.name}@${dep.version}`, {
cwd: this.tempDir,
stdio: options.verbose ? "inherit" : "pipe"
});
}
// Then install bmad-method
execSync(`npm install bmad-method@${this.upstreamVersion}`, {
cwd: this.tempDir,
stdio: options.verbose ? "inherit" : "pipe"
});
} catch (error) {
console.error(chalk.red(`Failed to install BMAD dependencies for ${command}`));
throw error;
}
// Find the bmad-method binary - check multiple possible locations
// Prioritize the actual installer binary over the wrapper
const possibleBinaries = [
path.join(this.tempDir, "node_modules", "bmad-method", "tools", "installer", "bin", "bmad.js"),
path.join(this.tempDir, "node_modules", ".bin", "bmad-method"),
path.join(this.tempDir, "node_modules", ".bin", "bmad"),
path.join(this.tempDir, "node_modules", "bmad-method", "tools", "bmad-npx-wrapper.js"),
path.join(this.tempDir, "node_modules", "bmad-method", "bin", "bmad.js"),
path.join(this.tempDir, "node_modules", "bmad-method", "bin", "bmad-method.js"),
path.join(this.tempDir, "node_modules", "bmad-method", "cli.js"),
];
let bmadPath = null;
console.log(chalk.dim("Searching for bmad-method binary..."));
for (const binPath of possibleBinaries) {
console.log(chalk.dim(` Checking: ${binPath}`));
if (fs.existsSync(binPath)) {
console.log(chalk.green(` ✓ Found: ${binPath}`));
bmadPath = binPath;
break;
}
}
if (!bmadPath) {
// List what's actually in the directories to help debug
console.error(chalk.red("Could not find bmad-method binary. Checking installed structure:"));
const nodeModulesPath = path.join(this.tempDir, "node_modules");
if (fs.existsSync(nodeModulesPath)) {
const bmadMethodPath = path.join(nodeModulesPath, "bmad-method");
if (fs.existsSync(bmadMethodPath)) {
console.error(chalk.yellow("Contents of bmad-method package:"));
const files = fs.readdirSync(bmadMethodPath);
files.forEach(file => console.error(` - ${file}`));
// Check tools directory specifically
const toolsPath = path.join(bmadMethodPath, "tools");
if (fs.existsSync(toolsPath)) {
console.error(chalk.yellow("Contents of tools directory:"));
const toolFiles = fs.readdirSync(toolsPath);
toolFiles.forEach(file => console.error(` - tools/${file}`));
}
}
// Check .bin directory
const binPath = path.join(nodeModulesPath, ".bin");
if (fs.existsSync(binPath)) {
console.error(chalk.yellow("Contents of .bin directory:"));
const binFiles = fs.readdirSync(binPath);
binFiles.forEach(file => console.error(` - .bin/${file}`));
}
}
throw new Error("Could not find bmad-method binary after installation");
}
// Build arguments array
const args = this.buildCommandArgs(command, options);
console.log(chalk.dim(`Running: node ${bmadPath} ${args.join(" ")}`));
// Run the command
return new Promise((resolve, reject) => {
// If we found a symlink in .bin, read where it points to
let actualBinaryPath = bmadPath;
if (bmadPath.includes('.bin') && fs.lstatSync(bmadPath).isSymbolicLink()) {
try {
actualBinaryPath = fs.readlinkSync(bmadPath);
// If it's a relative path, resolve it relative to the .bin directory
if (!path.isAbsolute(actualBinaryPath)) {
actualBinaryPath = path.resolve(path.dirname(bmadPath), actualBinaryPath);
}
console.log(chalk.dim(` Symlink points to: ${actualBinaryPath}`));
} catch (error) {
console.warn(chalk.yellow(` Warning: Could not resolve symlink: ${error.message}`));
}
}
// Check if the actual binary exists
if (!fs.existsSync(actualBinaryPath)) {
console.error(chalk.red(`Binary not found at: ${actualBinaryPath}`));
// Try to use npx directly instead
console.log(chalk.dim("Falling back to npx execution..."));
// Use execSync instead of spawn for better PATH handling
try {
const npxCommand = `npx --yes bmad-method@${this.upstreamVersion} ${args.join(' ')}`;
console.log(chalk.dim(`Running: ${npxCommand}`));
execSync(npxCommand, {
cwd: targetDir,
stdio: "inherit",
env: {
...process.env,
PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}`,
// Force npx to not use cache
npm_config_cache: path.join(this.tempDir, '.npm-cache'),
npm_config_prefer_online: 'true'
}
});
resolve({ success: true });
} catch (error) {
if (error.status === 0) {
resolve({ success: true });
} else {
reject(new Error(`BMAD ${command} failed: ${error.message}`));
}
}
return;
}
// Check if we're running the npx wrapper
const isNpxWrapper = actualBinaryPath.endsWith('bmad-npx-wrapper.js');
// Build the command args based on whether it's the wrapper or direct binary
let commandArgs;
let commandEnv = {
...process.env,
// Ensure proper module resolution
NODE_PATH: path.join(this.tempDir, "node_modules")
};
if (isNpxWrapper) {
// The npx wrapper expects to be run with specific args
// We need to make sure dependencies are available
commandArgs = [actualBinaryPath, ...args];
// Add the temp node_modules to NODE_PATH for dependency resolution
const existingNodePath = process.env.NODE_PATH || '';
commandEnv.NODE_PATH = existingNodePath ?
`${path.join(this.tempDir, "node_modules")}:${existingNodePath}` :
path.join(this.tempDir, "node_modules");
} else {
commandArgs = [actualBinaryPath, ...args];
}
const child = spawn("node", commandArgs, {
cwd: targetDir,
stdio: "inherit",
env: commandEnv
});
child.on("close", (code) => {
if (code === 0) {
resolve({ success: true });
} else {
reject(new Error(`BMAD ${command} failed with exit code ${code}`));
}
});
child.on("error", (error) => {
reject(error);
});
});
} finally {
// Clean up temporary directory
if (this.tempDir && fs.existsSync(this.tempDir)) {
try {
await fs.remove(this.tempDir);
} catch (error) {
console.warn(chalk.yellow("Could not clean up temporary directory:", this.tempDir));
}
}
}
}
/**
* Build install arguments array from options
* This keeps all argument handling in one place and makes it easy to stay in sync with upstream
*/
buildInstallArgs(options) {
const args = ["install"];
// Pass through all boolean flags
if (options.full) args.push("--full");
if (options.expansionOnly) args.push("--expansion-only");
if (options.force) args.push("--force");
if (options.dryRun) args.push("--dry-run");
// Note: --verbose is not supported by upstream BMAD, so we don't pass it
// Note: Web bundle options are handled separately after upstream installation
// since upstream installer doesn't support web bundle command line flags
// Pass through directory option - always use absolute path
if (options.directory) {
const absolutePath = path.resolve(options.directory);
args.push("--directory", absolutePath);
}
// Handle IDEs - upstream supports multiple IDEs via repeated --ide flags
if (options.ides && options.ides.length > 0) {
const upstreamIDEs = options.ides; // Upstream now supports all IDEs including Trae
for (const ide of upstreamIDEs) {
args.push("--ide", ide);
}
}
// Handle expansion packs - upstream supports multiple via repeated flags
if (options.expansionPacks && options.expansionPacks.length > 0) {
for (const pack of options.expansionPacks) {
args.push("--expansion-packs", pack);
}
}
// Pass through any additional unknown options to stay flexible
if (options.additionalArgs) {
args.push(...options.additionalArgs);
}
return args;
}
/**
* Build arguments for any command (install, update, etc.)
*/
buildCommandArgs(command, options) {
const args = [command];
// For install command, use specialized builder
if (command === "install") {
return this.buildInstallArgs(options);
}
// For other commands, pass through common flags
if (options.force) args.push("--force");
if (options.dryRun) args.push("--dry-run");
// Note: --verbose is not supported by upstream BMAD, so we don't pass it
if (options.directory) {
const absolutePath = path.resolve(options.directory);
args.push("--directory", absolutePath);
}
// Pass through any additional arguments
if (options.additionalArgs) {
args.push(...options.additionalArgs);
}
return args;
}
/**
* Detect missing dependencies in upstream package
*/
async detectMissingDependencies() {
// Known missing dependencies in bmad-method@4.31.0
// These are the actual dependencies from the package.json
const knownMissing = [
{ name: "@kayvan/markdown-tree-parser", version: "^1.5.0" },
{ name: "chalk", version: "^4.1.2" },
{ name: "commander", version: "^14.0.0" },
{ name: "fs-extra", version: "^11.3.0" },
{ name: "glob", version: "^11.0.3" },
{ name: "inquirer", version: "^8.2.6" },
{ name: "js-yaml", version: "^4.1.0" },
{ name: "ora", version: "^5.4.1" },
{ name: "yaml", version: "^2.3.2" } // Keep this as it may be needed too
];
// In the future, we could dynamically detect by trying to run upstream
// and parsing error messages, but for now use known list
return knownMissing;
}
/**
* Check if we can use npx directly (when upstream is fixed)
*/
async canUseNpx() {
// Always return false for now to avoid npx caching issues
// The wrapper method with fallback to npx provides better reliability
return false;
}
}
module.exports = BMADInstallerWrapper;