UNPKG

@neurolint/cli

Version:

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

495 lines (443 loc) 15.1 kB
import chalk from "chalk"; import ora from "ora"; import fs from "fs-extra"; import path from "path"; import axios from "axios"; import { glob } from "glob"; import { loadConfig, validateConfig } from "../utils/config"; interface FixOptions { layers?: string; recursive?: boolean; dryRun?: boolean; backup?: boolean; include?: string; exclude?: string; config?: string; } interface FixResult { success: boolean; filesModified: number; issuesFixed: number; layersUsed: number[]; fixes: Array<{ file: string; layer: number; rule: string; description: string; before: string; after: string; line: number; }>; performance: { duration: number; }; } /** * Layer Dependency Management (from IMPLEMENTATION_PATTERNS.md) * Validates and corrects layer selection based on dependencies */ function validateAndCorrectLayers(requestedLayers: number[]) { const DEPENDENCIES = { 1: [], // Configuration has no dependencies 2: [1], // Entity cleanup depends on config foundation 3: [1, 2], // Components depend on config + cleanup 4: [1, 2, 3], // Hydration depends on all previous layers 5: [1, 2, 3, 4], // Next.js depends on all core layers 6: [1, 2, 3, 4, 5], // Testing depends on all previous layers }; const LAYER_INFO = { 1: { name: "Configuration", critical: true }, 2: { name: "Entity Cleanup", critical: false }, 3: { name: "Components", critical: false }, 4: { name: "Hydration", critical: false }, 5: { name: "Next.js App Router", critical: false }, 6: { name: "Testing & Validation", critical: false }, }; const warnings: string[] = []; const autoAdded: number[] = []; let correctedLayers = [...requestedLayers]; // Sort layers in execution order correctedLayers.sort((a, b) => a - b); // Check dependencies for each requested layer for (const layerId of requestedLayers) { const dependencies = DEPENDENCIES[layerId as keyof typeof DEPENDENCIES] || []; const missingDeps = dependencies.filter( (dep) => !correctedLayers.includes(dep), ); if (missingDeps.length > 0) { // Auto-add missing dependencies correctedLayers.push(...missingDeps); autoAdded.push(...missingDeps); warnings.push( `Layer ${layerId} (${LAYER_INFO[layerId as keyof typeof LAYER_INFO]?.name}) requires ` + `${missingDeps.map((dep) => `${dep} (${LAYER_INFO[dep as keyof typeof LAYER_INFO]?.name})`).join(", ")}. ` + `Auto-added missing dependencies.`, ); } } // Remove duplicates and sort correctedLayers = [...new Set(correctedLayers)].sort((a, b) => a - b); return { correctedLayers, warnings, autoAdded, }; } export async function fixCommand(files: string[], options: FixOptions) { const spinner = ora("Initializing modernization fixes...").start(); const startTime = Date.now(); try { // Enhanced input validation if (!Array.isArray(files)) { spinner.fail("Invalid input: files must be an array"); console.log(chalk.red("ERROR: Invalid files parameter")); return; } if ( files.some( (file) => !file || typeof file !== "string" || file.trim().length === 0, ) ) { spinner.fail("Invalid file paths provided"); console.log( chalk.red("ERROR: All file paths must be valid non-empty strings"), ); return; } // Validate options if (options && typeof options !== "object") { spinner.fail("Invalid options provided"); console.log(chalk.red("ERROR: Options must be an object")); return; } // 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; } // Premium feature - require authentication if (!config.apiKey) { spinner.fail("Premium feature requires authentication"); console.log(chalk.cyan("Code fixes are a premium feature!")); console.log(chalk.white("Free tier includes unlimited analysis and modernization reports.")); console.log(chalk.white("Premium includes:")); console.log(chalk.gray(" - One-click automated fixes & migrations")); console.log(chalk.gray(" - Safe rollback & backup protection")); console.log(chalk.gray(" - Detailed migration reports (PDF/CSV)")); console.log(chalk.gray(" - Team collaboration & shared dashboards")); console.log(chalk.cyan("\nRun 'neurolint login' to authenticate for premium features")); console.log(chalk.cyan("Visit https://neurolint.dev to get started")); return; } // Parse layers const requestedLayers = options.layers ? options.layers .split(",") .map((l) => parseInt(l.trim())) .filter((l) => l >= 1 && l <= 6) : config.layers.enabled; // Apply Layer Dependency Management (from IMPLEMENTATION_PATTERNS.md) const layerValidation = validateAndCorrectLayers(requestedLayers); const layers = layerValidation.correctedLayers; if (layerValidation.warnings.length > 0) { layerValidation.warnings.forEach((warning) => console.log(chalk.yellow(`DEPENDENCY: ${warning}`)), ); } // Check premium features for layers 5 and 6 const premiumLayers = layers.filter((layer) => layer >= 5); if (premiumLayers.length > 0) { try { const userResponse = await axios.get( `${config.api.url}/auth/api-keys`, { headers: { "X-API-Key": config.apiKey }, timeout: 10000, }, ); // Handle different response structures from API const plan = userResponse.data.plan || userResponse.data.user?.plan || userResponse.data.apiKey?.plan || "free"; if (plan === "free" && premiumLayers.length > 0) { spinner.fail("Premium features required"); console.log( chalk.yellow( `Layers ${premiumLayers.join(", ")} require Pro plan ($24.99/month)`, ), ); console.log(chalk.gray("Upgrade at: https://neurolint.dev/pricing")); return; } } catch (error) { console.log( chalk.yellow("Unable to verify premium features, continuing..."), ); } } // Determine files to fix let targetFiles: string[] = []; if (files.length > 0) { targetFiles = files; } else { // Use glob patterns from config spinner.text = "Discovering files..."; try { for (const pattern of config.files.include) { try { const foundFiles = await glob(pattern, { cwd: process.cwd(), ignore: config.files.exclude, }); targetFiles.push(...foundFiles); } catch (globError) { console.warn(`Warning: Could not process pattern ${pattern}`); } } } 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 to fix"); console.log( chalk.yellow( "Try specifying files explicitly or check your include/exclude patterns", ), ); return; } // Remove duplicates and filter existing files targetFiles = [...new Set(targetFiles)]; const existingFiles = []; for (const file of targetFiles) { if (await fs.pathExists(file)) { existingFiles.push(file); } } if (existingFiles.length === 0) { spinner.fail("No valid files found"); return; } // Create backup directory if needed let backupDir: string | undefined; if ( options.backup || (!options.dryRun && !process.env.NEUROLINT_NO_BACKUP) ) { backupDir = path.join( process.cwd(), ".neurolint-backups", new Date().toISOString().split("T")[0], ); if (!options.dryRun) { await fs.ensureDir(backupDir); } } if (options.dryRun) { spinner.text = `Previewing fixes for ${existingFiles.length} files...`; } else { spinner.text = `Applying fixes to ${existingFiles.length} files with layers [${layers.join(", ")}]...`; } // Process files one by one since the API expects single files const allResults: any[] = []; let totalFixed = 0; let filesModified = 0; for (const file of existingFiles) { try { const code = await fs.readFile(file, "utf-8"); // Create backup if needed if (backupDir && !options.dryRun) { const backupPath = path.join(backupDir, file); await fs.ensureDir(path.dirname(backupPath)); await fs.copy(file, backupPath); } // Send fix request for single file const fixPayload = { code, filename: file, layers: layers.length === 1 ? layers[0].toString() : layers.join(","), applyFixes: !options.dryRun, // Apply fixes unless it's a dry run metadata: { recursive: options.recursive, backup: !!backupDir, dryRun: options.dryRun, }, }; const response = await axios.post( `${config.api.url}/analyze`, fixPayload, { headers: { "X-API-Key": config.apiKey, "Content-Type": "application/json", }, timeout: config.api.timeout || 60000, maxContentLength: 50 * 1024 * 1024, // 50MB max response validateStatus: (status) => status < 500, // Don't throw on 4xx errors }, ); const result = response.data; // If fixes were applied and we have the fixed code, write it back if (!options.dryRun && result.fixedCode && result.fixedCode !== code) { await fs.writeFile(file, result.fixedCode, "utf-8"); filesModified++; } allResults.push({ file, result }); totalFixed += result.analysis?.detectedIssues?.filter((issue: any) => issue.fixed) ?.length || 0; } catch (fileError) { console.log(chalk.yellow(`Warning: Could not fix ${file}`)); if (axios.isAxiosError(fileError)) { if (fileError.response?.status === 401) { console.log( chalk.red( "Authentication failed. Please run 'neurolint login' again.", ), ); } else if (fileError.response?.status === 403) { console.log( chalk.red("Access denied. Check your API permissions."), ); } else { console.log( chalk.gray( `API Error: ${fileError.response?.status} ${fileError.response?.statusText}`, ), ); } } else { console.log( chalk.gray( `Error: ${fileError instanceof Error ? fileError.message : String(fileError)}`, ), ); } } } const processingTime = Date.now() - startTime; if (options.dryRun) { spinner.succeed( `Fix preview completed for ${existingFiles.length} files`, ); } else { spinner.succeed(`Fixes applied to ${existingFiles.length} files`); } // Aggregate results const aggregatedResult = { filesModified: options.dryRun ? 0 : filesModified, issuesFixed: totalFixed, layersUsed: layers, results: allResults, performance: { duration: processingTime, }, }; // Display results console.log(); if (options.dryRun) { console.log(chalk.white.bold("Fix Preview Results")); } else { console.log(chalk.white.bold("Fix Results")); } console.log(); console.log( chalk.white("Files processed: ") + chalk.cyan(existingFiles.length), ); if (!options.dryRun) { console.log( chalk.white("Files modified: ") + chalk.cyan(aggregatedResult.filesModified), ); } console.log( chalk.white("Issues fixed: ") + (aggregatedResult.issuesFixed > 0 ? chalk.green(aggregatedResult.issuesFixed) : chalk.yellow("0")), ); console.log( chalk.white("Layers used: ") + chalk.gray(`[${aggregatedResult.layersUsed.join(", ")}]`), ); console.log( chalk.white("Duration: ") + chalk.gray(`${aggregatedResult.performance.duration}ms`), ); if (backupDir && !options.dryRun) { console.log(chalk.white("Backups saved to: ") + chalk.gray(backupDir)); } console.log(); if (aggregatedResult.issuesFixed > 0) { // Show summary of fixes by layer const fixesByLayer: Record<number, any[]> = {}; allResults.forEach(({ file, result }) => { if (result.analysis?.detectedIssues) { result.analysis.detectedIssues.forEach((issue: any) => { if (issue.fixed) { const layer = issue.layer || 1; if (!fixesByLayer[layer]) { fixesByLayer[layer] = []; } fixesByLayer[layer].push({ ...issue, file }); } }); } }); console.log(chalk.white("Fixes by Layer:")); for (const layer of aggregatedResult.layersUsed) { const layerFixes = fixesByLayer[layer] || []; const layerName = config.layers.config[layer]?.name || `Layer ${layer}`; console.log( chalk.gray(` ${layerName}: `) + (layerFixes.length > 0 ? chalk.green(`${layerFixes.length} fixes applied`) : chalk.gray("no fixes")), ); } console.log(); if (options.dryRun) { console.log(chalk.white("Next steps:")); console.log( chalk.gray( " • Run 'neurolint fix' without --dry-run to apply fixes", ), ); console.log(chalk.gray(" • Add --backup to create backup files")); } else { console.log(chalk.green("Fixes applied successfully!")); console.log( chalk.gray(" • Run 'neurolint analyze' to verify the fixes"), ); } } else { if (options.dryRun) { console.log(chalk.gray("No fixes would be applied.")); } else { console.log(chalk.gray("No fixes were needed.")); } } } catch (error) { spinner.fail("Fix operation failed"); console.log( chalk.red( `Error: ${error instanceof Error ? error.message : String(error)}`, ), ); } }