UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

458 lines (417 loc) 12.5 kB
import chalk from "chalk"; import axios, { AxiosError } from "axios"; export interface ErrorContext { operation: string; file?: string; layer?: number; config?: any; } export interface RecoveryAction { type: "retry" | "skip" | "abort" | "configure"; message: string; action?: () => Promise<void>; } /** * Robust error recovery system for CLI operations * Provides user-friendly error messages and recovery suggestions */ export class ErrorRecoveryManager { /** * Categorize and handle different types of errors */ static categorizeError( error: any, context: ErrorContext, ): { category: string; severity: "low" | "medium" | "high" | "critical"; message: string; recoveryActions: RecoveryAction[]; } { const errorMessage = error?.message || error?.toString() || "Unknown error"; // Network/API errors if (axios.isAxiosError(error)) { return this.handleApiError(error, context); } // File system errors if (errorMessage.includes("ENOENT") || errorMessage.includes("EACCES")) { return this.handleFileSystemError(error, context); } // Configuration errors if (errorMessage.includes("config") || errorMessage.includes("JSON")) { return this.handleConfigError(error, context); } // Validation errors if ( errorMessage.includes("Invalid") || errorMessage.includes("validation") ) { return this.handleValidationError(error, context); } // Permission errors if (errorMessage.includes("permission") || errorMessage.includes("EPERM")) { return this.handlePermissionError(error, context); } // Timeout errors if ( errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT") ) { return this.handleTimeoutError(error, context); } // Generic error return { category: "unknown", severity: "medium", message: `Unexpected error during ${context.operation}`, recoveryActions: [ { type: "retry", message: "Try the operation again", }, { type: "abort", message: "Report this issue with error details", }, ], }; } private static handleApiError(error: AxiosError, context: ErrorContext) { const status = error.response?.status; const statusText = error.response?.statusText; switch (status) { case 401: return { category: "authentication", severity: "high" as const, message: "Authentication failed - API key is invalid or expired", recoveryActions: [ { type: "configure" as const, message: "Run 'neurolint login' to re-authenticate", }, { type: "abort" as const, message: "Check your API key in the dashboard", }, ], }; case 403: return { category: "authorization", severity: "high" as const, message: "Access denied - insufficient permissions for this operation", recoveryActions: [ { type: "configure" as const, message: "Check your plan limits and permissions", }, { type: "abort" as const, message: "Contact support for access issues", }, ], }; case 404: return { category: "api", severity: "medium" as const, message: "API endpoint not found - server may be outdated", recoveryActions: [ { type: "configure" as const, message: "Check API URL configuration", }, { type: "retry" as const, message: "Update NeuroLint to the latest version", }, ], }; case 429: return { category: "rate-limit", severity: "medium" as const, message: "Rate limit exceeded - too many requests", recoveryActions: [ { type: "retry" as const, message: "Wait a few minutes and try again", }, { type: "configure" as const, message: "Consider upgrading your plan for higher limits", }, ], }; case 500: case 502: case 503: return { category: "server", severity: "high" as const, message: `Server error (${status}) - NeuroLint service may be down`, recoveryActions: [ { type: "retry" as const, message: "Try again in a few minutes", }, { type: "abort" as const, message: "Check NeuroLint status page for updates", }, ], }; default: if (error.code === "ECONNREFUSED") { return { category: "connection", severity: "critical" as const, message: "Cannot connect to NeuroLint server", recoveryActions: [ { type: "configure" as const, message: "Check if server is running (npm run dev)", }, { type: "configure" as const, message: "Verify API URL in configuration", }, { type: "abort" as const, message: "Check network connectivity", }, ], }; } return { category: "network", severity: "medium" as const, message: `Network error: ${statusText || error.message}`, recoveryActions: [ { type: "retry" as const, message: "Check network connection and try again", }, { type: "configure" as const, message: "Verify API URL and firewall settings", }, ], }; } } private static handleFileSystemError(error: any, context: ErrorContext) { const errorMessage = error.message || ""; if (errorMessage.includes("ENOENT")) { return { category: "file-not-found", severity: "medium" as const, message: `File or directory not found: ${context.file || "unknown"}`, recoveryActions: [ { type: "skip" as const, message: "Skip this file and continue", }, { type: "abort" as const, message: "Check file path and try again", }, ], }; } if (errorMessage.includes("EACCES")) { return { category: "permission", severity: "high" as const, message: `Permission denied accessing: ${context.file || "file"}`, recoveryActions: [ { type: "configure" as const, message: "Check file permissions (chmod/chown)", }, { type: "skip" as const, message: "Skip this file and continue", }, ], }; } return { category: "filesystem", severity: "medium" as const, message: `File system error: ${errorMessage}`, recoveryActions: [ { type: "retry" as const, message: "Try the operation again", }, { type: "skip" as const, message: "Skip problematic files", }, ], }; } private static handleConfigError(error: any, context: ErrorContext) { return { category: "configuration", severity: "high" as const, message: "Configuration file is invalid or corrupted", recoveryActions: [ { type: "configure" as const, message: "Run 'neurolint init --force' to reset configuration", }, { type: "configure" as const, message: "Check .neurolint.json for syntax errors", }, { type: "abort" as const, message: "Manually fix configuration file", }, ], }; } private static handleValidationError(error: any, context: ErrorContext) { return { category: "validation", severity: "medium" as const, message: `Input validation failed: ${error.message}`, recoveryActions: [ { type: "configure" as const, message: "Check command parameters and options", }, { type: "abort" as const, message: "Run with --help for usage information", }, ], }; } private static handlePermissionError(error: any, context: ErrorContext) { return { category: "permission", severity: "high" as const, message: "Insufficient permissions for this operation", recoveryActions: [ { type: "configure" as const, message: "Run with appropriate permissions (sudo if needed)", }, { type: "configure" as const, message: "Check file/directory ownership", }, { type: "skip" as const, message: "Skip files with permission issues", }, ], }; } private static handleTimeoutError(error: any, context: ErrorContext) { return { category: "timeout", severity: "medium" as const, message: "Operation timed out", recoveryActions: [ { type: "retry" as const, message: "Try again with longer timeout", }, { type: "configure" as const, message: "Increase timeout in configuration", }, { type: "skip" as const, message: "Process fewer files at once", }, ], }; } /** * Display error with recovery options */ static displayError(error: any, context: ErrorContext): void { const errorInfo = this.categorizeError(error, context); // Display error header const severityIcon = { low: chalk.blue("INFO"), medium: chalk.yellow("WARNING"), high: chalk.red("ERROR"), critical: chalk.red.bold("CRITICAL"), }[errorInfo.severity]; console.log(); console.log(`${severityIcon}: ${errorInfo.message}`); if (context.file) { console.log(chalk.gray(` File: ${context.file}`)); } if (context.layer) { console.log(chalk.gray(` Layer: ${context.layer}`)); } // Display recovery actions if (errorInfo.recoveryActions.length > 0) { console.log(chalk.white("\nSuggested actions:")); errorInfo.recoveryActions.forEach((action, index) => { const actionIcon = { retry: "↻", skip: "⏭", abort: "✗", configure: "⚙", }[action.type]; console.log( chalk.gray(` ${index + 1}. ${actionIcon} ${action.message}`), ); }); } // Additional context for developers if (process.env.NEUROLINT_DEBUG) { console.log(chalk.gray("\nDebug information:")); console.log(chalk.gray(` Error type: ${errorInfo.category}`)); console.log(chalk.gray(` Operation: ${context.operation}`)); if (error.stack) { console.log(chalk.gray(` Stack: ${error.stack.split("\n")[0]}`)); } } console.log(); } /** * Handle errors with automatic recovery */ static async handleErrorWithRecovery( error: any, context: ErrorContext, autoRecover: boolean = false, ): Promise<"retry" | "skip" | "abort"> { this.displayError(error, context); const errorInfo = this.categorizeError(error, context); // Automatic recovery for certain error types if (autoRecover) { if ( errorInfo.category === "file-not-found" || errorInfo.category === "permission" ) { console.log(chalk.yellow("Auto-recovering: Skipping problematic file")); return "skip"; } if (errorInfo.category === "rate-limit") { console.log( chalk.yellow("Auto-recovering: Waiting for rate limit reset"), ); await new Promise((resolve) => setTimeout(resolve, 60000)); // Wait 1 minute return "retry"; } } // For critical errors, always abort if (errorInfo.severity === "critical") { return "abort"; } // Default to skip for non-critical errors return "skip"; } }