UNPKG

@sethdouglasford/claude-flow

Version:

Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology

465 lines • 20.1 kB
/** * Migration Runner - Executes migration strategies */ import * as fs from "fs-extra"; import * as path from "node:path"; import * as crypto from "crypto"; import { fileURLToPath } from "node:url"; import { MigrationAnalyzer } from "./migration-analyzer"; import { logger } from "./logger"; import { ProgressReporter } from "./progress-reporter"; import { MigrationValidator } from "./migration-validator"; import glob from "glob"; import inquirer from "inquirer"; import chalk from "chalk"; // Get __dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const BACKUP_DIR = ".migration-backup"; export class MigrationRunner { options; progress; analyzer; validator; manifest; constructor(options) { this.options = options; this.progress = new ProgressReporter(); this.analyzer = new MigrationAnalyzer(); this.validator = new MigrationValidator(); this.manifest = this.loadManifest(); } async run() { const result = { success: false, filesModified: [], filesCreated: [], filesBackedUp: [], errors: [], warnings: [], }; try { // Analyze project this.progress.start("analyzing", "Analyzing project..."); const analysis = await this.analyzer.analyze(this.options.projectPath); // Show analysis and confirm if (!this.options.force && !this.options.dryRun) { this.analyzer.printAnalysis(analysis); const confirm = await this.confirmMigration(analysis); if (!confirm) { logger.info("Migration cancelled"); return result; } } // Create backup if (!this.options.dryRun && analysis.hasClaudeFolder) { this.progress.update("backing-up", "Creating backup..."); const backup = await this.createBackup(); result.rollbackPath = backup.timestamp.toISOString(); result.filesBackedUp = backup.files.map(f => f.path); } // Execute migration based on strategy this.progress.update("migrating", "Migrating files..."); switch (this.options.strategy) { case "full": await this.fullMigration(result); break; case "selective": await this.selectiveMigration(result, analysis); break; case "merge": await this.mergeMigration(result, analysis); break; } // Validate migration if (!this.options.skipValidation && !this.options.dryRun) { this.progress.update("validating", "Validating migration..."); const validation = await this.validator.validate(this.options.projectPath); if (!validation.valid) { result.errors.push(...validation.errors.map(e => ({ error: e }))); result.warnings.push(...validation.warnings); } } result.success = result.errors.length === 0; this.progress.complete(result.success ? "Migration completed successfully!" : "Migration completed with errors"); // Print summary this.printSummary(result); } catch (error) { result.errors.push({ error: error.message, stack: error.stack }); this.progress.error("Migration failed"); // Attempt rollback on failure if (result.rollbackPath && !this.options.dryRun) { logger.warn("Attempting automatic rollback..."); try { await this.rollback(result.rollbackPath); logger.success("Rollback completed"); } catch (rollbackError) { logger.error("Rollback failed:", rollbackError); } } } return result; } async fullMigration(result) { const sourcePath = path.join(__dirname, "../../.claude"); const targetPath = path.join(this.options.projectPath, ".claude"); if (this.options.dryRun) { logger.info("[DRY RUN] Would replace entire .claude folder"); return; } // Remove existing .claude folder if (await fs.pathExists(targetPath)) { await fs.remove(targetPath); } // Copy new .claude folder await fs.copy(sourcePath, targetPath); result.filesCreated.push(".claude"); // Copy other required files await this.copyRequiredFiles(result); } async selectiveMigration(result, analysis) { const sourcePath = path.join(__dirname, "../../.claude"); const targetPath = path.join(this.options.projectPath, ".claude"); // Ensure target directory exists await fs.ensureDir(targetPath); // Migrate commands selectively const commandsSource = path.join(sourcePath, "commands"); const commandsTarget = path.join(targetPath, "commands"); await fs.ensureDir(commandsTarget); // Copy optimized commands for (const command of Object.values(this.manifest.files.commands)) { const sourceFile = path.join(commandsSource, command.source); const targetFile = path.join(commandsTarget, command.target); if (this.options.preserveCustom && analysis.customCommands.includes(path.basename(command.target, ".md"))) { result.warnings.push(`Skipped ${command.target} (custom command preserved)`); continue; } if (this.options.dryRun) { logger.info(`[DRY RUN] Would copy ${command.source} to ${command.target}`); } else { await fs.copy(sourceFile, targetFile, { overwrite: true }); result.filesCreated.push(command.target); } } // Copy optimization guides const guides = [ "BATCHTOOLS_GUIDE.md", "BATCHTOOLS_BEST_PRACTICES.md", "MIGRATION_GUIDE.md", "PERFORMANCE_BENCHMARKS.md", ]; for (const guide of guides) { const sourceFile = path.join(sourcePath, guide); const targetFile = path.join(targetPath, guide); if (await fs.pathExists(sourceFile)) { if (this.options.dryRun) { logger.info(`[DRY RUN] Would copy ${guide}`); } else { await fs.copy(sourceFile, targetFile, { overwrite: true }); result.filesCreated.push(guide); } } } // Update configurations await this.updateConfigurations(result); } async mergeMigration(result, analysis) { // Similar to selective but merges configurations await this.selectiveMigration(result, analysis); // Merge configurations if (!this.options.dryRun) { await this.mergeConfigurations(result, analysis); } } async mergeConfigurations(result, analysis) { // Merge CLAUDE.md const claudeMdPath = path.join(this.options.projectPath, "CLAUDE.md"); if (await fs.pathExists(claudeMdPath)) { const existingContent = await fs.readFile(claudeMdPath, "utf-8"); const newContent = await this.getMergedClaudeMd(existingContent); await fs.writeFile(claudeMdPath, newContent); result.filesModified.push("CLAUDE.md"); } // Merge .roomodes const roomodesPath = path.join(this.options.projectPath, ".roomodes"); if (await fs.pathExists(roomodesPath)) { const existing = await fs.readJson(roomodesPath); const updated = await this.getMergedRoomodes(existing); await fs.writeJson(roomodesPath, updated, { spaces: 2 }); result.filesModified.push(".roomodes"); } } async copyRequiredFiles(result) { const files = [ { source: "CLAUDE.md", target: "CLAUDE.md" }, { source: ".roomodes", target: ".roomodes" }, ]; for (const file of files) { const sourcePath = path.join(__dirname, "../../", file.source); const targetPath = path.join(this.options.projectPath, file.target); if (await fs.pathExists(sourcePath)) { if (this.options.dryRun) { logger.info(`[DRY RUN] Would copy ${file.source}`); } else { await fs.copy(sourcePath, targetPath, { overwrite: true }); result.filesCreated.push(file.target); } } } } async updateConfigurations(result) { // Update package.json scripts if needed const packageJsonPath = path.join(this.options.projectPath, "package.json"); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); if (!packageJson.scripts) { packageJson.scripts = {}; } const scripts = { "migrate": "claude-flow migrate", "migrate:analyze": "claude-flow migrate analyze", "migrate:rollback": "claude-flow migrate rollback", }; let modified = false; for (const [name, command] of Object.entries(scripts)) { if (!packageJson.scripts[name]) { packageJson.scripts[name] = command; modified = true; } } if (modified && !this.options.dryRun) { await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); result.filesModified.push("package.json"); } } } async createBackup() { const backupDir = path.join(this.options.projectPath, this.options.backupDir || BACKUP_DIR); const timestamp = new Date(); const backupPath = path.join(backupDir, timestamp.toISOString().replace(/:/g, "-")); await fs.ensureDir(backupPath); const backup = { timestamp, version: "1.0.0", files: [], metadata: { strategy: this.options.strategy, projectPath: this.options.projectPath, }, }; // Backup .claude folder const claudePath = path.join(this.options.projectPath, ".claude"); if (await fs.pathExists(claudePath)) { await fs.copy(claudePath, path.join(backupPath, ".claude")); // Record backed up files const files = glob.sync("**/*", { cwd: claudePath, nodir: true }); for (const file of files) { const content = await fs.readFile(path.join(claudePath, file), "utf-8"); backup.files.push({ path: `.claude/${file}`, content, checksum: crypto.createHash("md5").update(content).digest("hex"), }); } } // Backup other important files const importantFiles = ["CLAUDE.md", ".roomodes", "package.json"]; for (const file of importantFiles) { const filePath = path.join(this.options.projectPath, file); if (await fs.pathExists(filePath)) { await fs.copy(filePath, path.join(backupPath, file)); const content = await fs.readFile(filePath, "utf-8"); backup.files.push({ path: file, content, checksum: crypto.createHash("md5").update(content).digest("hex"), }); } } // Save backup manifest await fs.writeJson(path.join(backupPath, "backup.json"), backup, { spaces: 2 }); logger.success(`Backup created at ${backupPath}`); return backup; } async rollback(timestamp) { const backupDir = path.join(this.options.projectPath, this.options.backupDir || BACKUP_DIR); if (!await fs.pathExists(backupDir)) { throw new Error("No backups found"); } let backupPath; if (timestamp) { backupPath = path.join(backupDir, timestamp); } else { // Use most recent backup const backups = await fs.readdir(backupDir); if (backups.length === 0) { throw new Error("No backups found"); } backups.sort().reverse(); backupPath = path.join(backupDir, backups[0]); } if (!await fs.pathExists(backupPath)) { throw new Error(`Backup not found: ${backupPath}`); } logger.info(`Rolling back from ${backupPath}...`); // Confirm rollback if (!this.options.force) { const confirm = await inquirer.prompt([{ type: "confirm", name: "proceed", message: "Are you sure you want to rollback? This will overwrite current files.", default: false, }]); if (!confirm.proceed) { logger.info("Rollback cancelled"); return; } } // Restore files const backup = await fs.readJson(path.join(backupPath, "backup.json")); for (const file of backup.files) { const targetPath = path.join(this.options.projectPath, file.path); await fs.ensureDir(path.dirname(targetPath)); await fs.writeFile(targetPath, file.content); } logger.success("Rollback completed successfully"); } async validate(verbose = false) { const validation = await this.validator.validate(this.options.projectPath); if (verbose) { this.validator.printValidation(validation); } return validation.valid; } async listBackups() { const backupDir = path.join(this.options.projectPath, this.options.backupDir || BACKUP_DIR); if (!await fs.pathExists(backupDir)) { logger.info("No backups found"); return; } const backups = await fs.readdir(backupDir); if (backups.length === 0) { logger.info("No backups found"); return; } console.log(chalk.bold("\nšŸ“¦ Available Backups")); console.log(chalk.gray("─".repeat(50))); for (const backup of backups.sort().reverse()) { const backupPath = path.join(backupDir, backup); const stats = await fs.stat(backupPath); const manifest = await fs.readJson(path.join(backupPath, "backup.json")).catch(() => null); console.log(`\n${chalk.bold(backup)}`); console.log(` Created: ${stats.mtime.toLocaleString()}`); console.log(` Size: ${(stats.size / 1024).toFixed(2)} KB`); if (manifest) { console.log(` Version: ${manifest.version}`); console.log(` Strategy: ${manifest.metadata.strategy}`); console.log(` Files: ${manifest.files.length}`); } } console.log(chalk.gray(`\n${"─".repeat(50)}`)); } async confirmMigration(analysis) { const questions = [ { type: "confirm", name: "proceed", message: `Proceed with ${this.options.strategy} migration?`, default: true, }, ]; if (analysis.customCommands.length > 0 && !this.options.preserveCustom) { questions.unshift({ type: "confirm", name: "preserveCustom", message: `Found ${analysis.customCommands.length} custom commands. Preserve them?`, default: true, }); } const answers = await inquirer.prompt(questions); if (answers.preserveCustom) { this.options.preserveCustom = true; } return answers.proceed; } loadManifest() { // This would normally load from a manifest file return { version: "1.0.0", files: { commands: { "sparc.md": { source: "sparc.md", target: "sparc.md" }, "sparc-architect.md": { source: "sparc/architect.md", target: "sparc-architect.md" }, "sparc-code.md": { source: "sparc/code.md", target: "sparc-code.md" }, "sparc-tdd.md": { source: "sparc/tdd.md", target: "sparc-tdd.md" }, "claude-flow-help.md": { source: "claude-flow-help.md", target: "claude-flow-help.md" }, "claude-flow-memory.md": { source: "claude-flow-memory.md", target: "claude-flow-memory.md" }, "claude-flow-swarm.md": { source: "claude-flow-swarm.md", target: "claude-flow-swarm.md" }, }, configurations: {}, templates: {}, }, }; } async getMergedClaudeMd(existingContent) { // Merge logic for CLAUDE.md const templatePath = path.join(__dirname, "../../CLAUDE.md"); const templateContent = await fs.readFile(templatePath, "utf-8"); // Simple merge: append custom content to template if (!existingContent.includes("SPARC Development Environment")) { return `${templateContent}\n\n## Previous Configuration\n\n${existingContent}`; } return templateContent; } async getMergedRoomodes(existing) { const templatePath = path.join(__dirname, "../../.roomodes"); const template = await fs.readJson(templatePath); // Merge custom modes with template const merged = { ...template }; const existingModes = existing; for (const [mode, config] of Object.entries(existingModes)) { if (!merged[mode]) { merged[mode] = config; } } return merged; } printSummary(result) { console.log(chalk.bold("\nšŸ“‹ Migration Summary")); console.log(chalk.gray("─".repeat(50))); console.log(`\n${chalk.bold("Status:")} ${result.success ? chalk.green("Success") : chalk.red("Failed")}`); if (result.filesCreated.length > 0) { console.log(`\n${chalk.bold("Files Created:")} ${chalk.green(result.filesCreated.length)}`); if (result.filesCreated.length <= 10) { result.filesCreated.forEach(file => console.log(` • ${file}`)); } } if (result.filesModified.length > 0) { console.log(`\n${chalk.bold("Files Modified:")} ${chalk.yellow(result.filesModified.length)}`); result.filesModified.forEach(file => console.log(` • ${file}`)); } if (result.filesBackedUp.length > 0) { console.log(`\n${chalk.bold("Files Backed Up:")} ${chalk.blue(result.filesBackedUp.length)}`); } if (result.warnings.length > 0) { console.log(`\n${chalk.bold("Warnings:")}`); result.warnings.forEach(warning => console.log(` āš ļø ${warning}`)); } if (result.errors.length > 0) { console.log(`\n${chalk.bold("Errors:")}`); result.errors.forEach(error => console.log(` āŒ ${error.error}`)); } if (result.rollbackPath) { console.log(`\n${chalk.bold("Rollback Available:")} ${result.rollbackPath}`); console.log(chalk.gray(` Run "claude-flow migrate rollback -t ${result.rollbackPath}" to revert`)); } console.log(chalk.gray(`\n${"─".repeat(50)}`)); } } //# sourceMappingURL=migration-runner.js.map