codesummary 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,478 +1,475 @@
1
- import chalk from 'chalk';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import os from 'os';
5
-
6
- /**
7
- * Error Handler and Validation Utilities for CodeSummary
8
- * Centralized error handling and validation logic
9
- */
10
- export class ErrorHandler {
11
- // Static cleanup registry for resources that need cleanup before exit
12
- static cleanupTasks = [];
13
- static isCleaningUp = false;
14
-
15
- /**
16
- * Register a cleanup task to be executed before process exit
17
- * @param {Function} cleanupFn - Function to call during cleanup
18
- * @param {string} description - Description of the cleanup task
19
- */
20
- static registerCleanup(cleanupFn, description = 'cleanup task') {
21
- if (typeof cleanupFn !== 'function') {
22
- console.warn(`WARNING: Invalid cleanup task: ${description}`);
23
- return;
24
- }
25
-
26
- this.cleanupTasks.push({ fn: cleanupFn, description });
27
- }
28
-
29
- /**
30
- * Execute all registered cleanup tasks
31
- * @param {boolean} verbose - Whether to log cleanup progress
32
- */
33
- static async executeCleanup(verbose = false) {
34
- if (this.isCleaningUp) {
35
- return; // Prevent recursive cleanup
36
- }
37
-
38
- this.isCleaningUp = true;
39
-
40
- if (verbose && this.cleanupTasks.length > 0) {
41
- console.log(chalk.yellow(`Cleaning up ${this.cleanupTasks.length} resources...`));
42
- }
43
-
44
- for (const task of this.cleanupTasks) {
45
- try {
46
- if (verbose) {
47
- console.log(chalk.gray(`- ${task.description}`));
48
- }
49
- await task.fn();
50
- } catch (error) {
51
- if (verbose) {
52
- console.warn(chalk.yellow(`WARNING: Cleanup failed for ${task.description}: ${error.message}`));
53
- }
54
- }
55
- }
56
-
57
- this.cleanupTasks = [];
58
- this.isCleaningUp = false;
59
- }
60
-
61
- /**
62
- * Safe process exit with cleanup
63
- * @param {number} code - Exit code
64
- * @param {string} reason - Reason for exit
65
- */
66
- static async safeExit(code = 0, reason = 'Process completed') {
67
- try {
68
- await this.executeCleanup(process.env.NODE_ENV === 'development');
69
- if (process.env.NODE_ENV === 'development') {
70
- console.log(chalk.green(`✓ Cleanup completed. ${reason}`));
71
- }
72
- } catch (error) {
73
- console.error(chalk.red(`ERROR during cleanup: ${error.message}`));
74
- } finally {
75
- process.exit(code);
76
- }
77
- }
78
- /**
79
- * Handle and format CLI errors consistently
80
- * @param {Error} error - The error object
81
- * @param {string} context - Context where error occurred
82
- * @param {boolean} exit - Whether to exit process
83
- */
84
- static async handleError(error, context = 'Unknown', exit = true) {
85
- console.error(chalk.red('ERROR:'), chalk.white(error.message));
86
-
87
- if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
88
- console.error(chalk.gray('Context:'), context);
89
- console.error(chalk.gray('Stack:'), error.stack);
90
- }
91
-
92
- if (exit) {
93
- await this.safeExit(1, `Error in ${context}`);
94
- }
95
- }
96
-
97
- /**
98
- * Handle configuration validation errors
99
- * @param {Error} error - Configuration error
100
- * @param {string} configPath - Path to config file
101
- */
102
- static async handleConfigError(error, configPath) {
103
- console.error(chalk.red('CONFIGURATION ERROR'));
104
- console.error(chalk.gray(`Config file: ${configPath}`));
105
- console.error(chalk.white(error.message));
106
- console.error(chalk.yellow('\nTry running: codesummary --reset-config'));
107
- await this.safeExit(1, 'Configuration error');
108
- }
109
-
110
- /**
111
- * Handle file system errors with helpful messages
112
- * @param {Error} error - File system error
113
- * @param {string} operation - The operation being performed
114
- * @param {string} filePath - Path involved in the operation
115
- */
116
- static handleFileSystemError(error, operation, filePath) {
117
- let message = `Failed to ${operation}`;
118
-
119
- switch (error.code) {
120
- case 'ENOENT':
121
- message = `File or directory not found: ${filePath}`;
122
- break;
123
- case 'EACCES':
124
- case 'EPERM':
125
- message = `Permission denied: ${filePath}`;
126
- console.error(chalk.yellow('SUGGESTION: Try running with elevated privileges or check file permissions'));
127
- break;
128
- case 'ENOSPC':
129
- message = 'No space left on device';
130
- break;
131
- case 'EMFILE':
132
- case 'ENFILE':
133
- message = 'Too many open files';
134
- break;
135
- default:
136
- message = `${message}: ${error.message}`;
137
- }
138
-
139
- console.error(chalk.red('FILE SYSTEM ERROR:'), chalk.white(message));
140
-
141
- if (error.code === 'EACCES' || error.code === 'EPERM') {
142
- console.error(chalk.yellow('SUGGESTIONS:'));
143
- console.error(chalk.gray(' - Check file/directory permissions'));
144
- console.error(chalk.gray(' - Try running as administrator/sudo'));
145
- console.error(chalk.gray(' - Ensure the file is not locked by another process'));
146
- }
147
- }
148
-
149
- /**
150
- * Handle PDF generation errors
151
- * @param {Error} error - PDF generation error
152
- * @param {string} outputPath - Intended output path
153
- */
154
- static async handlePDFError(error, outputPath) {
155
- console.error(chalk.red('PDF GENERATION FAILED'));
156
- console.error(chalk.gray(`Output path: ${outputPath}`));
157
-
158
- if (error.message.includes('ENOSPC')) {
159
- console.error(chalk.white('Not enough disk space to generate PDF'));
160
- console.error(chalk.yellow('SUGGESTION: Try freeing up disk space or using a different output location'));
161
- } else if (error.message.includes('EACCES')) {
162
- console.error(chalk.white('Permission denied writing to output location'));
163
- console.error(chalk.yellow('SUGGESTION: Check permissions or try a different output directory'));
164
- } else {
165
- console.error(chalk.white(error.message));
166
- }
167
-
168
- await this.safeExit(1, 'PDF generation failed');
169
- }
170
-
171
- /**
172
- * Validate file path for security and validity
173
- * @param {string} filePath - Path to validate
174
- * @param {object} options - Validation options
175
- * @returns {boolean} True if valid
176
- */
177
- static validatePath(filePath, options = {}) {
178
- const {
179
- mustExist = false,
180
- mustBeAbsolute = false,
181
- allowedExtensions = null,
182
- preventTraversal = true
183
- } = options;
184
-
185
- if (!filePath || typeof filePath !== 'string') {
186
- throw new Error('Invalid file path: must be a non-empty string');
187
- }
188
-
189
- // Normalize path to handle different separators
190
- const normalizedPath = path.normalize(filePath);
191
-
192
- // Enhanced path traversal prevention
193
- if (preventTraversal) {
194
- // Check for various path traversal patterns
195
- const dangerousPatterns = [
196
- /\.\./, // Standard traversal
197
- /\0/, // Null bytes
198
- /[<>"|?*]/, // Invalid Windows characters
199
- /\\\\[^\\]/, // UNC paths
200
- /\/(etc|proc|sys|dev)\//i // Sensitive Unix directories
201
- ];
202
-
203
- // Only block absolute paths if mustBeAbsolute is false
204
- if (!mustBeAbsolute) {
205
- dangerousPatterns.push(/^[A-Z]:\\/i); // Absolute Windows paths when not allowed
206
- dangerousPatterns.push(/^\/[^\/]/); // Absolute Unix paths when not allowed
207
- }
208
-
209
- for (const pattern of dangerousPatterns) {
210
- if (pattern.test(normalizedPath)) {
211
- throw new Error('Invalid file path: contains dangerous characters or patterns');
212
- }
213
- }
214
-
215
- // Additional Windows-specific checks
216
- if (process.platform === 'win32') {
217
- const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
218
- 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3',
219
- 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
220
-
221
- const baseName = path.basename(normalizedPath, path.extname(normalizedPath)).toUpperCase();
222
- if (reservedNames.includes(baseName)) {
223
- throw new Error('Invalid file path: uses reserved Windows device name');
224
- }
225
- }
226
-
227
- // Check path length limits
228
- if (normalizedPath.length > 260 && process.platform === 'win32') {
229
- throw new Error('Path too long for Windows filesystem (>260 characters)');
230
- }
231
- if (normalizedPath.length > 4096) {
232
- throw new Error('Path too long (>4096 characters)');
233
- }
234
- }
235
-
236
- // Check if path should be absolute
237
- if (mustBeAbsolute && !path.isAbsolute(normalizedPath)) {
238
- throw new Error('Path must be absolute');
239
- }
240
-
241
- // Check if file must exist
242
- if (mustExist && !fs.existsSync(normalizedPath)) {
243
- throw new Error(`Path does not exist: ${normalizedPath}`);
244
- }
245
-
246
- // Check file extension if specified
247
- if (allowedExtensions && allowedExtensions.length > 0) {
248
- const ext = path.extname(normalizedPath).toLowerCase();
249
- if (!allowedExtensions.includes(ext)) {
250
- throw new Error(`Invalid file extension. Allowed: ${allowedExtensions.join(', ')}`);
251
- }
252
- }
253
-
254
- return true;
255
- }
256
-
257
- /**
258
- * Validate configuration object structure
259
- * @param {object} config - Configuration to validate
260
- * @returns {boolean} True if valid
261
- */
262
- static validateConfig(config) {
263
- if (!config || typeof config !== 'object') {
264
- throw new Error('Configuration must be an object');
265
- }
266
-
267
- // Validate output section
268
- if (!config.output || typeof config.output !== 'object') {
269
- throw new Error('Configuration missing output section');
270
- }
271
-
272
- if (!config.output.mode || !['relative', 'fixed'].includes(config.output.mode)) {
273
- throw new Error('Output mode must be either "relative" or "fixed"');
274
- }
275
-
276
- if (config.output.mode === 'fixed' && !config.output.fixedPath) {
277
- throw new Error('Fixed output mode requires fixedPath');
278
- }
279
-
280
- // Validate allowedExtensions
281
- if (!Array.isArray(config.allowedExtensions)) {
282
- throw new Error('allowedExtensions must be an array');
283
- }
284
-
285
- if (config.allowedExtensions.length === 0) {
286
- throw new Error('At least one file extension must be allowed');
287
- }
288
-
289
- // Validate excludeDirs
290
- if (!Array.isArray(config.excludeDirs)) {
291
- throw new Error('excludeDirs must be an array');
292
- }
293
-
294
- // Validate excludeFiles (optional field for backward compatibility)
295
- if (config.excludeFiles && !Array.isArray(config.excludeFiles)) {
296
- throw new Error('excludeFiles must be an array if provided');
297
- }
298
-
299
- // Validate styles section
300
- if (!config.styles || typeof config.styles !== 'object') {
301
- throw new Error('Configuration missing styles section');
302
- }
303
-
304
- // Validate settings section
305
- if (!config.settings || typeof config.settings !== 'object') {
306
- throw new Error('Configuration missing settings section');
307
- }
308
-
309
- if (typeof config.settings.maxFilesBeforePrompt !== 'number' ||
310
- config.settings.maxFilesBeforePrompt < 1) {
311
- throw new Error('maxFilesBeforePrompt must be a positive number');
312
- }
313
-
314
- return true;
315
- }
316
-
317
- /**
318
- * Sanitize user input to prevent injection attacks
319
- * @param {string} input - User input to sanitize
320
- * @param {object} options - Sanitization options
321
- * @returns {string} Sanitized input
322
- */
323
- static sanitizeInput(input, options = {}) {
324
- if (typeof input !== 'string') {
325
- return '';
326
- }
327
-
328
- const {
329
- maxLength = 1000,
330
- allowPath = false,
331
- strictMode = true
332
- } = options;
333
-
334
- let sanitized = input;
335
-
336
- if (strictMode) {
337
- // Remove or replace dangerous characters
338
- sanitized = sanitized
339
- .replace(/[<>]/g, '') // Remove potential HTML/XML tags
340
- .replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters
341
- .replace(/[`${}]/g, '') // Remove template literal and variable expansion chars
342
- .replace(/[;&|]/g, ''); // Remove command injection chars
343
- }
344
-
345
- // For path inputs, allow specific characters
346
- if (allowPath) {
347
- // Allow path separators and basic path characters (more permissive for legitimate paths)
348
- sanitized = sanitized.replace(/[^a-zA-Z0-9\-_.\\/\\:\s()]/g, '');
349
- } else {
350
- // For non-path inputs, be more restrictive
351
- sanitized = sanitized.replace(/[^a-zA-Z0-9\-_.\s]/g, '');
352
- }
353
-
354
- return sanitized
355
- .trim()
356
- .substring(0, maxLength);
357
- }
358
-
359
- /**
360
- * Validate file content before processing
361
- * @param {string} filePath - Path to file
362
- * @param {Buffer} content - File content buffer
363
- * @returns {boolean} True if content appears to be text
364
- */
365
- static validateFileContent(filePath, content) {
366
- // Check for null bytes (common in binary files)
367
- if (content.includes(0)) {
368
- console.warn(chalk.yellow(`WARNING: Skipping potentially binary file: ${filePath}`));
369
- return false;
370
- }
371
-
372
- // Check file size (warn for very large files)
373
- const maxSize = 10 * 1024 * 1024; // 10MB
374
- if (content.length > maxSize) {
375
- console.warn(chalk.yellow(`WARNING: Large file detected (${Math.round(content.length / 1024 / 1024)}MB): ${filePath}`));
376
- console.warn(chalk.gray('This may affect PDF generation performance'));
377
- }
378
-
379
- return true;
380
- }
381
-
382
- /**
383
- * Graceful shutdown handler
384
- * @param {string} signal - Signal received
385
- */
386
- static async gracefulShutdown(signal) {
387
- console.log(chalk.yellow(`\nWARNING: Received ${signal}. Shutting down gracefully...`));
388
- await this.safeExit(0, `Received ${signal}`);
389
- }
390
-
391
- /**
392
- * Setup global error handlers
393
- */
394
- static setupGlobalHandlers() {
395
- // Handle uncaught exceptions
396
- process.on('uncaughtException', async (error) => {
397
- console.error(chalk.red('UNCAUGHT EXCEPTION:'));
398
- console.error(error.stack);
399
- console.error(chalk.yellow('Please report this error to: https://github.com/skamoll/CodeSummary/issues'));
400
- await this.safeExit(1, 'Uncaught exception');
401
- });
402
-
403
- // Handle unhandled promise rejections
404
- process.on('unhandledRejection', async (reason, promise) => {
405
- console.error(chalk.red('UNHANDLED PROMISE REJECTION:'));
406
- console.error('Promise:', promise);
407
- console.error('Reason:', reason);
408
- console.error(chalk.yellow('Please report this error to: https://github.com/skamoll/CodeSummary/issues'));
409
- await this.safeExit(1, 'Unhandled promise rejection');
410
- });
411
-
412
- // Handle graceful shutdown signals
413
- process.on('SIGINT', () => ErrorHandler.gracefulShutdown('SIGINT'));
414
- process.on('SIGTERM', () => ErrorHandler.gracefulShutdown('SIGTERM'));
415
-
416
- // Handle warnings
417
- process.on('warning', (warning) => {
418
- if (process.env.NODE_ENV === 'development') {
419
- console.warn(chalk.yellow('WARNING:'), warning.message);
420
- }
421
- });
422
- }
423
-
424
- /**
425
- * Create context-aware error wrapper
426
- * @param {string} context - Context description
427
- * @returns {Function} Error wrapper function
428
- */
429
- static createErrorWrapper(context) {
430
- return async (error) => {
431
- if (error.name === 'AbortError') {
432
- console.log(chalk.yellow('\nWARNING: Operation cancelled by user'));
433
- await this.safeExit(0, 'Operation cancelled by user');
434
- return;
435
- }
436
-
437
- await ErrorHandler.handleError(error, context);
438
- };
439
- }
440
-
441
- /**
442
- * Validate environment and dependencies
443
- */
444
- static async validateEnvironment() {
445
- // Check Node.js version
446
- const nodeVersion = process.version;
447
- const majorVersion = parseInt(nodeVersion.split('.')[0].substring(1));
448
-
449
- if (majorVersion < 18) {
450
- console.error(chalk.red('ERROR: Node.js version requirement not met'));
451
- console.error(chalk.white(`Current version: ${nodeVersion}`));
452
- console.error(chalk.white('Required version: >=18.0.0'));
453
- console.error(chalk.yellow('Please upgrade Node.js: https://nodejs.org/'));
454
- await this.safeExit(1, 'Node.js version incompatible');
455
- }
456
-
457
- // Check available memory
458
- const totalMemory = os.totalmem();
459
- const freeMemory = os.freemem();
460
- const memoryUsage = process.memoryUsage();
461
-
462
- if (freeMemory < 100 * 1024 * 1024) { // Less than 100MB free
463
- console.warn(chalk.yellow('WARNING: Low system memory detected'));
464
- console.warn(chalk.gray('PDF generation may be slower for large projects'));
465
- }
466
-
467
- // Check platform compatibility
468
- const platform = process.platform;
469
- const supportedPlatforms = ['win32', 'darwin', 'linux'];
470
-
471
- if (!supportedPlatforms.includes(platform)) {
472
- console.warn(chalk.yellow(`WARNING: Untested platform: ${platform}`));
473
- console.warn(chalk.gray('CodeSummary may not work correctly on this platform'));
474
- }
475
- }
476
- }
477
-
1
+ import chalk from 'chalk';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+
6
+ /**
7
+ * Error Handler and Validation Utilities for CodeSummary
8
+ * Centralized error handling and validation logic
9
+ */
10
+ export class ErrorHandler {
11
+ // Static cleanup registry for resources that need cleanup before exit
12
+ static cleanupTasks = [];
13
+ static isCleaningUp = false;
14
+
15
+ /**
16
+ * Register a cleanup task to be executed before process exit
17
+ * @param {Function} cleanupFn - Function to call during cleanup
18
+ * @param {string} description - Description of the cleanup task
19
+ */
20
+ static registerCleanup(cleanupFn, description = 'cleanup task') {
21
+ if (typeof cleanupFn !== 'function') {
22
+ console.warn(`WARNING: Invalid cleanup task: ${description}`);
23
+ return;
24
+ }
25
+
26
+ this.cleanupTasks.push({ fn: cleanupFn, description });
27
+ }
28
+
29
+ /**
30
+ * Execute all registered cleanup tasks
31
+ * @param {boolean} verbose - Whether to log cleanup progress
32
+ */
33
+ static async executeCleanup(verbose = false) {
34
+ if (this.isCleaningUp) {
35
+ return; // Prevent recursive cleanup
36
+ }
37
+
38
+ this.isCleaningUp = true;
39
+
40
+ if (verbose && this.cleanupTasks.length > 0) {
41
+ console.log(chalk.yellow(`Cleaning up ${this.cleanupTasks.length} resources...`));
42
+ }
43
+
44
+ for (const task of this.cleanupTasks) {
45
+ try {
46
+ if (verbose) {
47
+ console.log(chalk.gray(`- ${task.description}`));
48
+ }
49
+ await task.fn();
50
+ } catch (error) {
51
+ if (verbose) {
52
+ console.warn(chalk.yellow(`WARNING: Cleanup failed for ${task.description}: ${error.message}`));
53
+ }
54
+ }
55
+ }
56
+
57
+ this.cleanupTasks = [];
58
+ this.isCleaningUp = false;
59
+ }
60
+
61
+ /**
62
+ * Safe process exit with cleanup
63
+ * @param {number} code - Exit code
64
+ * @param {string} reason - Reason for exit
65
+ */
66
+ static async safeExit(code = 0, reason = 'Process completed') {
67
+ try {
68
+ await this.executeCleanup(process.env.NODE_ENV === 'development');
69
+ if (process.env.NODE_ENV === 'development') {
70
+ console.log(chalk.green(`✓ Cleanup completed. ${reason}`));
71
+ }
72
+ } catch (error) {
73
+ console.error(chalk.red(`ERROR during cleanup: ${error.message}`));
74
+ } finally {
75
+ process.exit(code);
76
+ }
77
+ }
78
+ /**
79
+ * Handle and format CLI errors consistently
80
+ * @param {Error} error - The error object
81
+ * @param {string} context - Context where error occurred
82
+ * @param {boolean} exit - Whether to exit process
83
+ */
84
+ static async handleError(error, context = 'Unknown', exit = true) {
85
+ console.error(chalk.red('ERROR:'), chalk.white(error.message));
86
+
87
+ if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
88
+ console.error(chalk.gray('Context:'), context);
89
+ console.error(chalk.gray('Stack:'), error.stack);
90
+ }
91
+
92
+ if (exit) {
93
+ await this.safeExit(1, `Error in ${context}`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Handle configuration validation errors
99
+ * @param {Error} error - Configuration error
100
+ * @param {string} configPath - Path to config file
101
+ */
102
+ static async handleConfigError(error, configPath) {
103
+ console.error(chalk.red('CONFIGURATION ERROR'));
104
+ console.error(chalk.gray(`Config file: ${configPath}`));
105
+ console.error(chalk.white(error.message));
106
+ console.error(chalk.yellow('\nTry running: codesummary --reset-config'));
107
+ await this.safeExit(1, 'Configuration error');
108
+ }
109
+
110
+ /**
111
+ * Handle file system errors with helpful messages
112
+ * @param {Error} error - File system error
113
+ * @param {string} operation - The operation being performed
114
+ * @param {string} filePath - Path involved in the operation
115
+ */
116
+ static handleFileSystemError(error, operation, filePath) {
117
+ let message = `Failed to ${operation}`;
118
+
119
+ switch (error.code) {
120
+ case 'ENOENT':
121
+ message = `File or directory not found: ${filePath}`;
122
+ break;
123
+ case 'EACCES':
124
+ case 'EPERM':
125
+ message = `Permission denied: ${filePath}`;
126
+ console.error(chalk.yellow('SUGGESTION: Try running with elevated privileges or check file permissions'));
127
+ break;
128
+ case 'ENOSPC':
129
+ message = 'No space left on device';
130
+ break;
131
+ case 'EMFILE':
132
+ case 'ENFILE':
133
+ message = 'Too many open files';
134
+ break;
135
+ default:
136
+ message = `${message}: ${error.message}`;
137
+ }
138
+
139
+ console.error(chalk.red('FILE SYSTEM ERROR:'), chalk.white(message));
140
+
141
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
142
+ console.error(chalk.yellow('SUGGESTIONS:'));
143
+ console.error(chalk.gray(' - Check file/directory permissions'));
144
+ console.error(chalk.gray(' - Try running as administrator/sudo'));
145
+ console.error(chalk.gray(' - Ensure the file is not locked by another process'));
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Handle PDF generation errors
151
+ * @param {Error} error - PDF generation error
152
+ * @param {string} outputPath - Intended output path
153
+ */
154
+ static async handlePDFError(error, outputPath) {
155
+ console.error(chalk.red('PDF GENERATION FAILED'));
156
+ console.error(chalk.gray(`Output path: ${outputPath}`));
157
+
158
+ if (error.message.includes('ENOSPC')) {
159
+ console.error(chalk.white('Not enough disk space to generate PDF'));
160
+ console.error(chalk.yellow('SUGGESTION: Try freeing up disk space or using a different output location'));
161
+ } else if (error.message.includes('EACCES')) {
162
+ console.error(chalk.white('Permission denied writing to output location'));
163
+ console.error(chalk.yellow('SUGGESTION: Check permissions or try a different output directory'));
164
+ } else {
165
+ console.error(chalk.white(error.message));
166
+ }
167
+
168
+ await this.safeExit(1, 'PDF generation failed');
169
+ }
170
+
171
+ /**
172
+ * Validate file path for security and validity
173
+ * @param {string} filePath - Path to validate
174
+ * @param {object} options - Validation options
175
+ * @returns {boolean} True if valid
176
+ */
177
+ static validatePath(filePath, options = {}) {
178
+ const {
179
+ mustExist = false,
180
+ mustBeAbsolute = false,
181
+ allowedExtensions = null,
182
+ preventTraversal = true
183
+ } = options;
184
+
185
+ if (!filePath || typeof filePath !== 'string') {
186
+ throw new Error('Invalid file path: must be a non-empty string');
187
+ }
188
+
189
+ // Normalize path to handle different separators
190
+ const normalizedPath = path.normalize(filePath);
191
+
192
+ // Enhanced path traversal prevention
193
+ if (preventTraversal) {
194
+ // Check for various path traversal patterns
195
+ const dangerousPatterns = [
196
+ /\.\./, // Standard traversal
197
+ /\0/, // Null bytes
198
+ /[<>"|?*]/, // Invalid Windows characters
199
+ /\\\\[^\\]/, // UNC paths
200
+ /\/(etc|proc|sys|dev)\//i // Sensitive Unix directories
201
+ ];
202
+
203
+ // Note: absolute paths are not inherently dangerous; traversal (..) is
204
+ // already caught above. mustBeAbsolute enforces requirements, not security.
205
+
206
+ for (const pattern of dangerousPatterns) {
207
+ if (pattern.test(normalizedPath)) {
208
+ throw new Error('Invalid file path: contains dangerous characters or patterns');
209
+ }
210
+ }
211
+
212
+ // Additional Windows-specific checks
213
+ if (process.platform === 'win32') {
214
+ const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
215
+ 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3',
216
+ 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
217
+
218
+ const baseName = path.basename(normalizedPath, path.extname(normalizedPath)).toUpperCase();
219
+ if (reservedNames.includes(baseName)) {
220
+ throw new Error('Invalid file path: uses reserved Windows device name');
221
+ }
222
+ }
223
+
224
+ // Check path length limits
225
+ if (normalizedPath.length > 260 && process.platform === 'win32') {
226
+ throw new Error('Path too long for Windows filesystem (>260 characters)');
227
+ }
228
+ if (normalizedPath.length > 4096) {
229
+ throw new Error('Path too long (>4096 characters)');
230
+ }
231
+ }
232
+
233
+ // Check if path should be absolute
234
+ if (mustBeAbsolute && !path.isAbsolute(normalizedPath)) {
235
+ throw new Error('Path must be absolute');
236
+ }
237
+
238
+ // Check if file must exist
239
+ if (mustExist && !fs.existsSync(normalizedPath)) {
240
+ throw new Error(`Path does not exist: ${normalizedPath}`);
241
+ }
242
+
243
+ // Check file extension if specified
244
+ if (allowedExtensions && allowedExtensions.length > 0) {
245
+ const ext = path.extname(normalizedPath).toLowerCase();
246
+ if (!allowedExtensions.includes(ext)) {
247
+ throw new Error(`Invalid file extension. Allowed: ${allowedExtensions.join(', ')}`);
248
+ }
249
+ }
250
+
251
+ return true;
252
+ }
253
+
254
+ /**
255
+ * Validate configuration object structure
256
+ * @param {object} config - Configuration to validate
257
+ * @returns {boolean} True if valid
258
+ */
259
+ static validateConfig(config) {
260
+ if (!config || typeof config !== 'object') {
261
+ throw new Error('Configuration must be an object');
262
+ }
263
+
264
+ // Validate output section
265
+ if (!config.output || typeof config.output !== 'object') {
266
+ throw new Error('Configuration missing output section');
267
+ }
268
+
269
+ if (!config.output.mode || !['relative', 'fixed'].includes(config.output.mode)) {
270
+ throw new Error('Output mode must be either "relative" or "fixed"');
271
+ }
272
+
273
+ if (config.output.mode === 'fixed' && !config.output.fixedPath) {
274
+ throw new Error('Fixed output mode requires fixedPath');
275
+ }
276
+
277
+ // Validate allowedExtensions
278
+ if (!Array.isArray(config.allowedExtensions)) {
279
+ throw new Error('allowedExtensions must be an array');
280
+ }
281
+
282
+ if (config.allowedExtensions.length === 0) {
283
+ throw new Error('At least one file extension must be allowed');
284
+ }
285
+
286
+ // Validate excludeDirs
287
+ if (!Array.isArray(config.excludeDirs)) {
288
+ throw new Error('excludeDirs must be an array');
289
+ }
290
+
291
+ // Validate excludeFiles (optional field for backward compatibility)
292
+ if (config.excludeFiles && !Array.isArray(config.excludeFiles)) {
293
+ throw new Error('excludeFiles must be an array if provided');
294
+ }
295
+
296
+ // Validate styles section
297
+ if (!config.styles || typeof config.styles !== 'object') {
298
+ throw new Error('Configuration missing styles section');
299
+ }
300
+
301
+ // Validate settings section
302
+ if (!config.settings || typeof config.settings !== 'object') {
303
+ throw new Error('Configuration missing settings section');
304
+ }
305
+
306
+ if (typeof config.settings.maxFilesBeforePrompt !== 'number' ||
307
+ config.settings.maxFilesBeforePrompt < 1) {
308
+ throw new Error('maxFilesBeforePrompt must be a positive number');
309
+ }
310
+
311
+ return true;
312
+ }
313
+
314
+ /**
315
+ * Sanitize user input to prevent injection attacks
316
+ * @param {string} input - User input to sanitize
317
+ * @param {object} options - Sanitization options
318
+ * @returns {string} Sanitized input
319
+ */
320
+ static sanitizeInput(input, options = {}) {
321
+ if (typeof input !== 'string') {
322
+ return '';
323
+ }
324
+
325
+ const {
326
+ maxLength = 1000,
327
+ allowPath = false,
328
+ strictMode = true
329
+ } = options;
330
+
331
+ let sanitized = input;
332
+
333
+ if (strictMode) {
334
+ // Remove or replace dangerous characters
335
+ sanitized = sanitized
336
+ .replace(/[<>]/g, '') // Remove potential HTML/XML tags
337
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters
338
+ .replace(/[`${}]/g, '') // Remove template literal and variable expansion chars
339
+ .replace(/[;&|]/g, ''); // Remove command injection chars
340
+ }
341
+
342
+ // For path inputs, allow specific characters including non-ASCII (e.g. accented
343
+ // letters in Windows user profile paths like C:\Users\Andrés\...)
344
+ if (allowPath) {
345
+ sanitized = sanitized.replace(/[^a-zA-Z0-9\-_.\\/\\:\s()\u00C0-\uFFFF]/g, '');
346
+ } else {
347
+ // For non-path inputs, be more restrictive
348
+ sanitized = sanitized.replace(/[^a-zA-Z0-9\-_.\s]/g, '');
349
+ }
350
+
351
+ return sanitized
352
+ .trim()
353
+ .substring(0, maxLength);
354
+ }
355
+
356
+ /**
357
+ * Validate file content before processing
358
+ * @param {string} filePath - Path to file
359
+ * @param {Buffer} content - File content buffer
360
+ * @returns {boolean} True if content appears to be text
361
+ */
362
+ static validateFileContent(filePath, content) {
363
+ // Check for null bytes (common in binary files)
364
+ if (content.includes(0)) {
365
+ console.warn(chalk.yellow(`WARNING: Skipping potentially binary file: ${filePath}`));
366
+ return false;
367
+ }
368
+
369
+ // Check file size (warn for very large files)
370
+ const maxSize = 10 * 1024 * 1024; // 10MB
371
+ if (content.length > maxSize) {
372
+ console.warn(chalk.yellow(`WARNING: Large file detected (${Math.round(content.length / 1024 / 1024)}MB): ${filePath}`));
373
+ console.warn(chalk.gray('This may affect PDF generation performance'));
374
+ }
375
+
376
+ return true;
377
+ }
378
+
379
+ /**
380
+ * Graceful shutdown handler
381
+ * @param {string} signal - Signal received
382
+ */
383
+ static async gracefulShutdown(signal) {
384
+ console.log(chalk.yellow(`\nWARNING: Received ${signal}. Shutting down gracefully...`));
385
+ await this.safeExit(0, `Received ${signal}`);
386
+ }
387
+
388
+ /**
389
+ * Setup global error handlers
390
+ */
391
+ static setupGlobalHandlers() {
392
+ // Handle uncaught exceptions
393
+ process.on('uncaughtException', async (error) => {
394
+ console.error(chalk.red('UNCAUGHT EXCEPTION:'));
395
+ console.error(error.stack);
396
+ console.error(chalk.yellow('Please report this error to: https://github.com/skamoll/CodeSummary/issues'));
397
+ await this.safeExit(1, 'Uncaught exception');
398
+ });
399
+
400
+ // Handle unhandled promise rejections
401
+ process.on('unhandledRejection', async (reason, promise) => {
402
+ console.error(chalk.red('UNHANDLED PROMISE REJECTION:'));
403
+ console.error('Promise:', promise);
404
+ console.error('Reason:', reason);
405
+ console.error(chalk.yellow('Please report this error to: https://github.com/skamoll/CodeSummary/issues'));
406
+ await this.safeExit(1, 'Unhandled promise rejection');
407
+ });
408
+
409
+ // Handle graceful shutdown signals
410
+ process.on('SIGINT', () => ErrorHandler.gracefulShutdown('SIGINT'));
411
+ process.on('SIGTERM', () => ErrorHandler.gracefulShutdown('SIGTERM'));
412
+
413
+ // Handle warnings
414
+ process.on('warning', (warning) => {
415
+ if (process.env.NODE_ENV === 'development') {
416
+ console.warn(chalk.yellow('WARNING:'), warning.message);
417
+ }
418
+ });
419
+ }
420
+
421
+ /**
422
+ * Create context-aware error wrapper
423
+ * @param {string} context - Context description
424
+ * @returns {Function} Error wrapper function
425
+ */
426
+ static createErrorWrapper(context) {
427
+ return async (error) => {
428
+ if (error.name === 'AbortError') {
429
+ console.log(chalk.yellow('\nWARNING: Operation cancelled by user'));
430
+ await this.safeExit(0, 'Operation cancelled by user');
431
+ return;
432
+ }
433
+
434
+ await ErrorHandler.handleError(error, context);
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Validate environment and dependencies
440
+ */
441
+ static async validateEnvironment() {
442
+ // Check Node.js version
443
+ const nodeVersion = process.version;
444
+ const majorVersion = parseInt(nodeVersion.split('.')[0].substring(1));
445
+
446
+ if (majorVersion < 18) {
447
+ console.error(chalk.red('ERROR: Node.js version requirement not met'));
448
+ console.error(chalk.white(`Current version: ${nodeVersion}`));
449
+ console.error(chalk.white('Required version: >=18.0.0'));
450
+ console.error(chalk.yellow('Please upgrade Node.js: https://nodejs.org/'));
451
+ await this.safeExit(1, 'Node.js version incompatible');
452
+ }
453
+
454
+ // Check available memory
455
+ const totalMemory = os.totalmem();
456
+ const freeMemory = os.freemem();
457
+ const memoryUsage = process.memoryUsage();
458
+
459
+ if (freeMemory < 100 * 1024 * 1024) { // Less than 100MB free
460
+ console.warn(chalk.yellow('WARNING: Low system memory detected'));
461
+ console.warn(chalk.gray('PDF generation may be slower for large projects'));
462
+ }
463
+
464
+ // Check platform compatibility
465
+ const platform = process.platform;
466
+ const supportedPlatforms = ['win32', 'darwin', 'linux'];
467
+
468
+ if (!supportedPlatforms.includes(platform)) {
469
+ console.warn(chalk.yellow(`WARNING: Untested platform: ${platform}`));
470
+ console.warn(chalk.gray('CodeSummary may not work correctly on this platform'));
471
+ }
472
+ }
473
+ }
474
+
478
475
  export default ErrorHandler;