UNPKG

@neurolint/cli

Version:

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

402 lines (352 loc) 12.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 inquirer from "inquirer"; import { loadConfig, validateConfig } from "../utils/config"; import { createBackup } from "../utils/backup"; import { validateFiles, validateLayerNumbers } from "../utils/validation"; import { withRetry } from "../utils/retry"; import { ProgressTracker, resumeOperation } from "../utils/progress"; interface FixOptions { layers?: string; recursive?: boolean; dryRun?: boolean; backup?: boolean; include?: string; exclude?: string; config?: string; output?: string; outputFile?: string; } export async function fixCommand(files: string[], options: FixOptions) { const spinner = ora("Initializing NeuroLint fixes...").start(); 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.white(`ERROR: ${error}`)), ); return; } // Check authentication if (!config.apiKey) { spinner.fail("Authentication required"); console.log(chalk.white('Run "neurolint login" to authenticate first')); return; } // Validate input parameters const layersValidation = validateLayerNumbers(options.layers || "1,2,3,4"); if (!layersValidation.valid) { spinner.fail("Invalid layer specification"); layersValidation.errors.forEach((error) => console.log(chalk.white(`ERROR: ${error}`)), ); return; } // Resolve file patterns const filePatterns = files.length > 0 ? files : config.files?.include || ["**/*.{ts,tsx,js,jsx}"]; const includePatterns = options.include?.split(",") || []; const excludePatterns = options.exclude?.split(",") || config.files?.exclude || ["node_modules/**", "dist/**", "build/**"]; spinner.text = "Discovering and validating files..."; // Validate files with comprehensive checks const fileValidation = await validateFiles( [...filePatterns, ...includePatterns], { maxFileSize: 10 * 1024 * 1024, // 10MB allowedExtensions: [".ts", ".tsx", ".js", ".jsx"], maxFiles: 500, // Lower limit for fixes due to backup overhead }, ); if (!fileValidation.valid) { spinner.fail("File validation failed"); fileValidation.errors.forEach((error) => console.log(chalk.white(`ERROR: ${error}`)), ); return; } if (fileValidation.warnings.length > 0) { spinner.warn("File validation warnings"); fileValidation.warnings.forEach((warning) => console.log(chalk.gray(`WARNING: ${warning}`)), ); } const uniqueFiles = fileValidation.files; if (uniqueFiles.length === 0) { spinner.fail("No valid files found matching the specified patterns"); return; } spinner.succeed(`Found ${uniqueFiles.length} valid files to fix`); // Parse layers const layers = options.layers ?.split(",") .map((l) => parseInt(l.trim())) || [1, 2, 3, 4]; if (options.dryRun) { console.log(chalk.white("\nDRY RUN MODE - No files will be modified\n")); } console.log(chalk.white(`\nFixing with layers: ${layers.join(", ")}\n`)); // Check for resumable operation const resumeState = await resumeOperation("fix"); let filesToProcess = uniqueFiles; if (resumeState && !options.dryRun) { const { resume } = await inquirer.prompt([ { type: "confirm", name: "resume", message: "Resume previous fix operation?", default: true, }, ]); if (resume) { filesToProcess = resumeState.files.remaining; console.log( chalk.white( `\nResuming fix operation with ${filesToProcess.length} remaining files\n`, ), ); } } // Initialize progress tracker const progress = new ProgressTracker("Fix", filesToProcess); await progress.start(); const results: any[] = []; const fixedFiles: string[] = []; // Process files with controlled concurrency and robust error handling const BATCH_SIZE = 3; // Even smaller for fixes due to file I/O const MAX_CONCURRENT = 2; for (let i = 0; i < filesToProcess.length; i += BATCH_SIZE) { const batch = filesToProcess.slice(i, i + BATCH_SIZE); // Process batch with controlled concurrency const semaphore = new Semaphore(MAX_CONCURRENT); const batchResults = await Promise.allSettled( batch.map(async (filePath) => { await semaphore.acquire(); try { const result = await withRetry( async () => { const content = await fs.readFile(filePath, "utf-8"); const relativePath = path.relative(process.cwd(), filePath); // Call NeuroLint transformation API with retry logic const response = await axios.post( `${config.api?.url || "https://app.neurolint.dev/api"}/transform`, { code: content, filePath: relativePath, layers, }, { headers: { "X-API-Key": config.apiKey, "Content-Type": "application/json", }, timeout: config.api?.timeout || 60000, }, ); const { transformed, layers: layerResults } = response.data; // Check if any changes were made const hasChanges = transformed !== content; if (hasChanges && !options.dryRun) { // Create backup if requested if (options.backup) { await createBackup(filePath, { maxBackups: 5 }); } // Write fixed content atomically const tempFile = `${filePath}.neurolint.tmp`; await fs.writeFile(tempFile, transformed, "utf-8"); await fs.move(tempFile, filePath); fixedFiles.push(relativePath); } return { file: relativePath, success: true, hasChanges, layers: layerResults, originalSize: content.length, transformedSize: transformed.length, }; }, { maxAttempts: 2, // Fewer retries for fixes due to potential file conflicts delay: 2000, onRetry: (error, attempt) => { console.log( chalk.gray( `Retrying fix for ${path.relative(process.cwd(), filePath)} (attempt ${attempt})`, ), ); }, }, ); await progress.markCompleted(filePath); return result; } catch (error) { await progress.markFailed( filePath, error instanceof Error ? error.message : "Unknown error", ); return { file: path.relative(process.cwd(), filePath), success: false, hasChanges: false, error: error instanceof Error ? error.message : "Unknown error", }; } finally { semaphore.release(); } }), ); // Collect results from settled promises batchResults.forEach((result) => { if (result.status === "fulfilled") { results.push(result.value); } }); } await progress.complete(true); // Display results console.log(chalk.white("\nFix Operation Complete\n")); const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); const withChanges = results.filter((r) => r.hasChanges); if (options.dryRun) { console.log(chalk.white("Dry Run Results:")); withChanges.forEach((result) => { console.log(`CHANGE ${result.file} - Would be modified`); }); } else { console.log(chalk.white("Fixed Files:")); fixedFiles.forEach((file) => { console.log(`PASS ${file}`); }); } // Summary statistics console.log(chalk.white("\nSummary:")); console.log(`Successfully processed: ${successful.length}`); console.log(`Files with changes: ${withChanges.length}`); if (failed.length > 0) { console.log(`Failed: ${failed.length}`); } // Show layer performance if (successful.length > 0) { const layerStats: Record<string, { applied: number; total: number }> = {}; successful.forEach((result: any) => { if (result.layers) { result.layers.forEach((layer: any) => { if (!layerStats[layer.id]) { layerStats[layer.id] = { applied: 0, total: 0 }; } layerStats[layer.id].total++; if (layer.status === "success" && layer.changes > 0) { layerStats[layer.id].applied++; } }); } }); console.log(chalk.white("\nLayer Applications:")); Object.entries(layerStats).forEach(([layerId, stats]: [string, any]) => { console.log( `Layer ${layerId}: ${stats.applied}/${stats.total} files modified`, ); }); } // Offer to run analysis after fixes if (!options.dryRun && fixedFiles.length > 0) { const { runAnalysis } = await inquirer.prompt([ { type: "confirm", name: "runAnalysis", message: "Would you like to run analysis on the fixed files to verify improvements?", default: true, }, ]); if (runAnalysis) { console.log(chalk.white("\nRunning verification analysis...\n")); const { analyzeCommand } = await import("./analyze"); await analyzeCommand(fixedFiles, { layers: options.layers, output: "summary", }); } } } catch (error) { spinner.fail( `Fix operation failed: ${error instanceof Error ? error.message : "Unknown error"}`, ); if (error instanceof Error) { if (error.message.includes("ECONNREFUSED")) { console.log( chalk.white("\nMake sure the NeuroLint server is running:"), ); console.log( chalk.gray(" npm run dev (in the main project directory)"), ); } else if ( error.message.includes("401") || error.message.includes("403") ) { console.log( chalk.white( '\nAuthentication failed. Run "neurolint login" to re-authenticate', ), ); } else if (error.message.includes("429")) { console.log( chalk.white("\nRate limit exceeded. Please wait before trying again"), ); } else if ( error.message.includes("EMFILE") || error.message.includes("ENFILE") ) { console.log( chalk.white( "\nToo many open files. Try reducing batch size or increasing system limits", ), ); } else if (error.message.includes("ENOSPC")) { console.log( chalk.white( "\nNo space left on device. Free up disk space and try again", ), ); } else if (error.message.includes("EACCES")) { console.log(chalk.white("\nPermission denied. Check file permissions")); } } process.exit(1); } } // Semaphore for controlling concurrency class Semaphore { private permits: number; private waiting: (() => void)[] = []; constructor(permits: number) { this.permits = permits; } async acquire(): Promise<void> { if (this.permits > 0) { this.permits--; return; } return new Promise((resolve) => { this.waiting.push(resolve); }); } release(): void { if (this.waiting.length > 0) { const resolve = this.waiting.shift()!; resolve(); } else { this.permits++; } } }