UNPKG

@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
#!/usr/bin/env node /** * 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;