UNPKG

@neurolint/cli

Version:

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

332 lines (291 loc) 10 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 { formatResults } from "../utils/formatter"; import { validateFiles, validateLayerNumbers, validateOutputFormat, } from "../utils/validation"; import { withRetry } from "../utils/retry"; import { ProgressTracker, resumeOperation } from "../utils/progress"; interface AnalyzeOptions { layers?: string; output?: string; outputFile?: string; recursive?: boolean; include?: string; exclude?: string; config?: string; } export async function analyzeCommand(files: string[], options: AnalyzeOptions) { const spinner = ora("Initializing NeuroLint analysis...").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; } const outputValidation = validateOutputFormat(options.output || "table"); if (!outputValidation.valid) { spinner.fail("Invalid output format"); outputValidation.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: 1000, }, ); 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 analyze`); // Parse layers const layers = options.layers ?.split(",") .map((l) => parseInt(l.trim())) || [1, 2, 3, 4]; console.log(chalk.white(`\nAnalyzing with layers: ${layers.join(", ")}\n`)); // Check for resumable operation const resumeState = await resumeOperation("analyze"); let filesToProcess = uniqueFiles; let results: any[] = []; if (resumeState) { console.log( chalk.blue("Would you like to resume the previous analysis?"), ); filesToProcess = resumeState.files.remaining; // Note: In a real implementation, we'd also load previous results } // Initialize progress tracker const progress = new ProgressTracker("Analysis", filesToProcess); await progress.start(); // Process files in batches with retry logic const BATCH_SIZE = 5; // Smaller batches for better error isolation const MAX_CONCURRENT = 3; 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 API with retry logic const response = await axios.post( `${config.api?.url || "https://app.neurolint.dev/api"}/analyze`, { code: content, filePath: relativePath, layers, }, { headers: { "X-API-Key": config.apiKey, "Content-Type": "application/json", }, timeout: config.api?.timeout || 30000, }, ); return { file: relativePath, success: true, ...response.data, }; }, { maxAttempts: 3, delay: 1000, onRetry: (error, attempt) => { console.log( chalk.gray( `Retrying ${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, 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); // Format and display results console.log(chalk.white("\nAnalysis Complete\n")); formatResults(results, options.output || "table", options.outputFile); // Summary statistics const successful = results.filter((r) => r.success); const failed = results.filter((r) => !r.success); console.log(chalk.white(`\nSummary:`)); console.log(chalk.white(`Successfully analyzed: ${successful.length}`)); if (failed.length > 0) { console.log(chalk.white(`Failed: ${failed.length}`)); } // Show layer-specific stats if (successful.length > 0) { const layerStats: Record<string, { passed: number; total: number }> = {}; successful.forEach((result: any) => { if (result.layers) { result.layers.forEach((layer: any) => { if (!layerStats[layer.id]) { layerStats[layer.id] = { passed: 0, total: 0 }; } layerStats[layer.id].total++; if (layer.status === "success") { layerStats[layer.id].passed++; } }); } }); console.log(chalk.white("\nLayer Performance:")); Object.entries(layerStats).forEach(([layerId, stats]: [string, any]) => { const percentage = Math.round((stats.passed / stats.total) * 100); console.log( chalk.white( `Layer ${layerId}: ${stats.passed}/${stats.total} (${percentage}%)`, ), ); }); } } catch (error) { spinner.fail( `Analysis 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", ), ); } } 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++; } } }