UNPKG

@taizo-pro/github-discussions-cli

Version:

A powerful command-line tool for interacting with GitHub Discussions without opening a browser

208 lines 8.45 kB
import chalk from 'chalk'; import { ErrorType } from '../../core/index.js'; import { promises as fs } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; export class EnhancedErrorHandler { static logDir = join(homedir(), '.github-discussions', 'logs'); static maxRetries = 3; static async handleError(error, context) { const enhancedError = this.enhanceError(error, context); // Display user-friendly error this.displayError(enhancedError); // Log to file for debugging await this.logError(enhancedError); // Suggest recovery options this.suggestRecovery(enhancedError); // Exit with appropriate code process.exit(this.getExitCode(enhancedError.type)); } static enhanceError(error, context) { if (this.isAppError(error)) { return { ...error, details: { ...error.details, context, timestamp: new Date(), }, }; } // Convert unknown errors to AppError const errorType = this.inferErrorType(error); return { type: errorType, message: error.message || 'An unknown error occurred', details: { originalError: error, context, timestamp: new Date(), stack: error.stack, }, suggestions: this.generateSuggestions(errorType, error), }; } static inferErrorType(error) { if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { return ErrorType.NETWORK_ERROR; } if (error.message?.includes('401') || error.message?.includes('credentials')) { return ErrorType.AUTHENTICATION_ERROR; } if (error.message?.includes('404')) { return ErrorType.API_ERROR; } return ErrorType.API_ERROR; } static generateSuggestions(type, error) { const suggestions = []; switch (type) { case ErrorType.AUTHENTICATION_ERROR: suggestions.push('Check your GitHub Personal Access Token', 'Run: gh-discussions config --token <new-token>', 'Ensure token has required scopes (repo or public_repo)'); break; case ErrorType.NETWORK_ERROR: suggestions.push('Check your internet connection', 'Verify GitHub API is accessible', 'Try again with DEBUG=1 for more details', 'Check if you\'re behind a proxy'); break; case ErrorType.API_ERROR: if (error.message?.includes('404')) { suggestions.push('Verify the repository exists and is accessible', 'Check if Discussions are enabled for the repository', 'Ensure the discussion number is correct'); } else { suggestions.push('Check GitHub API status at https://www.githubstatus.com/', 'Verify your request parameters', 'Try reducing the request size'); } break; default: suggestions.push('Check the error details above', 'Run with DEBUG=1 for more information', 'Report issue at https://github.com/taizo-pro/nooknote/issues'); } return suggestions; } static displayError(error) { console.error(); console.error(chalk.red('━'.repeat(60))); console.error(chalk.red.bold(`✗ ${this.getErrorTypeLabel(error.type)}`)); console.error(chalk.red('━'.repeat(60))); console.error(); // Main error message console.error(chalk.white.bold('Error:'), chalk.red(error.message)); // Context information if (error.details?.context) { console.error(); console.error(chalk.white.bold('Context:')); const ctx = error.details.context; if (ctx.operation) console.error(chalk.gray(` Operation: ${ctx.operation}`)); if (ctx.repository) console.error(chalk.gray(` Repository: ${ctx.repository}`)); if (ctx.discussionId) console.error(chalk.gray(` Discussion: #${ctx.discussionId}`)); } // Suggestions if (error.suggestions && error.suggestions.length > 0) { console.error(); console.error(chalk.yellow.bold('💡 Suggestions:')); error.suggestions.forEach((suggestion) => { console.error(chalk.yellow(` • ${suggestion}`)); }); } // Debug info if (process.env.DEBUG) { console.error(); console.error(chalk.gray.bold('Debug Information:')); console.error(chalk.gray(JSON.stringify(error.details, null, 2))); } else { console.error(); console.error(chalk.gray('Run with DEBUG=1 for detailed error information')); } console.error(); console.error(chalk.red('━'.repeat(60))); } static async logError(error) { try { await fs.mkdir(this.logDir, { recursive: true }); const logFile = join(this.logDir, `error-${Date.now()}.json`); const logContent = JSON.stringify({ ...error, environment: { node: process.version, platform: process.platform, cwd: process.cwd(), }, }, null, 2); await fs.writeFile(logFile, logContent, 'utf8'); // Clean old logs (keep last 10) const logs = await fs.readdir(this.logDir); const errorLogs = logs.filter(f => f.startsWith('error-')).sort(); if (errorLogs.length > 10) { for (const oldLog of errorLogs.slice(0, -10)) { await fs.unlink(join(this.logDir, oldLog)); } } } catch { // Silently ignore logging errors } } static suggestRecovery(error) { if (error.type === ErrorType.NETWORK_ERROR) { console.error(); console.error(chalk.cyan('💡 Tip: Network errors are often temporary.')); console.error(chalk.cyan(' The CLI will retry failed requests automatically.')); } } static getExitCode(type) { switch (type) { case ErrorType.AUTHENTICATION_ERROR: return 2; case ErrorType.NETWORK_ERROR: return 3; case ErrorType.CONFIGURATION_ERROR: return 4; case ErrorType.VALIDATION_ERROR: return 5; default: return 1; } } static getErrorTypeLabel(type) { switch (type) { case ErrorType.AUTHENTICATION_ERROR: return '🔐 Authentication Error'; case ErrorType.NETWORK_ERROR: return '🌐 Network Error'; case ErrorType.API_ERROR: return '⚠️ API Error'; case ErrorType.CONFIGURATION_ERROR: return '⚙️ Configuration Error'; case ErrorType.VALIDATION_ERROR: return '❌ Validation Error'; default: return '❗ Error'; } } static isAppError(error) { return error && typeof error === 'object' && 'type' in error && 'message' in error; } static async retryWithBackoff(operation, context, maxRetries = 3) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Don't retry auth errors if (this.inferErrorType(error) === ErrorType.AUTHENTICATION_ERROR) { throw error; } if (attempt < maxRetries) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); console.log(chalk.yellow(`⚠️ Attempt ${attempt} failed, retrying in ${delay}ms...`)); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } } //# sourceMappingURL=enhanced-error-handler.js.map