devflow-ai
Version:
Enterprise-grade AI agent orchestration with swarm management UI dashboard
310 lines (267 loc) ⢠10.2 kB
text/typescript
/**
* Migration Analyzer - Analyzes existing projects for migration readiness
*/
import * as fs from 'fs-extra';
import * as path from 'path';
import * as crypto from 'crypto';
import type { MigrationAnalysis, MigrationRisk } from './types.js';
import { logger } from './logger.js';
import * as chalk from 'chalk';
import { glob } from 'glob';
export class MigrationAnalyzer {
private optimizedCommands = [
'sparc',
'sparc-architect',
'sparc-code',
'sparc-tdd',
'claude-flow-help',
'claude-flow-memory',
'claude-flow-swarm',
];
async analyze(projectPath: string): Promise<MigrationAnalysis> {
logger.info(`Analyzing project at ${projectPath}...`);
const analysis: MigrationAnalysis = {
projectPath,
hasClaudeFolder: false,
hasOptimizedPrompts: false,
customCommands: [],
customConfigurations: {},
conflictingFiles: [],
migrationRisks: [],
recommendations: [],
timestamp: new Date(),
};
// Check for .claude folder
const claudePath = path.join(projectPath, '.claude');
if (await fs.pathExists(claudePath)) {
analysis.hasClaudeFolder = true;
// Analyze existing commands
await this.analyzeCommands(claudePath, analysis);
// Check for optimized prompts
await this.checkOptimizedPrompts(claudePath, analysis);
// Analyze configurations
await this.analyzeConfigurations(projectPath, analysis);
// Detect conflicts
await this.detectConflicts(projectPath, analysis);
}
// Generate risks and recommendations
this.assessRisks(analysis);
this.generateRecommendations(analysis);
return analysis;
}
private async analyzeCommands(claudePath: string, analysis: MigrationAnalysis): Promise<void> {
const commandsPath = path.join(claudePath, 'commands');
if (await fs.pathExists(commandsPath)) {
const files = await glob('**/*.md', { cwd: commandsPath });
for (const file of files) {
const commandName = path.basename(file, '.md');
if (!this.optimizedCommands.includes(commandName)) {
analysis.customCommands.push(commandName);
}
}
}
}
private async checkOptimizedPrompts(
claudePath: string,
analysis: MigrationAnalysis,
): Promise<void> {
// Check for key optimized prompt files
const optimizedFiles = [
'BATCHTOOLS_GUIDE.md',
'BATCHTOOLS_BEST_PRACTICES.md',
'MIGRATION_GUIDE.md',
'PERFORMANCE_BENCHMARKS.md',
];
let hasOptimized = 0;
for (const file of optimizedFiles) {
if (await fs.pathExists(path.join(claudePath, file))) {
hasOptimized++;
}
}
analysis.hasOptimizedPrompts = hasOptimized >= 2;
}
private async analyzeConfigurations(
projectPath: string,
analysis: MigrationAnalysis,
): Promise<void> {
// Check for CLAUDE.md
const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
if (await fs.pathExists(claudeMdPath)) {
const content = await fs.readFile(claudeMdPath, 'utf-8');
analysis.customConfigurations['CLAUDE.md'] = {
exists: true,
size: content.length,
hasCustomContent: !content.includes('SPARC Development Environment'),
};
}
// Check for .roomodes
const roomodesPath = path.join(projectPath, '.roomodes');
if (await fs.pathExists(roomodesPath)) {
try {
const roomodes = await fs.readJson(roomodesPath);
analysis.customConfigurations['.roomodes'] = {
exists: true,
modeCount: Object.keys(roomodes).length,
customModes: Object.keys(roomodes).filter(
(mode) => !['architect', 'code', 'tdd', 'debug', 'docs-writer'].includes(mode),
),
};
} catch (error) {
analysis.migrationRisks.push({
level: 'medium',
description: 'Invalid .roomodes file',
file: roomodesPath,
mitigation: 'File will be backed up and replaced',
});
}
}
}
private async detectConflicts(projectPath: string, analysis: MigrationAnalysis): Promise<void> {
// Check for files that might conflict with migration
const potentialConflicts = [
'.claude/commands/sparc.md',
'.claude/BATCHTOOLS_GUIDE.md',
'memory/memory-store.json',
'coordination/config.json',
];
for (const file of potentialConflicts) {
const filePath = path.join(projectPath, file);
if (await fs.pathExists(filePath)) {
const content = await fs.readFile(filePath, 'utf-8');
const checksum = crypto.createHash('md5').update(content).digest('hex');
// Check if it's a custom version
if (!this.isStandardFile(file, checksum)) {
analysis.conflictingFiles.push(file);
}
}
}
}
private isStandardFile(file: string, checksum: string): boolean {
// This would contain checksums of standard files
// For now, we'll assume all existing files are potentially custom
return false;
}
private assessRisks(analysis: MigrationAnalysis): void {
// High risk: Custom commands that might be overwritten
if (analysis.customCommands.length > 0) {
analysis.migrationRisks.push({
level: 'high',
description: `Found ${analysis.customCommands.length} custom commands that may be affected`,
mitigation: 'Use --preserve-custom flag or selective migration',
});
}
// Medium risk: Existing optimized prompts
if (analysis.hasOptimizedPrompts) {
analysis.migrationRisks.push({
level: 'medium',
description: 'Project already has some optimized prompts',
mitigation: 'Consider using merge strategy to preserve customizations',
});
}
// Low risk: No .claude folder
if (!analysis.hasClaudeFolder) {
analysis.migrationRisks.push({
level: 'low',
description: 'No existing .claude folder found',
mitigation: 'Fresh installation will be performed',
});
}
// High risk: Conflicting files
if (analysis.conflictingFiles.length > 0) {
analysis.migrationRisks.push({
level: 'high',
description: `${analysis.conflictingFiles.length} files may have custom modifications`,
mitigation: 'Files will be backed up before migration',
});
}
}
private generateRecommendations(analysis: MigrationAnalysis): void {
// Strategy recommendations
if (analysis.customCommands.length > 0 || analysis.conflictingFiles.length > 0) {
analysis.recommendations.push(
'Use "selective" or "merge" strategy to preserve customizations',
);
} else if (!analysis.hasClaudeFolder) {
analysis.recommendations.push('Use "full" strategy for clean installation');
}
// Backup recommendations
if (analysis.hasClaudeFolder) {
analysis.recommendations.push(
'Create a backup before migration (automatic with default settings)',
);
}
// Custom command recommendations
if (analysis.customCommands.length > 0) {
analysis.recommendations.push(
`Review custom commands: ${analysis.customCommands.join(', ')}`,
);
}
// Validation recommendations
if (analysis.migrationRisks.some((r) => r.level === 'high')) {
analysis.recommendations.push('Run with --dry-run first to preview changes');
}
}
printAnalysis(analysis: MigrationAnalysis, detailed: boolean = false): void {
console.log(chalk.bold('\nš Migration Analysis Report'));
console.log(chalk.gray('ā'.repeat(50)));
console.log(`\n${chalk.bold('Project:')} ${analysis.projectPath}`);
console.log(`${chalk.bold('Timestamp:')} ${analysis.timestamp.toISOString()}`);
// Status
console.log(chalk.bold('\nš Current Status:'));
console.log(
` ⢠.claude folder: ${analysis.hasClaudeFolder ? chalk.green('ā') : chalk.red('ā')}`,
);
console.log(
` ⢠Optimized prompts: ${analysis.hasOptimizedPrompts ? chalk.green('ā') : chalk.red('ā')}`,
);
console.log(
` ⢠Custom commands: ${analysis.customCommands.length > 0 ? chalk.yellow(analysis.customCommands.length) : chalk.green('0')}`,
);
console.log(
` ⢠Conflicts: ${analysis.conflictingFiles.length > 0 ? chalk.yellow(analysis.conflictingFiles.length) : chalk.green('0')}`,
);
// Risks
if (analysis.migrationRisks.length > 0) {
console.log(chalk.bold('\nā ļø Migration Risks:'));
analysis.migrationRisks.forEach((risk) => {
const icon = risk.level === 'high' ? 'š“' : risk.level === 'medium' ? 'š”' : 'š¢';
console.log(` ${icon} ${chalk.bold(risk.level.toUpperCase())}: ${risk.description}`);
if (risk.mitigation) {
console.log(` ${chalk.gray('ā')} ${chalk.italic(risk.mitigation)}`);
}
});
}
// Recommendations
if (analysis.recommendations.length > 0) {
console.log(chalk.bold('\nš” Recommendations:'));
analysis.recommendations.forEach((rec) => {
console.log(` ⢠${rec}`);
});
}
// Detailed information
if (detailed) {
if (analysis.customCommands.length > 0) {
console.log(chalk.bold('\nš§ Custom Commands:'));
analysis.customCommands.forEach((cmd) => {
console.log(` ⢠${cmd}`);
});
}
if (analysis.conflictingFiles.length > 0) {
console.log(chalk.bold('\nš Conflicting Files:'));
analysis.conflictingFiles.forEach((file) => {
console.log(` ⢠${file}`);
});
}
if (Object.keys(analysis.customConfigurations).length > 0) {
console.log(chalk.bold('\nāļø Configurations:'));
Object.entries(analysis.customConfigurations).forEach(([file, config]) => {
console.log(` ⢠${file}: ${JSON.stringify(config, null, 2)}`);
});
}
}
console.log(chalk.gray('\n' + 'ā'.repeat(50)));
}
async saveAnalysis(analysis: MigrationAnalysis, outputPath: string): Promise<void> {
await fs.writeJson(outputPath, analysis, { spaces: 2 });
}
}