UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

403 lines (361 loc) 14.5 kB
import chalk from "chalk"; import ora from "ora"; import { glob } from "glob"; import fs from "fs-extra"; import path from "path"; import axios from "axios"; import { loadConfig, validateConfig } from "../utils/config"; import { checkMigrationAccess, validateMigrationRequest, MIGRATION_TIERS } from "../../../lib/migration-tier-system"; interface MigrateOptions { layers?: string; output?: string; recursive?: boolean; include?: string; exclude?: string; config?: string; dryRun?: boolean; backup?: boolean; verbose?: boolean; } interface MigrationResult { success: boolean; file: string; transformed?: string; analysis: any; issues: any[]; fixes: any[]; performanceMetrics?: any; } /** * Perform full migration using NeuroLintProEnhanced with all layers * This leverages the existing infrastructure for enterprise-grade migrations */ async function performMigration( code: string, filename: string, config: any, options: MigrateOptions ): Promise<MigrationResult> { try { // Migration uses all layers (1-6) with proper tier authorization const migrationPayload = { code, filename, layers: "1,2,3,4,5,6", // All layers enabled for migration applyFixes: !options.dryRun, // REMOVED: migration bypass flag - now uses proper tier system metadata: { recursive: options.recursive, outputFormat: options.output || "detailed-json", verbose: options.verbose || true, backup: options.backup !== false, // Default to backup enabled migrationService: true, // Indicates this is a migration service request timestamp: new Date().toISOString(), }, }; const response = await axios.post( `${config.api.url}/analyze`, migrationPayload, { headers: { "X-API-Key": config.apiKey, "Content-Type": "application/json", "X-Migration-Service": "true", // Indicates migration service usage }, timeout: 300000, // 5 minutes for complex migrations maxContentLength: 100 * 1024 * 1024, // 100MB for large files } ); return { success: response.data.success || true, file: filename, transformed: response.data.transformed || response.data.code, analysis: response.data.analysis || response.data, issues: response.data.analysis?.detectedIssues || [], fixes: response.data.analysis?.appliedFixes || [], performanceMetrics: response.data.performanceMetrics, }; } catch (error) { console.log(chalk.yellow(`Warning: Migration failed for ${filename}`)); if (axios.isAxiosError(error)) { console.log(chalk.gray(`API Error: ${error.response?.status} ${error.response?.statusText}`)); } return { success: false, file: filename, analysis: {}, issues: [], fixes: [], }; } } /** * Generate detailed migration report */ function generateMigrationReport(results: MigrationResult[], options: MigrateOptions) { const totalFiles = results.length; const successfulFiles = results.filter(r => r.success).length; const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0); const totalFixes = results.reduce((sum, r) => sum + r.fixes.length, 0); const report = { migrationSummary: { totalFiles, successfulFiles, failedFiles: totalFiles - successfulFiles, totalIssues, totalFixes, timestamp: new Date().toISOString(), }, fileResults: results.map(r => ({ file: r.file, success: r.success, issuesFound: r.issues.length, fixesApplied: r.fixes.length, issues: r.issues, fixes: r.fixes, performanceMetrics: r.performanceMetrics, })), modernizationAreas: { configuration: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 1).length, 0), entityCleanup: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 2).length, 0), components: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 3).length, 0), hydration: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 4).length, 0), nextjsAppRouter: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 5).length, 0), testing: results.reduce((sum, r) => sum + r.issues.filter(i => i.layer === 6).length, 0), }, }; return report; } export async function migrateCommand(targetPath: string, options: MigrateOptions) { const spinner = ora("Initializing one-time migration...").start(); const startTime = Date.now(); try { // Load and validate configuration const config = await loadConfig(options.config); const configValidation = await validateConfig(config); if (!configValidation.valid) { spinner.fail("Configuration validation failed"); configValidation.errors.forEach((error) => console.log(chalk.red(`ERROR: ${error}`)) ); return; } // Verify API key for migration service if (!config.apiKey) { spinner.fail("Migration service requires authentication"); console.log(chalk.red("ERROR: API key required for one-time migration service")); console.log(chalk.gray("Please run 'neurolint login' first")); return; } // Verify migration access using proper tier system try { const userResponse = await axios.get( `${config.api.url}/auth/current-user`, { headers: { "X-API-Key": config.apiKey }, timeout: 10000, } ); const userData = userResponse.data.user || userResponse.data; const migrationAccess = checkMigrationAccess( userData.plan || userData.tier || "free", userData.migration_quote_approved, userData.migration_expires_at ? new Date(userData.migration_expires_at) : undefined ); if (!migrationAccess.hasAccess) { spinner.fail("Migration service access required"); console.log(chalk.yellow(migrationAccess.reason || "Migration service requires enterprise access")); console.log(chalk.gray("One-time migration service: $999-$9,999 (quote-based)")); console.log(chalk.gray("Request quote: https://app.neurolint.dev/migration-request")); console.log(chalk.gray("Contact: migration@neurolint.dev")); return; } // Display migration tier info if (migrationAccess.config) { console.log(chalk.gray(`Migration tier: ${migrationAccess.tier}`)); console.log(chalk.gray(`Support level: ${migrationAccess.config.supportLevel}`)); if (migrationAccess.tier === 'migration' && userData.migration_expires_at) { const expiresAt = new Date(userData.migration_expires_at); console.log(chalk.gray(`Service expires: ${expiresAt.toLocaleDateString()}`)); } } } catch (error) { spinner.fail("Unable to verify migration access"); console.log(chalk.red("Please contact support for migration service access")); console.log(chalk.gray("Support: migration-support@neurolint.dev")); return; } // Discover files to migrate spinner.text = "Discovering files for migration..."; const targetFiles: string[] = []; if (!targetPath) { targetPath = process.cwd(); } try { // Use comprehensive patterns for migration const migrationPatterns = [ "**/*.{ts,tsx,js,jsx}", "**/*.json", // Config files "**/package.json", "**/tsconfig.json", "**/next.config.js", ]; for (const pattern of migrationPatterns) { const foundFiles = await glob(pattern, { cwd: targetPath, ignore: [ "**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**", "**/.git/**", ], }); targetFiles.push(...foundFiles.map(f => path.resolve(targetPath, f))); } } catch (error) { spinner.fail("File discovery failed"); console.log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`)); return; } if (targetFiles.length === 0) { spinner.fail("No files found for migration"); console.log(chalk.yellow("No TypeScript, JavaScript, or configuration files found")); return; } // Remove duplicates and filter existing files const uniqueFiles = [...new Set(targetFiles)]; const existingFiles = []; for (const file of uniqueFiles) { if (await fs.pathExists(file)) { existingFiles.push(file); } } console.log(); console.log(chalk.white.bold("One-Time Migration Service")); console.log(chalk.gray(`Migrating ${existingFiles.length} files with all layers (1-6)`)); console.log(chalk.gray(`Target: ${targetPath}`)); console.log(chalk.gray(`Mode: ${options.dryRun ? "DRY RUN" : "LIVE MIGRATION"}`)); console.log(); if (options.dryRun) { console.log(chalk.yellow("WARNING: DRY RUN MODE - No files will be modified")); } else { console.log(chalk.green("LIVE MIGRATION - Files will be transformed")); if (options.backup !== false) { console.log(chalk.gray("Automatic backups enabled")); } } console.log(); // Create backup directory if not dry run let backupDir = ""; if (!options.dryRun && options.backup !== false) { backupDir = path.join(targetPath, `.neurolint-backup-${Date.now()}`); await fs.ensureDir(backupDir); console.log(chalk.gray(`Backup directory: ${backupDir}`)); } // Process files for migration spinner.text = `Migrating ${existingFiles.length} files...`; const migrationResults: MigrationResult[] = []; let processedCount = 0; for (const file of existingFiles) { try { processedCount++; spinner.text = `Migrating ${processedCount}/${existingFiles.length}: ${path.basename(file)}`; const code = await fs.readFile(file, "utf-8"); // Create backup if enabled if (!options.dryRun && options.backup !== false) { const relativePath = path.relative(targetPath, file); const backupFile = path.join(backupDir, relativePath); await fs.ensureDir(path.dirname(backupFile)); await fs.copy(file, backupFile); } // Perform migration const result = await performMigration(code, file, config, options); migrationResults.push(result); // Write transformed code if not dry run and transformation succeeded if (!options.dryRun && result.success && result.transformed) { await fs.writeFile(file, result.transformed, "utf-8"); } } catch (fileError) { console.log(chalk.yellow(`Warning: Could not process ${file}`)); migrationResults.push({ success: false, file, analysis: {}, issues: [], fixes: [], }); } } const processingTime = Date.now() - startTime; spinner.succeed(`Migration completed for ${existingFiles.length} files`); // Generate and display results const report = generateMigrationReport(migrationResults, options); console.log(); console.log(chalk.white.bold("Migration Results")); console.log(); console.log(chalk.white("Files processed: ") + chalk.cyan(report.migrationSummary.totalFiles)); console.log(chalk.white("Successful: ") + chalk.green(report.migrationSummary.successfulFiles)); console.log(chalk.white("Failed: ") + (report.migrationSummary.failedFiles > 0 ? chalk.red(report.migrationSummary.failedFiles) : chalk.green("0"))); console.log(chalk.white("Issues found: ") + chalk.yellow(report.migrationSummary.totalIssues)); console.log(chalk.white("Fixes applied: ") + chalk.green(report.migrationSummary.totalFixes)); console.log(chalk.white("Duration: ") + chalk.gray(`${Math.round(processingTime / 1000)}s`)); console.log(); // Show modernization areas console.log(chalk.white("Modernization by Layer:")); console.log(chalk.gray(` Configuration (Layer 1): ${report.modernizationAreas.configuration} issues`)); console.log(chalk.gray(` Entity Cleanup (Layer 2): ${report.modernizationAreas.entityCleanup} issues`)); console.log(chalk.gray(` Components (Layer 3): ${report.modernizationAreas.components} issues`)); console.log(chalk.gray(` Hydration (Layer 4): ${report.modernizationAreas.hydration} issues`)); console.log(chalk.gray(` Next.js App Router (Layer 5): ${report.modernizationAreas.nextjsAppRouter} issues`)); console.log(chalk.gray(` Testing (Layer 6): ${report.modernizationAreas.testing} issues`)); console.log(); // Save detailed report const reportPath = path.join(targetPath, `neurolint-migration-report-${Date.now()}.json`); await fs.writeJson(reportPath, report, { spaces: 2 }); console.log(chalk.white("Detailed report saved: ") + chalk.cyan(reportPath)); // Upload results to dashboard try { await axios.post( `${config.api.url}/analysis`, { type: "migration", results: report, metadata: { isMigration: true, timestamp: new Date().toISOString(), targetPath, options, }, }, { headers: { "X-API-Key": config.apiKey, "Content-Type": "application/json", }, } ); console.log(chalk.gray("Results uploaded to dashboard: https://app.neurolint.dev")); } catch (uploadError) { console.log(chalk.yellow("Warning: Could not upload results to dashboard")); } console.log(); if (options.dryRun) { console.log(chalk.white("Dry run completed - no files were modified")); console.log(chalk.gray("Remove --dry-run flag to apply migrations")); } else { console.log(chalk.white("Migration completed successfully")); if (options.backup !== false) { console.log(chalk.gray(`Backups available in: ${backupDir}`)); } } console.log(); console.log(chalk.white("30-day priority support included")); console.log(chalk.gray("Contact: migration-support@neurolint.dev")); console.log(chalk.gray("Dashboard: https://app.neurolint.dev")); } catch (error) { spinner.fail("Migration failed"); console.log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`)); } }