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.

531 lines (458 loc) 15.9 kB
const fs = require("fs-extra"); const path = require("path"); const archiver = require("archiver"); const yaml = require("js-yaml"); /** * BMAD Backup Management System * Handles automatic backup creation and management for BMAD installations */ class BMADBackupManager { constructor(targetDirectory = process.cwd(), backupDirectory = "bmad-backups") { this.targetDirectory = path.resolve(targetDirectory); this.backupDirectory = path.join(this.targetDirectory, backupDirectory); this.maxBackups = 5; this.bmadFolders = [ "bmad-core", "expansion-packs", "agents", ".bmad-core", ".bmad-ck-jira-integration", ".bmad-ck-llm-agent-dev", ".bmad-ck-parallel-dev", ".bmad-enterprise", ".cursor", "dist", "tools", ".bmad", ]; } /** * Get BMAD core version from core configuration manifest */ async getBMADCoreVersion() { try { // First try to read from the BMAD core config (the authoritative source) const coreConfigPath = path.join(this.targetDirectory, "bmad-core", "core-config.yaml"); if (await fs.pathExists(coreConfigPath)) { const configContent = await fs.readFile(coreConfigPath, "utf8"); const coreConfig = yaml.load(configContent); return coreConfig.version || "unknown"; } // Fallback: Try the installation manifest const manifestPath = path.join(this.targetDirectory, ".bmad-core", "install-manifest.yaml"); if (await fs.pathExists(manifestPath)) { const manifestContent = await fs.readFile(manifestPath, "utf8"); const manifest = yaml.load(manifestContent); return manifest.version || "unknown"; } } catch (error) { // Fallback for any issues reading manifest } return "unknown"; } /** * Get enterprise layer version from package.json */ async getEnterpriseVersion() { try { // Look for root package.json (bmad-enhanced main version) const rootPackagePath = path.join(__dirname, "..", "..", "package.json"); if (await fs.pathExists(rootPackagePath)) { const packageContent = await fs.readFile(rootPackagePath, "utf8"); const packageJson = JSON.parse(packageContent); return packageJson.version || "unknown"; } // Fallback to installer package.json if root not found const installerPackagePath = path.join(__dirname, "..", "package.json"); if (await fs.pathExists(installerPackagePath)) { const packageContent = await fs.readFile(installerPackagePath, "utf8"); const packageJson = JSON.parse(packageContent); return packageJson.version || "unknown"; } } catch (error) { // Fallback for any issues reading package.json } return "unknown"; } /** * Generate version-aware filename */ async generateVersionedFilename() { const coreVersion = await this.getBMADCoreVersion(); const enterpriseVersion = await this.getEnterpriseVersion(); const timestamp = this.generateTimestamp(); return `bmad-v${coreVersion}-ck-v${enterpriseVersion}-complete-${timestamp}.zip`; } /** * Find all existing BMAD folders */ async findBMADFolders() { const folders = []; for (const folderName of this.bmadFolders) { const folderPath = path.join(this.targetDirectory, folderName); const exists = await fs.pathExists(folderPath); let size = 0; if (exists) { size = await this.calculateFolderSize(folderPath); } folders.push({ name: folderName, path: folderPath, exists, size, }); } return folders; } /** * Calculate folder size recursively */ async calculateFolderSize(folderPath) { let totalSize = 0; try { const items = await fs.readdir(folderPath); for (const item of items) { const itemPath = path.join(folderPath, item); const stats = await fs.stat(itemPath); if (stats.isDirectory()) { totalSize += await this.calculateFolderSize(itemPath); } else { totalSize += stats.size; } } } catch (error) { // Handle permission errors gracefully return 0; } return totalSize; } /** * Create comprehensive backup of all existing BMAD folders */ async createBackups() { try { // Ensure backup directory exists await fs.ensureDir(this.backupDirectory); // Find existing BMAD folders const allFolders = await this.findBMADFolders(); const existingFolders = allFolders.filter((folder) => folder.exists); if (existingFolders.length === 0) { return { success: true, message: "No BMAD folders found to backup", folders: [], }; } // Create comprehensive backup with version information const backupFileName = await this.generateVersionedFilename(); const backupFilePath = path.join(this.backupDirectory, backupFileName); await this.createComprehensiveBackup(existingFolders, backupFilePath); // Cleanup old backups await this.cleanupOldBackups(); return { success: true, backupFile: backupFilePath, folders: existingFolders, message: `Backup created successfully: ${backupFileName}`, }; } catch (error) { return { success: false, error: error.message, message: `Backup failed: ${error.message}`, }; } } /** * Create a comprehensive backup with all BMAD folders in a single ZIP */ async createComprehensiveBackup(folders, outputPath) { return new Promise((resolve, reject) => { const output = fs.createWriteStream(outputPath); const archive = archiver("zip", { zlib: { level: 9 } }); output.on("close", () => resolve()); archive.on("error", reject); archive.pipe(output); // Add each folder to the archive with its original name folders.forEach((folder) => { archive.directory(folder.path, folder.name); }); archive.finalize(); }); } /** * List all existing backups */ async listBackups() { try { if (!(await fs.pathExists(this.backupDirectory))) { return []; } const files = await fs.readdir(this.backupDirectory); const backupFiles = files.filter( (file) => (file.startsWith("bmad-complete-") || file.startsWith("bmad-v")) && file.endsWith(".zip") ); const backups = []; for (const filename of backupFiles) { const filePath = path.join(this.backupDirectory, filename); const stats = await fs.stat(filePath); // Parse timestamp from filename (handles both old and new formats) let timestampMatch = filename.match( /bmad-complete-(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2})\.zip/ ); if (!timestampMatch) { // Try new versioned format: bmad-v4.20.0-ck-v1.2.3-complete-2025-06-29-19-36-40-506Z.zip timestampMatch = filename.match( /bmad-v[\d.]+(-\w+)?-v[\d.]+(-\w+)?-complete-(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}).*\.zip/ ); if (timestampMatch) timestampMatch[1] = timestampMatch[3]; } const timestamp = timestampMatch ? timestampMatch[1] : ""; backups.push({ filename, path: filePath, size: stats.size, created: stats.birthtime || stats.mtime || stats.ctime, timestamp, }); } // Sort by creation date (newest first) backups.sort((a, b) => b.created.getTime() - a.created.getTime()); return backups; } catch (error) { // Return empty array on error to handle gracefully return []; } } /** * Clean up old backups, keeping only the most recent ones */ async cleanupOldBackups() { try { const backups = await this.listBackups(); if (backups.length <= this.maxBackups) { return; // No cleanup needed } // Remove oldest backups const toDelete = backups.slice(this.maxBackups); for (const backup of toDelete) { await fs.remove(backup.path); } } catch (error) { // Cleanup errors shouldn't fail the backup process console.warn("Backup cleanup warning:", error.message); } } /** * Generate timestamp for backup files */ generateTimestamp() { const now = new Date(); return now.toISOString().replace(/[:.]/g, "-").replace("T", "-").split(".")[0]; } /** * Get human-readable file size */ formatFileSize(bytes) { if (bytes < 1024) return bytes + "B"; if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + "KB"; return Math.round(bytes / (1024 * 1024)) + "MB"; } /** * Create overlay backup system that combines standard BMAD manifest with CK extensions */ async createOverlayBackupList() { const backupTargets = { bmadCore: [], ckExtensions: [], discoveredFiles: [], metadata: { bmadManifestFound: false, bmadVersion: "unknown", ckVersion: "unknown", installationType: "unknown", }, }; // 1. Read standard BMAD manifest as foundation try { const manifestPath = path.join(this.targetDirectory, ".bmad-core", "install-manifest.yaml"); if (await fs.pathExists(manifestPath)) { const manifestContent = await fs.readFile(manifestPath, "utf8"); const manifest = yaml.load(manifestContent); backupTargets.metadata.bmadManifestFound = true; backupTargets.metadata.bmadVersion = manifest.version; backupTargets.metadata.installationType = manifest.install_type; // Extract all directories from manifest files const manifestDirs = new Set(); manifest.files.forEach((fileInfo) => { const filePath = typeof fileInfo === "string" ? fileInfo : fileInfo.path; const dir = path.dirname(filePath); if (dir && dir !== ".") { manifestDirs.add(dir.split("/")[0]); // Get top-level directory } }); backupTargets.bmadCore = Array.from(manifestDirs); console.log( `📋 Found BMAD manifest with ${manifest.files.length} files in directories:`, backupTargets.bmadCore ); } } catch (error) { console.warn(`⚠️ Could not read BMAD manifest: ${error.message}`); } // 2. Add CK-specific extensions (overlay) const ckExtensions = [ ".bmad-ck-jira-integration", ".bmad-ck-llm-agent-dev", ".bmad-ck-parallel-dev", ".bmad-enterprise", "eng-tools", // CK engineering tools "docs-ck", // CK documentation "tools/installer-ck", // CK installer "bmad-backups", // Backup directory itself ]; for (const extension of ckExtensions) { const extPath = path.join(this.targetDirectory, extension); if (await fs.pathExists(extPath)) { backupTargets.ckExtensions.push(extension); } } // 3. Smart discovery for additional BMAD-related directories const potentialDirs = await fs.readdir(this.targetDirectory, { withFileTypes: true }); for (const entry of potentialDirs) { if (entry.isDirectory()) { const dirName = entry.name; // Skip if already covered if ( backupTargets.bmadCore.includes(dirName) || backupTargets.ckExtensions.includes(dirName) ) { continue; } // Smart pattern detection for BMAD-related directories if (this.isBMADRelatedDirectory(dirName)) { backupTargets.discoveredFiles.push(dirName); } } } // 4. Add current CK version backupTargets.metadata.ckVersion = await this.getEnterpriseVersion(); return backupTargets; } /** * Smart detection for BMAD-related directories */ isBMADRelatedDirectory(dirName) { const bmadPatterns = [ /^\.bmad/, // Hidden BMAD dirs /^bmad-/, // BMAD prefixed /expansion-packs?$/, // Expansion packs /^agents?$/, // Agent directories /^\.cursor$/, // IDE integration /^dist$/, // Distribution /^tools$/, // Tools directory ]; return bmadPatterns.some((pattern) => pattern.test(dirName)); } /** * Create comprehensive backup using overlay system */ async createOverlayBackup() { try { await fs.ensureDir(this.backupDirectory); // Get overlay backup targets const targets = await this.createOverlayBackupList(); // Combine all backup targets const allDirectories = [ ...targets.bmadCore, ...targets.ckExtensions, ...targets.discoveredFiles, ]; // Remove duplicates and filter existing const uniqueDirs = [...new Set(allDirectories)]; const existingFolders = []; for (const dirName of uniqueDirs) { const dirPath = path.join(this.targetDirectory, dirName); if (await fs.pathExists(dirPath)) { const size = await this.calculateFolderSize(dirPath); existingFolders.push({ name: dirName, path: dirPath, exists: true, size, source: this.categorizeSource(dirName, targets), }); } } if (existingFolders.length === 0) { return { success: true, message: "No BMAD/CK directories found to backup", folders: [], metadata: targets.metadata, }; } // Create backup with enhanced metadata const backupFileName = await this.generateVersionedFilename(); const backupFilePath = path.join(this.backupDirectory, backupFileName); await this.createComprehensiveBackup(existingFolders, backupFilePath); // Save backup metadata await this.saveBackupMetadata(backupFilePath, targets, existingFolders); await this.cleanupOldBackups(); return { success: true, backupFile: backupFilePath, folders: existingFolders, metadata: targets.metadata, message: `Overlay backup created: ${backupFileName}`, coverage: { bmadCore: targets.bmadCore.length, ckExtensions: targets.ckExtensions.length, discovered: targets.discoveredFiles.length, total: existingFolders.length, }, }; } catch (error) { return { success: false, error: error.message, message: `Overlay backup failed: ${error.message}`, }; } } /** * Categorize backup source for reporting */ categorizeSource(dirName, targets) { if (targets.bmadCore.includes(dirName)) return "bmad-core"; if (targets.ckExtensions.includes(dirName)) return "ck-extension"; if (targets.discoveredFiles.includes(dirName)) return "discovered"; return "unknown"; } /** * Save backup metadata for audit trail */ async saveBackupMetadata(backupFilePath, targets, folders) { const metadataPath = backupFilePath.replace(".zip", "-metadata.json"); const metadata = { created: new Date().toISOString(), bmadVersion: targets.metadata.bmadVersion, ckVersion: targets.metadata.ckVersion, installationType: targets.metadata.installationType, manifestFound: targets.metadata.bmadManifestFound, coverage: { bmadCoreDirs: targets.bmadCore, ckExtensions: targets.ckExtensions, discoveredDirs: targets.discoveredFiles, }, backedUpFolders: folders.map((f) => ({ name: f.name, size: f.size, source: f.source, })), }; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); } } module.exports = BMADBackupManager;