@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
JavaScript
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;