dna-template-cli
Version:
DNA Template CLI v0.3.4 - Enhanced Commands Added (enhanced-create, enhanced-list, enhanced-validate)
540 lines • 24.2 kB
JavaScript
;
/**
* @fileoverview Enhanced error handler with comprehensive error recovery and user guidance
* Provides intelligent error handling, automatic fixes, and detailed troubleshooting guidance
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnhancedErrorHandler = void 0;
const tslib_1 = require("tslib");
const chalk_compat_1 = tslib_1.__importDefault(require("./chalk-compat"));
const boxen_compat_1 = tslib_1.__importDefault(require("./boxen-compat"));
const inquirer_compat_1 = tslib_1.__importDefault(require("./inquirer-compat"));
const error_types_1 = require("../lib/errors/error-types");
const logger_1 = require("./logger");
const environment_1 = require("../environments/environment");
class EnhancedErrorHandler {
constructor() {
this.errorHistory = [];
this.recoveryAttempts = new Map();
this.maxRetryAttempts = 3;
}
static getInstance() {
if (!EnhancedErrorHandler.instance) {
EnhancedErrorHandler.instance = new EnhancedErrorHandler();
}
return EnhancedErrorHandler.instance;
}
/**
* Main error handling entry point
*/
async handleError(error, options = {}) {
const defaultOptions = {
interactive: true,
autoFix: false,
showStackTrace: environment_1.environment.debug,
gracefulDegradation: true,
maxRetries: 3,
};
const handlingOptions = { ...defaultOptions, ...options };
let dnaError;
// Convert error to DNAError if it's not already
if (error instanceof error_types_1.DNAError) {
dnaError = error;
}
else if (error instanceof Error) {
dnaError = this.convertToDNAError(error);
}
else {
dnaError = new error_types_1.ValidationError(`Unknown error: ${String(error)}`, 'UNKNOWN_ERROR', 'Please try again or contact support');
}
// Add to error history
this.errorHistory.push(dnaError);
// Log error details
this.logErrorDetails(dnaError, handlingOptions);
// Check if we've exceeded retry attempts
const retryCount = this.recoveryAttempts.get(dnaError.code) || 0;
if (retryCount >= handlingOptions.maxRetries) {
await this.displayFinalErrorMessage(dnaError);
return false;
}
// Create recovery plan
const recoveryPlan = this.createRecoveryPlan(dnaError);
// Display error to user
await this.displayErrorMessage(dnaError, recoveryPlan, handlingOptions);
// Attempt automatic recovery if possible
if (handlingOptions.autoFix && recoveryPlan.autoFixAvailable) {
const recovered = await this.attemptAutoRecovery(dnaError);
if (recovered) {
logger_1.logger.success('Error automatically resolved!');
return true;
}
}
// Interactive recovery if enabled
if (handlingOptions.interactive && recoveryPlan.canRecover) {
const recovered = await this.attemptInteractiveRecovery(dnaError, recoveryPlan);
if (recovered) {
return true;
}
}
// Graceful degradation if enabled
if (handlingOptions.gracefulDegradation && this.canDegrade(dnaError)) {
const degraded = await this.attemptGracefulDegradation(dnaError);
if (degraded) {
logger_1.logger.warn('Continuing with reduced functionality...');
return true;
}
}
// Increment retry count
this.recoveryAttempts.set(dnaError.code, retryCount + 1);
return false;
}
/**
* Handle multiple errors with prioritization
*/
async handleMultipleErrors(errors, options = {}) {
const dnaErrors = errors.map(error => {
if (error instanceof error_types_1.DNAError)
return error;
if (error instanceof Error)
return this.convertToDNAError(error);
return new error_types_1.ValidationError(`Unknown error: ${String(error)}`, 'UNKNOWN_ERROR', 'Please try again or contact support');
});
// Sort errors by severity
const sortedErrors = (0, error_types_1.sortErrorsBySeverity)(dnaErrors);
// Check for critical errors
const criticalErrors = (0, error_types_1.getCriticalErrors)(sortedErrors);
if (criticalErrors.length > 0) {
logger_1.logger.error(`Found ${criticalErrors.length} critical error(s) that must be resolved first.`);
for (const criticalError of criticalErrors) {
const recovered = await this.handleError(criticalError, options);
if (!recovered) {
return false; // Stop if critical error can't be recovered
}
}
}
// Handle remaining errors by category
const categorizedErrors = this.categorizeErrors(sortedErrors.filter(e => e.severity !== error_types_1.ErrorSeverity.CRITICAL));
for (const [category, categoryErrors] of categorizedErrors) {
logger_1.logger.info(`Handling ${categoryErrors.length} ${category} error(s)...`);
for (const error of categoryErrors) {
const recovered = await this.handleError(error, { ...options, interactive: false });
if (!recovered && !(0, error_types_1.isRecoverable)(error)) {
return false;
}
}
}
return true;
}
/**
* Generate comprehensive error report
*/
generateErrorReport() {
const report = {
timestamp: new Date().toISOString(),
totalErrors: this.errorHistory.length,
errorsByCategory: this.getErrorCountByCategory(),
errorsBySeverity: this.getErrorCountBySeverity(),
mostCommonErrors: this.getMostCommonErrors(),
recoveryAttempts: Object.fromEntries(this.recoveryAttempts),
recentErrors: this.errorHistory.slice(-10).map(error => ({
code: error.code,
message: error.message,
category: error.category,
severity: error.severity,
timestamp: error.timestamp,
})),
};
return JSON.stringify(report, null, 2);
}
/**
* Clear error history
*/
clearHistory() {
this.errorHistory = [];
this.recoveryAttempts.clear();
}
/**
* Get error statistics
*/
getErrorStats() {
const total = this.errorHistory.length;
const recovered = Array.from(this.recoveryAttempts.values()).filter(count => count > 0).length;
return {
total,
byCategory: this.getErrorCountByCategory(),
bySeverity: this.getErrorCountBySeverity(),
recoveryRate: total > 0 ? (recovered / total) * 100 : 0,
};
}
// Private methods
convertToDNAError(error) {
// Convert different error types to appropriate DNAError subclasses
if (error.message.includes('ENOENT') || error.message.includes('file not found')) {
return new error_types_1.FilesystemError(error.message, 'FILE_NOT_FOUND', 'Check if the file exists and path is correct');
}
if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
return new error_types_1.FilesystemError(error.message, 'PERMISSION_DENIED', 'Check file permissions or run with appropriate privileges');
}
if (error.message.includes('ECONNREFUSED') || error.message.includes('network')) {
return new error_types_1.NetworkError(error.message, 'NETWORK_ERROR', 'Check internet connection and try again');
}
if (error.message.includes('npm') || error.message.includes('dependency')) {
return new error_types_1.DependencyError(error.message, 'DEPENDENCY_ERROR', 'Check package manager and dependency configuration');
}
// Default to validation error
return new error_types_1.ValidationError(error.message, 'GENERIC_ERROR', 'Please check the operation and try again');
}
createRecoveryPlan(error) {
const plan = {
canRecover: (0, error_types_1.isRecoverable)(error),
autoFixAvailable: (0, error_types_1.isAutoFixable)(error),
manualSteps: [],
estimatedTime: '1-2 minutes',
riskLevel: 'low',
alternativeApproaches: [],
};
// Customize plan based on error category
switch (error.category) {
case error_types_1.ErrorCategory.VALIDATION:
plan.manualSteps = [
'Review the validation error details',
'Correct the invalid input or configuration',
'Retry the operation',
];
plan.estimatedTime = '1-2 minutes';
plan.riskLevel = 'low';
break;
case error_types_1.ErrorCategory.TEMPLATE:
plan.manualSteps = [
'Check template availability and version',
'Update template registry if needed',
'Verify template compatibility',
'Try alternative template if available',
];
plan.estimatedTime = '2-5 minutes';
plan.riskLevel = 'medium';
plan.alternativeApproaches = [
'Use a different template',
'Create custom template configuration',
'Contact template author for support',
];
break;
case error_types_1.ErrorCategory.FILESYSTEM:
plan.manualSteps = [
'Check file and directory permissions',
'Verify available disk space',
'Ensure target directory is writable',
'Clear temporary files if needed',
];
plan.estimatedTime = '2-3 minutes';
plan.riskLevel = 'medium';
plan.alternativeApproaches = [
'Use different output directory',
'Run with elevated privileges',
'Free up disk space',
];
break;
case error_types_1.ErrorCategory.NETWORK:
plan.manualSteps = [
'Check internet connection',
'Verify proxy settings if behind firewall',
'Wait and retry if service is temporarily unavailable',
'Use offline mode if available',
];
plan.estimatedTime = '1-5 minutes';
plan.riskLevel = 'low';
plan.alternativeApproaches = [
'Use cached templates',
'Download templates manually',
'Use offline installation',
];
break;
case error_types_1.ErrorCategory.DEPENDENCY:
plan.manualSteps = [
'Clear package manager cache',
'Update package manager to latest version',
'Check and fix package.json syntax',
'Try alternative package manager',
];
plan.estimatedTime = '3-10 minutes';
plan.riskLevel = 'medium';
plan.alternativeApproaches = [
'Skip dependency installation temporarily',
'Install dependencies manually',
'Use different package manager (npm/yarn/pnpm)',
];
break;
case error_types_1.ErrorCategory.SYSTEM:
plan.manualSteps = [
'Check system requirements',
'Update required tools to compatible versions',
'Install missing system dependencies',
'Restart terminal/shell if needed',
];
plan.estimatedTime = '5-15 minutes';
plan.riskLevel = 'high';
plan.alternativeApproaches = [
'Use Docker for isolated environment',
'Use cloud-based development environment',
'Downgrade to compatible versions',
];
break;
case error_types_1.ErrorCategory.ROLLBACK:
plan.canRecover = false;
plan.manualSteps = [
'Review rollback failure details',
'Manually clean up partially created files',
'Check file system integrity',
'Contact support if data loss occurred',
];
plan.estimatedTime = '5-30 minutes';
plan.riskLevel = 'high';
break;
}
if (error.suggestion) {
plan.manualSteps.unshift(error.suggestion);
}
return plan;
}
async displayErrorMessage(error, recoveryPlan, options) {
const severity = this.getSeverityIcon(error.severity);
const category = this.getCategoryIcon(error.category);
let errorContent = `${severity} ${chalk_compat_1.default.red.bold('Error:')} ${error.message}\n`;
errorContent += `${chalk_compat_1.default.gray(`Code: ${error.code} | Category: ${category} ${error.category}`)}\n`;
if (error.timestamp) {
errorContent += `${chalk_compat_1.default.gray(`Time: ${error.timestamp.toLocaleString()}`)}\n`;
}
if (recoveryPlan.canRecover) {
errorContent += `\n${chalk_compat_1.default.green.bold('Recovery Available:')} This error can be resolved\n`;
if (recoveryPlan.autoFixAvailable) {
errorContent += `${chalk_compat_1.default.blue('• Auto-fix available')}\n`;
}
errorContent += `${chalk_compat_1.default.blue(`• Estimated time: ${recoveryPlan.estimatedTime}`)}\n`;
errorContent += `${chalk_compat_1.default.blue(`• Risk level: ${this.getRiskIcon(recoveryPlan.riskLevel)} ${recoveryPlan.riskLevel}`)}\n`;
}
else {
errorContent += `\n${chalk_compat_1.default.red.bold('Recovery:')} Manual intervention required\n`;
}
if (error.suggestion) {
errorContent += `\n${chalk_compat_1.default.yellow.bold('Suggestion:')} ${error.suggestion}\n`;
}
if (recoveryPlan.manualSteps.length > 0) {
errorContent += `\n${chalk_compat_1.default.blue.bold('Recovery Steps:')}\n`;
recoveryPlan.manualSteps.forEach((step, index) => {
errorContent += `${chalk_compat_1.default.blue(`${index + 1}.`)} ${step}\n`;
});
}
if (recoveryPlan.alternativeApproaches.length > 0) {
errorContent += `\n${chalk_compat_1.default.cyan.bold('Alternative Approaches:')}\n`;
recoveryPlan.alternativeApproaches.forEach((approach, index) => {
errorContent += `${chalk_compat_1.default.cyan(`•`)} ${approach}\n`;
});
}
if (error.helpUrl) {
errorContent += `\n${chalk_compat_1.default.magenta.bold('Help:')} ${error.helpUrl}\n`;
}
if (options.showStackTrace && error.stack) {
errorContent += `\n${chalk_compat_1.default.gray.bold('Stack Trace:')}\n${chalk_compat_1.default.gray(error.stack)}\n`;
}
if (!environment_1.environment.debug) {
errorContent += `\n${chalk_compat_1.default.gray('For more details, run with --debug flag')}\n`;
}
console.error((0, boxen_compat_1.default)(errorContent, {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: this.getBorderColor(error.severity),
}));
}
async displayFinalErrorMessage(error) {
const errorContent = `${chalk_compat_1.default.red.bold('UNRECOVERABLE ERROR')}\n\n` +
`${error.message}\n\n` +
`${chalk_compat_1.default.yellow('This error could not be automatically resolved after multiple attempts.')}\n` +
`${chalk_compat_1.default.yellow('Please review the error details and try manual resolution.')}\n\n` +
`${chalk_compat_1.default.gray(`Error Code: ${error.code}`)}\n` +
`${chalk_compat_1.default.gray(`Category: ${error.category}`)}\n` +
`${chalk_compat_1.default.gray(`Severity: ${error.severity}`)}\n`;
console.error((0, boxen_compat_1.default)(errorContent, {
padding: 1,
margin: 1,
borderStyle: 'double',
borderColor: 'red',
}));
}
async attemptAutoRecovery(error) {
if (!error.autoFix) {
return false;
}
logger_1.logger.info('Attempting automatic recovery...');
try {
await error.autoFix();
logger_1.logger.success('Automatic recovery successful!');
return true;
}
catch (recoveryError) {
logger_1.logger.error(`Automatic recovery failed: ${recoveryError instanceof Error ? recoveryError.message : 'Unknown error'}`);
return false;
}
}
async attemptInteractiveRecovery(error, plan) {
const choices = [];
if (plan.autoFixAvailable) {
choices.push({ name: 'Try automatic fix', value: 'auto' });
}
choices.push({ name: 'Follow manual steps', value: 'manual' }, { name: 'Skip this error', value: 'skip' }, { name: 'Abort operation', value: 'abort' });
if (plan.alternativeApproaches.length > 0) {
choices.push({ name: 'Show alternative approaches', value: 'alternatives' });
}
const { action } = await inquirer_compat_1.default.prompt([
{
type: 'list',
name: 'action',
message: 'How would you like to proceed?',
choices,
},
]);
switch (action) {
case 'auto':
return await this.attemptAutoRecovery(error);
case 'manual':
console.log(chalk_compat_1.default.blue.bold('\nManual Recovery Steps:'));
plan.manualSteps.forEach((step, index) => {
console.log(`${chalk_compat_1.default.blue(`${index + 1}.`)} ${step}`);
});
const { completed } = await inquirer_compat_1.default.prompt([
{
type: 'confirm',
name: 'completed',
message: 'Have you completed the manual steps?',
default: false,
},
]);
return completed;
case 'alternatives':
console.log(chalk_compat_1.default.cyan.bold('\nAlternative Approaches:'));
plan.alternativeApproaches.forEach((approach, index) => {
console.log(`${chalk_compat_1.default.cyan(`${index + 1}.`)} ${approach}`);
});
return await this.attemptInteractiveRecovery(error, plan);
case 'skip':
logger_1.logger.warn('Skipping error - this may cause issues later');
return true;
case 'abort':
return false;
default:
return false;
}
}
async attemptGracefulDegradation(error) {
// Implement graceful degradation based on error type
switch (error.category) {
case error_types_1.ErrorCategory.NETWORK:
logger_1.logger.warn('Network error - switching to offline mode');
return true;
case error_types_1.ErrorCategory.DEPENDENCY:
logger_1.logger.warn('Dependency error - skipping optional dependencies');
return true;
default:
return false;
}
}
canDegrade(error) {
return error.category === error_types_1.ErrorCategory.NETWORK ||
error.category === error_types_1.ErrorCategory.DEPENDENCY ||
error.severity === error_types_1.ErrorSeverity.LOW;
}
logErrorDetails(error, options) {
if (environment_1.environment.debug) {
logger_1.logger.debug('Error details:', {
code: error.code,
category: error.category,
severity: error.severity,
recoverable: error.recoverable,
autoFixable: error.autoFixable,
timestamp: error.timestamp,
context: error.context,
});
}
// Log to error tracking service in production
if (environment_1.environment.production && environment_1.environment.telemetryEnabled) {
// TODO: Implement error tracking service integration
logger_1.logger.debug('Error would be reported to tracking service');
}
}
categorizeErrors(errors) {
const categorized = new Map();
for (const error of errors) {
if (!categorized.has(error.category)) {
categorized.set(error.category, []);
}
categorized.get(error.category).push(error);
}
return categorized;
}
getErrorCountByCategory() {
const counts = {};
for (const error of this.errorHistory) {
counts[error.category] = (counts[error.category] || 0) + 1;
}
return counts;
}
getErrorCountBySeverity() {
const counts = {};
for (const error of this.errorHistory) {
counts[error.severity] = (counts[error.severity] || 0) + 1;
}
return counts;
}
getMostCommonErrors() {
const counts = {};
for (const error of this.errorHistory) {
counts[error.code] = (counts[error.code] || 0) + 1;
}
return Object.entries(counts)
.map(([code, count]) => ({ code, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 5);
}
getSeverityIcon(severity) {
switch (severity) {
case error_types_1.ErrorSeverity.CRITICAL: return '🚨';
case error_types_1.ErrorSeverity.HIGH: return '❌';
case error_types_1.ErrorSeverity.MEDIUM: return '⚠️';
case error_types_1.ErrorSeverity.LOW: return 'ℹ️';
default: return '❓';
}
}
getCategoryIcon(category) {
switch (category) {
case error_types_1.ErrorCategory.VALIDATION: return '✅';
case error_types_1.ErrorCategory.TEMPLATE: return '📋';
case error_types_1.ErrorCategory.FILESYSTEM: return '📁';
case error_types_1.ErrorCategory.NETWORK: return '🌐';
case error_types_1.ErrorCategory.DEPENDENCY: return '📦';
case error_types_1.ErrorCategory.SYSTEM: return '⚙️';
case error_types_1.ErrorCategory.CONFIGURATION: return '🔧';
case error_types_1.ErrorCategory.ROLLBACK: return '↩️';
case error_types_1.ErrorCategory.SECURITY: return '🔒';
default: return '❓';
}
}
getRiskIcon(risk) {
switch (risk) {
case 'low': return '🟢';
case 'medium': return '🟡';
case 'high': return '🔴';
default: return '⚪';
}
}
getBorderColor(severity) {
switch (severity) {
case error_types_1.ErrorSeverity.CRITICAL: return 'red';
case error_types_1.ErrorSeverity.HIGH: return 'red';
case error_types_1.ErrorSeverity.MEDIUM: return 'yellow';
case error_types_1.ErrorSeverity.LOW: return 'blue';
default: return 'gray';
}
}
}
exports.EnhancedErrorHandler = EnhancedErrorHandler;
//# sourceMappingURL=enhanced-error-handler.js.map