@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
JavaScript
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
};