@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
357 lines (314 loc) • 12.4 kB
text/typescript
import chalk from "chalk";
import ora from "ora";
import { table } from "table";
import fs from "fs-extra";
import path from "path";
import { loadConfig } from "../utils/config";
import { formatDetailedJSON, formatHTMLReport } from "../utils/formatter";
interface AnalyticsOptions {
timeRange?: string;
output?: string;
outputFile?: string;
detailed?: boolean;
teamId?: string;
}
export async function analyticsCommand(options: AnalyticsOptions) {
const spinner = ora("Loading analytics data...").start();
try {
// Load configuration
const config = await loadConfig();
if (!config.apiKey) {
spinner.fail("Authentication required");
console.log(chalk.white('Run "neurolint login" to authenticate first'));
return;
}
// Fetch analytics from API
const params = new URLSearchParams({
timeRange: options.timeRange || 'week',
detailed: (options.detailed || false).toString()
});
if (options.teamId) {
params.append('teamId', options.teamId);
}
const response = await fetch(`${config.api?.url || "https://app.neurolint.dev/api"}/analytics?${params}`, {
headers: {
"X-API-Key": config.apiKey,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
const result = await response.json();
spinner.succeed("Analytics data loaded successfully");
// Format and display results based on output format
await displayAnalytics(result.analytics, options);
} catch (error) {
spinner.fail(`Analytics failed: ${error instanceof Error ? error.message : "Unknown error"}`);
if (error instanceof Error) {
if (error.message.includes("ECONNREFUSED")) {
console.log(chalk.white("\nMake sure the NeuroLint server is running"));
} else if (error.message.includes("401") || error.message.includes("403")) {
console.log(chalk.white('\nAuthentication failed. Run "neurolint login" to re-authenticate'));
} else if (error.message.includes("429")) {
console.log(chalk.white("\nRate limit exceeded. Please wait before trying again"));
}
}
process.exit(1);
}
}
async function displayAnalytics(analytics: any, options: AnalyticsOptions) {
const format = options.output || "table";
switch (format) {
case "json":
const jsonOutput = JSON.stringify(analytics, null, 2);
if (options.outputFile) {
await fs.writeFile(options.outputFile, jsonOutput);
console.log(chalk.green(`Analytics data saved to: ${options.outputFile}`));
} else {
console.log(jsonOutput);
}
break;
case "detailed-json":
const detailedOutput = formatDetailedJSON(analytics);
if (options.outputFile) {
await fs.writeFile(options.outputFile, detailedOutput);
console.log(chalk.green(`Detailed analytics saved to: ${options.outputFile}`));
} else {
console.log(detailedOutput);
}
break;
case "html":
const htmlOutput = formatHTMLReport(analytics);
const htmlFile = options.outputFile || `neurolint-analytics-${Date.now()}.html`;
await fs.writeFile(htmlFile, htmlOutput);
console.log(chalk.green(`HTML analytics report generated: ${htmlFile}`));
break;
case "csv":
await generateCSVReport(analytics, options.outputFile);
break;
case "table":
default:
displayTableAnalytics(analytics);
break;
}
}
function displayTableAnalytics(analytics: any) {
console.log(chalk.white.bold("\nNeuroLint Analytics Report\n"));
// Overview Section
console.log(chalk.blue.bold("Overview"));
const overviewData = [
["Metric", "Value"],
["Total Analyses", analytics.overview.totalAnalyses.toLocaleString()],
["Success Rate", `${analytics.overview.successRate.toFixed(1)}%`],
["Avg Execution Time", `${analytics.overview.avgExecutionTime}ms`],
["Issues Found", analytics.issueAnalysis.totalIssuesFound.toLocaleString()]
];
console.log(table(overviewData, {
border: {
topBody: chalk.gray('─'),
topJoin: chalk.gray('┬'),
topLeft: chalk.gray('┌'),
topRight: chalk.gray('┐'),
bottomBody: chalk.gray('─'),
bottomJoin: chalk.gray('┴'),
bottomLeft: chalk.gray('└'),
bottomRight: chalk.gray('┘'),
bodyLeft: chalk.gray('│'),
bodyRight: chalk.gray('│'),
bodyJoin: chalk.gray('│'),
joinBody: chalk.gray('─'),
joinLeft: chalk.gray('├'),
joinRight: chalk.gray('┤'),
joinJoin: chalk.gray('┼')
},
columnDefault: {
paddingLeft: 1,
paddingRight: 1
}
}));
// Issue Types Section
if (Object.keys(analytics.issueAnalysis.issueTypes).length > 0) {
console.log(chalk.blue.bold("\nTop Issue Types"));
const issueData = [["Issue Type", "Count", "Percentage"]];
const totalIssues = analytics.issueAnalysis.totalIssuesFound;
Object.entries(analytics.issueAnalysis.issueTypes)
.sort(([,a], [,b]) => (b as number) - (a as number))
.slice(0, 10)
.forEach(([type, count]) => {
const percentage = ((count as number) / totalIssues * 100).toFixed(1);
issueData.push([
type.replace(/-/g, ' '),
(count as number).toLocaleString(),
`${percentage}%`
]);
});
console.log(table(issueData, {
border: {
topBody: chalk.gray('─'),
topJoin: chalk.gray('┬'),
topLeft: chalk.gray('┌'),
topRight: chalk.gray('┐'),
bottomBody: chalk.gray('─'),
bottomJoin: chalk.gray('┴'),
bottomLeft: chalk.gray('└'),
bottomRight: chalk.gray('┘'),
bodyLeft: chalk.gray('│'),
bodyRight: chalk.gray('│'),
bodyJoin: chalk.gray('│'),
joinBody: chalk.gray('─'),
joinLeft: chalk.gray('├'),
joinRight: chalk.gray('┤'),
joinJoin: chalk.gray('┼')
}
}));
}
// Layer Performance Section
if (analytics.layerPerformance && analytics.layerPerformance.length > 0) {
console.log(chalk.blue.bold("\nLayer Performance"));
const layerData = [["Layer", "Name", "Success Rate", "Avg Time", "Issues"]];
analytics.layerPerformance
.sort((a: any, b: any) => a.layerId - b.layerId)
.forEach((layer: any) => {
layerData.push([
layer.layerId.toString(),
layer.layerName,
`${layer.successRate.toFixed(1)}%`,
`${layer.avgTime.toFixed(0)}ms`,
layer.issues.toString()
]);
});
console.log(table(layerData, {
border: {
topBody: chalk.gray('─'),
topJoin: chalk.gray('┬'),
topLeft: chalk.gray('┌'),
topRight: chalk.gray('┐'),
bottomBody: chalk.gray('─'),
bottomJoin: chalk.gray('┴'),
bottomLeft: chalk.gray('└'),
bottomRight: chalk.gray('┘'),
bodyLeft: chalk.gray('│'),
bodyRight: chalk.gray('│'),
bodyJoin: chalk.gray('│'),
joinBody: chalk.gray('─'),
joinLeft: chalk.gray('├'),
joinRight: chalk.gray('┤'),
joinJoin: chalk.gray('┼')
}
}));
}
// File Types Section
if (Object.keys(analytics.fileAnalysis.fileTypes).length > 0) {
console.log(chalk.blue.bold("\nFile Types Analysis"));
const fileData = [["File Type", "Count", "Percentage"]];
const totalFiles = Object.values(analytics.fileAnalysis.fileTypes).reduce((sum: number, count) => sum + (count as number), 0);
Object.entries(analytics.fileAnalysis.fileTypes)
.sort(([,a], [,b]) => (b as number) - (a as number))
.slice(0, 8)
.forEach(([type, count]) => {
const percentage = ((count as number) / totalFiles * 100).toFixed(1);
fileData.push([
`.${type}`,
(count as number).toLocaleString(),
`${percentage}%`
]);
});
console.log(table(fileData, {
border: {
topBody: chalk.gray('─'),
topJoin: chalk.gray('┬'),
topLeft: chalk.gray('┌'),
topRight: chalk.gray('┐'),
bottomBody: chalk.gray('─'),
bottomJoin: chalk.gray('┴'),
bottomLeft: chalk.gray('└'),
bottomRight: chalk.gray('┘'),
bodyLeft: chalk.gray('│'),
bodyRight: chalk.gray('│'),
bodyJoin: chalk.gray('│'),
joinBody: chalk.gray('─'),
joinLeft: chalk.gray('├'),
joinRight: chalk.gray('┤'),
joinJoin: chalk.gray('┼')
}
}));
}
// Code Quality Metrics (if detailed)
if (analytics.codeQualityMetrics) {
console.log(chalk.blue.bold("\nCode Quality Metrics"));
const qualityData = [
["Metric", "Score"],
["Quality Score", `${analytics.codeQualityMetrics.qualityScore.toFixed(0)}/100`],
["Improvement Rate", `${analytics.codeQualityMetrics.improvementRate > 0 ? '+' : ''}${analytics.codeQualityMetrics.improvementRate.toFixed(1)}%`],
["Maintainability Index", `${analytics.codeQualityMetrics.maintainabilityIndex.toFixed(0)}/100`],
["Total Issues", analytics.codeQualityMetrics.totalIssues.toLocaleString()]
];
console.log(table(qualityData, {
border: {
topBody: chalk.gray('─'),
topJoin: chalk.gray('┬'),
topLeft: chalk.gray('┌'),
topRight: chalk.gray('┐'),
bottomBody: chalk.gray('─'),
bottomJoin: chalk.gray('┴'),
bottomLeft: chalk.gray('└'),
bottomRight: chalk.gray('┘'),
bodyLeft: chalk.gray('│'),
bodyRight: chalk.gray('│'),
bodyJoin: chalk.gray('│'),
joinBody: chalk.gray('─'),
joinLeft: chalk.gray('├'),
joinRight: chalk.gray('┤'),
joinJoin: chalk.gray('┼')
}
}));
}
// Recommendations
if (analytics.recommendations && analytics.recommendations.length > 0) {
console.log(chalk.blue.bold("\nRecommendations"));
analytics.recommendations.forEach((recommendation: string, index: number) => {
console.log(chalk.white(`${index + 1}. ${recommendation}`));
});
}
console.log(chalk.gray("\n" + "─".repeat(60)));
console.log(chalk.white("For detailed analytics, visit: https://neurolint.dev/dashboard"));
console.log(chalk.gray("Generated by NeuroLint CLI v1.0.0"));
}
async function generateCSVReport(analytics: any, outputFile?: string) {
const csvFile = outputFile || `neurolint-analytics-${Date.now()}.csv`;
let csvContent = "Type,Metric,Value\n";
// Overview metrics
csvContent += `Overview,Total Analyses,${analytics.overview.totalAnalyses}\n`;
csvContent += `Overview,Success Rate,${analytics.overview.successRate.toFixed(2)}\n`;
csvContent += `Overview,Average Execution Time (ms),${analytics.overview.avgExecutionTime}\n`;
csvContent += `Overview,Total Issues Found,${analytics.issueAnalysis.totalIssuesFound}\n`;
// Issue types
Object.entries(analytics.issueAnalysis.issueTypes).forEach(([type, count]) => {
csvContent += `Issue Type,${type.replace(/,/g, ';')},${count}\n`;
});
// Layer performance
analytics.layerPerformance?.forEach((layer: any) => {
csvContent += `Layer ${layer.layerId},Success Rate,${layer.successRate.toFixed(2)}\n`;
csvContent += `Layer ${layer.layerId},Average Time (ms),${layer.avgTime.toFixed(2)}\n`;
csvContent += `Layer ${layer.layerId},Issues Found,${layer.issues}\n`;
});
// File types
Object.entries(analytics.fileAnalysis.fileTypes).forEach(([type, count]) => {
csvContent += `File Type,.${type},${count}\n`;
});
await fs.writeFile(csvFile, csvContent);
console.log(chalk.green(`CSV analytics report generated: ${csvFile}`));
}
// Add analytics command to CLI
export function addAnalyticsCommand(program: any) {
program
.command("analytics")
.description("View detailed analytics and usage insights")
.option("-t, --time-range <range>", "Time range (day|week|month|quarter|year)", "week")
.option("-o, --output <format>", "Output format (table|json|detailed-json|html|csv)", "table")
.option("--output-file <file>", "Save output to file")
.option("--detailed", "Include detailed metrics and trends")
.option("--team-id <id>", "Include team analytics")
.action(analyticsCommand);
}