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
JavaScript
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;