@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
JavaScript
#!/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('"') || code.includes('&') || 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