UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

333 lines 12.7 kB
import chalk from 'chalk'; import * as yaml from 'js-yaml'; import * as clack from '@clack/prompts'; import { enhanceError } from '@xec-sh/core'; import { ValidationError } from './validation.js'; export class XecError extends Error { constructor(message, code, details) { super(message); this.code = code; this.details = details; this.name = 'XecError'; } } export class ConfigurationError extends XecError { constructor(message, field, suggestion) { super(message, 'CONFIG_ERROR', { field, suggestion }); this.name = 'ConfigurationError'; } } export class ModuleError extends XecError { constructor(message, moduleName, suggestion) { super(message, 'MODULE_ERROR', { field: moduleName, suggestion }); this.name = 'ModuleError'; } } export class TaskError extends XecError { constructor(message, taskName, suggestion) { super(message, 'TASK_ERROR', { field: taskName, suggestion }); this.name = 'TaskError'; } } export class RecipeError extends XecError { constructor(message, recipeName, suggestion) { super(message, 'RECIPE_ERROR', { field: recipeName, suggestion }); this.name = 'RecipeError'; } } export class NetworkError extends XecError { constructor(message, url, suggestion) { super(message, 'NETWORK_ERROR', { field: url, suggestion }); this.name = 'NetworkError'; } } export class FileSystemError extends XecError { constructor(message, path, suggestion) { super(message, 'FILESYSTEM_ERROR', { field: path, suggestion }); this.name = 'FileSystemError'; } } export class TimeoutError extends XecError { constructor(message, operation, suggestion) { super(message, 'TIMEOUT_ERROR', { field: operation, suggestion }); this.name = 'TimeoutError'; } } export function handleError(error, options) { if (options.quiet && !isCriticalError(error)) { process.exit(1); } const enhancedError = enhanceErrorWithContext(error, options); if (process.env['XEC_DEBUG'] === '1' || process.env['XEC_DEBUG'] === 'true') { console.error(chalk.red('\n=== DEBUG ERROR INFO ===')); console.error(chalk.gray('Error type:'), error.constructor.name); console.error(chalk.gray('Error code:'), error.code || 'none'); console.error(chalk.gray('Error message:'), error.message); if (error.path) console.error(chalk.gray('Path:'), error.path); if (error.syscall) console.error(chalk.gray('Syscall:'), error.syscall); if (error.stack) { console.error(chalk.gray('\nStack trace:')); console.error(chalk.gray(error.stack)); } console.error(chalk.red('======================\n')); } if (options.output === 'json') { console.error(JSON.stringify(formatEnhancedErrorAsJSON(enhancedError), null, 2)); } else if (options.output === 'yaml') { console.error(yaml.dump(formatEnhancedErrorAsJSON(enhancedError))); } else { displayEnhancedError(enhancedError, options); } process.exit(getExitCode(error)); } function isCriticalError(error) { return error instanceof ValidationError || error instanceof ConfigurationError || error.code === 'MODULE_NOT_FOUND' || error.code === 'PERMISSION_DENIED'; } function extractErrorInfo(error) { const baseInfo = { error: true, message: error.message || 'Unknown error', timestamp: new Date().toISOString(), }; if (error instanceof XecError) { return { ...baseInfo, type: error.name, code: error.code, field: error.details?.field, suggestion: error.details?.suggestion, documentation: error.details?.documentation, }; } if (error instanceof ValidationError) { return { ...baseInfo, type: 'ValidationError', code: 'VALIDATION_ERROR', field: error.field, suggestion: getValidationSuggestion(error), }; } if (error.code) { return { ...baseInfo, type: 'SystemError', code: error.code, path: error.path, syscall: error.syscall, suggestion: getSystemErrorSuggestion(error), }; } return { ...baseInfo, type: 'Error', stack: error.stack, }; } function enhanceErrorWithContext(error, options) { const context = { cwd: process.cwd(), timestamp: new Date() }; if (error.context && error.suggestions) { Object.assign(error.context, context); return error; } return enhanceError(error, context); } function formatEnhancedErrorAsJSON(error) { return { error: true, message: error.message, code: error.code || 'UNKNOWN_ERROR', timestamp: new Date().toISOString(), context: error.context, suggestions: error.suggestions, systemInfo: error.systemInfo, type: error.name }; } function displayEnhancedError(error, options) { const formatted = error.format ? error.format(options.verbose) : error.message; const lines = formatted.split('\n'); lines.forEach(line => { if (!line) return; if (line.startsWith('Error:')) { clack.log.error(chalk.bold(line)); } else if (line.includes('Context:') || line.includes('Suggestions:')) { console.error(chalk.yellow(line)); } else if (line.includes('Try:') || line.includes('See:')) { console.error(chalk.cyan(line)); } else if (line.includes('Code:')) { console.error(chalk.gray(line)); } else { console.error(line); } }); if (!options.verbose) { console.error(''); console.error(chalk.dim('Run with --verbose for more details')); } if (error.context?.command) { const baseCommand = error.context.command.split(' ')[0]; console.error(chalk.dim(`Run 'xec ${baseCommand} --help' for usage information`)); } } function displayTextError(errorInfo, options) { clack.log.error(chalk.bold(errorInfo.message)); if (errorInfo.field) { console.error(chalk.gray(`Field: ${errorInfo.field}`)); } if (errorInfo.code) { console.error(chalk.gray(`Code: ${errorInfo.code}`)); } if (errorInfo.suggestion) { console.error(); console.error(chalk.yellow('💡 Suggestion:')); console.error(chalk.yellow(` ${errorInfo.suggestion}`)); } if (errorInfo.documentation) { console.error(); console.error(chalk.blue('📚 Documentation:')); console.error(chalk.blue(` ${errorInfo.documentation}`)); } if (options.verbose && errorInfo.stack) { console.error(); console.error(chalk.gray('Stack trace:')); console.error(chalk.gray(errorInfo.stack)); } if (options.verbose) { console.error(); console.error(chalk.gray('Debug information:')); console.error(chalk.gray(` Time: ${errorInfo.timestamp}`)); console.error(chalk.gray(` Type: ${errorInfo.type}`)); if (errorInfo.path) { console.error(chalk.gray(` Path: ${errorInfo.path}`)); } if (errorInfo.syscall) { console.error(chalk.gray(` Syscall: ${errorInfo.syscall}`)); } } } function getExitCode(error) { if (error instanceof ValidationError) return 2; if (error instanceof ConfigurationError) return 3; if (error instanceof ModuleError) return 4; if (error instanceof TaskError) return 5; if (error instanceof RecipeError) return 6; if (error instanceof NetworkError) return 7; if (error instanceof FileSystemError) return 8; if (error instanceof TimeoutError) return 9; if (error.code === 'ENOENT') return 10; if (error.code === 'EACCES') return 11; if (error.code === 'ENOTDIR') return 12; if (error.code === 'EISDIR') return 13; return 1; } function getValidationSuggestion(error) { if (error.field === 'filePath') { return 'Check that the file path is correct and the file exists'; } if (error.field === 'directoryPath') { return 'Check that the directory path is correct and the directory exists'; } if (error.field === 'json') { return 'Ensure the JSON is properly formatted with matching quotes and brackets'; } if (error.field === 'variables') { return 'Use JSON format like \'{"key": "value"}\' or key=value pairs'; } if (error.field === 'timeout') { return 'Use formats like "30s", "5m", "1h" or number in milliseconds'; } if (error.field === 'hostPattern') { return 'Use valid hostname, IP address, or wildcard pattern'; } if (error.field === 'tagPattern') { return 'Use alphanumeric characters, dots, hyphens, and underscores only'; } return 'Check the documentation for valid input formats'; } function getSystemErrorSuggestion(error) { switch (error.code) { case 'ENOENT': return 'Check that the file or directory exists'; case 'EACCES': return 'Check file permissions or run with appropriate privileges'; case 'ENOTDIR': return 'Path should point to a directory, not a file'; case 'EISDIR': return `Path points to a directory but a file was expected${error.path ? ': ' + error.path : ''}. Check the command or script path.`; case 'EMFILE': return 'Too many open files. Try closing some applications'; case 'ENOMEM': return 'Insufficient memory. Try freeing up system resources'; case 'ENOSPC': return 'Insufficient disk space. Free up some disk space'; case 'ETIMEDOUT': return 'Operation timed out. Try again or increase timeout'; case 'ECONNREFUSED': return 'Connection refused. Check if the service is running'; case 'EHOSTUNREACH': return 'Host unreachable. Check network connectivity'; case 'EADDRINUSE': return 'Address already in use. Try using a different port'; default: return 'Check system resources and try again'; } } export function withErrorHandling(fn, options) { return async (...args) => { try { return await fn(...args); } catch (error) { handleError(error, options); throw error; } }; } export function createContextError(message, context, suggestion) { return new XecError(message, 'CONTEXT_ERROR', { field: context, suggestion, }); } export const errorMessages = { fileNotFound: (path) => new FileSystemError(`File not found: ${path}`, path, 'Check that the file path is correct and the file exists'), directoryNotFound: (path) => new FileSystemError(`Directory not found: ${path}`, path, 'Check that the directory path is correct and the directory exists'), moduleNotFound: (name) => new ModuleError(`Module not found: ${name}`, name, 'Check that the module is installed and the name is correct'), taskNotFound: (name) => new TaskError(`Task not found: ${name}`, name, 'Check that the task exists and is loaded from the correct module'), recipeNotFound: (name) => new RecipeError(`Recipe not found: ${name}`, name, 'Check that the recipe file exists and is in the correct location'), configurationInvalid: (field, reason) => new ConfigurationError(`Invalid configuration for ${field}: ${reason}`, field, 'Check the configuration file format and required fields'), networkTimeout: (url) => new NetworkError(`Network timeout: ${url}`, url, 'Check network connectivity and try again with a longer timeout'), permissionDenied: (path) => new FileSystemError(`Permission denied: ${path}`, path, 'Check file permissions or run with appropriate privileges'), operationFailed: (operation, reason) => new XecError(`Operation failed: ${operation} - ${reason}`, 'OPERATION_FAILED', { suggestion: 'Check the error details and try again' }), resourceNotFound: (resource) => new XecError(`Resource not found: ${resource}`, 'RESOURCE_NOT_FOUND', { suggestion: 'Check that the resource exists and is accessible' }), invalidInput: (field, reason) => new ValidationError(`Invalid input for ${field}: ${reason}`, field), }; //# sourceMappingURL=error-handler.js.map