@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
581 lines (539 loc) • 19.4 kB
text/typescript
import chalk from "chalk";
import fs from "fs-extra";
import path from "path";
export function formatResults(results: any[], format: string, outputFile?: string) {
switch (format) {
case "json":
const basicJson = JSON.stringify(results, null, 2);
if (outputFile) {
fs.writeFileSync(outputFile, basicJson);
console.log(chalk.green(`JSON results written to: ${outputFile}`));
} else {
console.log(basicJson);
}
break;
case "detailed-json":
const detailedJson = formatDetailedJSON(results);
if (outputFile) {
fs.writeFileSync(outputFile, detailedJson);
console.log(chalk.green(`Detailed JSON results written to: ${outputFile}`));
} else {
console.log(detailedJson);
}
break;
case "compact-json":
const compactJson = formatCompactJSON(results);
if (outputFile) {
fs.writeFileSync(outputFile, compactJson);
console.log(chalk.green(`Compact JSON results written to: ${outputFile}`));
} else {
console.log(compactJson);
}
break;
case "html":
const htmlOutput = formatHTMLReport(results);
const htmlFile = outputFile || `neurolint-report-${Date.now()}.html`;
fs.writeFileSync(htmlFile, htmlOutput);
console.log(chalk.green(`HTML report generated: ${htmlFile}`));
break;
case "summary":
formatSummary(results);
break;
case "table":
default:
formatTable(results);
break;
}
}
function formatTable(results: any[]) {
console.log(chalk.white("\nAnalysis Results:\n"));
results.forEach((result, index) => {
const status = result.success ? chalk.white("PASS") : chalk.white("FAIL");
console.log(`${status} ${result.file}`);
if (result.success && result.layers) {
result.layers.forEach((layer: any) => {
const layerStatus =
layer.status === "success" ? chalk.white("PASS") : chalk.gray("SKIP");
const changes = layer.changes || 0;
console.log(
` ${layerStatus} Layer ${layer.id}: ${layer.name} ${changes > 0 ? chalk.gray(`(${changes} changes)`) : ""}`,
);
if (layer.insights && layer.insights.length > 0) {
layer.insights.forEach((insight: any) => {
const severity = chalk.gray("•");
console.log(` ${severity} ${insight.message}`);
});
}
});
} else if (!result.success) {
console.log(` ${chalk.white("ERROR:")} ${result.error}`);
}
if (index < results.length - 1) {
console.log("");
}
});
}
function formatSummary(results: any[]) {
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);
console.log(chalk.white("\nAnalysis Summary\n"));
console.log(chalk.white(`Files processed: ${results.length}`));
console.log(chalk.white(`Successful: ${successful.length}`));
if (failed.length > 0) {
console.log(chalk.white(`Failed: ${failed.length}`));
}
if (successful.length > 0) {
// Calculate layer statistics
const layerStats: Record<
string,
{ name: string; files: number; changes: number; issues: number }
> = {};
let totalIssues = 0;
successful.forEach((result: any) => {
if (result.layers) {
result.layers.forEach((layer: any) => {
if (!layerStats[layer.id]) {
layerStats[layer.id] = {
name: layer.name,
files: 0,
changes: 0,
issues: 0,
};
}
layerStats[layer.id].files++;
layerStats[layer.id].changes += layer.changes || 0;
if (layer.insights) {
layerStats[layer.id].issues += layer.insights.length;
totalIssues += layer.insights.length;
}
});
}
});
console.log(chalk.white(`Total issues found: ${totalIssues}`));
console.log(chalk.white("\nLayer Performance:"));
Object.entries(layerStats).forEach(([layerId, stats]: [string, any]) => {
console.log(chalk.white(`Layer ${layerId} (${stats.name}):`));
console.log(
chalk.gray(
` Files: ${stats.files}, Changes: ${stats.changes}, Issues: ${stats.issues}`,
),
);
});
}
}
function formatDetailedJSON(results: any[]): string {
const timestamp = new Date().toISOString();
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);
// Calculate comprehensive statistics
const stats = {
totalFiles: results.length,
successfulFiles: successful.length,
failedFiles: failed.length,
totalIssues: 0,
totalChanges: 0,
totalExecutionTime: 0,
layerBreakdown: {} as Record<string, any>
};
// Process layer statistics
successful.forEach((result: any) => {
if (result.layers) {
result.layers.forEach((layer: any) => {
const layerId = layer.id;
if (!stats.layerBreakdown[layerId]) {
stats.layerBreakdown[layerId] = {
name: layer.name,
filesProcessed: 0,
totalChanges: 0,
totalIssues: 0,
successRate: 0,
avgExecutionTime: 0,
issues: []
};
}
stats.layerBreakdown[layerId].filesProcessed++;
stats.layerBreakdown[layerId].totalChanges += layer.changes || 0;
if (layer.insights) {
stats.layerBreakdown[layerId].totalIssues += layer.insights.length;
stats.layerBreakdown[layerId].issues.push(...layer.insights);
stats.totalIssues += layer.insights.length;
}
stats.totalChanges += layer.changes || 0;
if (layer.executionTime) {
stats.totalExecutionTime += layer.executionTime;
}
});
}
});
// Calculate success rates
Object.keys(stats.layerBreakdown).forEach(layerId => {
const layer = stats.layerBreakdown[layerId];
layer.successRate = Math.round((layer.filesProcessed / successful.length) * 100);
});
const detailedReport = {
metadata: {
timestamp,
version: "1.1.0",
generatedBy: "NeuroLint CLI",
analysisDuration: stats.totalExecutionTime
},
summary: stats,
results: results.map(result => ({
file: result.file,
success: result.success,
error: result.error || null,
layers: result.layers || [],
metrics: {
linesOfCode: result.linesOfCode || 0,
complexity: result.complexity || 0,
maintainabilityIndex: result.maintainabilityIndex || 0
},
recommendations: result.recommendations || []
})),
layerDetails: stats.layerBreakdown,
failedFiles: failed.map(result => ({
file: result.file,
error: result.error,
timestamp: new Date().toISOString()
}))
};
return JSON.stringify(detailedReport, null, 2);
}
function formatCompactJSON(results: any[]): string {
const compactResults = results.map(result => ({
f: result.file, // file
s: result.success ? 1 : 0, // success
e: result.error || null, // error
l: result.layers ? result.layers.map((layer: any) => ({
i: layer.id, // id
n: layer.name, // name
c: layer.changes || 0, // changes
t: layer.executionTime || 0, // time
s: layer.status === 'success' ? 1 : 0, // status
is: layer.insights ? layer.insights.length : 0 // issues count
})) : []
}));
const summary = {
t: new Date().toISOString(), // timestamp
tf: results.length, // total files
sf: results.filter(r => r.success).length, // successful files
ff: results.filter(r => !r.success).length // failed files
};
return JSON.stringify({ s: summary, r: compactResults });
}
function formatHTMLReport(results: any[]): string {
const timestamp = new Date().toISOString();
const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);
// Calculate statistics
const stats = {
totalFiles: results.length,
successfulFiles: successful.length,
failedFiles: failed.length,
totalIssues: 0,
totalChanges: 0,
layerStats: {} as Record<string, any>
};
successful.forEach((result: any) => {
if (result.layers) {
result.layers.forEach((layer: any) => {
const layerId = layer.id;
if (!stats.layerStats[layerId]) {
stats.layerStats[layerId] = {
name: layer.name,
files: 0,
changes: 0,
issues: 0,
topIssues: []
};
}
stats.layerStats[layerId].files++;
stats.layerStats[layerId].changes += layer.changes || 0;
if (layer.insights) {
stats.layerStats[layerId].issues += layer.insights.length;
stats.totalIssues += layer.insights.length;
// Store top issues for this layer
layer.insights.forEach((insight: any) => {
if (stats.layerStats[layerId].topIssues.length < 5) {
stats.layerStats[layerId].topIssues.push({
...insight,
file: result.file
});
}
});
}
stats.totalChanges += layer.changes || 0;
});
}
});
const 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>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0a;
color: #ffffff;
line-height: 1.6;
padding: 20px;
}
.container { max-width: 1200px; margin: 0 auto; }
.header {
background: rgba(33, 150, 243, 0.1);
padding: 30px;
border-radius: 12px;
border: 1px solid rgba(33, 150, 243, 0.3);
margin-bottom: 30px;
text-align: center;
}
.header h1 {
color: #2196f3;
font-size: 2.5rem;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
color: #888;
font-size: 1.1rem;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.metric-card {
background: rgba(255, 255, 255, 0.05);
padding: 25px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(33, 150, 243, 0.15);
}
.metric-value {
font-size: 2.5rem;
font-weight: bold;
color: #2196f3;
margin-bottom: 5px;
}
.metric-label {
color: #ccc;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.section {
background: rgba(255, 255, 255, 0.03);
padding: 25px;
margin-bottom: 25px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.section h2 {
color: #2196f3;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid rgba(33, 150, 243, 0.3);
padding-bottom: 10px;
}
.layer-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.layer-card {
background: rgba(255, 255, 255, 0.05);
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2196f3;
}
.layer-title {
color: #2196f3;
font-weight: 600;
margin-bottom: 10px;
font-size: 1.1rem;
}
.layer-stats {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 0.9rem;
color: #ccc;
}
.issue-list {
list-style: none;
}
.issue-item {
background: rgba(255, 255, 255, 0.05);
padding: 10px;
margin: 8px 0;
border-radius: 6px;
border-left: 3px solid;
font-size: 0.9rem;
}
.issue-error { border-left-color: #f44336; }
.issue-warning { border-left-color: #ff9800; }
.issue-info { border-left-color: #2196f3; }
.file-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.file-table th,
.file-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.file-table th {
background: rgba(33, 150, 243, 0.1);
color: #2196f3;
font-weight: 600;
}
.status-success { color: #4caf50; }
.status-failed { color: #f44336; }
.footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: #666;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
@media (max-width: 768px) {
.metrics-grid { grid-template-columns: 1fr; }
.layer-grid { grid-template-columns: 1fr; }
.header h1 { font-size: 2rem; }
.container { padding: 10px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>NeuroLint Analysis Report</h1>
<p>Generated on ${new Date(timestamp).toLocaleString()}</p>
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value">${stats.totalFiles}</div>
<div class="metric-label">Files Analyzed</div>
</div>
<div class="metric-card">
<div class="metric-value">${stats.totalIssues}</div>
<div class="metric-label">Issues Found</div>
</div>
<div class="metric-card">
<div class="metric-value">${stats.totalChanges}</div>
<div class="metric-label">Changes Applied</div>
</div>
<div class="metric-card">
<div class="metric-value">${Object.keys(stats.layerStats).length}</div>
<div class="metric-label">Layers Processed</div>
</div>
</div>
${Object.keys(stats.layerStats).length > 0 ? `
<div class="section">
<h2>Layer Analysis</h2>
<div class="layer-grid">
${Object.entries(stats.layerStats).map(([layerId, data]: [string, any]) => `
<div class="layer-card">
<div class="layer-title">Layer ${layerId}: ${data.name}</div>
<div class="layer-stats">
<span>Files: ${data.files}</span>
<span>Issues: ${data.issues}</span>
<span>Changes: ${data.changes}</span>
</div>
${data.topIssues.length > 0 ? `
<ul class="issue-list">
${data.topIssues.map((issue: any) => `
<li class="issue-item issue-${issue.severity || 'info'}">
<strong>${issue.message || issue.description}</strong>
<div style="font-size: 0.8rem; color: #888; margin-top: 4px;">${issue.file}</div>
</li>
`).join('')}
</ul>
` : '<p style="color: #888; font-style: italic;">No issues found</p>'}
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="section">
<h2>File Results</h2>
<table class="file-table">
<thead>
<tr>
<th>File</th>
<th>Status</th>
<th>Issues</th>
<th>Changes</th>
</tr>
</thead>
<tbody>
${results.map((result: any) => {
const totalIssues = result.layers ?
result.layers.reduce((sum: number, layer: any) => sum + (layer.insights?.length || 0), 0) : 0;
const totalChanges = result.layers ?
result.layers.reduce((sum: number, layer: any) => sum + (layer.changes || 0), 0) : 0;
return `
<tr>
<td>${result.file}</td>
<td class="${result.success ? 'status-success' : 'status-failed'}">
${result.success ? 'SUCCESS' : 'FAILED'}
</td>
<td>${totalIssues}</td>
<td>${totalChanges}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
${failed.length > 0 ? `
<div class="section">
<h2>Failed Files</h2>
<ul class="issue-list">
${failed.map((result: any) => `
<li class="issue-item issue-error">
<strong>${result.file}</strong>
<div style="margin-top: 5px; color: #ccc;">${result.error}</div>
</li>
`).join('')}
</ul>
</div>
` : ''}
<div class="section">
<h2>Recommendations</h2>
<ul style="list-style: disc; margin-left: 20px; color: #ccc;">
<li>Review all detected issues and apply necessary fixes</li>
<li>Focus on high-severity issues first to maximize impact</li>
<li>Test your application thoroughly after applying changes</li>
<li>Consider upgrading to NeuroLint Professional for advanced features</li>
${stats.totalIssues > 0 ? '<li>Run NeuroLint fix command to automatically resolve issues</li>' : ''}
</ul>
</div>
<div class="footer">
<p>Generated by NeuroLint CLI v1.0.0 | Visit <a href="https://neurolint.dev" style="color: #2196f3;">neurolint.dev</a> for more information</p>
</div>
</div>
</body>
</html>`;
return html;
}
// Export individual formatting functions for reuse
export {
formatDetailedJSON,
formatCompactJSON,
formatHTMLReport,
formatTable,
formatSummary
};