UNPKG

codesummary

Version:

Cross-platform CLI tool that generates professional PDF documentation and RAG-optimized JSON outputs from project source code. Perfect for code reviews, audits, documentation, and AI/ML applications with semantic chunking and precision offsets.

478 lines (411 loc) 16.9 kB
import chalk from 'chalk'; import path from 'path'; import fs from 'fs'; import os from 'os'; /** * Error Handler and Validation Utilities for CodeSummary * Centralized error handling and validation logic */ export class ErrorHandler { // Static cleanup registry for resources that need cleanup before exit static cleanupTasks = []; static isCleaningUp = false; /** * Register a cleanup task to be executed before process exit * @param {Function} cleanupFn - Function to call during cleanup * @param {string} description - Description of the cleanup task */ static registerCleanup(cleanupFn, description = 'cleanup task') { if (typeof cleanupFn !== 'function') { console.warn(`WARNING: Invalid cleanup task: ${description}`); return; } this.cleanupTasks.push({ fn: cleanupFn, description }); } /** * Execute all registered cleanup tasks * @param {boolean} verbose - Whether to log cleanup progress */ static async executeCleanup(verbose = false) { if (this.isCleaningUp) { return; // Prevent recursive cleanup } this.isCleaningUp = true; if (verbose && this.cleanupTasks.length > 0) { console.log(chalk.yellow(`Cleaning up ${this.cleanupTasks.length} resources...`)); } for (const task of this.cleanupTasks) { try { if (verbose) { console.log(chalk.gray(`- ${task.description}`)); } await task.fn(); } catch (error) { if (verbose) { console.warn(chalk.yellow(`WARNING: Cleanup failed for ${task.description}: ${error.message}`)); } } } this.cleanupTasks = []; this.isCleaningUp = false; } /** * Safe process exit with cleanup * @param {number} code - Exit code * @param {string} reason - Reason for exit */ static async safeExit(code = 0, reason = 'Process completed') { try { await this.executeCleanup(process.env.NODE_ENV === 'development'); if (process.env.NODE_ENV === 'development') { console.log(chalk.green(`✓ Cleanup completed. ${reason}`)); } } catch (error) { console.error(chalk.red(`ERROR during cleanup: ${error.message}`)); } finally { process.exit(code); } } /** * Handle and format CLI errors consistently * @param {Error} error - The error object * @param {string} context - Context where error occurred * @param {boolean} exit - Whether to exit process */ static async handleError(error, context = 'Unknown', exit = true) { console.error(chalk.red('ERROR:'), chalk.white(error.message)); if (process.env.NODE_ENV === 'development' || process.env.DEBUG) { console.error(chalk.gray('Context:'), context); console.error(chalk.gray('Stack:'), error.stack); } if (exit) { await this.safeExit(1, `Error in ${context}`); } } /** * Handle configuration validation errors * @param {Error} error - Configuration error * @param {string} configPath - Path to config file */ static async handleConfigError(error, configPath) { console.error(chalk.red('CONFIGURATION ERROR')); console.error(chalk.gray(`Config file: ${configPath}`)); console.error(chalk.white(error.message)); console.error(chalk.yellow('\nTry running: codesummary --reset-config')); await this.safeExit(1, 'Configuration error'); } /** * Handle file system errors with helpful messages * @param {Error} error - File system error * @param {string} operation - The operation being performed * @param {string} filePath - Path involved in the operation */ static handleFileSystemError(error, operation, filePath) { let message = `Failed to ${operation}`; switch (error.code) { case 'ENOENT': message = `File or directory not found: ${filePath}`; break; case 'EACCES': case 'EPERM': message = `Permission denied: ${filePath}`; console.error(chalk.yellow('SUGGESTION: Try running with elevated privileges or check file permissions')); break; case 'ENOSPC': message = 'No space left on device'; break; case 'EMFILE': case 'ENFILE': message = 'Too many open files'; break; default: message = `${message}: ${error.message}`; } console.error(chalk.red('FILE SYSTEM ERROR:'), chalk.white(message)); if (error.code === 'EACCES' || error.code === 'EPERM') { console.error(chalk.yellow('SUGGESTIONS:')); console.error(chalk.gray(' - Check file/directory permissions')); console.error(chalk.gray(' - Try running as administrator/sudo')); console.error(chalk.gray(' - Ensure the file is not locked by another process')); } } /** * Handle PDF generation errors * @param {Error} error - PDF generation error * @param {string} outputPath - Intended output path */ static async handlePDFError(error, outputPath) { console.error(chalk.red('PDF GENERATION FAILED')); console.error(chalk.gray(`Output path: ${outputPath}`)); if (error.message.includes('ENOSPC')) { console.error(chalk.white('Not enough disk space to generate PDF')); console.error(chalk.yellow('SUGGESTION: Try freeing up disk space or using a different output location')); } else if (error.message.includes('EACCES')) { console.error(chalk.white('Permission denied writing to output location')); console.error(chalk.yellow('SUGGESTION: Check permissions or try a different output directory')); } else { console.error(chalk.white(error.message)); } await this.safeExit(1, 'PDF generation failed'); } /** * Validate file path for security and validity * @param {string} filePath - Path to validate * @param {object} options - Validation options * @returns {boolean} True if valid */ static validatePath(filePath, options = {}) { const { mustExist = false, mustBeAbsolute = false, allowedExtensions = null, preventTraversal = true } = options; if (!filePath || typeof filePath !== 'string') { throw new Error('Invalid file path: must be a non-empty string'); } // Normalize path to handle different separators const normalizedPath = path.normalize(filePath); // Enhanced path traversal prevention if (preventTraversal) { // Check for various path traversal patterns const dangerousPatterns = [ /\.\./, // Standard traversal /\0/, // Null bytes /[<>"|?*]/, // Invalid Windows characters /\\\\[^\\]/, // UNC paths /\/(etc|proc|sys|dev)\//i // Sensitive Unix directories ]; // Only block absolute paths if mustBeAbsolute is false if (!mustBeAbsolute) { dangerousPatterns.push(/^[A-Z]:\\/i); // Absolute Windows paths when not allowed dangerousPatterns.push(/^\/[^\/]/); // Absolute Unix paths when not allowed } for (const pattern of dangerousPatterns) { if (pattern.test(normalizedPath)) { throw new Error('Invalid file path: contains dangerous characters or patterns'); } } // Additional Windows-specific checks if (process.platform === 'win32') { const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']; const baseName = path.basename(normalizedPath, path.extname(normalizedPath)).toUpperCase(); if (reservedNames.includes(baseName)) { throw new Error('Invalid file path: uses reserved Windows device name'); } } // Check path length limits if (normalizedPath.length > 260 && process.platform === 'win32') { throw new Error('Path too long for Windows filesystem (>260 characters)'); } if (normalizedPath.length > 4096) { throw new Error('Path too long (>4096 characters)'); } } // Check if path should be absolute if (mustBeAbsolute && !path.isAbsolute(normalizedPath)) { throw new Error('Path must be absolute'); } // Check if file must exist if (mustExist && !fs.existsSync(normalizedPath)) { throw new Error(`Path does not exist: ${normalizedPath}`); } // Check file extension if specified if (allowedExtensions && allowedExtensions.length > 0) { const ext = path.extname(normalizedPath).toLowerCase(); if (!allowedExtensions.includes(ext)) { throw new Error(`Invalid file extension. Allowed: ${allowedExtensions.join(', ')}`); } } return true; } /** * Validate configuration object structure * @param {object} config - Configuration to validate * @returns {boolean} True if valid */ static validateConfig(config) { if (!config || typeof config !== 'object') { throw new Error('Configuration must be an object'); } // Validate output section if (!config.output || typeof config.output !== 'object') { throw new Error('Configuration missing output section'); } if (!config.output.mode || !['relative', 'fixed'].includes(config.output.mode)) { throw new Error('Output mode must be either "relative" or "fixed"'); } if (config.output.mode === 'fixed' && !config.output.fixedPath) { throw new Error('Fixed output mode requires fixedPath'); } // Validate allowedExtensions if (!Array.isArray(config.allowedExtensions)) { throw new Error('allowedExtensions must be an array'); } if (config.allowedExtensions.length === 0) { throw new Error('At least one file extension must be allowed'); } // Validate excludeDirs if (!Array.isArray(config.excludeDirs)) { throw new Error('excludeDirs must be an array'); } // Validate excludeFiles (optional field for backward compatibility) if (config.excludeFiles && !Array.isArray(config.excludeFiles)) { throw new Error('excludeFiles must be an array if provided'); } // Validate styles section if (!config.styles || typeof config.styles !== 'object') { throw new Error('Configuration missing styles section'); } // Validate settings section if (!config.settings || typeof config.settings !== 'object') { throw new Error('Configuration missing settings section'); } if (typeof config.settings.maxFilesBeforePrompt !== 'number' || config.settings.maxFilesBeforePrompt < 1) { throw new Error('maxFilesBeforePrompt must be a positive number'); } return true; } /** * Sanitize user input to prevent injection attacks * @param {string} input - User input to sanitize * @param {object} options - Sanitization options * @returns {string} Sanitized input */ static sanitizeInput(input, options = {}) { if (typeof input !== 'string') { return ''; } const { maxLength = 1000, allowPath = false, strictMode = true } = options; let sanitized = input; if (strictMode) { // Remove or replace dangerous characters sanitized = sanitized .replace(/[<>]/g, '') // Remove potential HTML/XML tags .replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters .replace(/[`${}]/g, '') // Remove template literal and variable expansion chars .replace(/[;&|]/g, ''); // Remove command injection chars } // For path inputs, allow specific characters if (allowPath) { // Allow path separators and basic path characters (more permissive for legitimate paths) sanitized = sanitized.replace(/[^a-zA-Z0-9\-_.\\/\\:\s()]/g, ''); } else { // For non-path inputs, be more restrictive sanitized = sanitized.replace(/[^a-zA-Z0-9\-_.\s]/g, ''); } return sanitized .trim() .substring(0, maxLength); } /** * Validate file content before processing * @param {string} filePath - Path to file * @param {Buffer} content - File content buffer * @returns {boolean} True if content appears to be text */ static validateFileContent(filePath, content) { // Check for null bytes (common in binary files) if (content.includes(0)) { console.warn(chalk.yellow(`WARNING: Skipping potentially binary file: ${filePath}`)); return false; } // Check file size (warn for very large files) const maxSize = 10 * 1024 * 1024; // 10MB if (content.length > maxSize) { console.warn(chalk.yellow(`WARNING: Large file detected (${Math.round(content.length / 1024 / 1024)}MB): ${filePath}`)); console.warn(chalk.gray('This may affect PDF generation performance')); } return true; } /** * Graceful shutdown handler * @param {string} signal - Signal received */ static async gracefulShutdown(signal) { console.log(chalk.yellow(`\nWARNING: Received ${signal}. Shutting down gracefully...`)); await this.safeExit(0, `Received ${signal}`); } /** * Setup global error handlers */ static setupGlobalHandlers() { // Handle uncaught exceptions process.on('uncaughtException', async (error) => { console.error(chalk.red('UNCAUGHT EXCEPTION:')); console.error(error.stack); console.error(chalk.yellow('Please report this error to: https://github.com/skamoll/CodeSummary/issues')); await this.safeExit(1, 'Uncaught exception'); }); // Handle unhandled promise rejections process.on('unhandledRejection', async (reason, promise) => { console.error(chalk.red('UNHANDLED PROMISE REJECTION:')); console.error('Promise:', promise); console.error('Reason:', reason); console.error(chalk.yellow('Please report this error to: https://github.com/skamoll/CodeSummary/issues')); await this.safeExit(1, 'Unhandled promise rejection'); }); // Handle graceful shutdown signals process.on('SIGINT', () => ErrorHandler.gracefulShutdown('SIGINT')); process.on('SIGTERM', () => ErrorHandler.gracefulShutdown('SIGTERM')); // Handle warnings process.on('warning', (warning) => { if (process.env.NODE_ENV === 'development') { console.warn(chalk.yellow('WARNING:'), warning.message); } }); } /** * Create context-aware error wrapper * @param {string} context - Context description * @returns {Function} Error wrapper function */ static createErrorWrapper(context) { return async (error) => { if (error.name === 'AbortError') { console.log(chalk.yellow('\nWARNING: Operation cancelled by user')); await this.safeExit(0, 'Operation cancelled by user'); return; } await ErrorHandler.handleError(error, context); }; } /** * Validate environment and dependencies */ static async validateEnvironment() { // Check Node.js version const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.split('.')[0].substring(1)); if (majorVersion < 18) { console.error(chalk.red('ERROR: Node.js version requirement not met')); console.error(chalk.white(`Current version: ${nodeVersion}`)); console.error(chalk.white('Required version: >=18.0.0')); console.error(chalk.yellow('Please upgrade Node.js: https://nodejs.org/')); await this.safeExit(1, 'Node.js version incompatible'); } // Check available memory const totalMemory = os.totalmem(); const freeMemory = os.freemem(); const memoryUsage = process.memoryUsage(); if (freeMemory < 100 * 1024 * 1024) { // Less than 100MB free console.warn(chalk.yellow('WARNING: Low system memory detected')); console.warn(chalk.gray('PDF generation may be slower for large projects')); } // Check platform compatibility const platform = process.platform; const supportedPlatforms = ['win32', 'darwin', 'linux']; if (!supportedPlatforms.includes(platform)) { console.warn(chalk.yellow(`WARNING: Untested platform: ${platform}`)); console.warn(chalk.gray('CodeSummary may not work correctly on this platform')); } } } export default ErrorHandler;