@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
600 lines (542 loc) • 19 kB
text/typescript
import chalk from "chalk";
import ora from "ora";
import { glob } from "glob";
import fs from "fs-extra";
import path from "path";
import axios from "axios";
import { loadConfig, validateConfig } from "../utils/config";
/**
* Free tier local analysis - basic pattern detection
*/
async function performLocalAnalysis(code: string, filename: string, layers: number[]) {
const issues: any[] = [];
const ext = path.extname(filename);
// Basic modernization checks for React/Next.js files
if (['.tsx', '.jsx', '.ts', '.js'].includes(ext)) {
// Layer 1: Configuration & TypeScript modernization
if (layers.includes(1)) {
if (code.includes('React.FC') && !code.includes('import { FC }')) {
issues.push({
layer: 1,
rule: 'typescript-modernization',
severity: 'warning',
message: 'Consider using direct FC import instead of React.FC',
line: 1
});
}
}
// Layer 2: Legacy pattern detection
if (layers.includes(2)) {
if (code.includes('class ') && code.includes('extends Component')) {
issues.push({
layer: 2,
rule: 'class-to-hooks',
severity: 'info',
message: 'Class component detected - consider migrating to hooks',
line: code.split('\n').findIndex(line => line.includes('class ')) + 1
});
}
if (code.includes('componentDidMount') || code.includes('componentWillUnmount')) {
issues.push({
layer: 2,
rule: 'lifecycle-hooks',
severity: 'info',
message: 'Legacy lifecycle methods - consider useEffect hook',
line: 1
});
}
}
// Layer 3: React 18 compatibility
if (layers.includes(3)) {
const mapMatches = code.match(/\.map\(\([^)]+\)\s*=>\s*<[^>]+(?!\s+key=)/g);
if (mapMatches) {
issues.push({
layer: 3,
rule: 'react-keys',
severity: 'warning',
message: 'Missing key prop in mapped React elements',
line: 1
});
}
if (code.includes('ReactDOM.render')) {
issues.push({
layer: 3,
rule: 'react-18-root',
severity: 'warning',
message: 'Use createRoot() instead of ReactDOM.render() for React 18',
line: 1
});
}
}
// Layer 4: Next.js App Router migration opportunities
if (layers.includes(4) && filename.includes('pages/')) {
issues.push({
layer: 4,
rule: 'app-router-migration',
severity: 'info',
message: 'Consider migrating to Next.js App Router (app/ directory)',
line: 1
});
}
// Layer 5: Modern Next.js patterns
if (layers.includes(5)) {
if (code.includes('getServerSideProps') || code.includes('getStaticProps')) {
issues.push({
layer: 5,
rule: 'data-fetching-modernization',
severity: 'info',
message: 'Consider using App Router data fetching patterns',
line: 1
});
}
}
}
return {
analysis: {
detectedIssues: issues,
summary: {
totalIssues: issues.length,
byLayer: layers.reduce((acc, layer) => {
acc[layer] = issues.filter(i => i.layer === layer).length;
return acc;
}, {} as Record<number, number>)
}
},
modernizationScore: Math.max(0, 100 - (issues.length * 10)),
isFreeAnalysis: true
};
}
interface AnalyzeOptions {
layers?: string;
output?: string;
recursive?: boolean;
include?: string;
exclude?: string;
config?: string;
}
interface AnalysisResult {
success: boolean;
filesAnalyzed: number;
issuesFound: number;
layersUsed: number[];
issues: Array<{
file: string;
layer: number;
rule: string;
severity: "error" | "warning" | "info";
message: string;
line?: number;
column?: number;
}>;
performance: {
duration: number;
layerTimes: Record<number, number>;
};
}
/**
* Layer Dependency Management (from IMPLEMENTATION_PATTERNS.md)
* Validates and corrects layer selection based on dependencies
*/
function validateAndCorrectLayers(requestedLayers: number[]) {
const DEPENDENCIES = {
1: [], // Configuration has no dependencies
2: [1], // Entity cleanup depends on config foundation
3: [1, 2], // Components depend on config + cleanup
4: [1, 2, 3], // Hydration depends on all previous layers
5: [1, 2, 3, 4], // Next.js depends on all core layers
6: [1, 2, 3, 4, 5], // Testing depends on all previous layers
};
const LAYER_INFO = {
1: { name: "Configuration", critical: true },
2: { name: "Entity Cleanup", critical: false },
3: { name: "Components", critical: false },
4: { name: "Hydration", critical: false },
5: { name: "Next.js App Router", critical: false },
6: { name: "Testing & Validation", critical: false },
};
const warnings: string[] = [];
const autoAdded: number[] = [];
let correctedLayers = [...requestedLayers];
// Sort layers in execution order
correctedLayers.sort((a, b) => a - b);
// Check dependencies for each requested layer
for (const layerId of requestedLayers) {
const dependencies =
DEPENDENCIES[layerId as keyof typeof DEPENDENCIES] || [];
const missingDeps = dependencies.filter(
(dep) => !correctedLayers.includes(dep),
);
if (missingDeps.length > 0) {
// Auto-add missing dependencies
correctedLayers.push(...missingDeps);
autoAdded.push(...missingDeps);
warnings.push(
`Layer ${layerId} (${LAYER_INFO[layerId as keyof typeof LAYER_INFO]?.name}) requires ` +
`${missingDeps.map((dep) => `${dep} (${LAYER_INFO[dep as keyof typeof LAYER_INFO]?.name})`).join(", ")}. ` +
`Auto-added missing dependencies.`,
);
}
}
// Remove duplicates and sort
correctedLayers = [...new Set(correctedLayers)].sort((a, b) => a - b);
return {
correctedLayers,
warnings,
autoAdded,
};
}
export async function analyzeCommand(files: string[], options: AnalyzeOptions) {
const spinner = ora("Initializing modernization analysis...").start();
const startTime = Date.now();
try {
// Enhanced input validation
if (!Array.isArray(files)) {
spinner.fail("Invalid input: files must be an array");
console.log(chalk.red("ERROR: Invalid files parameter"));
return;
}
if (
files.some(
(file) => !file || typeof file !== "string" || file.trim().length === 0,
)
) {
spinner.fail("Invalid file paths provided");
console.log(
chalk.red("ERROR: All file paths must be valid non-empty strings"),
);
return;
}
// Validate options
if (options && typeof options !== "object") {
spinner.fail("Invalid options provided");
console.log(chalk.red("ERROR: Options must be an object"));
return;
}
// Load and validate configuration
const config = await loadConfig(options.config);
const configValidation = await validateConfig(config);
if (!configValidation.valid) {
spinner.fail("Configuration validation failed");
configValidation.errors.forEach((error) =>
console.log(chalk.red(`ERROR: ${error}`)),
);
return;
}
// Check for free tier vs premium mode
const isFreeMode = !config.apiKey;
if (isFreeMode) {
spinner.text = "Running free tier modernization analysis...";
console.log(chalk.cyan("\nFree Tier: Unlimited scanning & basic modernization analysis"));
console.log(chalk.gray("Premium: Detailed reports, one-click fixes & team collaboration\n"));
} else {
spinner.text = "Running premium analysis...";
}
// Parse layers
const requestedLayers = options.layers
? options.layers
.split(",")
.map((l: string) => parseInt(l.trim()))
.filter((l: number) => l >= 1 && l <= 6)
: config.layers.enabled;
// Apply Layer Dependency Management (from IMPLEMENTATION_PATTERNS.md)
const layerValidation = validateAndCorrectLayers(requestedLayers);
const layers = layerValidation.correctedLayers;
if (layerValidation.warnings.length > 0) {
layerValidation.warnings.forEach((warning) =>
console.log(chalk.yellow(`DEPENDENCY: ${warning}`)),
);
}
// Check premium features for layers 5 and 6
const premiumLayers = layers.filter((layer) => layer >= 5);
if (premiumLayers.length > 0) {
try {
const userResponse = await axios.get(
`${config.api.url}/auth/api-keys`,
{
headers: { "X-API-Key": config.apiKey },
timeout: 10000,
},
);
// Handle different response structures from API
const plan =
userResponse.data.plan ||
userResponse.data.user?.plan ||
userResponse.data.apiKey?.plan ||
"free";
if (plan === "free" && premiumLayers.length > 0) {
spinner.fail("Premium features required");
console.log(
chalk.yellow(
`Layers ${premiumLayers.join(", ")} require Professional plan ($29/month)`,
),
);
console.log(chalk.gray("Upgrade at: https://neurolint.dev/pricing"));
return;
}
} catch (error) {
console.log(
chalk.yellow("Unable to verify premium features, continuing..."),
);
}
}
// Determine files to analyze
let targetFiles: string[] = [];
if (files.length > 0) {
targetFiles = files;
} else {
// Use glob patterns from config
spinner.text = "Discovering files...";
try {
for (const pattern of config.files.include) {
try {
const foundFiles = await glob(pattern, {
cwd: process.cwd(),
ignore: config.files.exclude,
});
targetFiles.push(...foundFiles);
} catch (globError) {
console.warn(`Warning: Could not process pattern ${pattern}`);
}
}
} catch (error) {
spinner.fail("File discovery failed");
console.log(
chalk.red(
`Error: ${error instanceof Error ? error.message : String(error)}`,
),
);
return;
}
}
if (targetFiles.length === 0) {
spinner.fail("No files found to analyze");
console.log(
chalk.yellow(
"Try specifying files explicitly or check your include/exclude patterns",
),
);
return;
}
// Remove duplicates and filter existing files
targetFiles = [...new Set(targetFiles)];
const existingFiles = [];
for (const file of targetFiles) {
if (await fs.pathExists(file)) {
existingFiles.push(file);
}
}
if (existingFiles.length === 0) {
spinner.fail("No valid files found");
return;
}
spinner.text = `Analyzing ${existingFiles.length} files with layers [${layers.join(", ")}]...`;
// Process files one by one since the API expects single files
const allResults: any[] = [];
let totalIssues = 0;
for (const file of existingFiles) {
try {
const code = await fs.readFile(file, "utf-8");
let result;
if (isFreeMode) {
// Free tier: Local basic analysis
result = await performLocalAnalysis(code, file, layers);
} else {
// Premium tier: Full API analysis
const analysisPayload = {
code,
filename: file,
layers: layers.length === 1 ? layers[0].toString() : layers.join(","),
applyFixes: false,
metadata: {
recursive: options.recursive,
outputFormat: options.output || config.output.format,
verbose: config.output.verbose,
},
};
const response = await axios.post(
`${config.api.url}/analyze`,
analysisPayload,
{
headers: {
"X-API-Key": config.apiKey,
"Content-Type": "application/json",
},
timeout: config.api.timeout || 60000,
maxContentLength: 50 * 1024 * 1024, // 50MB max response
validateStatus: (status) => status < 500, // Don't throw on 4xx errors
},
);
result = response.data;
}
// Basic validation following IMPLEMENTATION_PATTERNS.md
if (!result || typeof result !== "object") {
console.log(chalk.yellow(`Warning: Invalid response for ${file}`));
continue;
}
allResults.push({ file, result });
// Handle different response structures
const detectedIssues =
result.analysis?.detectedIssues ||
result.detectedIssues ||
result.layers?.flatMap((l) => l.detectedIssues) ||
[];
totalIssues += detectedIssues.length;
} catch (fileError) {
console.log(chalk.yellow(`Warning: Could not analyze ${file}`));
if (axios.isAxiosError(fileError)) {
if (fileError.response?.status === 401) {
console.log(
chalk.red(
"Authentication failed. Please run 'neurolint login' again.",
),
);
} else if (fileError.response?.status === 403) {
console.log(
chalk.red("Access denied. Check your API permissions."),
);
} else {
console.log(
chalk.gray(
`API Error: ${fileError.response?.status} ${fileError.response?.statusText}`,
),
);
}
} else {
console.log(
chalk.gray(
`Error: ${fileError instanceof Error ? fileError.message : String(fileError)}`,
),
);
}
}
}
const processingTime = Date.now() - startTime;
spinner.succeed(`Analysis completed for ${existingFiles.length} files`);
// Aggregate results
const aggregatedResult = {
filesAnalyzed: existingFiles.length,
issuesFound: totalIssues,
layersUsed: layers,
results: allResults,
performance: {
duration: processingTime,
layerTimes: {},
},
};
// Display results
console.log();
console.log(chalk.white.bold("Analysis Results"));
console.log();
console.log(
chalk.white("Files analyzed: ") +
chalk.cyan(aggregatedResult.filesAnalyzed),
);
console.log(
chalk.white("Issues found: ") +
(aggregatedResult.issuesFound > 0
? chalk.yellow(aggregatedResult.issuesFound)
: chalk.green("0")),
);
console.log(
chalk.white("Layers used: ") +
chalk.gray(`[${aggregatedResult.layersUsed.join(", ")}]`),
);
console.log(
chalk.white("Duration: ") +
chalk.gray(`${aggregatedResult.performance.duration}ms`),
);
console.log();
if (aggregatedResult.issuesFound > 0) {
// Group issues by layer and file
const issuesByLayer: Record<number, any[]> = {};
allResults.forEach(({ file, result }) => {
// Handle different response structures
const detectedIssues =
result.analysis?.detectedIssues ||
result.detectedIssues ||
result.layers?.flatMap((l: any) => l.detectedIssues || []) ||
[];
detectedIssues.forEach((issue: any) => {
const layer = issue.layer || 1;
if (!issuesByLayer[layer]) {
issuesByLayer[layer] = [];
}
issuesByLayer[layer].push({ ...issue, file });
});
});
console.log(chalk.white("Issues by Layer:"));
for (const layer of aggregatedResult.layersUsed) {
const layerIssues = issuesByLayer[layer] || [];
const layerName = config.layers.config[layer]?.name || `Layer ${layer}`;
console.log(
chalk.gray(` ${layerName}: `) +
(layerIssues.length > 0
? chalk.yellow(`${layerIssues.length} issues`)
: chalk.green("PASSED")),
);
// Show first few issues for each layer
if (
layerIssues.length > 0 &&
(options.output === "table" || !options.output)
) {
layerIssues.slice(0, 3).forEach((issue) => {
const location = issue.line
? `:${issue.line}${issue.column ? `:${issue.column}` : ""}`
: "";
console.log(
chalk.gray(
` ${issue.file}${location} - ${issue.message || issue.description || "Issue detected"}`,
),
);
});
if (layerIssues.length > 3) {
console.log(
chalk.gray(` ... and ${layerIssues.length - 3} more`),
);
}
}
}
console.log();
// Output formatted results if requested
if (options.output === "json") {
console.log(JSON.stringify(aggregatedResult, null, 2));
}
if (isFreeMode) {
console.log(chalk.white("Next steps:"));
console.log(chalk.cyan("Want to fix these issues automatically?"));
console.log(chalk.gray("Run 'neurolint login' to access premium features:"));
console.log(chalk.gray(" - One-click modernization fixes"));
console.log(chalk.gray(" - Detailed migration reports (PDF/CSV)"));
console.log(chalk.gray(" - Safe rollback & backup protection"));
console.log(chalk.gray(" - Team collaboration & shared dashboards"));
console.log(chalk.yellow("TIP: Try layer-specific CLIs: neurolint-config, neurolint-hydration"));
console.log(chalk.cyan("\nUpgrade at https://neurolint.dev/pricing (starts at $9/month)"));
} else {
console.log(chalk.white("Next steps:"));
console.log(
chalk.gray(" • Run 'neurolint fix' to automatically fix issues"),
);
console.log(
chalk.gray(
" • Run 'neurolint analyze --output=json' for detailed results",
),
);
}
} else {
if (isFreeMode) {
console.log(chalk.green("No modernization issues found! Your React/Next.js code is up to date."));
console.log(chalk.yellow("TIP: Try specific areas: neurolint-hydration, neurolint-approuter"));
console.log(chalk.cyan("Premium features at https://neurolint.dev/pricing (starts at $9/month)"));
} else {
console.log(chalk.green("No issues found! Your code looks great."));
}
}
} catch (error) {
spinner.fail("Analysis initialization failed");
console.log(
chalk.red(
`Error: ${error instanceof Error ? error.message : String(error)}`,
),
);
}
}