@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
text/typescript
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";
}
}