UNPKG

claude-flow

Version:

Enterprise-grade AI agent orchestration with WASM-powered ReasoningBank memory and AgentDB vector database (always uses latest agentic-flow)

586 lines (489 loc) • 18.5 kB
import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); /** * Migration Runner - Executes migration strategies */ import * as fs from 'fs-extra'; import * as path from 'path'; import * as crypto from 'crypto'; import type { MigrationOptions, MigrationResult, MigrationBackup, BackupFile, ValidationResult, MigrationProgress, MigrationManifest, } from './types.js'; import { MigrationAnalyzer } from './migration-analyzer.js'; import { logger } from './logger.js'; import { ProgressReporter } from './progress-reporter.js'; import { MigrationValidator } from './migration-validator.js'; import { glob } from 'glob'; import * as inquirer from 'inquirer'; import * as chalk from 'chalk'; export class MigrationRunner { private options: MigrationOptions; private progress: ProgressReporter; private analyzer: MigrationAnalyzer; private validator: MigrationValidator; private manifest: MigrationManifest; constructor(options: MigrationOptions) { this.options = options; this.progress = new ProgressReporter(); this.analyzer = new MigrationAnalyzer(); this.validator = new MigrationValidator(); this.manifest = this.loadManifest(); } async run(): Promise<MigrationResult> { const result: MigrationResult = { 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 instanceof Error ? error.message : String(error), 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; } private async fullMigration(result: MigrationResult): Promise<void> { 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); } private async selectiveMigration(result: MigrationResult, analysis: any): Promise<void> { 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 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); } private async mergeMigration(result: MigrationResult, analysis: any): Promise<void> { // Similar to selective but merges configurations await this.selectiveMigration(result, analysis); // Merge configurations if (!this.options.dryRun) { await this.mergeConfigurations(result, analysis); } } private async mergeConfigurations(result: MigrationResult, analysis: any): Promise<void> { // 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'); } } private async copyRequiredFiles(result: MigrationResult): Promise<void> { 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); } } } } private async updateConfigurations(result: MigrationResult): Promise<void> { // 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'); } } } private async createBackup(): Promise<MigrationBackup> { const backupDir = path.join( this.options.projectPath, this.options.backupDir || '.claude-backup', ); const timestamp = new Date(); const backupPath = path.join(backupDir, timestamp.toISOString().replace(/:/g, '-')); await fs.ensureDir(backupPath); const backup: MigrationBackup = { 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 = await glob('**/*', { 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?: string): Promise<void> { const backupDir = path.join( this.options.projectPath, this.options.backupDir || '.claude-backup', ); if (!(await fs.pathExists(backupDir))) { throw new Error('No backups found'); } let backupPath: string; 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: boolean = false): Promise<boolean> { const validation = await this.validator.validate(this.options.projectPath); if (verbose) { this.validator.printValidation(validation); } return validation.valid; } async listBackups(): Promise<void> { const backupDir = path.join( this.options.projectPath, this.options.backupDir || '.claude-backup', ); 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))); } private async confirmMigration(analysis: any): Promise<boolean> { 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; } private loadManifest(): MigrationManifest { // This would normally load from a manifest file return { version: '1.0.0', files: { commands: [ { source: 'sparc.md', target: 'sparc.md' }, { source: 'sparc/architect.md', target: 'sparc-architect.md' }, { source: 'sparc/code.md', target: 'sparc-code.md' }, { source: 'sparc/tdd.md', target: 'sparc-tdd.md' }, { source: 'claude-flow-help.md', target: 'claude-flow-help.md' }, { source: 'claude-flow-memory.md', target: 'claude-flow-memory.md' }, { source: 'claude-flow-swarm.md', target: 'claude-flow-swarm.md' }, ], configurations: {}, templates: {}, }, }; } private async getMergedClaudeMd(existingContent: string): Promise<string> { // 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; } private async getMergedRoomodes(existing: any): Promise<any> { const templatePath = path.join(__dirname, '../../.roomodes'); const template = await fs.readJson(templatePath); // Merge custom modes with template const merged = { ...template }; for (const [mode, config] of Object.entries(existing)) { if (!merged[mode]) { merged[mode] = config; } } return merged; } private printSummary(result: MigrationResult): void { 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))); } }