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.

722 lines (613 loc) • 23.1 kB
#!/usr/bin/env node /** * Cloud Kinetix Enhanced Commands * * This module implements CK-specific commands that extend BMAD functionality */ // Import ES module loader const { getChalk, getOra } = require("./es-module-loader"); const fs = require("fs-extra"); const path = require("path"); const archiver = require("archiver"); const yaml = require("js-yaml"); // Import the proper BMADBackupManager from the dedicated module const BMADBackupManager = require("./bmad-backup-manager"); /** * BMAD Configuration Merge Manager * Intelligently merges core-config.yaml files during updates */ class BMADConfigMerger { constructor(targetDirectory) { this.targetDirectory = path.resolve(targetDirectory); this.configPath = path.join(this.targetDirectory, ".bmad-core", "core-config.yaml"); this.backupConfigPath = path.join(this.targetDirectory, ".bmad-core", "core-config.backup.yaml"); } /** * Check if config merge is needed and perform interactive merge */ async handleConfigMerge(verbose = false) { const chalk = await getChalk(); if (!(await fs.pathExists(this.configPath))) { if (verbose) { console.log(chalk.gray("šŸ“„ No existing core-config.yaml found, will use new configuration")); } return { merged: false, reason: "no-existing-config" }; } return await this.performConfigMerge(verbose); } /** * Perform the actual config merge with user interaction */ async performConfigMerge(verbose = false) { try { // Load existing config (backed up before installation) const existingConfig = await this.loadYamlFile(this.backupConfigPath); const newConfig = await this.loadYamlFile(this.configPath); if (!existingConfig || !newConfig) { if (verbose) { console.log(chalk.yellow("āš ļø Could not load config files for comparison")); } return { merged: false, reason: "config-load-failed" }; } // Analyze differences const analysis = this.analyzeConfigDifferences(existingConfig, newConfig); if (analysis.conflicts.length === 0 && analysis.newKeys.length === 0) { if (verbose) { console.log(chalk.green("šŸ“„ Configuration is already up to date")); } return { merged: false, reason: "no-changes-needed" }; } // Present merge options to user const mergeResult = await this.presentMergeOptions( analysis, existingConfig, newConfig, verbose ); if (mergeResult.action === "merge") { await this.saveMergedConfig(mergeResult.config); return { merged: true, conflicts: analysis.conflicts.length, newKeys: analysis.newKeys.length, preservedKeys: analysis.preservedKeys.length, }; } return { merged: false, reason: "user-cancelled" }; } catch (error) { console.error(chalk.red(`āŒ Config merge failed: ${error.message}`)); return { merged: false, reason: "merge-error", error: error.message }; } } /** * Analyze differences between existing and new config */ analyzeConfigDifferences(existingConfig, newConfig) { const conflicts = []; const newKeys = []; const preservedKeys = []; // Check for conflicts and new keys this.compareObjects(existingConfig, newConfig, "", conflicts, newKeys, preservedKeys); return { conflicts, newKeys, preservedKeys }; } /** * Recursively compare configuration objects */ compareObjects(existing, newConfig, path, conflicts, newKeys, preservedKeys) { // Check for new keys in newConfig for (const key in newConfig) { const currentPath = path ? `${path}.${key}` : key; if (!(key in existing)) { newKeys.push({ key: currentPath, value: newConfig[key], type: typeof newConfig[key], }); } else if ( typeof existing[key] === "object" && typeof newConfig[key] === "object" && existing[key] !== null && newConfig[key] !== null && !Array.isArray(existing[key]) && !Array.isArray(newConfig[key]) ) { // Recursively compare nested objects this.compareObjects( existing[key], newConfig[key], currentPath, conflicts, newKeys, preservedKeys ); } else if (JSON.stringify(existing[key]) !== JSON.stringify(newConfig[key])) { conflicts.push({ key: currentPath, existingValue: existing[key], newValue: newConfig[key], type: typeof newConfig[key], }); } else { preservedKeys.push({ key: currentPath, value: existing[key], }); } } // Check for keys that exist in existing but not in new (preserved) for (const key in existing) { const currentPath = path ? `${path}.${key}` : key; if (!(key in newConfig)) { preservedKeys.push({ key: currentPath, value: existing[key], preserved: true, }); } } } /** * Present merge options to the user interactively */ async presentMergeOptions(analysis, existingConfig, newConfig, verbose) { console.log(chalk.blue.bold("\nšŸ”§ Configuration Merge Required\n")); if (analysis.newKeys.length > 0) { console.log(chalk.green(`šŸ“„ New configuration options (${analysis.newKeys.length}):`)); analysis.newKeys.forEach((item) => { console.log(chalk.green(` + ${item.key}: ${this.formatValue(item.value)}`)); }); } if (analysis.conflicts.length > 0) { console.log(chalk.yellow(`\n⚔ Configuration conflicts (${analysis.conflicts.length}):`)); analysis.conflicts.forEach((conflict, index) => { console.log(chalk.yellow(`\n ${index + 1}. ${conflict.key}:`)); console.log(chalk.gray(` Current: ${this.formatValue(conflict.existingValue)}`)); console.log(chalk.gray(` New: ${this.formatValue(conflict.newValue)}`)); }); } if (analysis.preservedKeys.length > 0 && verbose) { console.log(chalk.gray(`\nšŸ“‹ Unchanged settings (${analysis.preservedKeys.length}):`)); analysis.preservedKeys.slice(0, 5).forEach((item) => { console.log(chalk.gray(` = ${item.key}: ${this.formatValue(item.value)}`)); }); if (analysis.preservedKeys.length > 5) { console.log(chalk.gray(` ... and ${analysis.preservedKeys.length - 5} more`)); } } // Use inquirer for interactive choices const inquirer = require("inquirer"); if (analysis.conflicts.length > 0) { // Handle conflicts one by one const mergedConfig = { ...newConfig }; for (const conflict of analysis.conflicts) { const choices = [ { name: `Keep current value: ${this.formatValue(conflict.existingValue)}`, value: "keep", short: "Keep current", }, { name: `Use new value: ${this.formatValue(conflict.newValue)}`, value: "new", short: "Use new", }, ]; const answer = await inquirer.prompt([ { type: "list", name: "choice", message: `How to handle "${conflict.key}"?`, choices: choices, }, ]); if (answer.choice === "keep") { this.setNestedValue(mergedConfig, conflict.key, conflict.existingValue); } // If 'new', the value is already in mergedConfig } // Add any preserved keys that don't exist in new config analysis.preservedKeys .filter((p) => p.preserved) .forEach((item) => { this.setNestedValue(mergedConfig, item.key, item.value); }); return { action: "merge", config: mergedConfig }; } else { // No conflicts, just new keys - auto-merge const mergedConfig = { ...existingConfig, ...newConfig }; console.log(chalk.green("\nāœ… Auto-merging configuration (no conflicts detected)")); return { action: "merge", config: mergedConfig }; } } /** * Set a nested value in an object using dot notation */ setNestedValue(obj, path, value) { const keys = path.split("."); let current = obj; for (let i = 0; i < keys.length - 1; i++) { if (!(keys[i] in current) || typeof current[keys[i]] !== "object") { current[keys[i]] = {}; } current = current[keys[i]]; } current[keys[keys.length - 1]] = value; } /** * Format a value for display */ formatValue(value) { if (value === null) return "null"; if (value === undefined) return "undefined"; if (typeof value === "string") return `"${value}"`; if (Array.isArray(value)) return `[${value.length} items]`; if (typeof value === "object") return `{${Object.keys(value).length} keys}`; return String(value); } /** * Load and parse a YAML file */ async loadYamlFile(filePath) { try { if (!(await fs.pathExists(filePath))) { return null; } const content = await fs.readFile(filePath, "utf8"); return yaml.load(content); } catch (error) { throw new Error(`Failed to load YAML from ${filePath}: ${error.message}`); } } /** * Save merged configuration to file */ async saveMergedConfig(config) { try { const yamlContent = yaml.dump(config, { indent: 2, lineWidth: 120, noRefs: true, }); await fs.writeFile(this.configPath, yamlContent, "utf8"); console.log(chalk.green("āœ… Configuration merged and saved successfully")); } catch (error) { throw new Error(`Failed to save merged config: ${error.message}`); } } /** * Create a backup of the current config before installation */ async backupCurrentConfig() { try { if (await fs.pathExists(this.configPath)) { await fs.copy(this.configPath, this.backupConfigPath); return true; } return false; } catch (error) { console.warn(chalk.yellow(`āš ļø Could not backup config: ${error.message}`)); return false; } } } /** * Enhanced install-all command * Delegates to upstream BMAD install with automatic expansion pack selection */ async function installAll(directory = process.cwd(), options = {}) { const chalk = await getChalk(); const ora = await getOra(); const startTime = Date.now(); // Extract options with defaults const { verbose = false, skipBackup = false } = options; console.log(chalk.blue.bold("\nšŸš€ BMAD Enhanced: Complete Installation\n")); console.log(chalk.gray("Delegating to upstream BMAD with all expansion packs...\n")); try { // Step 1: Install core BMAD framework using NPX const { spawn } = require("child_process"); // Build install command args const args = ["bmad-method", "install", "--full"]; // Add directory if specified if (directory !== process.cwd()) { args.push("--directory", directory); } console.log(chalk.gray(`Step 1: Installing core BMAD framework...`)); console.log(chalk.gray(`Running: npx ${args.join(" ")}\n`)); // Execute NPX command directly await new Promise((resolve, reject) => { const child = spawn("npx", args, { cwd: directory, stdio: "inherit", env: { ...process.env, BMAD_AUTO_CONFIRM: "true", BMAD_NON_INTERACTIVE: "true", CI: "true", }, }); child.on("close", (code) => { if (code === 0) { resolve({ exitCode: code }); } else { reject(new Error(`NPX command failed with exit code ${code}`)); } }); child.on("error", reject); }); // Step 2: Install CK expansion packs manually console.log(chalk.gray(`\nStep 2: Installing Cloud-Kinetix expansion packs...`)); await installCKExpansionPacks(directory); const duration = ((Date.now() - startTime) / 1000).toFixed(1); console.log(chalk.green("\nāœ… Installation Summary:")); console.log(`šŸ“ Directory: ${directory}`); console.log(`ā±ļø Duration: ${duration}s`); console.log(`šŸŽÆ Method: NPX bmad-method install + CK expansion packs`); console.log(`šŸ“¦ Components: Complete BMAD framework with Cloud-Kinetix enhancements`); console.log(chalk.green("\nšŸŽ‰ Your BMAD Enhanced installation is ready to use!")); return { success: true, duration, method: "delegated" }; } catch (error) { console.error(chalk.red("\nāŒ Error during installation:")); console.error(chalk.red(error.message)); if (verbose) { console.error(chalk.gray("\nFull error details:")); console.error(error); } throw error; } } /** * Install Cloud-Kinetix expansion packs directly to user's project * This happens after the core BMAD framework is installed */ async function installCKExpansionPacks(directory) { const fs = require("fs-extra"); const path = require("path"); try { // Source: our expansion packs (from published package) const sourceDir = path.join(__dirname, "..", "expansion-packs"); if (!(await fs.pathExists(sourceDir))) { console.warn(chalk.yellow(`āš ļø Source expansion packs directory not found: ${sourceDir}`)); return; } const entries = await fs.readdir(sourceDir, { withFileTypes: true }); let installedCount = 0; for (const entry of entries) { if (entry.isDirectory() && entry.name.startsWith("ck-")) { const sourcePackDir = path.join(sourceDir, entry.name); const targetPackDir = path.join(directory, `.${entry.name}`); // Copy the entire expansion pack directory await fs.copy(sourcePackDir, targetPackDir, { overwrite: true }); console.log(chalk.green(` āœ“ Installed ${entry.name}`)); installedCount++; } } if (installedCount > 0) { console.log(chalk.green(`\nšŸŽ‰ Successfully installed ${installedCount} Cloud-Kinetix expansion packs!`)); } } catch (error) { console.warn(chalk.yellow(`āš ļø Could not install CK expansion packs: ${error.message}`)); } } /** * Validate setup and configuration */ async function validateSetup(args = [], options = {}, delegate = null) { // If running in test mode (no delegate), perform basic validation if (!delegate) { const checks = []; const cwd = process.cwd(); // Check for BMAD core installation const bmadCorePath = path.join(cwd, ".bmad-core"); const bmadCoreExists = await fs.pathExists(bmadCorePath); checks.push({ name: "BMAD Core Installation", passed: bmadCoreExists, message: bmadCoreExists ? "BMAD core found" : "BMAD core not found - run install first", }); // Check for expansion packs const expansionPath = path.join(cwd, "expansion-packs"); const expansionExists = await fs.pathExists(expansionPath); checks.push({ name: "Expansion Packs", passed: expansionExists, message: expansionExists ? "Expansion packs directory found" : "No expansion packs installed", }); // Check for config file const configPath = path.join(cwd, ".bmad-core", "core-config.yaml"); const configExists = await fs.pathExists(configPath); checks.push({ name: "Configuration", passed: configExists, message: configExists ? "Core configuration found" : "Core configuration missing", }); return { success: checks.every((check) => check.passed), checks, }; } // Full validation with delegate const chalk = await getChalk(); const ora = await getOra(); console.log(chalk.blue.bold("\nšŸ” Validating BMAD Enhanced Setup\n")); const spinner = ora("Running validation checks...").start(); const issues = []; try { // Check upstream BMAD status spinner.text = "Checking upstream BMAD..."; try { await delegate.delegateToUpstream("status", [], { silent: true }); } catch (error) { issues.push("Upstream BMAD validation failed"); } // Check CK enhancements spinner.text = "Checking CK enhancements..."; const versionInfo = await delegate.getVersionInfo(); if (!versionInfo.upstream || versionInfo.upstream === "unknown") { issues.push("Cannot determine upstream BMAD version"); } spinner.succeed("Validation completed!"); console.log(chalk.green("\nšŸ“Š Validation Results:")); console.log(`šŸ”§ BMAD Enhanced: v${versionInfo.enhanced}`); console.log(`šŸ“¦ Upstream BMAD: v${versionInfo.upstream}`); if (issues.length > 0) { console.log(chalk.yellow("\nāš ļø Issues Found:")); issues.forEach((issue) => console.log(chalk.yellow(` • ${issue}`))); } else { console.log(chalk.green("\nāœ… All validation checks passed!")); } return { success: issues.length === 0, issues }; } catch (error) { spinner.fail("Validation failed"); throw error; } } /** * List available backups */ async function listBackups(args, options, delegate) { const chalk = await getChalk(); const directory = args.find((arg) => arg.startsWith("--directory="))?.split("=")[1] || args[args.indexOf("--directory") + 1] || process.cwd(); const backupManager = new BMADBackupManager(directory); return await backupManager.listBackups(); } /** * Create standard backup */ async function createBackup(args = [], options = {}) { const directory = options.directory || process.cwd(); const backupManager = new BMADBackupManager(directory); return await backupManager.createBackups(); } /** * Create overlay backup using BMAD manifest + CK extensions */ async function createOverlayBackup(args = [], options = {}) { const directory = options.directory || process.cwd(); const backupManager = new BMADBackupManager(directory); const result = await backupManager.createOverlayBackup(); if (result.success) { const chalk = await getChalk(); console.log(chalk.green("āœ… Overlay backup completed successfully!")); console.log(`šŸ“¦ Backup file: ${result.backupFile}`); console.log(`šŸ“Š Coverage: ${result.coverage.total} directories total`); console.log(` • BMAD Core: ${result.coverage.bmadCore} directories`); console.log(` • CK Extensions: ${result.coverage.ckExtensions} directories`); console.log(` • Auto-discovered: ${result.coverage.discovered} directories`); console.log("\nšŸ“ Backed up directories:"); result.folders.forEach((folder) => { const size = formatSize(folder.size); const source = folder.source.toUpperCase().padEnd(12); console.log(` • [${source}] ${folder.name} (${size})`); }); console.log("\nšŸ“‹ Metadata:"); console.log(` • BMAD Version: ${result.metadata.bmadVersion}`); console.log(` • CK Version: ${result.metadata.ckVersion}`); console.log(` • Installation Type: ${result.metadata.installationType}`); console.log(` • Manifest Found: ${result.metadata.bmadManifestFound ? "āœ…" : "āŒ"}`); if (result.metadata.bmadManifestFound) { console.log("\nšŸ’” Overlay backup successfully used BMAD manifest as foundation"); } else { console.log("\nāš ļø No BMAD manifest found - used pattern detection only"); } } else { console.log("āŒ Overlay backup failed:", result.message); } return result; } function formatSize(bytes) { if (bytes < 1024) return bytes + "B"; if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + "KB"; return Math.round(bytes / (1024 * 1024)) + "MB"; } /** * Show status information */ async function showStatus(directory) { const chalk = await getChalk(); console.log(chalk.blue.bold("\nšŸ” BMAD Enhanced Status\n")); // Add status logic here console.log("Status functionality to be implemented"); } /** * Show configuration help */ async function showConfigHelp() { const chalk = await getChalk(); console.log(chalk.blue.bold("\nāš™ļø BMAD Enhanced Configuration\n")); console.log("Configuration help to be implemented"); } async function handleCommand(command, options = {}) { const directory = options.directory || process.cwd(); try { switch (command) { case "install-all": await installAll(directory, options); break; case "status": await showStatus(directory); break; case "backup": await createBackup([], options); break; case "overlay-backup": await createOverlayBackup([], options); break; case "list-backups": await listBackups([], options); break; case "config": await showConfigHelp(); break; case "help": default: showHelp(); break; } } catch (error) { console.error("āŒ Command failed:", error.message); if (options.verbose) { console.error(error.stack); } } } function showHelp() { console.log("\n🌟 BMAD Enhanced (Cloud Kinetix Edition)"); console.log("========================================\n"); console.log("šŸ“– Available Commands:"); console.log(" bmad-ck install-all # Complete BMAD Enhanced installation"); console.log(" bmad-ck status # Check installation status"); console.log(" bmad-ck backup # Create standard backup"); console.log(" bmad-ck overlay-backup # Create overlay backup (manifest + CK)"); console.log(" bmad-ck list-backups # View available backups"); console.log(" bmad-ck config # Configuration help"); console.log(" bmad-ck help # Show this help message\n"); console.log("šŸŽÆ Components: All agents, expansion packs, and IDE configurations"); console.log("šŸ”§ Enterprise: JIRA integration, AI development tools, parallel development"); console.log("šŸ¢ Professional: Backup management, config merging, enterprise deployment\n"); console.log("šŸ“š Examples:"); console.log(" bmad-ck install-all --directory=/my/project"); console.log(" bmad-ck overlay-backup # Uses BMAD manifest + CK extensions"); console.log(" bmad-ck status --verbose\n"); const directory = process.cwd(); console.log(`šŸ“ Current directory: ${directory}`); console.log(`šŸ“ Backups location: ${path.join(directory, "bmad-backups")}`); console.log(' Use "bmad-ck list-backups" to see all available backups.'); console.log("\n�� Quick Commands:"); console.log(" bmad-ck config # Configure your preferences"); console.log(" bmad-ck status # Check installation status"); console.log(" bmad-ck list # See available agents"); console.log(" bmad-ck list-backups # View available backups"); console.log(" bmad-ck overlay-backup # Smart backup with manifest detection"); } module.exports = { // Command exports (for CLI) "install-all": installAll, // Function exports (for testing) installAll, // Class exports (for testing) BMADBackupManager, BMADConfigMerger, };