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,471 lines (1,311 loc) 215 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'); const { execSync } = require('child_process'); // 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'), withOfficialCodemods: args.includes('--with-official-codemods'), skipOfficialCodemods: args.includes('--skip-official'), useRecipe: args.includes('--recipe'), codemodVersion: null, 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]; } else if (args[i] === '--codemod-version' && i + 1 < args.length) { options.codemodVersion = args[i + 1]; } else if (args[i].startsWith('--codemod-version=')) { options.codemodVersion = 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`); } // Official codemods configuration // React codemods use: npx codemod@latest react/19/<transform> --target <path> // Next.js codemods use: npx @next/codemod@latest <transform> <path> const OFFICIAL_CODEMODS = { react19: [ { name: 'replace-reactdom-render', transform: 'react/19/replace-reactdom-render', description: 'Converts ReactDOM.render() to createRoot().render()' }, { name: 'replace-string-ref', transform: 'react/19/replace-string-ref', description: 'Converts string refs to callback refs' }, { name: 'replace-act-import', transform: 'react/19/replace-act-import', description: 'Updates act import from react-dom/test-utils to react' }, { name: 'replace-use-form-state', transform: 'react/19/replace-use-form-state', description: 'Replaces useFormState with useActionState' } ], react19Recipe: [ { name: 'migration-recipe', transform: 'react/19/migration-recipe', description: 'Runs all React 19 codemods at once (recommended)' } ], nextjs15: [ { name: 'next-async-request-api', transform: 'next-async-request-api', description: 'Makes cookies(), headers(), params async' }, { name: 'next-request-geo-ip', transform: 'next-request-geo-ip', description: 'Migrates geo/ip to @vercel/functions' }, { name: 'app-dir-runtime-config-experimental-edge', transform: 'app-dir-runtime-config-experimental-edge', description: 'Changes experimental-edge to edge runtime' } ], nextjs16: [ { name: 'remove-experimental-ppr', transform: 'remove-experimental-ppr', description: 'Removes experimental_ppr Route Segment Config' }, { name: 'remove-unstable-prefix', transform: 'remove-unstable-prefix', description: 'Removes unstable_ prefix from stabilized APIs' }, { name: 'middleware-to-proxy', transform: 'middleware-to-proxy', description: 'Migrates middleware convention to proxy' } ] }; /** * Capture file hashes for change detection * @param {string} targetPath - Directory to scan * @returns {Promise<Map<string, string>>} Map of file paths to content hashes */ async function captureFileHashes(targetPath) { const crypto = require('crypto'); const fileHashes = new Map(); try { const files = await getFiles(targetPath, ['**/*.{ts,tsx,js,jsx}'], [ '**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**' ]); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); const hash = crypto.createHash('sha256').update(content).digest('hex'); fileHashes.set(file, hash); } catch (e) { // Skip unreadable files } } } catch (e) { // Return empty map on error } return fileHashes; } /** * Compare file hashes to detect changes * @param {Map<string, string>} before - Hashes before changes * @param {Map<string, string>} after - Hashes after changes * @returns {Object} Changed, added, and removed files */ function compareFileHashes(before, after) { const changed = []; const added = []; const removed = []; // Check for changed and removed files for (const [file, hash] of before) { if (!after.has(file)) { removed.push(file); } else if (after.get(file) !== hash) { changed.push(file); } } // Check for added files for (const file of after.keys()) { if (!before.has(file)) { added.push(file); } } return { changed, added, removed }; } /** * Run official framework codemods (Phase 1) * Now with backup support and file change tracking for auditability * @param {Object} config - Configuration object * @param {string} config.framework - 'react19', 'react19Recipe', 'nextjs15', or 'nextjs16' * @param {string} config.targetPath - Path to the project * @param {boolean} config.dryRun - Whether to skip execution (just report) * @param {boolean} config.verbose - Whether to show detailed output * @param {string} [config.codemodVersion] - Optional version to pin codemods (e.g., '1.0.0') * For React: pins 'codemod' package (e.g., codemod@1.0.0) * For Next.js: pins '@next/codemod' package (e.g., @next/codemod@15.0.0) * @param {boolean} [config.enableBackup=true] - Whether to create backups before codemods * @param {boolean} [config.trackChanges=true] - Whether to track file-level changes * @returns {Promise<Object>} Results summary with file manifest */ async function runOfficialCodemods({ framework, targetPath, dryRun, verbose, codemodVersion, enableBackup = true, trackChanges = true }) { const codemods = OFFICIAL_CODEMODS[framework]; if (!codemods || codemods.length === 0) { return { success: 0, skipped: 0, errors: 0, results: [], manifest: { changed: [], added: [], removed: [] } }; } const isReactFramework = framework.startsWith('react'); const frameworkLabel = isReactFramework ? 'React 19' : framework === 'nextjs15' ? 'Next.js 15' : 'Next.js 16'; // Determine package version suffix - only for the relevant package type const reactCodemodPkg = codemodVersion && isReactFramework ? `codemod@${codemodVersion}` : 'codemod@latest'; const nextCodemodPkg = codemodVersion && !isReactFramework ? `@next/codemod@${codemodVersion}` : '@next/codemod@latest'; console.log(`\n[Phase 1] Running official ${frameworkLabel} codemods...`); if (codemodVersion) { const pinnedPkg = isReactFramework ? `codemod@${codemodVersion}` : `@next/codemod@${codemodVersion}`; console.log(` [INFO] Using pinned version: ${pinnedPkg}`); } console.log(` [INFO] Target: ${targetPath}`); console.log(` [INFO] Codemods to run: ${codemods.length}`); if (dryRun) { console.log(' [INFO] Dry-run mode - skipping official codemods execution'); console.log(' [INFO] The following codemods would run:'); codemods.forEach((codemod, index) => { console.log(` ${index + 1}/${codemods.length} ${codemod.name}: ${codemod.description}`); }); return { success: 0, skipped: codemods.length, errors: 0, results: codemods.map(c => ({ name: c.name, status: 'skipped', reason: 'dry-run' })), version: codemodVersion || 'latest', manifest: { changed: [], added: [], removed: [] } }; } // Check if npx is available try { execSync('npx --version', { stdio: 'pipe' }); } catch (error) { console.log(' [WARN] npx not found. Skipping official codemods.'); console.log(' [INFO] Install Node.js 18+ for codemod support.'); console.log(' [INFO] You can run codemods manually later.'); return { success: 0, skipped: codemods.length, errors: 0, results: codemods.map(c => ({ name: c.name, status: 'skipped', reason: 'npx not found' })), version: codemodVersion || 'latest', manifest: { changed: [], added: [], removed: [] } }; } // Capture file state before codemods for change tracking let beforeHashes = new Map(); if (trackChanges) { console.log(' [INFO] Capturing file state for change tracking...'); beforeHashes = await captureFileHashes(targetPath); } // Create backup before running official codemods (for rollback safety) let backupInfo = null; if (enableBackup && !dryRun) { try { const backupManager = createBackupManager({ verbose }); console.log(' [INFO] Creating backup before official codemods...'); // Backup files that will be affected const filesToBackup = await getFiles(targetPath, ['**/*.{ts,tsx,js,jsx}'], [ '**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**' ]); const backupResults = []; for (const file of filesToBackup.slice(0, 100)) { // Limit to first 100 files for performance try { const result = await backupManager.createBackup(file, `phase1-${framework}`); if (result && result.backupPath) { backupResults.push({ file, backupPath: result.backupPath }); } } catch (e) { // Continue on backup errors for individual files } } backupInfo = { timestamp: new Date().toISOString(), framework, filesBackedUp: backupResults.length, backupPaths: backupResults.slice(0, 10).map(r => r.backupPath) // First 10 for reference }; if (verbose) { console.log(` [INFO] Backed up ${backupResults.length} files`); } } catch (error) { console.log(' [WARN] Backup creation failed, continuing without backup'); if (verbose) { console.log(` ${error.message}`); } } } const results = []; let successCount = 0; let skipCount = 0; let errorCount = 0; // Escape path for shell safety (handle spaces, quotes, and special chars) const escapedPath = `"${targetPath.replace(/["'\\`$]/g, '\\$&')}"`; for (let i = 0; i < codemods.length; i++) { const codemod = codemods[i]; const progress = `[${i + 1}/${codemods.length}]`; try { // Build command based on framework type let command; if (framework.startsWith('react')) { // React codemods use: npx codemod@<version> <transform> --target <path> command = `npx --yes ${reactCodemodPkg} ${codemod.transform} --target ${escapedPath}`; } else { // Next.js codemods use: npx @next/codemod@<version> <transform> <path> command = `npx --yes ${nextCodemodPkg} ${codemod.transform} ${escapedPath}`; } // Show progress with codemod name console.log(` ${progress} Running ${codemod.name}...`); if (verbose) { console.log(` Command: ${command}`); } const output = execSync(command, { cwd: process.cwd(), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 120000 // 2 minute timeout per codemod }); // Parse output to determine if changes were made const hasTransforms = output.includes('transform') || output.includes('changed') || output.includes('modified') || output.includes('file'); if (hasTransforms) { console.log(` ${progress} [OK] ${codemod.name} - completed`); if (verbose && output.trim()) { const lines = output.trim().split('\n').slice(0, 5); lines.forEach(line => console.log(` ${line}`)); if (output.trim().split('\n').length > 5) { console.log(' ...(truncated)'); } } results.push({ name: codemod.name, status: 'success', output: output.trim() }); successCount++; } else { console.log(` ${progress} [SKIP] ${codemod.name} - no changes needed`); results.push({ name: codemod.name, status: 'skipped', reason: 'no changes needed' }); skipCount++; } } catch (error) { // Check if it's a legitimate error vs just "no changes" const stderr = error.stderr || ''; const stdout = error.stdout || ''; if (stderr.includes('No files matched') || stderr.includes('no files') || stdout.includes('0 files')) { console.log(` ${progress} [SKIP] ${codemod.name} - no matching files`); results.push({ name: codemod.name, status: 'skipped', reason: 'no matching files' }); skipCount++; } else { console.log(` ${progress} [ERROR] ${codemod.name} - command failed`); if (verbose) { console.log(` ${error.message}`); if (stderr) console.log(` ${stderr.slice(0, 200)}`); } results.push({ name: codemod.name, status: 'error', error: error.message }); errorCount++; } } } // Capture file state after codemods for change detection let manifest = { changed: [], added: [], removed: [] }; if (trackChanges) { console.log(' [INFO] Detecting file changes...'); const afterHashes = await captureFileHashes(targetPath); manifest = compareFileHashes(beforeHashes, afterHashes); if (manifest.changed.length > 0 || manifest.added.length > 0) { console.log(` [INFO] Files modified: ${manifest.changed.length}, added: ${manifest.added.length}`); if (verbose && manifest.changed.length > 0) { console.log(' [MANIFEST] Changed files:'); manifest.changed.slice(0, 10).forEach(f => console.log(` - ${f}`)); if (manifest.changed.length > 10) { console.log(` ... and ${manifest.changed.length - 10} more`); } } } } // Summary console.log(` [DONE] Phase 1 complete: ${successCount} success, ${skipCount} skipped, ${errorCount} errors`); if (errorCount > 0) { console.log(' [NOTE] Codemod errors are non-blocking. NeuroLint will continue.'); } if (backupInfo) { console.log(` [INFO] Rollback available: ${backupInfo.filesBackedUp} files backed up`); } return { success: successCount, skipped: skipCount, errors: errorCount, results, version: codemodVersion || 'latest', manifest, backup: backupInfo }; } // 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: re