@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
JavaScript
/**
* 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,
};