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
text/typescript
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)));
}
}