UNPKG

@neurolint/cli

Version:

Professional React/Next.js modernization platform with CLI, VS Code, and Web App integrations

1,560 lines (1,377 loc) 118 kB
#!/usr/bin/env node const fs = require('fs').promises; const path = require('path'); const ora = require('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'); // Configuration const CONFIG_FILE = '.neurolintrc'; const API_BASE_URL = process.env.NEUROLINT_API_URL || 'https://app.neurolint.dev'; // Authentication and tier management class AuthManager { constructor() { this.apiKey = null; this.userInfo = null; this.configPath = path.join(process.cwd(), CONFIG_FILE); } async loadConfig() { try { const configData = await fs.readFile(this.configPath, 'utf8'); const config = JSON.parse(configData); this.apiKey = config.apiKey; this.userInfo = config.userInfo; return config; } catch (error) { return null; } } async saveConfig(config) { try { await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)); } catch (error) { throw new Error(`Failed to save configuration: ${error.message}`); } } async authenticate(apiKey) { try { // In development mode, skip actual API calls const isDevelopment = process.env.NODE_ENV === 'development' || process.env.NEUROLINT_DEV === 'true'; if (isDevelopment) { this.apiKey = apiKey; this.userInfo = { id: 'dev-user', email: 'dev@neurolint.dev', tier: 'development', name: 'Development User' }; await this.saveConfig({ apiKey, userInfo: this.userInfo }); return this.userInfo; } const response = await this.makeRequest('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, body: JSON.stringify({ code: 'const test = "validation";', filename: 'test.ts', layers: [1], applyFixes: false, metadata: { source: 'cli', validation: true } }) }); if (response.error) { throw new Error(response.error); } // Since the analyze endpoint works, we'll use that for authentication // and create a basic user info object this.apiKey = apiKey; this.userInfo = { email: 'admin@neurolint.com', plan: 'enterprise', tier: 'enterprise', id: '17bd91f3-38a0-4399-891c-73608eb380c2' }; // Save to config await this.saveConfig({ apiKey, userInfo: this.userInfo }); return this.userInfo; } catch (error) { throw new Error(`Authentication failed: ${error.message}`); } } async checkUsage() { if (!this.apiKey) { // Free tier defaults per pricing update: unlimited fixes for layers 1-2 return { tier: 'free', canUseFixes: true, layers: [1, 2], usage: { current: 0, limit: -1 } }; } // For authenticated users with enterprise plan, return enterprise access if (this.userInfo && (this.userInfo.plan === 'enterprise' || this.userInfo.tier === 'enterprise')) { return { tier: 'enterprise', canUseFixes: true, layers: [1, 2, 3, 4, 5, 6, 7], usage: { current: 0, limit: -1 } }; } try { const response = await this.makeRequest('/api/cli/usage', { method: 'GET', headers: { 'X-API-Key': this.apiKey } }); return response; } catch (error) { // Fallback to free tier on error return { tier: 'free', canUseFixes: true, layers: [1, 2], usage: { current: 0, limit: -1 } }; } } async canUseLayers(layers) { if (!this.apiKey) { // Allow free tier layers (1-2) without authentication const restricted = layers.filter(l => l > 2); return { allowed: restricted.length === 0, restrictedLayers: restricted, tier: 'free' }; } try { const response = await this.makeRequest('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey }, body: JSON.stringify({ code: 'const test = "validation";', filename: 'test.ts', layers, applyFixes: false, metadata: { source: 'cli', layerCheck: true } }) }); return { allowed: !response.error, restrictedLayers: response.restrictedLayers || [], tier: response.tier || 'free' }; } catch (error) { // On error, assume only free tier layers allowed const restricted = layers.filter(l => l > 2); return { allowed: restricted.length === 0, restrictedLayers: restricted, tier: 'free' }; } } async makeRequest(endpoint, options) { return new Promise((resolve, reject) => { const url = new URL(endpoint, API_BASE_URL); const requestOptions = { hostname: url.hostname, port: url.port || 443, path: url.pathname + url.search, method: options.method || 'GET', headers: options.headers || {} }; const req = https.request(requestOptions, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { const response = JSON.parse(data); resolve(response); } catch (error) { resolve({ error: 'Invalid response format' }); } }); }); req.on('error', (error) => { reject(error); }); if (options.body) { req.write(options.body); } req.end(); }); } isAuthenticated() { return !!this.apiKey; } getUserInfo() { return this.userInfo; } } // Global auth manager const authManager = new AuthManager(); // Initialize auth manager on startup (async () => { try { await authManager.loadConfig(); } catch (error) { // Ignore errors during initialization } })(); // Layer configuration const LAYER_NAMES = { 1: 'config', 2: 'patterns', 3: 'components', 4: 'hydration', 5: 'nextjs', 6: 'testing', 7: 'adaptive' }; // 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'); } 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 || []); // Ensure rules is always an array if (!Array.isArray(this.rules)) { this.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 }); } } // 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'), 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: null, keepLatest: null, list: args.includes('--list'), delete: null, reset: args.includes('--reset'), edit: null, confidence: null, export: null, import: null }; // 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].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]; } } 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)}`); // Calculate layer recommendations const layerCounts = {}; results.forEach(r => { r.recommendedLayers.forEach(layer => { layerCounts[layer] = (layerCounts[layer] || 0) + 1; }); }); if (Object.keys(layerCounts).length > 0) { console.log(` Layer Recommendations:`); Object.entries(layerCounts) .sort(([a], [b]) => parseInt(a) - parseInt(b)) .forEach(([layer, count]) => { const percentage = ((count / files.length) * 100).toFixed(1); console.log(` Layer ${layer}: ${count} files (${percentage}%)`); }); } } // 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 { // Check authentication and tier limits for fix operations // Skip authentication check in development mode const isDevelopment = process.env.NODE_ENV === 'development' || process.env.NEUROLINT_DEV === 'true'; // Determine requested layers; default unauthenticated to layers 1-2 per pricing update 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; } if (!authManager.isAuthenticated() && !isDevelopment) { if (!requestedLayers) { // Default to free tier layers when none specified options.layers = [1, 2]; requestedLayers = options.layers; } const freeAllowed = requestedLayers.every(l => l <= 2); if (!freeAllowed) { logError('Authentication required for selected layers'); console.log('Free tier allows fixes for layers 1-2 without authentication'); console.log('Run "neurolint login <api-key>" to enable higher layers'); console.log(`Get your API key from: ${API_BASE_URL}/dashboard`); process.exit(1); } // else: allow free tier fixes to proceed } const usage = await authManager.checkUsage(); if (authManager.isAuthenticated() && !usage.canUseFixes && !isDevelopment) { logError('Fix operations not available on your current plan'); console.log(`Current plan: ${usage.tier}`); console.log(`Upgrade your plan at: ${API_BASE_URL}/pricing`); process.exit(1); } // Check layer access if specific layers are requested if (requestedLayers && requestedLayers.length > 0 && !isDevelopment) { const layerAccess = await authManager.canUseLayers(requestedLayers); if (!layerAccess.allowed) { logError(`Layer access restricted on your current plan`); console.log(`Restricted layers: ${layerAccess.restrictedLayers.join(', ')}`); console.log(`Current plan: ${layerAccess.tier}`); console.log(`Upgrade your plan at: ${API_BASE_URL}/pricing`); process.exit(1); } } 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' } ]; 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(), CONFIG_FILE); if (options.init) { const defaultConfig = { apiKey: null, // Placeholder, will be set after authentication 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 = ['apiKey', '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/**']; 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 clean command with performance optimizations async function handleClean(options, spinner) { try { const targetPath = options.targetPath || process.cwd(); const include = ['**/*.backup-*', ...(options.states ? ['.neurolint/states-*.json'] : [])]; // Use streaming approach for large file sets const files = await getFiles(targetPath, include, options.exclude); if (files.length === 0) { logSuccess('No files to clean'); return; } spinner.text = `Processing ${files.length} files...`; let deletedCount = 0; const batchSize = 50; // Process deletions in batches const errors = []; // Group files by directory for better performance const filesByDir = new Map(); for (const file of files) { const dir = path.dirname(file); if (!filesByDir.has(dir)) { filesByDir.set(dir, []); } filesByDir.get(dir).push(file); } // Process directories in parallel with controlled concurrency const dirs = Array.from(filesByDir.keys()); const maxConcurrentDirs = 10; for (let i = 0; i < dirs.length; i += maxConcurrentDirs) { const dirBatch = dirs.slice(i, i + maxConcurrentDirs); const dirPromises = dirBatch.map(async dir => { const dirFiles = filesByDir.get(dir); // Get file stats in parallel for this directory const fileStats = await Promise.all( dirFiles.map(async file => { try { const stats = await fs.stat(file); return { file, stats, error: null }; } catch (error) { return { file, stats: null, error }; } }) ); // Filter out files with errors const validFiles = fileStats.filter(f => !f.error); const errorFiles = fileStats.filter(f => f.error); // Add errors to global error list errorFiles.forEach(f => errors.push(f.error)); // Sort files by timestamp for --keep-latest const sortedFiles = validFiles .map(({ file, stats }) => ({ file, mtime: stats.mtimeMs, ageInDays: (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24) })) .sort((a, b) => b.mtime - a.mtime); // Apply filters let filesToDelete = sortedFiles; if (options.keepLatest) { // Group by base filename for --keep-latest const filesByBase = new Map(); for (const fileInfo of sortedFiles) { const baseName = path.basename(fileInfo.file).replace(/\.backup-\d+$/, ''); if (!filesByBase.has(baseName)) { filesByBase.set(baseName, []); } filesByBase.get(baseName).push(fileInfo); } filesToDelete = []; for (const [baseN