@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
333 lines • 12.7 kB
JavaScript
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