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.
- package/CHANGELOG.md +234 -190
- package/LICENSE +674 -674
- package/README.md +483 -607
- package/bin/codesummary.js +12 -12
- package/features.md +418 -502
- package/package.json +95 -95
- package/rag-schema.json +113 -113
- package/src/cli.js +599 -540
- package/src/configManager.js +880 -827
- package/src/errorHandler.js +474 -477
- package/src/index.js +25 -25
- package/src/llmGenerator.js +189 -0
- package/src/pdfGenerator.js +408 -475
- package/src/ragConfig.js +369 -373
- package/src/ragGenerator.js +1739 -1757
- package/src/scanner.js +386 -467
- package/src/utils.js +139 -0
package/src/errorHandler.js
CHANGED
|
@@ -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
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (normalizedPath.length >
|
|
229
|
-
throw new Error('Path too long
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
*
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
.replace(/[
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
sanitized = sanitized.replace(/[^a-zA-Z0-9\-_
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
*
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
console.error(chalk.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
process.on('
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
console.error(chalk.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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;
|