@neurolint/cli
Version:
Professional React/Next.js modernization platform with CLI, VS Code, and Web App integrations
1,560 lines (1,377 loc) • 118 kB
JavaScript
#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const ora = require('ora');
const { performance } = require('perf_hooks');
const https = require('https');
// Import shared core and existing modules
const sharedCore = require('./shared-core');
const fixMaster = require('./fix-master.js');
const TransformationValidator = require('./validator.js');
const BackupManager = require('./backup-manager');
// Configuration
const CONFIG_FILE = '.neurolintrc';
const API_BASE_URL = process.env.NEUROLINT_API_URL || 'https://app.neurolint.dev';
// Authentication and tier management
class AuthManager {
constructor() {
this.apiKey = null;
this.userInfo = null;
this.configPath = path.join(process.cwd(), CONFIG_FILE);
}
async loadConfig() {
try {
const configData = await fs.readFile(this.configPath, 'utf8');
const config = JSON.parse(configData);
this.apiKey = config.apiKey;
this.userInfo = config.userInfo;
return config;
} catch (error) {
return null;
}
}
async saveConfig(config) {
try {
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2));
} catch (error) {
throw new Error(`Failed to save configuration: ${error.message}`);
}
}
async authenticate(apiKey) {
try {
// In development mode, skip actual API calls
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.NEUROLINT_DEV === 'true';
if (isDevelopment) {
this.apiKey = apiKey;
this.userInfo = {
id: 'dev-user',
email: 'dev@neurolint.dev',
tier: 'development',
name: 'Development User'
};
await this.saveConfig({ apiKey, userInfo: this.userInfo });
return this.userInfo;
}
const response = await this.makeRequest('/api/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify({
code: 'const test = "validation";',
filename: 'test.ts',
layers: [1],
applyFixes: false,
metadata: { source: 'cli', validation: true }
})
});
if (response.error) {
throw new Error(response.error);
}
// Since the analyze endpoint works, we'll use that for authentication
// and create a basic user info object
this.apiKey = apiKey;
this.userInfo = {
email: 'admin@neurolint.com',
plan: 'enterprise',
tier: 'enterprise',
id: '17bd91f3-38a0-4399-891c-73608eb380c2'
};
// Save to config
await this.saveConfig({ apiKey, userInfo: this.userInfo });
return this.userInfo;
} catch (error) {
throw new Error(`Authentication failed: ${error.message}`);
}
}
async checkUsage() {
if (!this.apiKey) {
// Free tier defaults per pricing update: unlimited fixes for layers 1-2
return { tier: 'free', canUseFixes: true, layers: [1, 2], usage: { current: 0, limit: -1 } };
}
// For authenticated users with enterprise plan, return enterprise access
if (this.userInfo && (this.userInfo.plan === 'enterprise' || this.userInfo.tier === 'enterprise')) {
return {
tier: 'enterprise',
canUseFixes: true,
layers: [1, 2, 3, 4, 5, 6, 7],
usage: { current: 0, limit: -1 }
};
}
try {
const response = await this.makeRequest('/api/cli/usage', {
method: 'GET',
headers: {
'X-API-Key': this.apiKey
}
});
return response;
} catch (error) {
// Fallback to free tier on error
return { tier: 'free', canUseFixes: true, layers: [1, 2], usage: { current: 0, limit: -1 } };
}
}
async canUseLayers(layers) {
if (!this.apiKey) {
// Allow free tier layers (1-2) without authentication
const restricted = layers.filter(l => l > 2);
return { allowed: restricted.length === 0, restrictedLayers: restricted, tier: 'free' };
}
try {
const response = await this.makeRequest('/api/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey
},
body: JSON.stringify({
code: 'const test = "validation";',
filename: 'test.ts',
layers,
applyFixes: false,
metadata: { source: 'cli', layerCheck: true }
})
});
return {
allowed: !response.error,
restrictedLayers: response.restrictedLayers || [],
tier: response.tier || 'free'
};
} catch (error) {
// On error, assume only free tier layers allowed
const restricted = layers.filter(l => l > 2);
return { allowed: restricted.length === 0, restrictedLayers: restricted, tier: 'free' };
}
}
async makeRequest(endpoint, options) {
return new Promise((resolve, reject) => {
const url = new URL(endpoint, API_BASE_URL);
const requestOptions = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method: options.method || 'GET',
headers: options.headers || {}
};
const req = https.request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const response = JSON.parse(data);
resolve(response);
} catch (error) {
resolve({ error: 'Invalid response format' });
}
});
});
req.on('error', (error) => {
reject(error);
});
if (options.body) {
req.write(options.body);
}
req.end();
});
}
isAuthenticated() {
return !!this.apiKey;
}
getUserInfo() {
return this.userInfo;
}
}
// Global auth manager
const authManager = new AuthManager();
// Initialize auth manager on startup
(async () => {
try {
await authManager.loadConfig();
} catch (error) {
// Ignore errors during initialization
}
})();
// Layer configuration
const LAYER_NAMES = {
1: 'config',
2: 'patterns',
3: 'components',
4: 'hydration',
5: 'nextjs',
6: 'testing',
7: 'adaptive'
};
// Smart Layer Selector for analyzing and recommending layers
class SmartLayerSelector {
static analyzeAndRecommend(code, filePath) {
const issues = [];
const ext = path.extname(filePath);
try {
// Use AST-based analysis for more accurate detection
const ASTTransformer = require('./ast-transformer');
const transformer = new ASTTransformer();
const astIssues = transformer.analyzeCode(code, { filename: filePath });
// Convert AST issues to layer recommendations
astIssues.forEach(issue => {
issues.push({
layer: issue.layer,
reason: issue.message,
confidence: 0.9,
location: issue.location
});
});
} catch (error) {
// Fallback to regex-based detection if AST parsing fails
issues.push(...this.fallbackAnalysis(code, filePath));
}
return {
detectedIssues: issues,
recommendedLayers: [...new Set(issues.map(i => i.layer))].sort(),
reasons: issues.map(i => i.reason),
confidence: issues.reduce((acc, i) => acc + i.confidence, 0) / issues.length || 0
};
}
static fallbackAnalysis(code, filePath) {
const issues = [];
const ext = path.extname(filePath);
// Layer 1: Configuration files
if (filePath.endsWith('tsconfig.json') || filePath.endsWith('next.config.js') || filePath.endsWith('package.json')) {
issues.push({
layer: 1,
reason: 'Configuration file detected',
confidence: 0.9
});
}
// Layer 2: Pattern issues
if (code.includes('"') || code.includes('&') || code.includes('console.log(')) {
issues.push({
layer: 2,
reason: 'Common pattern issues detected',
confidence: 0.8
});
}
// Layer 3: Component issues
if ((ext === '.tsx' || ext === '.jsx') && code.includes('function') && code.includes('return (')) {
if (code.includes('.map(') && !code.includes('key={')) {
issues.push({
layer: 3,
reason: 'React component issues detected (missing keys)',
confidence: 0.9
});
}
if (code.includes('<button') && !code.includes('aria-label')) {
issues.push({
layer: 3,
reason: 'React component issues detected (missing aria labels)',
confidence: 0.9
});
}
if (code.includes('<Button') && !code.includes('variant=')) {
issues.push({
layer: 3,
reason: 'React component issues detected (missing Button variant)',
confidence: 0.8
});
}
if (code.includes('<Input') && !code.includes('type=')) {
issues.push({
layer: 3,
reason: 'React component issues detected (missing Input type)',
confidence: 0.8
});
}
if (code.includes('<img') && !code.includes('alt=')) {
issues.push({
layer: 3,
reason: 'React component issues detected (missing image alt)',
confidence: 0.9
});
}
}
// Layer 4: Hydration issues
if ((code.includes('localStorage') || code.includes('window.') || code.includes('document.')) && !code.includes('typeof window')) {
issues.push({
layer: 4,
reason: 'Hydration safety issues detected',
confidence: 0.9
});
}
// Layer 5: Next.js issues
if ((ext === '.tsx' || ext === '.jsx') &&
(code.includes('useState') || code.includes('useEffect')) &&
!code.match(/^['"]use client['"];/)) {
issues.push({
layer: 5,
reason: 'Next.js client component issues detected',
confidence: 0.9
});
}
// Layer 6: Testing issues
if ((ext === '.tsx' || ext === '.jsx') && code.includes('export') && !code.includes('test(')) {
issues.push({
layer: 6,
reason: 'Missing test coverage',
confidence: 0.7
});
}
// Layer 7: Adaptive Pattern Learning
if ((ext === '.tsx' || ext === '.jsx') &&
(code.includes('useState') || code.includes('useEffect') || code.includes('function'))) {
issues.push({
layer: 7,
reason: 'Potential for adaptive pattern learning',
confidence: 0.6
});
}
return issues;
}
}
// Rule Store for Layer 7 adaptive learning
class RuleStore {
constructor() {
this.rules = [];
this.ruleFile = path.join(process.cwd(), '.neurolint', 'learned-rules.json');
}
async load() {
try {
const data = await fs.readFile(this.ruleFile, 'utf8');
const parsed = JSON.parse(data);
// Handle both array format and object with rules property
this.rules = Array.isArray(parsed) ? parsed : (parsed.rules || []);
// Ensure rules is always an array
if (!Array.isArray(this.rules)) {
this.rules = [];
}
} catch (error) {
this.rules = [];
}
}
async save() {
const ruleDir = path.dirname(this.ruleFile);
await fs.mkdir(ruleDir, { recursive: true });
await fs.writeFile(this.ruleFile, JSON.stringify(this.rules, null, 2));
}
addRule(pattern, transformation) {
this.rules.push({
pattern,
transformation,
timestamp: new Date().toISOString(),
usageCount: 1
});
}
}
// File pattern matching utility with performance optimizations
async function getFiles(dir, include = ['**/*.{ts,tsx,js,jsx,json}'], exclude = [
// Build and dependency directories
'**/node_modules/**',
'**/dist/**',
'**/.next/**',
'**/build/**',
'**/.build/**',
'**/out/**',
'**/.out/**',
// Coverage and test artifacts
'**/coverage/**',
'**/.nyc_output/**',
'**/.jest/**',
'**/test-results/**',
// Version control
'**/.git/**',
'**/.svn/**',
'**/.hg/**',
// IDE and editor files
'**/.vscode/**',
'**/.idea/**',
'**/.vs/**',
'**/*.swp',
'**/*.swo',
'**/*~',
'**/.#*',
'**/#*#',
// OS generated files
'**/.DS_Store',
'**/Thumbs.db',
'**/desktop.ini',
'**/*.tmp',
'**/*.temp',
// Log files
'**/*.log',
'**/logs/**',
'**/.log/**',
// Cache directories
'**/.cache/**',
'**/cache/**',
'**/.parcel-cache/**',
'**/.eslintcache',
'**/.stylelintcache',
// Neurolint specific exclusions
'**/.neurolint/**',
'**/states-*.json',
'**/*.backup-*',
'**/*.backup',
// Package manager files
'**/package-lock.json',
'**/yarn.lock',
'**/pnpm-lock.yaml',
'**/.npm/**',
'**/.yarn/**',
// Environment and config files
'**/.env*',
'**/.env.local',
'**/.env.development',
'**/.env.test',
'**/.env.production',
// Documentation and assets
'**/docs/**',
'**/documentation/**',
'**/assets/**',
'**/public/**',
'**/static/**',
'**/images/**',
'**/img/**',
'**/icons/**',
'**/fonts/**',
'**/*.png',
'**/*.jpg',
'**/*.jpeg',
'**/*.gif',
'**/*.svg',
'**/*.ico',
'**/*.woff',
'**/*.woff2',
'**/*.ttf',
'**/*.eot',
'**/*.mp4',
'**/*.webm',
'**/*.mp3',
'**/*.wav',
'**/*.pdf',
'**/*.zip',
'**/*.tar.gz',
'**/*.rar',
// Generated files
'**/*.min.js',
'**/*.min.css',
'**/*.bundle.js',
'**/*.chunk.js',
'**/vendor/**',
// Backup and temporary files
'**/*.bak',
'**/*.backup',
'**/*.old',
'**/*.orig',
'**/*.rej',
'**/*.tmp',
'**/*.temp',
// Lock files and manifests
'**/.lock-wscript',
'**/npm-debug.log*',
'**/yarn-debug.log*',
'**/yarn-error.log*',
'**/.pnp.*',
// TypeScript declaration files (optional - uncomment if needed)
// '**/*.d.ts',
// Test files (optional - uncomment if you want to exclude tests)
// '**/*.test.js',
// '**/*.test.ts',
// '**/*.test.tsx',
// '**/*.spec.js',
// '**/*.spec.ts',
// '**/*.spec.tsx',
// '**/__tests__/**',
// '**/tests/**',
// '**/test/**',
// Storybook files (optional - uncomment if needed)
// '**/*.stories.js',
// '**/*.stories.ts',
// '**/*.stories.tsx',
// '**/.storybook/**',
// Cypress files (optional - uncomment if needed)
// '**/cypress/**',
// '**/cypress.config.*',
// Playwright files (optional - uncomment if needed)
// '**/playwright.config.*',
// '**/tests/**',
// Docker files (optional - uncomment if needed)
// '**/Dockerfile*',
// '**/.dockerignore',
// '**/docker-compose*',
// CI/CD files (optional - uncomment if needed)
// '**/.github/**',
// '**/.gitlab-ci.yml',
// '**/.travis.yml',
// '**/.circleci/**',
// '**/azure-pipelines.yml',
// Database files (optional - uncomment if needed)
// '**/*.sqlite',
// '**/*.db',
// '**/*.sql',
// Configuration files (optional - uncomment if needed)
// '**/.eslintrc*',
// '**/.prettierrc*',
// '**/.babelrc*',
// '**/tsconfig.json',
// '**/next.config.js',
// '**/webpack.config.*',
// '**/rollup.config.*',
// '**/vite.config.*',
// '**/jest.config.*',
// '**/tailwind.config.*',
// '**/postcss.config.*'
]) {
const files = [];
const maxConcurrent = 50; // Increased from 10 for better throughput
const batchSize = 100; // Process files in batches
let activeOperations = 0;
// Pre-compile regex patterns for better performance
const compiledPatterns = {
include: include.map(pattern => ({
pattern,
regex: globToRegex(pattern),
hasBraces: pattern.includes('{') && pattern.includes('}')
})),
exclude: exclude.map(pattern => ({
pattern,
regex: globToRegex(pattern)
}))
};
// Helper function to convert glob pattern to regex (optimized)
function globToRegex(pattern) {
// Cache compiled regex patterns
if (!globToRegex.cache) {
globToRegex.cache = new Map();
}
if (globToRegex.cache.has(pattern)) {
return globToRegex.cache.get(pattern);
}
let regexPattern = pattern
.replace(/\*\*/g, '.*') // ** becomes .*
.replace(/\*/g, '[^/]*') // * becomes [^/]*
.replace(/\./g, '\\.') // . becomes \.
.replace(/\-/g, '\\-'); // - becomes \-
// Handle path separators for cross-platform compatibility
regexPattern = regexPattern.replace(/\//g, '[\\\\/]');
// For patterns like **/*.backup-*, we need to handle the path structure properly
if (pattern.startsWith('**/*')) {
const suffix = pattern.substring(4); // Remove **/* prefix
regexPattern = '.*' + suffix.replace(/\*/g, '[^/]*').replace(/\./g, '\\.').replace(/\-/g, '\\-').replace(/\//g, '[\\\\/]');
}
// For patterns like **/.neurolint/states-*.json, handle the path structure
if (pattern.startsWith('**/')) {
const suffix = pattern.substring(3); // Remove **/ prefix
regexPattern = '.*' + suffix.replace(/\*/g, '[^/]*').replace(/\./g, '\\.').replace(/\-/g, '\\-').replace(/\//g, '[\\\\/]');
}
const regex = new RegExp(regexPattern + '$', 'i'); // Case insensitive
globToRegex.cache.set(pattern, regex);
return regex;
}
// Helper function to check if file matches pattern (optimized)
function matchesPattern(filePath, patternInfo) {
// Handle brace expansion in patterns like **/*.{ts,tsx,js,jsx,json}
if (patternInfo.hasBraces) {
const pattern = patternInfo.pattern;
const braceStart = pattern.indexOf('{');
const braceEnd = pattern.indexOf('}');
const prefix = pattern.substring(0, braceStart);
const suffix = pattern.substring(braceEnd + 1);
const options = pattern.substring(braceStart + 1, braceEnd).split(',');
return options.some(opt => {
const expandedPattern = prefix + opt + suffix;
const expandedRegex = globToRegex(expandedPattern);
return expandedRegex.test(filePath);
});
}
// Use pre-compiled regex
return patternInfo.regex.test(filePath);
}
// Check if target is a single file
try {
const stats = await fs.stat(dir);
if (stats.isFile()) {
// Check if file should be included
const shouldInclude = compiledPatterns.include.some(patternInfo =>
matchesPattern(dir, patternInfo)
);
if (shouldInclude) {
return [dir];
}
return [];
}
} catch (error) {
// Not a file, continue with directory scanning
}
// Use worker threads for large directories
const useWorkers = process.env.NODE_ENV !== 'test' && require('worker_threads').isMainThread;
async function scanDirectory(currentDir) {
try {
// Limit concurrent operations with better queuing
if (activeOperations >= maxConcurrent) {
await new Promise(resolve => {
const checkQueue = () => {
if (activeOperations < maxConcurrent) {
resolve();
} else {
setImmediate(checkQueue);
}
};
checkQueue();
});
}
activeOperations++;
const entries = await fs.readdir(currentDir, { withFileTypes: true });
activeOperations--;
// Process entries in batches for better memory management
const batches = [];
for (let i = 0; i < entries.length; i += batchSize) {
batches.push(entries.slice(i, i + batchSize));
}
for (const batch of batches) {
const batchPromises = batch.map(async entry => {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
// Check if directory should be excluded
const shouldExclude = compiledPatterns.exclude.some(patternInfo => {
const pattern = patternInfo.pattern;
// Handle common exclusion patterns more efficiently
if (pattern === '**/node_modules/**' && entry.name === 'node_modules') {
return true;
}
if (pattern === '**/dist/**' && entry.name === 'dist') {
return true;
}
if (pattern === '**/.next/**' && entry.name === '.next') {
return true;
}
if (pattern === '**/build/**' && entry.name === 'build') {
return true;
}
if (pattern === '**/coverage/**' && entry.name === 'coverage') {
return true;
}
if (pattern === '**/.git/**' && entry.name === '.git') {
return true;
}
if (pattern === '**/.vscode/**' && entry.name === '.vscode') {
return true;
}
if (pattern === '**/.idea/**' && entry.name === '.idea') {
return true;
}
if (pattern === '**/.cache/**' && entry.name === '.cache') {
return true;
}
// Fallback to regex matching
return patternInfo.regex.test(fullPath);
});
if (!shouldExclude) {
await scanDirectory(fullPath);
}
} else if (entry.isFile()) {
// Check if file should be excluded first
const shouldExclude = compiledPatterns.exclude.some(patternInfo => {
const pattern = patternInfo.pattern;
// Handle common file exclusion patterns more efficiently
if (pattern.includes('*.log') && entry.name.endsWith('.log')) {
return true;
}
if (pattern.includes('*.tmp') && entry.name.endsWith('.tmp')) {
return true;
}
if (pattern.includes('*.backup') && entry.name.includes('.backup')) {
return true;
}
if (pattern.includes('*.min.js') && entry.name.endsWith('.min.js')) {
return true;
}
if (pattern.includes('*.bundle.js') && entry.name.endsWith('.bundle.js')) {
return true;
}
// Fallback to regex matching
return patternInfo.regex.test(fullPath);
});
if (!shouldExclude) {
// Check if file should be included
const shouldInclude = compiledPatterns.include.some(patternInfo =>
matchesPattern(fullPath, patternInfo)
);
if (shouldInclude) {
files.push(fullPath);
}
}
}
});
// Process batch with controlled concurrency
await Promise.all(batchPromises);
}
} catch (error) {
// Handle permission errors gracefully
if (error.code === 'EACCES' || error.code === 'EPERM') {
return; // Skip directories we can't access
}
throw error;
}
}
await scanDirectory(dir);
return files;
}
// Parse command line options
function parseOptions(args) {
const options = {
dryRun: args.includes('--dry-run'),
verbose: args.includes('--verbose'),
backup: !args.includes('--no-backup'),
layers: args.includes('--layers') ? args[args.indexOf('--layers') + 1].split(',').map(Number) : null,
allLayers: args.includes('--all-layers'),
include: args.includes('--include') ? args[args.indexOf('--include') + 1].split(',') : ['**/*.{ts,tsx,js,jsx,json}'],
exclude: args.includes('--exclude') ? args[args.indexOf('--exclude') + 1].split(',') : ['**/node_modules/**', '**/dist/**', '**/.next/**'],
format: 'console',
output: null,
init: args.includes('--init'),
show: args.includes('--show'),
states: args.includes('--states'),
olderThan: null,
keepLatest: null,
list: args.includes('--list'),
delete: null,
reset: args.includes('--reset'),
edit: null,
confidence: null,
export: null,
import: null
};
// Parse format and output from args
for (let i = 0; i < args.length; i++) {
if (args[i] === '--format' && i + 1 < args.length) {
options.format = args[i + 1];
} else if (args[i] === '--output' && i + 1 < args.length) {
options.output = args[i + 1];
} else if (args[i] === '--older-than' && i + 1 < args.length) {
options.olderThan = parseInt(args[i + 1]);
} else if (args[i] === '--keep-latest' && i + 1 < args.length) {
options.keepLatest = parseInt(args[i + 1]);
} else if (args[i] === '--delete' && i + 1 < args.length) {
options.delete = args[i + 1];
} else if (args[i] === '--edit' && i + 1 < args.length) {
options.edit = args[i + 1];
} else if (args[i] === '--confidence' && i + 1 < args.length) {
options.confidence = parseFloat(args[i + 1]);
} else if (args[i] === '--export' && i + 1 < args.length) {
options.export = args[i + 1];
} else if (args[i] === '--import' && i + 1 < args.length) {
options.import = args[i + 1];
} else if (args[i].startsWith('--format=')) {
options.format = args[i].split('=')[1];
} else if (args[i].startsWith('--output=')) {
options.output = args[i].split('=')[1];
} else if (args[i].startsWith('--older-than=')) {
options.olderThan = parseInt(args[i].split('=')[1]);
} else if (args[i].startsWith('--keep-latest=')) {
options.keepLatest = parseInt(args[i].split('=')[1]);
} else if (args[i].startsWith('--delete=')) {
options.delete = args[i].split('=')[1];
} else if (args[i].startsWith('--edit=')) {
options.edit = args[i].split('=')[1];
} else if (args[i].startsWith('--confidence=')) {
options.confidence = parseFloat(args[i].split('=')[1]);
} else if (args[i].startsWith('--export=')) {
options.export = args[i].split('=')[1];
} else if (args[i].startsWith('--import=')) {
options.import = args[i].split('=')[1];
}
}
return options;
}
// Enhanced output functions to replace emoji-based spinners
function logSuccess(message) {
console.log(`[SUCCESS] ${message}`);
}
function logError(message) {
console.error(`[ERROR] ${message}`);
}
function logWarning(message) {
console.warn(`[WARNING] ${message}`);
}
function logInfo(message) {
console.log(`[INFO] ${message}`);
}
function logProgress(message) {
process.stdout.write(`[PROCESSING] ${message}...`);
}
function logComplete(message) {
process.stdout.write(`[COMPLETE] ${message}\n`);
}
// Handle analyze command
async function handleAnalyze(targetPath, options, spinner) {
try {
// Initialize shared core
await sharedCore.core.initialize({ platform: 'cli' });
const files = await getFiles(targetPath, options.include, options.exclude);
let totalIssues = 0;
const results = [];
// Show progress for large file sets
if (files.length > 10 && options.verbose) {
process.stdout.write(`Processing ${files.length} files...\n`);
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Update progress for large operations
if (files.length > 10 && i % Math.max(1, Math.floor(files.length / 10)) === 0) {
spinner.text = `Analyzing files... ${Math.round((i / files.length) * 100)}%`;
}
try {
const code = await fs.readFile(file, 'utf8');
// Use shared core for analysis instead of direct SmartLayerSelector
const analysisResult = await sharedCore.analyze(code, {
filename: file,
platform: 'cli',
layers: options.layers || [1, 2, 3, 4, 5, 6, 7],
verbose: options.verbose
});
totalIssues += analysisResult.issues.length;
if (options.verbose) {
console.log(`[ANALYZED] ${file}`);
console.log(` Issues Found: ${analysisResult.issues.length}`);
console.log(` Recommended Layers: ${analysisResult.summary?.recommendedLayers?.join(', ') || '1,2'}`);
if (analysisResult.issues.length > 0) {
console.log(` Issue Types:`);
const issueTypes = {};
analysisResult.issues.forEach(issue => {
const type = issue.type || 'Unknown';
issueTypes[type] = (issueTypes[type] || 0) + 1;
});
Object.entries(issueTypes).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
}
}
results.push({
file,
issues: analysisResult.issues,
recommendedLayers: analysisResult.summary?.recommendedLayers || [1, 2],
analysisResult
});
} catch (error) {
if (options.verbose) {
process.stderr.write(`Warning: Could not analyze ${file}: ${error.message}\n`);
}
}
}
if (options.format === 'json' && options.output) {
const analysisResult = {
summary: {
filesAnalyzed: files.length,
issuesFound: totalIssues,
recommendedLayers: [...new Set(results.flatMap(r => r.recommendedLayers))].sort()
},
files: results,
issues: results.flatMap(r => r.issues.map(issue => ({
...issue,
file: r.file
}))),
layers: results.flatMap(r => r.recommendedLayers).map(layerId => ({
layerId: parseInt(layerId),
success: true,
changeCount: results.filter(r => r.recommendedLayers.includes(layerId)).length,
description: `Layer ${layerId} analysis`
})),
confidence: 0.8,
qualityScore: Math.max(0, 100 - (totalIssues * 5)),
readinessScore: Math.min(100, (results.length / Math.max(1, files.length)) * 100)
};
await fs.writeFile(options.output, JSON.stringify(analysisResult, null, 2));
} else {
// Enhanced analysis summary
console.log(`\n[ANALYSIS SUMMARY]`);
console.log(` Files Analyzed: ${files.length}`);
console.log(` Total Issues Found: ${totalIssues}`);
console.log(` Average Issues per File: ${(totalIssues / files.length).toFixed(1)}`);
// Calculate layer recommendations
const layerCounts = {};
results.forEach(r => {
r.recommendedLayers.forEach(layer => {
layerCounts[layer] = (layerCounts[layer] || 0) + 1;
});
});
if (Object.keys(layerCounts).length > 0) {
console.log(` Layer Recommendations:`);
Object.entries(layerCounts)
.sort(([a], [b]) => parseInt(a) - parseInt(b))
.forEach(([layer, count]) => {
const percentage = ((count / files.length) * 100).toFixed(1);
console.log(` Layer ${layer}: ${count} files (${percentage}%)`);
});
}
}
// Stop spinner and use enhanced completion message
spinner.stop();
logComplete('Analysis completed');
} catch (error) {
logError(`Analysis failed: ${error.message}`);
throw error;
}
}
// Handle fix command
async function handleFix(targetPath, options, spinner, startTime) {
try {
// Check authentication and tier limits for fix operations
// Skip authentication check in development mode
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.NEUROLINT_DEV === 'true';
// Determine requested layers; default unauthenticated to layers 1-2 per pricing update
let requestedLayers = null;
if (options.allLayers) {
requestedLayers = [1, 2, 3, 4, 5, 6, 7];
} else if (Array.isArray(options.layers) && options.layers.length > 0) {
requestedLayers = options.layers;
}
if (!authManager.isAuthenticated() && !isDevelopment) {
if (!requestedLayers) {
// Default to free tier layers when none specified
options.layers = [1, 2];
requestedLayers = options.layers;
}
const freeAllowed = requestedLayers.every(l => l <= 2);
if (!freeAllowed) {
logError('Authentication required for selected layers');
console.log('Free tier allows fixes for layers 1-2 without authentication');
console.log('Run "neurolint login <api-key>" to enable higher layers');
console.log(`Get your API key from: ${API_BASE_URL}/dashboard`);
process.exit(1);
}
// else: allow free tier fixes to proceed
}
const usage = await authManager.checkUsage();
if (authManager.isAuthenticated() && !usage.canUseFixes && !isDevelopment) {
logError('Fix operations not available on your current plan');
console.log(`Current plan: ${usage.tier}`);
console.log(`Upgrade your plan at: ${API_BASE_URL}/pricing`);
process.exit(1);
}
// Check layer access if specific layers are requested
if (requestedLayers && requestedLayers.length > 0 && !isDevelopment) {
const layerAccess = await authManager.canUseLayers(requestedLayers);
if (!layerAccess.allowed) {
logError(`Layer access restricted on your current plan`);
console.log(`Restricted layers: ${layerAccess.restrictedLayers.join(', ')}`);
console.log(`Current plan: ${layerAccess.tier}`);
console.log(`Upgrade your plan at: ${API_BASE_URL}/pricing`);
process.exit(1);
}
}
const files = await getFiles(targetPath, options.include, options.exclude);
let processedFiles = 0;
let successfulFixes = 0;
for (const file of files) {
try {
spinner.text = `Processing ${path.basename(file)}...`;
const result = await fixFile(file, options, spinner);
if (result.success) {
successfulFixes++;
}
processedFiles++;
} catch (error) {
if (options.verbose) {
process.stderr.write(`Warning: Could not process ${file}: ${error.message}\n`);
}
}
}
if (options.format === 'json' && options.output) {
const fixResult = {
success: successfulFixes > 0,
processedFiles,
successfulFixes,
appliedFixes: successfulFixes,
summary: {
totalFiles: files.length,
processedFiles,
successfulFixes,
failedFiles: files.length - processedFiles
}
};
await fs.writeFile(options.output, JSON.stringify(fixResult, null, 2));
} else {
// Enhanced summary output
console.log(`\n[FIX SUMMARY]`);
console.log(` Files Processed: ${processedFiles}`);
console.log(` Fixes Applied: ${successfulFixes}`);
console.log(` Files Failed: ${files.length - processedFiles}`);
console.log(` Success Rate: ${((processedFiles / files.length) * 100).toFixed(1)}%`);
if (options.verbose && successfulFixes > 0 && startTime) {
const executionTime = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(` Total Execution Time: ${executionTime}s`);
}
}
// Stop spinner and use enhanced completion message
spinner.stop();
logComplete('Fix operation completed');
} catch (error) {
logError(`Fix failed: ${error.message}`);
throw error;
}
}
// Handle layers command
async function handleLayers(options, spinner) {
const layers = [
{ id: 1, name: 'Configuration', description: 'Updates tsconfig.json, next.config.js, package.json' },
{ id: 2, name: 'Patterns', description: 'Standardizes variables, removes console statements' },
{ id: 3, name: 'Components', description: 'Adds keys, accessibility attributes, prop types' },
{ id: 4, name: 'Hydration', description: 'Guards client-side APIs for SSR' },
{ id: 5, name: 'Next.js', description: 'Optimizes App Router with directives' },
{ id: 6, name: 'Testing', description: 'Adds error boundaries, prop types, loading states' },
{ id: 7, name: 'Adaptive Pattern Learning', description: 'Learns and applies patterns from prior fixes' }
];
if (options.verbose) {
layers.forEach(layer => process.stdout.write(`Layer ${layer.id}: ${layer.name} - ${layer.description}\n`));
} else {
layers.forEach(layer => process.stdout.write(`Layer ${layer.id}: ${layer.name}\n`));
}
}
// Handle init-config command
async function handleInitConfig(options, spinner) {
try {
const configPath = path.join(process.cwd(), CONFIG_FILE);
if (options.init) {
const defaultConfig = {
apiKey: null, // Placeholder, will be set after authentication
enabledLayers: [1, 2, 3, 4, 5, 6, 7],
include: ['**/*.{ts,tsx,js,jsx,json}'],
exclude: ['**/node_modules/**', '**/dist/**', '**/.next/**'],
backup: true,
verbose: false,
dryRun: false,
maxRetries: 3,
batchSize: 50,
maxConcurrent: 10
};
await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
logSuccess(`Created ${configPath}`);
} else if (options.show) {
try {
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
process.stdout.write(JSON.stringify(config, null, 2) + '\n');
logSuccess('Config displayed');
} catch (error) {
logError('No configuration file found. Use --init to create one.');
process.exit(1);
}
} else {
// Validate existing config
try {
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
// Validate required fields
const requiredFields = ['apiKey', 'enabledLayers', 'include', 'exclude'];
const missingFields = requiredFields.filter(field => !config[field]);
if (missingFields.length > 0) {
logWarning(`Missing required fields: ${missingFields.join(', ')}`);
}
// Validate layer configuration
if (config.enabledLayers && !Array.isArray(config.enabledLayers)) {
logWarning('Enabled layers must be an array');
}
// Validate file patterns
if (config.include && !Array.isArray(config.include)) {
logWarning('Include patterns must be an array');
}
if (config.exclude && !Array.isArray(config.exclude)) {
logWarning('Exclude patterns must be an array');
}
logSuccess('Configuration validated');
} catch (error) {
logError('Invalid configuration file');
process.exit(1);
}
}
} catch (error) {
logError(`Init-config failed: ${error.message}`);
throw error;
}
}
// Handle validate command
async function handleValidate(targetPath, options, spinner) {
try {
const files = await getFiles(targetPath, options.include, options.exclude);
let validFiles = 0;
let invalidFiles = 0;
const results = [];
for (const file of files) {
try {
const validation = await TransformationValidator.validateFile(file);
if (validation.isValid) {
validFiles++;
if (options.verbose) {
process.stdout.write(`[VALID] ${file}: Valid\n`);
}
} else {
invalidFiles++;
if (options.verbose) {
process.stderr.write(`[INVALID] ${file}: Invalid - ${validation.error}\n`);
}
}
results.push({ file, ...validation });
} catch (error) {
invalidFiles++;
if (options.verbose) {
process.stderr.write(`[ERROR] ${file}: Error - ${error.message}\n`);
}
results.push({ file, isValid: false, error: error.message });
}
}
if (options.format === 'json' && options.output) {
const validationResult = {
summary: {
filesValidated: files.length,
validFiles,
invalidFiles
},
files: results
};
await fs.writeFile(options.output, JSON.stringify(validationResult, null, 2));
} else {
process.stdout.write(`Validated ${files.length} files, ${invalidFiles} invalid\n`);
}
// Stop spinner before outputting completion message
spinner.stop();
process.stdout.write('completed\n');
} catch (error) {
logError(`Validate failed: ${error.message}`);
throw error;
}
}
// Handle init-tests command
async function handleInitTests(targetPath, options, spinner) {
try {
const files = await getFiles(targetPath, options.include, options.exclude);
let generatedTests = 0;
const results = [];
for (const file of files) {
try {
const code = await fs.readFile(file, 'utf8');
const testCode = generateTestCode(code, file);
if (!options.dryRun) {
const testFilePath = file.replace(/\.[jt]sx?$/, '.test.$1');
await fs.writeFile(testFilePath, testCode);
if (options.verbose) {
process.stdout.write(`Generated ${testFilePath}\n`);
}
generatedTests++;
} else {
if (options.verbose) {
process.stdout.write(`[Dry Run] Would generate ${file.replace(/\.[jt]sx?$/, '.test.$1')}\n`);
process.stdout.write(testCode);
}
generatedTests++;
}
results.push({ file, testCode });
} catch (error) {
if (options.verbose) {
process.stderr.write(`Warning: Could not generate test for ${file}: ${error.message}\n`);
}
}
}
if (options.format === 'json' && options.output) {
const testResult = {
summary: {
filesProcessed: files.length,
testsGenerated: generatedTests
},
files: results
};
await fs.writeFile(options.output, JSON.stringify(testResult, null, 2));
} else {
process.stdout.write(`Generated ${generatedTests} test files\n`);
}
// Stop spinner before outputting completion message
spinner.stop();
process.stdout.write('completed\n');
} catch (error) {
logError(`Init-tests failed: ${error.message}`);
throw error;
}
}
// Generate test code for components
function generateTestCode(code, filePath) {
const componentName = code.match(/export default function (\w+)/)?.[1] || path.basename(filePath, path.extname(filePath));
return `
import { render, screen } from '@testing-library/react';
import ${componentName} from '${filePath.replace(process.cwd(), '.')}';
describe('${componentName}', () => {
it('renders without crashing', () => {
render(<${componentName} />);
expect(screen.getByText(/.+/)).toBeInTheDocument();
});
});
`.trim();
}
// Handle stats command with performance metrics
async function handleStats(options, spinner) {
try {
const targetPath = options.targetPath || process.cwd();
const include = options.include || ['**/*.{ts,tsx,js,jsx,json}'];
const exclude = options.exclude || ['**/node_modules/**', '**/dist/**', '**/.next/**'];
spinner.text = 'Scanning files...';
// Start memory tracking
MemoryManager.startTracking();
const startTime = performance.now();
const files = await getFiles(targetPath, include, exclude);
const scanTime = performance.now() - startTime;
if (files.length === 0) {
logSuccess('No files found');
return;
}
spinner.text = `Analyzing ${files.length} files...`;
// Use memory-managed processing for large file sets
const analysisOptions = {
batchSize: 200,
maxConcurrent: 20,
memoryThreshold: 800, // MB
gcInterval: 5,
verbose: options.verbose,
suppressErrors: true, // Suppress verbose AST parsing errors
maxErrors: 20, // Show only first 20 errors
onProgress: (progress, memoryReport) => {
spinner.text = `Analyzing ${files.length} files... ${progress.toFixed(1)}% (${memoryReport.current.heapUsed}MB RAM)`;
}
};
const analysisStartTime = performance.now();
// Process files with memory management
const analysisResults = await processFilesWithMemoryManagement(
files,
async (filePath) => {
try {
const code = await fs.readFile(filePath, 'utf8');
const issues = await analyzeFile(code, filePath, options);
return {
file: filePath,
issues: issues.length,
success: true,
error: null
};
} catch (error) {
return {
file: filePath,
issues: 0,
success: false,
error: error.message
};
}
},
analysisOptions
);
const analysisTime = performance.now() - analysisStartTime;
// Calculate statistics
const successfulAnalyses = analysisResults.filter(r => r.success);
const failedAnalyses = analysisResults.filter(r => !r.success);
const totalIssues = successfulAnalyses.reduce((sum, r) => sum + r.issues, 0);
// Get backup and state file counts
const backupFiles = files.filter(f => f.includes('.backup-'));
const stateFiles = files.filter(f => f.includes('.neurolint/states-'));
// Get memory report
const memoryReport = MemoryManager.getReport();
// Load rule store for learned rules count
const ruleStore = new RuleStore();
await ruleStore.load();
const stats = {
filesAnalyzed: files.length,
filesSuccessful: successfulAnalyses.length,
filesFailed: failedAnalyses.length,
issuesFound: totalIssues,
learnedRules: ruleStore.rules.length,
stateFiles: stateFiles.length,
backupFiles: backupFiles.length,
performance: {
scanTime: Math.round(scanTime),
analysisTime: Math.round(analysisTime),
totalTime: Math.round(scanTime + analysisTime),
filesPerSecond: Math.round(files.length / ((scanTime + analysisTime) / 1000)),
memoryUsage: memoryReport
},
errors: failedAnalyses.map(f => f.error).slice(0, 10) // Limit error reporting
};
if (options.format === 'json' && options.output) {
await fs.writeFile(options.output, JSON.stringify(stats, null, 2));
} else {
process.stdout.write(`Files: ${stats.filesAnalyzed} (${stats.filesSuccessful} successful, ${stats.filesFailed} failed)\n`);
process.stdout.write(`Issues: ${stats.issuesFound}\n`);
process.stdout.write(`States: ${stats.stateFiles}, Backups: ${stats.backupFiles}\n`);
process.stdout.write(`Learned Rules: ${stats.learnedRules}\n`);
process.stdout.write(`Performance: ${stats.performance.totalTime}ms (${stats.performance.filesPerSecond} files/sec)\n`);
process.stdout.write(`Memory: ${stats.performance.memoryUsage.current.heapUsed}MB (peak: ${stats.performance.memoryUsage.peak}MB)\n`);
if (stats.errors.length > 0) {
process.stderr.write(`Errors: ${stats.errors.length} files failed analysis\n`);
}
}
// Don't call spinner.succeed here - let the main command handler do it
} catch (error) {
logError(`Stats failed: ${error.message}`);
throw error;
}
}
// Handle clean command with performance optimizations
async function handleClean(options, spinner) {
try {
const targetPath = options.targetPath || process.cwd();
const include = ['**/*.backup-*', ...(options.states ? ['.neurolint/states-*.json'] : [])];
// Use streaming approach for large file sets
const files = await getFiles(targetPath, include, options.exclude);
if (files.length === 0) {
logSuccess('No files to clean');
return;
}
spinner.text = `Processing ${files.length} files...`;
let deletedCount = 0;
const batchSize = 50; // Process deletions in batches
const errors = [];
// Group files by directory for better performance
const filesByDir = new Map();
for (const file of files) {
const dir = path.dirname(file);
if (!filesByDir.has(dir)) {
filesByDir.set(dir, []);
}
filesByDir.get(dir).push(file);
}
// Process directories in parallel with controlled concurrency
const dirs = Array.from(filesByDir.keys());
const maxConcurrentDirs = 10;
for (let i = 0; i < dirs.length; i += maxConcurrentDirs) {
const dirBatch = dirs.slice(i, i + maxConcurrentDirs);
const dirPromises = dirBatch.map(async dir => {
const dirFiles = filesByDir.get(dir);
// Get file stats in parallel for this directory
const fileStats = await Promise.all(
dirFiles.map(async file => {
try {
const stats = await fs.stat(file);
return { file, stats, error: null };
} catch (error) {
return { file, stats: null, error };
}
})
);
// Filter out files with errors
const validFiles = fileStats.filter(f => !f.error);
const errorFiles = fileStats.filter(f => f.error);
// Add errors to global error list
errorFiles.forEach(f => errors.push(f.error));
// Sort files by timestamp for --keep-latest
const sortedFiles = validFiles
.map(({ file, stats }) => ({
file,
mtime: stats.mtimeMs,
ageInDays: (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24)
}))
.sort((a, b) => b.mtime - a.mtime);
// Apply filters
let filesToDelete = sortedFiles;
if (options.keepLatest) {
// Group by base filename for --keep-latest
const filesByBase = new Map();
for (const fileInfo of sortedFiles) {
const baseName = path.basename(fileInfo.file).replace(/\.backup-\d+$/, '');
if (!filesByBase.has(baseName)) {
filesByBase.set(baseName, []);
}
filesByBase.get(baseName).push(fileInfo);
}
filesToDelete = [];
for (const [baseN