UNPKG

@neurolint/cli

Version:

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

577 lines (503 loc) 17.3 kB
const chalk = require('chalk'); const fs = require('fs-extra'); const path = require('path'); /** * Output formatter for NeuroLint CLI * Supports console, JSON, and HTML output formats */ /** * Format analysis results for console output */ function formatConsoleOutput(results, options = {}) { if (results.length === 0) { return chalk.green('SUCCESS: No issues detected in analyzed files\n'); } const totalIssues = results.reduce((sum, r) => sum + (r.detectedIssues?.length || 0), 0); let output = chalk.yellow(`\nAnalysis Summary:\n`); output += ` Files analyzed: ${results.length}\n`; output += ` Issues found: ${totalIssues}\n`; // Group issues by layer const issuesByLayer = {}; results.forEach(result => { if (result.detectedIssues) { result.detectedIssues.forEach(issue => { const layer = issue.layer || 'Unknown'; if (!issuesByLayer[layer]) issuesByLayer[layer] = []; issuesByLayer[layer].push(issue); }); } }); Object.entries(issuesByLayer).forEach(([layer, issues]) => { output += `\nLayer ${layer} Issues (${issues.length}):\n`; issues.slice(0, 3).forEach(issue => { const severity = issue.severity || 'info'; const icon = severity === 'error' ? '[ERROR]' : severity === 'warning' ? '[WARN]' : '[INFO]'; output += ` ${icon} ${issue.description || issue.type}\n`; }); if (issues.length > 3) { output += ` ... and ${issues.length - 3} more\n`; } }); output += chalk.cyan('\nNext steps:\n'); output += ' - Run "neurolint fix" to apply fixes (requires Premium)\n'; output += ' - Run "neurolint layers" to see layer information\n'; output += ' - Get premium access at: https://neurolint.dev/pricing\n'; return output; } /** * Format analysis results for JSON output */ function formatJSONOutput(results, options = {}) { const summary = { timestamp: new Date().toISOString(), totalFiles: results.length, totalIssues: results.reduce((sum, r) => sum + (r.detectedIssues?.length || 0), 0), layers: getLayerSummary(results), files: results.map(result => ({ filePath: result.filePath, issues: result.detectedIssues || [], metrics: result.metrics || {}, recommendations: result.recommendations || [] })) }; return JSON.stringify(summary, null, 2); } /** * Generate HTML report */ async function generateHTMLReport(results, outputPath) { const summary = { totalFiles: results.length, totalIssues: results.reduce((sum, r) => sum + (r.detectedIssues?.length || 0), 0), timestamp: new Date().toISOString(), layers: getLayerSummary(results) }; const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>NeuroLint Analysis Report</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; background: #0a0a0a; color: white; } .header { background: rgba(255,255,255,0.05); padding: 20px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.15); margin-bottom: 20px; } .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px; } .metric { background: rgba(255,255,255,0.05); padding: 15px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); } .metric-value { font-size: 24px; font-weight: bold; color: #2196f3; } .layer-section { background: rgba(255,255,255,0.03); padding: 20px; margin-bottom: 20px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); } .issue { background: rgba(255,255,255,0.05); padding: 10px; margin: 10px 0; border-radius: 6px; border-left: 4px solid; } .error { border-left-color: #e53e3e; } .warning { border-left-color: #ff9800; } .info { border-left-color: #2196f3; } .file-path { color: #888; font-size: 12px; } h1, h2, h3 { color: #2196f3; } </style> </head> <body> <div class="header"> <h1>NeuroLint Analysis Report</h1> <p>Generated on ${new Date(summary.timestamp).toLocaleString()}</p> </div> <div class="summary"> <div class="metric"> <div class="metric-value">${summary.totalFiles}</div> <div>Files Analyzed</div> </div> <div class="metric"> <div class="metric-value">${summary.totalIssues}</div> <div>Issues Found</div> </div> <div class="metric"> <div class="metric-value">${Object.keys(summary.layers).length}</div> <div>Layers Analyzed</div> </div> </div> ${Object.entries(summary.layers).map(([layer, data]) => ` <div class="layer-section"> <h2>Layer ${layer}: ${getLayerName(layer)}</h2> <p>Issues: ${data.issues} | Files: ${data.files}</p> ${data.topIssues.map(issue => ` <div class="issue ${issue.severity || 'info'}"> <strong>${issue.description || issue.type}</strong> <div class="file-path">${issue.filePath}</div> </div> `).join('')} </div> `).join('')} <div class="layer-section"> <h2>Recommendations</h2> <p>Based on the analysis, consider the following actions:</p> <ul> <li>Run <code>neurolint fix</code> to apply automated fixes</li> <li>Focus on Layer ${getMostCriticalLayer(summary.layers)} issues first</li> <li>Upgrade to NeuroLint Professional for advanced fixes</li> </ul> </div> </body> </html>`; await fs.writeFile(outputPath, html); return outputPath; } /** * Create enhanced progress tracker with detailed reporting */ function createProgressTracker(total, options = {}) { const startTime = Date.now(); let current = 0; let errors = 0; let warnings = 0; let fixes = 0; const width = options.width || 40; const showDetails = options.verbose || false; const showETA = options.showETA !== false; // Store recent files for display const recentFiles = []; const maxRecentFiles = 3; function formatTime(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } function estimateTimeRemaining() { if (current === 0) return 'calculating...'; const elapsed = Date.now() - startTime; const avgTimePerFile = elapsed / current; const remaining = (total - current) * avgTimePerFile; return formatTime(remaining); } function getProgressBar() { const percent = Math.round((current / total) * 100); const filled = Math.round((current / total) * width); const bar = '█'.repeat(filled) + '░'.repeat(width - filled); return { bar, percent }; } function updateDisplay() { const { bar, percent } = getProgressBar(); const eta = showETA ? ` ETA: ${estimateTimeRemaining()}` : ''; const stats = showDetails ? ` | ${chalk.red(errors + ' errors')} ${chalk.yellow(warnings + ' warnings')} ${chalk.green(fixes + ' fixes')}` : ''; // Clear the line and write progress process.stdout.write('\r\x1b[K'); // Clear line process.stdout.write(`${chalk.blue(bar)} ${chalk.cyan(percent + '%')} (${current}/${total})${eta}${stats}`); if (showDetails && recentFiles.length > 0) { process.stdout.write('\n'); recentFiles.forEach((file, index) => { const prefix = index === recentFiles.length - 1 ? '→' : ' '; process.stdout.write(` ${chalk.gray(prefix)} ${chalk.white(path.basename(file.name))}${file.status ? ` ${file.status}` : ''}\n`); }); process.stdout.write(`\x1b[${recentFiles.length + 1}A`); // Move cursor up } } return { update(filePath, result = {}) { current++; // Update statistics if (result.errors) errors += result.errors; if (result.warnings) warnings += result.warnings; if (result.fixes) fixes += result.fixes; // Update recent files const fileStatus = result.errors > 0 ? chalk.red('[ERROR]') : result.warnings > 0 ? chalk.yellow('[WARN]') : result.fixes > 0 ? chalk.green('[FIXED]') : chalk.gray('[OK]'); recentFiles.push({ name: filePath, status: fileStatus }); if (recentFiles.length > maxRecentFiles) { recentFiles.shift(); } updateDisplay(); if (current === total) { process.stdout.write('\n'); if (showDetails) { process.stdout.write('\n'); } } }, updateStatus(message) { if (showDetails) { process.stdout.write(`\r\x1b[K${chalk.blue('ℹ')} ${message}\n`); updateDisplay(); } }, error(filePath, error) { errors++; if (showDetails) { process.stdout.write(`\r\x1b[K${chalk.red('[ERROR]')} ${path.basename(filePath)}: ${error}\n`); updateDisplay(); } }, complete() { const elapsed = Date.now() - startTime; const totalTime = formatTime(elapsed); if (current < total) { process.stdout.write('\n'); } // Final summary console.log(chalk.green(`\nProcessing complete in ${totalTime}`)); if (showDetails || errors > 0 || warnings > 0) { console.log(chalk.gray(` Files processed: ${current}/${total}`)); if (errors > 0) console.log(chalk.red(` Errors: ${errors}`)); if (warnings > 0) console.log(chalk.yellow(` Warnings: ${warnings}`)); if (fixes > 0) console.log(chalk.green(` Fixes applied: ${fixes}`)); } }, getStats() { return { current, total, errors, warnings, fixes, elapsed: Date.now() - startTime, eta: current > 0 ? ((Date.now() - startTime) / current) * (total - current) : 0 }; } }; } /** * Enhanced progress indicator for layer processing */ function createLayerProgressTracker(layers, options = {}) { const startTime = Date.now(); let currentLayer = 0; const layerNames = { 1: 'Configuration', 2: 'Content Cleanup', 3: 'Component Intelligence', 4: 'Hydration Safety', 5: 'App Router Optimization', 6: 'Quality Enforcement' }; return { startLayer(layerId, fileCount = 1) { currentLayer++; const layerName = layerNames[layerId] || `Layer ${layerId}`; const progress = `[${currentLayer}/${layers.length}]`; console.log(`\n${chalk.blue(progress)} ${chalk.white.bold(layerName)}`); console.log(chalk.gray(`Processing ${fileCount} file${fileCount !== 1 ? 's' : ''}...`)); return createProgressTracker(fileCount, { ...options, showDetails: false }); }, completeLayer(layerId, result) { const layerName = layerNames[layerId] || `Layer ${layerId}`; const time = result.executionTime ? ` in ${Math.round(result.executionTime)}ms` : ''; const changes = result.changeCount > 0 ? chalk.green(` (+${result.changeCount} changes)`) : ''; console.log(`${chalk.green('[COMPLETE]')} ${layerName}${time}${changes}`); if (result.improvements && result.improvements.length > 0) { result.improvements.forEach(improvement => { console.log(` ${chalk.gray('•')} ${improvement}`); }); } }, complete() { const elapsed = Date.now() - startTime; console.log(`\n${chalk.green('All layers completed')} in ${Math.round(elapsed)}ms`); } }; } /** * Real-time statistics display */ function createStatsDisplay(options = {}) { const stats = { filesProcessed: 0, issuesFound: 0, fixesApplied: 0, errorsEncountered: 0, startTime: Date.now() }; let intervalId = null; function updateDisplay() { const elapsed = Math.round((Date.now() - stats.startTime) / 1000); const rate = stats.filesProcessed > 0 ? (stats.filesProcessed / elapsed).toFixed(1) : '0'; if (options.realTime) { process.stdout.write('\r\x1b[K'); // Clear line process.stdout.write( `${chalk.cyan('Stats:')} ` + `${chalk.white(stats.filesProcessed)} files | ` + `${chalk.yellow(stats.issuesFound)} issues | ` + `${chalk.green(stats.fixesApplied)} fixes | ` + `${chalk.red(stats.errorsEncountered)} errors | ` + `${chalk.gray(rate + ' files/s')}` ); } } return { start() { if (options.realTime) { intervalId = setInterval(updateDisplay, 1000); } }, update(newStats) { Object.assign(stats, newStats); if (!options.realTime) { updateDisplay(); } }, stop() { if (intervalId) { clearInterval(intervalId); intervalId = null; } process.stdout.write('\n'); }, getStats() { return { ...stats }; } }; } /** * Format fix results with enhanced details */ function formatFixResults(results, layers, options = {}) { if (results.length === 0) { return chalk.yellow('No fixes were applied\n'); } const totalFixes = results.reduce((sum, r) => sum + (r.fixesApplied || 0), 0); const byLayer = results.reduce((acc, result) => { result.layers?.forEach(layer => { if (!acc[layer]) acc[layer] = { files: 0, fixes: 0 }; acc[layer].files++; acc[layer].fixes += result.fixesApplied || 0; }); return acc; }, {}); let output = chalk.green.bold(`\nFix Summary\n`); output += chalk.white(` Files modified: ${chalk.cyan(results.length)}\n`); output += chalk.white(` Total fixes applied: ${chalk.green(totalFixes)}\n`); output += chalk.white(` Layers used: ${chalk.blue(layers.join(', '))}\n`); // Layer breakdown if (Object.keys(byLayer).length > 0) { output += chalk.gray('\n Layer breakdown:\n'); Object.entries(byLayer).forEach(([layer, stats]) => { const layerName = { 1: 'Configuration', 2: 'Content', 3: 'Components', 4: 'Hydration', 5: 'App Router', 6: 'Quality' }[layer] || `Layer ${layer}`; output += chalk.gray(` ${layerName}: ${stats.fixes} fixes in ${stats.files} files\n`); }); } // Top files by changes const sortedResults = [...results] .filter(r => r.fixesApplied > 0) .sort((a, b) => (b.fixesApplied || 0) - (a.fixesApplied || 0)) .slice(0, 5); if (sortedResults.length > 0) { output += chalk.gray('\n Most changed files:\n'); sortedResults.forEach(result => { const fileName = path.basename(result.filePath); const fixCount = result.fixesApplied || 0; output += chalk.gray(` ${fileName}: ${chalk.green(fixCount)} fixes\n`); }); } if (results.length > 5) { output += chalk.gray(` ... and ${results.length - 5} more files\n`); } output += chalk.cyan('\nRecommendations:\n'); output += ' - Test your application thoroughly\n'; output += ' - Review the changes before committing\n'; output += ' - Run "neurolint analyze" to verify fixes\n'; return output; } // Helper functions function getLayerSummary(results) { const layers = {}; results.forEach(result => { if (result.detectedIssues) { result.detectedIssues.forEach(issue => { const layer = issue.layer || 'Unknown'; if (!layers[layer]) { layers[layer] = { issues: 0, files: new Set(), topIssues: [] }; } layers[layer].issues++; layers[layer].files.add(result.filePath); if (layers[layer].topIssues.length < 5) { layers[layer].topIssues.push({ ...issue, filePath: result.filePath }); } }); } }); // Convert sets to counts Object.keys(layers).forEach(layer => { layers[layer].files = layers[layer].files.size; }); return layers; } function getLayerName(layerId) { const names = { 1: 'Configuration', 2: 'Content Standardization', 3: 'Component Intelligence', 4: 'Hydration Mastery', 5: 'App Router Optimization', 6: 'Quality Enforcement' }; return names[layerId] || 'Unknown'; } function getMostCriticalLayer(layers) { let maxIssues = 0; let criticalLayer = '1'; Object.entries(layers).forEach(([layer, data]) => { if (data.issues > maxIssues) { maxIssues = data.issues; criticalLayer = layer; } }); return criticalLayer; } module.exports = { formatConsoleOutput, formatJSONOutput, generateHTMLReport, createProgressTracker, createLayerProgressTracker, createStatsDisplay, formatFixResults };