UNPKG

@neurolint/cli

Version:

NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support

1,523 lines (1,353 loc) 196 kB
#!/usr/bin/env node /** * NeuroLint CLI - Main Entry Point * * Copyright (c) 2025 NeuroLint * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const fs = require('fs').promises; const path = require('path'); const ora = require('./simple-ora'); const { performance } = require('perf_hooks'); const https = require('https'); // Import shared core and existing modules const sharedCore = require('./shared-core'); const fixMaster = require('./fix-master.js'); const TransformationValidator = require('./validator.js'); const BackupManager = require('./backup-manager'); const { ProductionBackupManager } = require('./backup-manager-production'); const { CVE_2025_55182, isVulnerableReactVersion, getPatchedReactVersion, isVulnerableNextVersion, getPatchedNextVersion, formatPatchedVersionsList } = require('./shared-core/security-constants'); // Backup Manager Factory function createBackupManager(options = {}) { const useProduction = options.production || process.env.NEUROLINT_PRODUCTION === 'true' || process.env.NODE_ENV === 'production'; if (useProduction) { return new ProductionBackupManager({ backupDir: options.backupDir || '.neurolint-backups', maxBackups: options.maxBackups || 50, environment: process.env.NODE_ENV || 'production', loggerConfig: { enableConsole: options.verbose || false, enableFile: true, logLevel: options.verbose ? 'DEBUG' : 'INFO' }, ...options }); } else { return new BackupManager({ backupDir: options.backupDir || '.neurolint-backups', maxBackups: options.maxBackups || 10, ...options }); } } // Layer configuration const LAYER_NAMES = { 1: 'config', 2: 'patterns', 3: 'components', 4: 'hydration', 5: 'nextjs', 6: 'testing', 7: 'adaptive', 8: 'security-forensics' }; // Layer 8: Security Forensics (lazy loaded) let Layer8SecurityForensics = null; function getLayer8() { if (!Layer8SecurityForensics) { Layer8SecurityForensics = require('./scripts/fix-layer-8-security'); } return Layer8SecurityForensics; } // Smart Layer Selector for analyzing and recommending layers class SmartLayerSelector { static analyzeAndRecommend(code, filePath) { const issues = []; const ext = path.extname(filePath); try { // Use AST-based analysis for more accurate detection const ASTTransformer = require('./ast-transformer'); const transformer = new ASTTransformer(); const astIssues = transformer.analyzeCode(code, { filename: filePath }); // Convert AST issues to layer recommendations astIssues.forEach(issue => { issues.push({ layer: issue.layer, reason: issue.message, confidence: 0.9, location: issue.location }); }); } catch (error) { // Fallback to regex-based detection if AST parsing fails issues.push(...this.fallbackAnalysis(code, filePath)); } return { detectedIssues: issues, recommendedLayers: [...new Set(issues.map(i => i.layer))].sort(), reasons: issues.map(i => i.reason), confidence: issues.reduce((acc, i) => acc + i.confidence, 0) / issues.length || 0 }; } static fallbackAnalysis(code, filePath) { const issues = []; const ext = path.extname(filePath); // Layer 1: Configuration files if (filePath.endsWith('tsconfig.json') || filePath.endsWith('next.config.js') || filePath.endsWith('package.json')) { issues.push({ layer: 1, reason: 'Configuration file detected', confidence: 0.9 }); } // Layer 2: Pattern issues if (code.includes('&quot;') || code.includes('&amp;') || code.includes('console.log(')) { issues.push({ layer: 2, reason: 'Common pattern issues detected', confidence: 0.8 }); } // Layer 3: Component issues if ((ext === '.tsx' || ext === '.jsx') && code.includes('function') && code.includes('return (')) { if (code.includes('.map(') && !code.includes('key={')) { issues.push({ layer: 3, reason: 'React component issues detected (missing keys)', confidence: 0.9 }); } if (code.includes('<button') && !code.includes('aria-label')) { issues.push({ layer: 3, reason: 'React component issues detected (missing aria labels)', confidence: 0.9 }); } if (code.includes('<Button') && !code.includes('variant=')) { issues.push({ layer: 3, reason: 'React component issues detected (missing Button variant)', confidence: 0.8 }); } if (code.includes('<Input') && !code.includes('type=')) { issues.push({ layer: 3, reason: 'React component issues detected (missing Input type)', confidence: 0.8 }); } if (code.includes('<img') && !code.includes('alt=')) { issues.push({ layer: 3, reason: 'React component issues detected (missing image alt)', confidence: 0.9 }); } } // Layer 4: Hydration issues if ((code.includes('localStorage') || code.includes('window.') || code.includes('document.')) && !code.includes('typeof window')) { issues.push({ layer: 4, reason: 'Hydration safety issues detected', confidence: 0.9 }); } // Layer 5: Next.js issues if ((ext === '.tsx' || ext === '.jsx') && (code.includes('useState') || code.includes('useEffect')) && !code.match(/^['"]use client['"];/)) { issues.push({ layer: 5, reason: 'Next.js client component issues detected', confidence: 0.9 }); } // Layer 6: Testing issues if ((ext === '.tsx' || ext === '.jsx') && code.includes('export') && !code.includes('test(')) { issues.push({ layer: 6, reason: 'Missing test coverage', confidence: 0.7 }); } // Layer 7: Adaptive Pattern Learning if ((ext === '.tsx' || ext === '.jsx') && (code.includes('useState') || code.includes('useEffect') || code.includes('function'))) { issues.push({ layer: 7, reason: 'Potential for adaptive pattern learning', confidence: 0.6 }); } return issues; } } // Rule Store for Layer 7 adaptive learning class RuleStore { constructor() { this._rules = []; this.ruleFile = path.join(process.cwd(), '.neurolint', 'learned-rules.json'); } get rules() { return this._rules || []; } set rules(value) { this._rules = Array.isArray(value) ? value : []; } async load() { try { const data = await fs.readFile(this.ruleFile, 'utf8'); const parsed = JSON.parse(data); // Handle both array format and object with rules property this.rules = Array.isArray(parsed) ? parsed : (parsed.rules || []); } catch (error) { this.rules = []; } } async save() { const ruleDir = path.dirname(this.ruleFile); await fs.mkdir(ruleDir, { recursive: true }); await fs.writeFile(this.ruleFile, JSON.stringify(this.rules, null, 2)); } addRule(pattern, transformation) { this.rules.push({ pattern, transformation, timestamp: new Date().toISOString(), usageCount: 1 }); } } // Community CTA helper - displays GitHub, docs, and support links const NEUROLINT_GITHUB = 'https://github.com/Alcatecablee/Neurolint'; const NEUROLINT_DOCS = 'https://github.com/Alcatecablee/Neurolint/blob/main/CLI_USAGE.md'; const NEUROLINT_ISSUES = 'https://github.com/Alcatecablee/Neurolint/issues'; function printCommunityCTA(context = 'default') { const isQuiet = process.argv.includes('--quiet') || process.argv.includes('-q'); if (isQuiet) return; const separator = '\x1b[2m' + '─'.repeat(60) + '\x1b[0m'; if (context === 'help') { console.log(` ${separator} \x1b[1m\x1b[36mJoin the NeuroLint Community\x1b[0m [STAR] Star us on GitHub: ${NEUROLINT_GITHUB} [DOCS] Documentation: ${NEUROLINT_DOCS} [HELP] Report issues: ${NEUROLINT_ISSUES} ${separator}`); } else if (context === 'version') { console.log(`\x1b[2m-> Star us: ${NEUROLINT_GITHUB}\x1b[0m`); } else if (context === 'success') { console.log(`\n\x1b[2mLove NeuroLint? Star us on GitHub: ${NEUROLINT_GITHUB}\x1b[0m`); } else if (context === 'first-run') { console.log(` \x1b[1m\x1b[32mWelcome to NeuroLint!\x1b[0m Get started: -> Run \x1b[1mneurolint --help\x1b[0m to see all commands -> Run \x1b[1mneurolint analyze .\x1b[0m to analyze your project Join our community: [STAR] Star us: ${NEUROLINT_GITHUB} [DOCS] Docs: ${NEUROLINT_DOCS} `); } } // File pattern matching utility with performance optimizations async function getFiles(dir, include = ['**/*.{ts,tsx,js,jsx,json}'], exclude = [ // Build and dependency directories '**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**', '**/.build/**', '**/out/**', '**/.out/**', // Coverage and test artifacts '**/coverage/**', '**/.nyc_output/**', '**/.jest/**', '**/test-results/**', // Version control '**/.git/**', '**/.svn/**', '**/.hg/**', // IDE and editor files '**/.vscode/**', '**/.idea/**', '**/.vs/**', '**/*.swp', '**/*.swo', '**/*~', '**/.#*', '**/#*#', // OS generated files '**/.DS_Store', '**/Thumbs.db', '**/desktop.ini', '**/*.tmp', '**/*.temp', // Log files '**/*.log', '**/logs/**', '**/.log/**', // Cache directories '**/.cache/**', '**/cache/**', '**/.parcel-cache/**', '**/.eslintcache', '**/.stylelintcache', // Neurolint specific exclusions '**/.neurolint/**', '**/states-*.json', '**/*.backup-*', '**/*.backup', // Package manager files '**/package-lock.json', '**/yarn.lock', '**/pnpm-lock.yaml', '**/.npm/**', '**/.yarn/**', // Environment and config files '**/.env*', '**/.env.local', '**/.env.development', '**/.env.test', '**/.env.production', // Documentation and assets '**/docs/**', '**/documentation/**', '**/assets/**', '**/public/**', '**/static/**', '**/images/**', '**/img/**', '**/icons/**', '**/fonts/**', '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.svg', '**/*.ico', '**/*.woff', '**/*.woff2', '**/*.ttf', '**/*.eot', '**/*.mp4', '**/*.webm', '**/*.mp3', '**/*.wav', '**/*.pdf', '**/*.zip', '**/*.tar.gz', '**/*.rar', // Generated files '**/*.min.js', '**/*.min.css', '**/*.bundle.js', '**/*.chunk.js', '**/vendor/**', // Backup and temporary files '**/*.bak', '**/*.backup', '**/*.old', '**/*.orig', '**/*.rej', '**/*.tmp', '**/*.temp', // Lock files and manifests '**/.lock-wscript', '**/npm-debug.log*', '**/yarn-debug.log*', '**/yarn-error.log*', '**/.pnp.*', // TypeScript declaration files (optional - uncomment if needed) // '**/*.d.ts', // Test files (optional - uncomment if you want to exclude tests) // '**/*.test.js', // '**/*.test.ts', // '**/*.test.tsx', // '**/*.spec.js', // '**/*.spec.ts', // '**/*.spec.tsx', // '**/__tests__/**', // '**/tests/**', // '**/test/**', // Storybook files (optional - uncomment if needed) // '**/*.stories.js', // '**/*.stories.ts', // '**/*.stories.tsx', // '**/.storybook/**', // Cypress files (optional - uncomment if needed) // '**/cypress/**', // '**/cypress.config.*', // Playwright files (optional - uncomment if needed) // '**/playwright.config.*', // '**/tests/**', // Docker files (optional - uncomment if needed) // '**/Dockerfile*', // '**/.dockerignore', // '**/docker-compose*', // CI/CD files (optional - uncomment if needed) // '**/.github/**', // '**/.gitlab-ci.yml', // '**/.travis.yml', // '**/.circleci/**', // '**/azure-pipelines.yml', // Database files (optional - uncomment if needed) // '**/*.sqlite', // '**/*.db', // '**/*.sql', // Configuration files (optional - uncomment if needed) // '**/.eslintrc*', // '**/.prettierrc*', // '**/.babelrc*', // '**/tsconfig.json', // '**/next.config.js', // '**/webpack.config.*', // '**/rollup.config.*', // '**/vite.config.*', // '**/jest.config.*', // '**/tailwind.config.*', // '**/postcss.config.*' ]) { const files = []; const maxConcurrent = 50; // Increased from 10 for better throughput const batchSize = 100; // Process files in batches let activeOperations = 0; // Pre-compile regex patterns for better performance const compiledPatterns = { include: include.map(pattern => ({ pattern, regex: globToRegex(pattern), hasBraces: pattern.includes('{') && pattern.includes('}') })), exclude: exclude.map(pattern => ({ pattern, regex: globToRegex(pattern) })) }; // Helper function to convert glob pattern to regex (optimized) function globToRegex(pattern) { // Cache compiled regex patterns if (!globToRegex.cache) { globToRegex.cache = new Map(); } if (globToRegex.cache.has(pattern)) { return globToRegex.cache.get(pattern); } let regexPattern = pattern .replace(/\*\*/g, '.*') // ** becomes .* .replace(/\*/g, '[^/]*') // * becomes [^/]* .replace(/\./g, '\\.') // . becomes \. .replace(/\-/g, '\\-'); // - becomes \- // Handle path separators for cross-platform compatibility regexPattern = regexPattern.replace(/\//g, '[\\\\/]'); // For patterns like **/*.backup-*, we need to handle the path structure properly if (pattern.startsWith('**/*')) { const suffix = pattern.substring(4); // Remove **/* prefix regexPattern = '.*' + suffix.replace(/\*/g, '[^/]*').replace(/\./g, '\\.').replace(/\-/g, '\\-').replace(/\//g, '[\\\\/]'); } // For patterns like **/.neurolint/states-*.json, handle the path structure if (pattern.startsWith('**/')) { const suffix = pattern.substring(3); // Remove **/ prefix regexPattern = '.*' + suffix.replace(/\*/g, '[^/]*').replace(/\./g, '\\.').replace(/\-/g, '\\-').replace(/\//g, '[\\\\/]'); } const regex = new RegExp(regexPattern + '$', 'i'); // Case insensitive globToRegex.cache.set(pattern, regex); return regex; } // Helper function to check if file matches pattern (optimized) function matchesPattern(filePath, patternInfo) { // Handle brace expansion in patterns like **/*.{ts,tsx,js,jsx,json} if (patternInfo.hasBraces) { const pattern = patternInfo.pattern; const braceStart = pattern.indexOf('{'); const braceEnd = pattern.indexOf('}'); const prefix = pattern.substring(0, braceStart); const suffix = pattern.substring(braceEnd + 1); const options = pattern.substring(braceStart + 1, braceEnd).split(','); return options.some(opt => { const expandedPattern = prefix + opt + suffix; const expandedRegex = globToRegex(expandedPattern); return expandedRegex.test(filePath); }); } // Use pre-compiled regex return patternInfo.regex.test(filePath); } // Check if target is a single file try { const stats = await fs.stat(dir); if (stats.isFile()) { // Check if file should be included const shouldInclude = compiledPatterns.include.some(patternInfo => matchesPattern(dir, patternInfo) ); if (shouldInclude) { return [dir]; } return []; } } catch (error) { // Not a file, continue with directory scanning } // Use worker threads for large directories const useWorkers = process.env.NODE_ENV !== 'test' && require('worker_threads').isMainThread; async function scanDirectory(currentDir) { try { // Limit concurrent operations with better queuing if (activeOperations >= maxConcurrent) { await new Promise(resolve => { const checkQueue = () => { if (activeOperations < maxConcurrent) { resolve(); } else { setImmediate(checkQueue); } }; checkQueue(); }); } activeOperations++; const entries = await fs.readdir(currentDir, { withFileTypes: true }); activeOperations--; // Process entries in batches for better memory management const batches = []; for (let i = 0; i < entries.length; i += batchSize) { batches.push(entries.slice(i, i + batchSize)); } for (const batch of batches) { const batchPromises = batch.map(async entry => { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { // Check if directory should be excluded const shouldExclude = compiledPatterns.exclude.some(patternInfo => { const pattern = patternInfo.pattern; // Handle common exclusion patterns more efficiently if (pattern === '**/node_modules/**' && entry.name === 'node_modules') { return true; } if (pattern === '**/dist/**' && entry.name === 'dist') { return true; } if (pattern === '**/.next/**' && entry.name === '.next') { return true; } if (pattern === '**/build/**' && entry.name === 'build') { return true; } if (pattern === '**/coverage/**' && entry.name === 'coverage') { return true; } if (pattern === '**/.git/**' && entry.name === '.git') { return true; } if (pattern === '**/.vscode/**' && entry.name === '.vscode') { return true; } if (pattern === '**/.idea/**' && entry.name === '.idea') { return true; } if (pattern === '**/.cache/**' && entry.name === '.cache') { return true; } // Fallback to regex matching return patternInfo.regex.test(fullPath); }); if (!shouldExclude) { await scanDirectory(fullPath); } } else if (entry.isFile()) { // Check if file should be excluded first const shouldExclude = compiledPatterns.exclude.some(patternInfo => { const pattern = patternInfo.pattern; // Handle common file exclusion patterns more efficiently if (pattern.includes('*.log') && entry.name.endsWith('.log')) { return true; } if (pattern.includes('*.tmp') && entry.name.endsWith('.tmp')) { return true; } if (pattern.includes('*.backup') && entry.name.includes('.backup')) { return true; } if (pattern.includes('*.min.js') && entry.name.endsWith('.min.js')) { return true; } if (pattern.includes('*.bundle.js') && entry.name.endsWith('.bundle.js')) { return true; } // Fallback to regex matching return patternInfo.regex.test(fullPath); }); if (!shouldExclude) { // Check if file should be included const shouldInclude = compiledPatterns.include.some(patternInfo => matchesPattern(fullPath, patternInfo) ); if (shouldInclude) { files.push(fullPath); } } } }); // Process batch with controlled concurrency await Promise.all(batchPromises); } } catch (error) { // Handle permission errors gracefully if (error.code === 'EACCES' || error.code === 'EPERM') { return; // Skip directories we can't access } throw error; } } await scanDirectory(dir); return files; } // Parse command line options function parseOptions(args) { const options = { dryRun: args.includes('--dry-run'), verbose: args.includes('--verbose'), production: args.includes('--production'), backup: !args.includes('--no-backup'), layers: args.includes('--layers') ? args[args.indexOf('--layers') + 1].split(',').map(Number) : null, allLayers: args.includes('--all-layers'), include: args.includes('--include') ? args[args.indexOf('--include') + 1].split(',') : ['**/*.{ts,tsx,js,jsx,json}'], exclude: args.includes('--exclude') ? args[args.indexOf('--exclude') + 1].split(',') : ['**/node_modules/**', '**/dist/**', '**/.next/**'], format: 'console', output: null, init: args.includes('--init'), show: args.includes('--show'), states: args.includes('--states'), olderThan: undefined, keepLatest: undefined, list: args.includes('--list'), delete: undefined, reset: args.includes('--reset'), edit: undefined, confidence: undefined, export: undefined, import: undefined, yes: args.includes('--yes'), target: undefined }; // Parse format and output from args for (let i = 0; i < args.length; i++) { if (args[i] === '--format' && i + 1 < args.length) { options.format = args[i + 1]; } else if (args[i] === '--output' && i + 1 < args.length) { options.output = args[i + 1]; } else if (args[i] === '--older-than' && i + 1 < args.length) { options.olderThan = parseInt(args[i + 1]); } else if (args[i] === '--keep-latest' && i + 1 < args.length) { options.keepLatest = parseInt(args[i + 1]); } else if (args[i] === '--delete' && i + 1 < args.length) { options.delete = args[i + 1]; } else if (args[i] === '--edit' && i + 1 < args.length) { options.edit = args[i + 1]; } else if (args[i] === '--confidence' && i + 1 < args.length) { options.confidence = parseFloat(args[i + 1]); } else if (args[i] === '--export' && i + 1 < args.length) { options.export = args[i + 1]; } else if (args[i] === '--import' && i + 1 < args.length) { options.import = args[i + 1]; } else if (args[i] === '--target' && i + 1 < args.length) { options.target = args[i + 1]; } else if (args[i].startsWith('--format=')) { options.format = args[i].split('=')[1]; } else if (args[i].startsWith('--output=')) { options.output = args[i].split('=')[1]; } else if (args[i].startsWith('--older-than=')) { options.olderThan = parseInt(args[i].split('=')[1]); } else if (args[i].startsWith('--keep-latest=')) { options.keepLatest = parseInt(args[i].split('=')[1]); } else if (args[i].startsWith('--delete=')) { options.delete = args[i].split('=')[1]; } else if (args[i].startsWith('--edit=')) { options.edit = args[i].split('=')[1]; } else if (args[i].startsWith('--confidence=')) { options.confidence = parseFloat(args[i].split('=')[1]); } else if (args[i].startsWith('--export=')) { options.export = args[i].split('=')[1]; } else if (args[i].startsWith('--import=')) { options.import = args[i].split('=')[1]; } else if (args[i].startsWith('--target=')) { options.target = args[i].split('=')[1]; } } return options; } // Enhanced output functions to replace emoji-based spinners function logSuccess(message) { console.log(`[SUCCESS] ${message}`); } function logError(message) { console.error(`[ERROR] ${message}`); } function logWarning(message) { console.warn(`[WARNING] ${message}`); } function logInfo(message) { console.log(`[INFO] ${message}`); } function logProgress(message) { process.stdout.write(`[PROCESSING] ${message}...`); } function logComplete(message) { process.stdout.write(`[COMPLETE] ${message}\n`); } // Handle analyze command async function handleAnalyze(targetPath, options, spinner) { try { // Initialize shared core await sharedCore.core.initialize({ platform: 'cli' }); const files = await getFiles(targetPath, options.include, options.exclude); let totalIssues = 0; const results = []; // Show progress for large file sets if (files.length > 10 && options.verbose) { process.stdout.write(`Processing ${files.length} files...\n`); } for (let i = 0; i < files.length; i++) { const file = files[i]; // Update progress for large operations if (files.length > 10 && i % Math.max(1, Math.floor(files.length / 10)) === 0) { spinner.text = `Analyzing files... ${Math.round((i / files.length) * 100)}%`; } try { const code = await fs.readFile(file, 'utf8'); // Use shared core for analysis instead of direct SmartLayerSelector const analysisResult = await sharedCore.analyze(code, { filename: file, platform: 'cli', layers: options.layers || [1, 2, 3, 4, 5, 6, 7], verbose: options.verbose }); totalIssues += analysisResult.issues.length; if (options.verbose) { console.log(`[ANALYZED] ${file}`); console.log(` Issues Found: ${analysisResult.issues.length}`); console.log(` Recommended Layers: ${analysisResult.summary?.recommendedLayers?.join(', ') || '1,2'}`); if (analysisResult.issues.length > 0) { console.log(` Issue Types:`); const issueTypes = {}; analysisResult.issues.forEach(issue => { const type = issue.type || 'Unknown'; issueTypes[type] = (issueTypes[type] || 0) + 1; }); Object.entries(issueTypes).forEach(([type, count]) => { console.log(` ${type}: ${count}`); }); } } results.push({ file, issues: analysisResult.issues, recommendedLayers: analysisResult.summary?.recommendedLayers || [1, 2], analysisResult }); } catch (error) { if (options.verbose) { process.stderr.write(`Warning: Could not analyze ${file}: ${error.message}\n`); } } } if (options.format === 'json' && options.output) { const analysisResult = { summary: { filesAnalyzed: files.length, issuesFound: totalIssues, recommendedLayers: [...new Set(results.flatMap(r => r.recommendedLayers))].sort() }, files: results, issues: results.flatMap(r => r.issues.map(issue => ({ ...issue, file: r.file }))), layers: results.flatMap(r => r.recommendedLayers).map(layerId => ({ layerId: parseInt(layerId), success: true, changeCount: results.filter(r => r.recommendedLayers.includes(layerId)).length, description: `Layer ${layerId} analysis` })), confidence: 0.8, qualityScore: Math.max(0, 100 - (totalIssues * 5)), readinessScore: Math.min(100, (results.length / Math.max(1, files.length)) * 100) }; await fs.writeFile(options.output, JSON.stringify(analysisResult, null, 2)); } else { // Enhanced analysis summary console.log(`\n[ANALYSIS SUMMARY]`); console.log(` Files Analyzed: ${files.length}`); console.log(` Total Issues Found: ${totalIssues}`); console.log(` Average Issues per File: ${(totalIssues / files.length).toFixed(1)}`); console.log(` Layer Recommendations:`); const layerStats = []; results.forEach(r => { r.recommendedLayers.forEach(layer => { const layerName = LAYER_NAMES[layer]; if (!layerStats.some(stat => stat.layer === layer)) { layerStats.push({ layer, count: 0, percentage: 0 }); } const index = layerStats.findIndex(stat => stat.layer === layer); layerStats[index].count++; }); }); layerStats.forEach(({ layer, count }) => { const percentage = ((count / files.length) * 100); console.log(` Layer ${layer}: ${count} files (${percentage.toFixed(1)}%)`); }); console.log(`[COMPLETE] Analysis completed`); } // Stop spinner and use enhanced completion message spinner.stop(); logComplete('Analysis completed'); } catch (error) { logError(`Analysis failed: ${error.message}`); throw error; } } // Handle fix command async function handleFix(targetPath, options, spinner, startTime) { try { // All layers are now free - no authentication checks needed // Determine requested layers; default to all layers if not specified let requestedLayers = null; if (options.allLayers) { requestedLayers = [1, 2, 3, 4, 5, 6, 7]; } else if (Array.isArray(options.layers) && options.layers.length > 0) { requestedLayers = options.layers; } const files = await getFiles(targetPath, options.include, options.exclude); let processedFiles = 0; let successfulFixes = 0; for (const file of files) { try { spinner.text = `Processing ${path.basename(file)}...`; const result = await fixFile(file, options, spinner); if (result.success) { successfulFixes++; } processedFiles++; } catch (error) { if (options.verbose) { process.stderr.write(`Warning: Could not process ${file}: ${error.message}\n`); } } } if (options.format === 'json' && options.output) { const fixResult = { success: successfulFixes > 0, processedFiles, successfulFixes, appliedFixes: successfulFixes, summary: { totalFiles: files.length, processedFiles, successfulFixes, failedFiles: files.length - processedFiles } }; await fs.writeFile(options.output, JSON.stringify(fixResult, null, 2)); } else { // Enhanced summary output console.log(`\n[FIX SUMMARY]`); console.log(` Files Processed: ${processedFiles}`); console.log(` Fixes Applied: ${successfulFixes}`); console.log(` Files Failed: ${files.length - processedFiles}`); console.log(` Success Rate: ${((processedFiles / files.length) * 100).toFixed(1)}%`); if (options.verbose && successfulFixes > 0 && startTime) { const executionTime = ((Date.now() - startTime) / 1000).toFixed(2); console.log(` Total Execution Time: ${executionTime}s`); } } // Stop spinner and use enhanced completion message spinner.stop(); logComplete('Fix operation completed'); } catch (error) { logError(`Fix failed: ${error.message}`); throw error; } } // Handle layers command async function handleLayers(options, spinner) { const layers = [ { id: 1, name: 'Configuration', description: 'Updates tsconfig.json, next.config.js, package.json' }, { id: 2, name: 'Patterns', description: 'Standardizes variables, removes console statements' }, { id: 3, name: 'Components', description: 'Adds keys, accessibility attributes, prop types' }, { id: 4, name: 'Hydration', description: 'Guards client-side APIs for SSR' }, { id: 5, name: 'Next.js', description: 'Optimizes App Router with directives' }, { id: 6, name: 'Testing', description: 'Adds error boundaries, prop types, loading states' }, { id: 7, name: 'Adaptive Pattern Learning', description: 'Learns and applies patterns from prior fixes' }, { id: 8, name: 'Security Forensics', description: 'Detects IoCs, supply chain attacks, and CVE-2025-55182 vulnerabilities' } ]; if (options.verbose) { layers.forEach(layer => process.stdout.write(`Layer ${layer.id}: ${layer.name} - ${layer.description}\n`)); } else { layers.forEach(layer => process.stdout.write(`Layer ${layer.id}: ${layer.name}\n`)); } } // Handle init-config command async function handleInitConfig(options, spinner) { try { const configPath = path.join(process.cwd(), '.neurolintrc'); // Default to --init if no flag is provided if (!options.init && !options.show) { options.init = true; } if (options.init) { const defaultConfig = { enabledLayers: [1, 2, 3, 4, 5, 6, 7], include: ['**/*.{ts,tsx,js,jsx,json}'], exclude: ['**/node_modules/**', '**/dist/**', '**/.next/**'], backup: true, verbose: false, dryRun: false, maxRetries: 3, batchSize: 50, maxConcurrent: 10 }; await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); logSuccess(`Created ${configPath}`); } else if (options.show) { try { const config = JSON.parse(await fs.readFile(configPath, 'utf8')); process.stdout.write(JSON.stringify(config, null, 2) + '\n'); logSuccess('Config displayed'); } catch (error) { logError('No configuration file found. Use --init to create one.'); process.exit(1); } } else { // Validate existing config try { const config = JSON.parse(await fs.readFile(configPath, 'utf8')); // Validate required fields const requiredFields = ['enabledLayers', 'include', 'exclude']; const missingFields = requiredFields.filter(field => !config[field]); if (missingFields.length > 0) { logWarning(`Missing required fields: ${missingFields.join(', ')}`); } // Validate layer configuration if (config.enabledLayers && !Array.isArray(config.enabledLayers)) { logWarning('Enabled layers must be an array'); } // Validate file patterns if (config.include && !Array.isArray(config.include)) { logWarning('Include patterns must be an array'); } if (config.exclude && !Array.isArray(config.exclude)) { logWarning('Exclude patterns must be an array'); } logSuccess('Configuration validated'); } catch (error) { logError('Invalid configuration file'); process.exit(1); } } } catch (error) { logError(`Init-config failed: ${error.message}`); throw error; } } // Handle validate command async function handleValidate(targetPath, options, spinner) { try { const files = await getFiles(targetPath, options.include, options.exclude); let validFiles = 0; let invalidFiles = 0; const results = []; for (const file of files) { try { const validation = await TransformationValidator.validateFile(file); if (validation.isValid) { validFiles++; if (options.verbose) { process.stdout.write(`[VALID] ${file}: Valid\n`); } } else { invalidFiles++; if (options.verbose) { process.stderr.write(`[INVALID] ${file}: Invalid - ${validation.error}\n`); } } results.push({ file, ...validation }); } catch (error) { invalidFiles++; if (options.verbose) { process.stderr.write(`[ERROR] ${file}: Error - ${error.message}\n`); } results.push({ file, isValid: false, error: error.message }); } } if (options.format === 'json' && options.output) { const validationResult = { summary: { filesValidated: files.length, validFiles, invalidFiles }, files: results }; await fs.writeFile(options.output, JSON.stringify(validationResult, null, 2)); } else { process.stdout.write(`Validated ${files.length} files, ${invalidFiles} invalid\n`); } // Stop spinner before outputting completion message spinner.stop(); process.stdout.write('completed\n'); } catch (error) { logError(`Validate failed: ${error.message}`); throw error; } } // Handle init-tests command async function handleInitTests(targetPath, options, spinner) { try { const files = await getFiles(targetPath, options.include, options.exclude); let generatedTests = 0; const results = []; for (const file of files) { try { const code = await fs.readFile(file, 'utf8'); const testCode = generateTestCode(code, file); if (!options.dryRun) { const testFilePath = file.replace(/\.[jt]sx?$/, '.test.$1'); await fs.writeFile(testFilePath, testCode); if (options.verbose) { process.stdout.write(`Generated ${testFilePath}\n`); } generatedTests++; } else { if (options.verbose) { process.stdout.write(`[Dry Run] Would generate ${file.replace(/\.[jt]sx?$/, '.test.$1')}\n`); process.stdout.write(testCode); } generatedTests++; } results.push({ file, testCode }); } catch (error) { if (options.verbose) { process.stderr.write(`Warning: Could not generate test for ${file}: ${error.message}\n`); } } } if (options.format === 'json' && options.output) { const testResult = { summary: { filesProcessed: files.length, testsGenerated: generatedTests }, files: results }; await fs.writeFile(options.output, JSON.stringify(testResult, null, 2)); } else { process.stdout.write(`Generated ${generatedTests} test files\n`); } // Stop spinner before outputting completion message spinner.stop(); process.stdout.write('completed\n'); } catch (error) { logError(`Init-tests failed: ${error.message}`); throw error; } } // Generate test code for components function generateTestCode(code, filePath) { const componentName = code.match(/export default function (\w+)/)?.[1] || path.basename(filePath, path.extname(filePath)); return ` import { render, screen } from '@testing-library/react'; import ${componentName} from '${filePath.replace(process.cwd(), '.')}'; describe('${componentName}', () => { it('renders without crashing', () => { render(<${componentName} />); expect(screen.getByText(/.+/)).toBeInTheDocument(); }); }); `.trim(); } // Handle stats command with performance metrics async function handleStats(options, spinner) { try { const targetPath = options.targetPath || process.cwd(); const include = options.include || ['**/*.{ts,tsx,js,jsx,json}']; const exclude = options.exclude || ['**/node_modules/**', '**/dist/**', '**/.next/**']; // Validate that the target path exists try { const pathStats = await fs.stat(targetPath); if (!pathStats.isDirectory() && !pathStats.isFile()) { throw new Error(`Path is neither a file nor directory: ${targetPath}`); } } catch (error) { if (error.code === 'ENOENT') { throw new Error(`Path does not exist: ${targetPath}`); } throw error; } spinner.text = 'Scanning files...'; // Start memory tracking MemoryManager.startTracking(); const startTime = performance.now(); const files = await getFiles(targetPath, include, exclude); const scanTime = performance.now() - startTime; if (files.length === 0) { logSuccess('No files found'); return; } spinner.text = `Analyzing ${files.length} files...`; // Use memory-managed processing for large file sets const analysisOptions = { batchSize: 200, maxConcurrent: 20, memoryThreshold: 800, // MB gcInterval: 5, verbose: options.verbose, suppressErrors: true, // Suppress verbose AST parsing errors maxErrors: 20, // Show only first 20 errors onProgress: (progress, memoryReport) => { spinner.text = `Analyzing ${files.length} files... ${progress.toFixed(1)}% (${memoryReport.current.heapUsed}MB RAM)`; } }; const analysisStartTime = performance.now(); // Process files with memory management const analysisResults = await processFilesWithMemoryManagement( files, async (filePath) => { try { const code = await fs.readFile(filePath, 'utf8'); const issues = await analyzeFile(code, filePath, options); return { file: filePath, issues: issues.length, success: true, error: null }; } catch (error) { return { file: filePath, issues: 0, success: false, error: error.message }; } }, analysisOptions ); const analysisTime = performance.now() - analysisStartTime; // Calculate statistics const successfulAnalyses = analysisResults.filter(r => r.success); const failedAnalyses = analysisResults.filter(r => !r.success); const totalIssues = successfulAnalyses.reduce((sum, r) => sum + r.issues, 0); // Get backup and state file counts const backupFiles = files.filter(f => f.includes('.backup-')); const stateFiles = files.filter(f => f.includes('.neurolint/states-')); // Get memory report const memoryReport = MemoryManager.getReport(); // Load rule store for learned rules count const ruleStore = new RuleStore(); await ruleStore.load(); const stats = { filesAnalyzed: files.length, filesSuccessful: successfulAnalyses.length, filesFailed: failedAnalyses.length, issuesFound: totalIssues, learnedRules: ruleStore.rules.length, stateFiles: stateFiles.length, backupFiles: backupFiles.length, performance: { scanTime: Math.round(scanTime), analysisTime: Math.round(analysisTime), totalTime: Math.round(scanTime + analysisTime), filesPerSecond: Math.round(files.length / ((scanTime + analysisTime) / 1000)), memoryUsage: memoryReport }, errors: failedAnalyses.map(f => f.error).slice(0, 10) // Limit error reporting }; if (options.format === 'json' && options.output) { await fs.writeFile(options.output, JSON.stringify(stats, null, 2)); } else { process.stdout.write(`Files: ${stats.filesAnalyzed} (${stats.filesSuccessful} successful, ${stats.filesFailed} failed)\n`); process.stdout.write(`Issues: ${stats.issuesFound}\n`); process.stdout.write(`States: ${stats.stateFiles}, Backups: ${stats.backupFiles}\n`); process.stdout.write(`Learned Rules: ${stats.learnedRules}\n`); process.stdout.write(`Performance: ${stats.performance.totalTime}ms (${stats.performance.filesPerSecond} files/sec)\n`); process.stdout.write(`Memory: ${stats.performance.memoryUsage.current.heapUsed}MB (peak: ${stats.performance.memoryUsage.peak}MB)\n`); if (stats.errors.length > 0) { process.stderr.write(`Errors: ${stats.errors.length} files failed analysis\n`); } } // Don't call spinner.succeed here - let the main command handler do it } catch (error) { logError(`Stats failed: ${error.message}`); throw error; } } // Handle React 19 migration command async function handleReact19Migration(targetPath, options, spinner) { try { logInfo('Starting React 19 migration...'); const files = await getFiles(targetPath, options.include, options.exclude); let processedFiles = 0; let successfulMigrations = 0; let noChangeCount = 0; let errorCount = 0; const migrationResults = []; if (files.length === 0) { logWarning('No files found for React 19 migration'); return; } spinner.text = `Migrating ${files.length} files to React 19...`; // Process files with React 19 specific layers (2, 3, and 5) const react19Layers = [2, 3, 5]; // Layer 2: Patterns, Layer 3: Components, Layer 5: DOM APIs for (const file of files) { try { spinner.text = `Migrating ${path.basename(file)} to React 19...`; const code = await fs.readFile(file, 'utf8'); // Skip files that don't need React 19 migration if (!needsReact19Migration(code)) { if (options.verbose) { logInfo(`Skipped ${path.basename(file)} - no React 19 migration needed`); } noChangeCount++; migrationResults.push({ file: path.relative(process.cwd(), file), success: false, error: "No React 19 migration needed" }); processedFiles++; continue; } // Apply React 19 migrations using fix-master with specific layers const result = await fixMaster.executeLayers(code, react19Layers, { dryRun: options.dryRun, verbose: options.verbose, filePath: file, noDeps: true, // Skip automatic dependency resolution for React 19 react19Only: true // Only apply React 19 transformations }); if (result.successfulLayers > 0) { successfulMigrations++; const react19Changes = (result.results || []).filter(r => r && r.changes && Array.isArray(r.changes) && r.changes.some(c => c.type && c.type.startsWith('react19')) ); migrationResults.push({ file: path.relative(process.cwd(), file), success: true, changes: react19Changes.length, details: react19Changes.map(r => r.changes).flat() }); if (options.verbose) { logSuccess(`Migrated ${path.basename(file)} to React 19 (${react19Changes.length} changes)`); react19Changes.forEach(change => { change.changes?.forEach(c => { if (c.type?.startsWith('react19')) { logInfo(` ${c.description}`); } }); }); } } else { noChangeCount++; migrationResults.push({ file: path.relative(process.cwd(), file), success: false, error: 'No React 19 changes applied' }); } processedFiles++; } catch (error) { errorCount++; migrationResults.push({ file: path.relative(process.cwd(), file), success: false, error: error.message }); if (options.verbose) { logError(`Failed to migrate ${path.basename(file)}: ${error.message}`); } } } // Generate migration report if (options.format === 'json' && options.output) { const migrationReport = { summary: { totalFiles: files.length, processedFiles, successfulMigrations, noChange: noChangeCount, errors: errorCount }, migrations: migrationResults, timestamp: new Date().toISOString(), reactVersion: '19.0.0' }; await fs.writeFile(options.output, JSON.stringify(migrationReport, null, 2)); logSuccess(`Migration report saved to ${options.output}`); } else { // Console output console.log(`\n[REACT 19 MIGRATION SUMMARY]`); console.log(` Total Files: ${files.length}`); console.log(` Processed Files: ${processedFiles}`); console.log(` Successful Migrations: ${successfulMigrations}`); console.log(` No Changes Needed: ${noChangeCount}`); console.log(` Errors: ${errorCount}`); console.log(` Success Rate: ${((successfulMigrations / processedFiles) * 100).toFixed(1)}%`); if (options.verbose && migrationResults.length > 0) { console.log(`\n[MIGRATION DETAILS]`); migrationResults.forEach(result => { if (result.success && result.changes > 0) { console.log(` ${result.file}: ${result.changes} React 19 changes`); result.details?.forEach(detail => { if (detail.type?.startsWith('react19')) { console.log(` - ${detail.description}`); } }); } else if (!result.success) { if (result.error === 'No React 19 changes applied') { console.log(` ${result.file}: No changes needed`); } else { console.log(` ${result.file}: Failed - ${result.error}`); } } }); } if (successfulMigrations > 0) { console.log(`\n[NEXT STEPS]`); console.log(` 1. Update package.json to use React 19: npm install react@19 react-dom@19`); console.log(` 2. Update @types/react and @types/react-dom if using TypeScript`); console.log(` 3. Test your application thoroughly`); console.log(` 4. Review warnings for manual migration tasks`); console.log(` 5. Consider running official React 19 codemods for additional fixes`); } } spinner.stop(); logComplete('React 19 migration completed'); } catch (error) { logError(`React 19 migration failed: ${error.message}`); throw error; } } /** * Check if