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/src/cli.js CHANGED
@@ -1,541 +1,600 @@
1
- import inquirer from 'inquirer';
2
- import chalk from 'chalk';
3
- import path from 'path';
4
- import fs from 'fs-extra';
5
- import ora from 'ora';
6
-
7
- import ConfigManager from './configManager.js';
8
- import Scanner from './scanner.js';
9
- import PDFGenerator from './pdfGenerator.js';
10
- import ErrorHandler from './errorHandler.js';
11
-
12
- /**
13
- * Command Line Interface for CodeSummary
14
- * Handles user interaction and orchestrates the scanning and PDF generation process
15
- */
16
- export class CLI {
17
- constructor() {
18
- this.configManager = new ConfigManager();
19
- this.config = null;
20
- this.scanner = null;
21
- this.pdfGenerator = null;
22
- }
23
-
24
- /**
25
- * Main entry point for CLI execution
26
- * @param {Array} args - Command line arguments
27
- */
28
- async run(args = []) {
29
- try {
30
- // Parse command line arguments
31
- const options = await this.parseArguments(args);
32
-
33
- // Handle special commands
34
- if (options.showConfig) {
35
- await this.showConfig();
36
- return;
37
- }
38
-
39
- if (options.resetConfig) {
40
- await this.resetConfig();
41
- return;
42
- }
43
-
44
- if (options.config) {
45
- await this.editConfig();
46
- return;
47
- }
48
-
49
- // Main scanning and PDF generation flow
50
- await this.executeMainFlow(options);
51
-
52
- } catch (error) {
53
- ErrorHandler.handleError(error, 'CLI Operation');
54
- }
55
- }
56
-
57
- /**
58
- * Parse command line arguments
59
- * @param {Array} args - Raw arguments
60
- * @returns {object} Parsed options
61
- */
62
- async parseArguments(args) {
63
- const options = {
64
- output: null,
65
- showConfig: false,
66
- resetConfig: false,
67
- config: false,
68
- help: false,
69
- version: false,
70
- noInteractive: false,
71
- format: 'pdf'
72
- };
73
-
74
- for (let i = 0; i < args.length; i++) {
75
- const arg = args[i];
76
-
77
- switch (arg) {
78
- case '--output':
79
- case '-o':
80
- if (i + 1 >= args.length) {
81
- throw new Error(`Option ${arg} requires a value`);
82
- }
83
- i++; // Move to next argument
84
- const outputPath = args[i];
85
-
86
- // Validate output path
87
- if (!outputPath || outputPath.trim().length === 0) {
88
- throw new Error(`Option ${arg} requires a non-empty path`);
89
- }
90
-
91
- // Sanitize and validate path
92
- const sanitizedPath = ErrorHandler.sanitizeInput(outputPath, {
93
- allowPath: true,
94
- maxLength: 500,
95
- strictMode: true
96
- });
97
-
98
- if (sanitizedPath !== outputPath) {
99
- console.warn(chalk.yellow(`WARNING: Output path was sanitized: ${outputPath} -> ${sanitizedPath}`));
100
- }
101
-
102
- try {
103
- ErrorHandler.validatePath(sanitizedPath, {
104
- preventTraversal: true,
105
- mustBeAbsolute: false
106
- });
107
- } catch (error) {
108
- throw new Error(`Invalid output path: ${error.message}`);
109
- }
110
-
111
- options.output = sanitizedPath;
112
- break;
113
- case '--show-config':
114
- options.showConfig = true;
115
- break;
116
- case '--reset-config':
117
- options.resetConfig = true;
118
- break;
119
- case 'config':
120
- options.config = true;
121
- break;
122
- case '--help':
123
- case '-h':
124
- options.help = true;
125
- break;
126
- case '--version':
127
- case '-v':
128
- options.version = true;
129
- break;
130
- case '--no-interactive':
131
- options.noInteractive = true;
132
- break;
133
- case '--format':
134
- case '-f':
135
- if (i + 1 >= args.length) {
136
- throw new Error(`Option ${arg} requires a value (pdf or rag)`);
137
- }
138
- i++;
139
- const format = args[i].toLowerCase();
140
- if (!['pdf', 'rag'].includes(format)) {
141
- throw new Error(`Invalid format: ${format}. Use 'pdf' or 'rag'`);
142
- }
143
- options.format = format;
144
- break;
145
- default:
146
- if (arg.startsWith('-')) {
147
- throw new Error(`Unknown option: ${arg}`);
148
- }
149
- // Allow non-option arguments (for future extensibility)
150
- break;
151
- }
152
- }
153
-
154
- if (options.help) {
155
- this.showHelp();
156
- await ErrorHandler.safeExit(0, 'Help displayed');
157
- }
158
-
159
- if (options.version) {
160
- await this.showVersion();
161
- await ErrorHandler.safeExit(0, 'Version displayed');
162
- }
163
-
164
- return options;
165
- }
166
-
167
- /**
168
- * Execute the main scanning and PDF generation flow
169
- * @param {object} options - Parsed command line options
170
- */
171
- async executeMainFlow(options) {
172
- // Load or create configuration
173
- this.config = await this.loadConfiguration();
174
-
175
- // Initialize components
176
- this.scanner = new Scanner(this.config);
177
- this.pdfGenerator = new PDFGenerator(this.config);
178
-
179
- // Determine scan path (default: current working directory)
180
- const scanPath = process.cwd();
181
- const projectName = path.basename(scanPath);
182
-
183
- console.log(chalk.cyan(`CodeSummary - Scanning project: ${chalk.bold(projectName)}\n`));
184
-
185
- // Scan directory
186
- const spinner = ora('Scanning directory structure...').start();
187
- const filesByExtension = await this.scanner.scanDirectory(scanPath);
188
- spinner.succeed('Directory scan completed');
189
-
190
- // Check if any supported files were found
191
- if (Object.keys(filesByExtension).length === 0) {
192
- console.log(chalk.red('ERROR: No supported files found. Nothing to document.'));
193
- await ErrorHandler.safeExit(1, 'No supported files found');
194
- }
195
-
196
- // Display scan summary
197
- this.scanner.displayScanSummary(filesByExtension);
198
-
199
- // Let user select extensions to include
200
- const selectedExtensions = await this.selectExtensions(filesByExtension);
201
-
202
- if (selectedExtensions.length === 0) {
203
- console.log(chalk.yellow('WARNING: No extensions selected. Exiting.'));
204
- await ErrorHandler.safeExit(0, 'No extensions selected');
205
- }
206
-
207
- // Check file count threshold
208
- const totalFiles = this.calculateTotalFiles(filesByExtension, selectedExtensions);
209
- await this.checkFileCountThreshold(totalFiles);
210
-
211
- // Generate output based on format
212
- if (options.format === 'rag') {
213
- // Generate RAG-optimized output
214
- const ragGenerator = await import('./ragGenerator.js');
215
- const ragOutputPath = this.determineRagOutputPath(options.output, projectName);
216
-
217
- // Ensure output directory exists
218
- await fs.ensureDir(path.dirname(ragOutputPath));
219
-
220
- const generationSpinner = ora('Generating RAG-optimized output...').start();
221
- const result = await ragGenerator.default.generateRagOutput(
222
- filesByExtension,
223
- selectedExtensions,
224
- ragOutputPath,
225
- projectName,
226
- scanPath
227
- );
228
- generationSpinner.succeed('RAG output generation completed');
229
-
230
- // Display RAG success summary
231
- await this.displayRagCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.totalChunks);
232
- } else {
233
- // Generate PDF (default behavior)
234
- const outputPath = this.determineOutputPath(options.output, projectName);
235
-
236
- // Ensure output directory exists
237
- await PDFGenerator.ensureOutputDirectory(path.dirname(outputPath));
238
-
239
- // Generate PDF
240
- const generationSpinner = ora('Generating PDF document...').start();
241
- const result = await this.pdfGenerator.generatePDF(
242
- filesByExtension,
243
- selectedExtensions,
244
- outputPath,
245
- projectName
246
- );
247
- generationSpinner.succeed('PDF generation completed');
248
-
249
- // Display success summary
250
- await this.displayCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.pageCount);
251
- }
252
- }
253
-
254
- /**
255
- * Load configuration (with first-run setup if needed)
256
- * @returns {object} Configuration object
257
- */
258
- async loadConfiguration() {
259
- let config = await this.configManager.loadConfig();
260
-
261
- if (!config) {
262
- // First run - trigger setup wizard
263
- config = await this.configManager.runFirstTimeSetup();
264
- } else {
265
- console.log(chalk.gray(`Using configuration from ${this.configManager.configPath}`));
266
- }
267
-
268
- return config;
269
- }
270
-
271
- /**
272
- * Let user select which extensions to include
273
- * @param {object} filesByExtension - Available files by extension
274
- * @returns {Array} Selected extensions
275
- */
276
- async selectExtensions(filesByExtension) {
277
- const extensionInfo = this.scanner.getExtensionInfo(filesByExtension);
278
-
279
- const choices = extensionInfo.map(info => ({
280
- name: `${info.extension} → ${info.description} (${info.count} files)`,
281
- value: info.extension,
282
- checked: true // Pre-select all detected extensions
283
- }));
284
-
285
- const { selectedExtensions } = await inquirer.prompt([{
286
- type: 'checkbox',
287
- name: 'selectedExtensions',
288
- message: 'Select file extensions to include:',
289
- choices,
290
- validate: (answer) => {
291
- if (answer.length === 0) {
292
- return 'You must select at least one extension.';
293
- }
294
- return true;
295
- }
296
- }]);
297
-
298
- return selectedExtensions;
299
- }
300
-
301
- /**
302
- * Calculate total files for selected extensions
303
- * @param {object} filesByExtension - Files by extension
304
- * @param {Array} selectedExtensions - Selected extensions
305
- * @returns {number} Total file count
306
- */
307
- calculateTotalFiles(filesByExtension, selectedExtensions) {
308
- return selectedExtensions.reduce((total, ext) => {
309
- return total + (filesByExtension[ext]?.length || 0);
310
- }, 0);
311
- }
312
-
313
- /**
314
- * Check if file count exceeds threshold and prompt user
315
- * @param {number} totalFiles - Total file count
316
- */
317
- async checkFileCountThreshold(totalFiles) {
318
- if (totalFiles > this.config.settings.maxFilesBeforePrompt) {
319
- console.log(chalk.yellow(`WARNING: Found ${totalFiles} files. Generating the PDF may take a while.`));
320
-
321
- const { shouldContinue } = await inquirer.prompt([{
322
- type: 'confirm',
323
- name: 'shouldContinue',
324
- message: 'Do you want to continue?',
325
- default: true
326
- }]);
327
-
328
- if (!shouldContinue) {
329
- console.log(chalk.gray('Operation cancelled by user.'));
330
- await ErrorHandler.safeExit(0, 'Operation cancelled by user');
331
- }
332
- }
333
- }
334
-
335
- /**
336
- * Determine final output path for RAG format
337
- * @param {string} overridePath - Optional override path from CLI
338
- * @param {string} projectName - Project name
339
- * @returns {string} Final output path
340
- */
341
- determineRagOutputPath(overridePath, projectName) {
342
- let outputDir;
343
-
344
- if (overridePath) {
345
- const sanitizedPath = ErrorHandler.sanitizeInput(overridePath);
346
- ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
347
- outputDir = path.resolve(sanitizedPath);
348
- } else {
349
- if (this.config.output.mode === 'relative') {
350
- outputDir = process.cwd();
351
- } else {
352
- outputDir = path.resolve(this.config.output.fixedPath);
353
- }
354
- }
355
-
356
- const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
357
- return path.join(outputDir, `${sanitizedProjectName}_rag.json`);
358
- }
359
-
360
- /**
361
- * Determine final output path for PDF
362
- * @param {string} overridePath - Optional override path from CLI
363
- * @param {string} projectName - Project name
364
- * @returns {string} Final output path
365
- */
366
- determineOutputPath(overridePath, projectName) {
367
- let outputDir;
368
-
369
- if (overridePath) {
370
- // Validate and sanitize override path from CLI
371
- const sanitizedPath = ErrorHandler.sanitizeInput(overridePath);
372
- ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
373
-
374
- outputDir = path.resolve(sanitizedPath);
375
- console.log(chalk.gray(`PDF will be saved to: ${outputDir}`));
376
- } else {
377
- // Use config settings
378
- if (this.config.output.mode === 'relative') {
379
- outputDir = process.cwd();
380
- } else {
381
- outputDir = path.resolve(this.config.output.fixedPath);
382
- }
383
- }
384
-
385
- // Sanitize project name for filename
386
- const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
387
-
388
- return PDFGenerator.generateOutputPath(sanitizedProjectName, outputDir);
389
- }
390
-
391
- /**
392
- * Display RAG completion summary
393
- * @param {string} outputPath - Generated RAG JSON path
394
- * @param {Array} selectedExtensions - Selected extensions
395
- * @param {number} totalFiles - Total files processed
396
- * @param {number} totalChunks - Number of chunks generated
397
- */
398
- async displayRagCompletionSummary(outputPath, selectedExtensions, totalFiles, totalChunks) {
399
- const stats = await fs.stat(outputPath);
400
- const fileSizeFormatted = this.formatFileSize(stats.size);
401
-
402
- console.log(chalk.green('\nSUCCESS: RAG-optimized output generated successfully!\n'));
403
- console.log(chalk.cyan('Summary:'));
404
- console.log(chalk.gray(` Output: ${outputPath}`));
405
- console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`));
406
- console.log(chalk.gray(` Total files: ${totalFiles}`));
407
- console.log(chalk.gray(` Total chunks: ${totalChunks}`));
408
- console.log(chalk.gray(` JSON size: ${fileSizeFormatted}`));
409
- console.log(chalk.gray(` Ready for RAG/LLM ingestion`));
410
- console.log();
411
- }
412
-
413
- /**
414
- * Display completion summary
415
- * @param {string} outputPath - Generated PDF path
416
- * @param {Array} selectedExtensions - Selected extensions
417
- * @param {number} totalFiles - Total files processed
418
- * @param {number|string} pageCount - Number of pages in PDF or 'N/A'
419
- */
420
- async displayCompletionSummary(outputPath, selectedExtensions, totalFiles, pageCount) {
421
- // Get PDF stats
422
- const stats = await fs.stat(outputPath);
423
- const fileSizeFormatted = this.formatFileSize(stats.size);
424
-
425
- console.log(chalk.green('\nSUCCESS: PDF generation completed successfully!\n'));
426
- console.log(chalk.cyan('Summary:'));
427
- console.log(chalk.gray(` Output: ${outputPath}`));
428
- console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`));
429
- console.log(chalk.gray(` Total files: ${totalFiles}`));
430
- if (pageCount !== 'N/A') {
431
- console.log(chalk.gray(` Total pages: ${pageCount}`));
432
- }
433
- console.log(chalk.gray(` PDF size: ${fileSizeFormatted}`));
434
- console.log();
435
- }
436
-
437
- /**
438
- * Show current configuration
439
- */
440
- async showConfig() {
441
- const config = await this.configManager.loadConfig();
442
- if (config) {
443
- this.configManager.displayConfig(config);
444
- } else {
445
- console.log(chalk.yellow('WARNING: No configuration found. Run codesummary to set up.'));
446
- }
447
- }
448
-
449
- /**
450
- * Reset configuration
451
- */
452
- async resetConfig() {
453
- await this.configManager.resetConfig();
454
- console.log(chalk.green('SUCCESS: Configuration reset. Run codesummary to set up again.'));
455
- }
456
-
457
- /**
458
- * Edit configuration interactively
459
- */
460
- async editConfig() {
461
- let config = await this.configManager.loadConfig();
462
-
463
- if (!config) {
464
- console.log(chalk.yellow('WARNING: No configuration found. Running first-time setup...'));
465
- config = await this.configManager.runFirstTimeSetup();
466
- } else {
467
- config = await this.configManager.editConfig(config);
468
- }
469
- }
470
-
471
- /**
472
- * Format file size in human readable format
473
- * @param {number} bytes - Size in bytes
474
- * @returns {string} Formatted size
475
- */
476
- formatFileSize(bytes) {
477
- const units = ['B', 'KB', 'MB', 'GB'];
478
- let size = bytes;
479
- let unitIndex = 0;
480
-
481
- while (size >= 1024 && unitIndex < units.length - 1) {
482
- size /= 1024;
483
- unitIndex++;
484
- }
485
-
486
- return `${size.toFixed(1)} ${units[unitIndex]}`;
487
- }
488
-
489
- /**
490
- * Show version information
491
- */
492
- async showVersion() {
493
- try {
494
- // Get the current module directory and resolve package.json
495
- const currentDir = path.dirname(new URL(import.meta.url).pathname);
496
- // Handle Windows paths by removing leading slash if present
497
- const normalizedDir = process.platform === 'win32' && currentDir.startsWith('/')
498
- ? currentDir.slice(1)
499
- : currentDir;
500
- const packageJsonPath = path.resolve(normalizedDir, '..', 'package.json');
501
-
502
- const packageJson = await fs.readJson(packageJsonPath);
503
- console.log(`CodeSummary v${packageJson.version}`);
504
- } catch (error) {
505
- console.log('CodeSummary version unknown');
506
- }
507
- }
508
-
509
- /**
510
- * Show help information
511
- */
512
- showHelp() {
513
- console.log(chalk.cyan('\nCodeSummary - Generate PDF documentation from source code\n'));
514
-
515
- console.log(chalk.white('Usage:'));
516
- console.log(' codesummary [options] Scan current directory and generate PDF');
517
- console.log(' codesummary config Edit configuration settings');
518
- console.log();
519
-
520
- console.log(chalk.white('Options:'));
521
- console.log(' -o, --output <path> Override output directory');
522
- console.log(' -f, --format <format> Output format: pdf (default) or rag');
523
- console.log(' --show-config Display current configuration');
524
- console.log(' --reset-config Reset configuration to defaults');
525
- console.log(' -h, --help Show this help message');
526
- console.log(' -v, --version Show version information');
527
- console.log();
528
-
529
- console.log(chalk.white('Examples:'));
530
- console.log(' codesummary Scan current project (PDF)');
531
- console.log(' codesummary --format rag Generate RAG-optimized JSON');
532
- console.log(' codesummary --output ./docs Save output to ./docs folder');
533
- console.log(' codesummary config Edit settings');
534
- console.log(' codesummary --show-config View current settings');
535
- console.log();
536
-
537
- console.log(chalk.gray('For more information, visit: https://github.com/skamoll/CodeSummary'));
538
- }
539
- }
540
-
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import ora from 'ora';
6
+ import { createRequire } from 'module';
7
+
8
+ import ConfigManager from './configManager.js';
9
+ import Scanner from './scanner.js';
10
+ import PDFGenerator from './pdfGenerator.js';
11
+ import RagGenerator from './ragGenerator.js';
12
+ import LlmGenerator from './llmGenerator.js';
13
+ import ErrorHandler from './errorHandler.js';
14
+ import { formatFileSize, resolveVersionedPath } from './utils.js';
15
+
16
+ const _require = createRequire(import.meta.url);
17
+
18
+ /**
19
+ * Command Line Interface for CodeSummary
20
+ * Handles user interaction and orchestrates the scanning and PDF generation process
21
+ */
22
+ export class CLI {
23
+ constructor() {
24
+ this.configManager = new ConfigManager();
25
+ this.config = null;
26
+ this.scanner = null;
27
+ this.pdfGenerator = null;
28
+ }
29
+
30
+ /**
31
+ * Main entry point for CLI execution
32
+ * @param {Array} args - Command line arguments
33
+ */
34
+ async run(args = []) {
35
+ try {
36
+ // Parse command line arguments
37
+ const options = await this.parseArguments(args);
38
+
39
+ // Handle special commands
40
+ if (options.showConfig) {
41
+ await this.showConfig();
42
+ return;
43
+ }
44
+
45
+ if (options.resetConfig) {
46
+ await this.resetConfig();
47
+ return;
48
+ }
49
+
50
+ if (options.config) {
51
+ await this.editConfig();
52
+ return;
53
+ }
54
+
55
+ // Main scanning and PDF generation flow
56
+ await this.executeMainFlow(options);
57
+
58
+ } catch (error) {
59
+ ErrorHandler.handleError(error, 'CLI Operation');
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Parse command line arguments
65
+ * @param {Array} args - Raw arguments
66
+ * @returns {object} Parsed options
67
+ */
68
+ async parseArguments(args) {
69
+ const options = {
70
+ output: null,
71
+ showConfig: false,
72
+ resetConfig: false,
73
+ config: false,
74
+ help: false,
75
+ version: false,
76
+ noInteractive: false,
77
+ format: 'pdf'
78
+ };
79
+
80
+ for (let i = 0; i < args.length; i++) {
81
+ const arg = args[i];
82
+
83
+ switch (arg) {
84
+ case '--output':
85
+ case '-o':
86
+ if (i + 1 >= args.length) {
87
+ throw new Error(`Option ${arg} requires a value`);
88
+ }
89
+ i++; // Move to next argument
90
+ const outputPath = args[i];
91
+
92
+ // Validate output path
93
+ if (!outputPath || outputPath.trim().length === 0) {
94
+ throw new Error(`Option ${arg} requires a non-empty path`);
95
+ }
96
+
97
+ // Sanitize and validate path
98
+ const sanitizedPath = ErrorHandler.sanitizeInput(outputPath, {
99
+ allowPath: true,
100
+ maxLength: 500,
101
+ strictMode: true
102
+ });
103
+
104
+ if (sanitizedPath !== outputPath) {
105
+ console.warn(chalk.yellow(`WARNING: Output path was sanitized: ${outputPath} -> ${sanitizedPath}`));
106
+ }
107
+
108
+ try {
109
+ ErrorHandler.validatePath(sanitizedPath, {
110
+ preventTraversal: true,
111
+ mustBeAbsolute: false
112
+ });
113
+ } catch (error) {
114
+ throw new Error(`Invalid output path: ${error.message}`);
115
+ }
116
+
117
+ options.output = sanitizedPath;
118
+ break;
119
+ case '--show-config':
120
+ options.showConfig = true;
121
+ break;
122
+ case '--reset-config':
123
+ options.resetConfig = true;
124
+ break;
125
+ case 'config':
126
+ options.config = true;
127
+ break;
128
+ case '--help':
129
+ case '-h':
130
+ options.help = true;
131
+ break;
132
+ case '--version':
133
+ case '-v':
134
+ options.version = true;
135
+ break;
136
+ case '--no-interactive':
137
+ options.noInteractive = true;
138
+ break;
139
+ case '--format':
140
+ case '-f':
141
+ if (i + 1 >= args.length) {
142
+ throw new Error(`Option ${arg} requires a value (pdf or rag)`);
143
+ }
144
+ i++;
145
+ const format = args[i].toLowerCase();
146
+ if (!['pdf', 'rag', 'both', 'llm'].includes(format)) {
147
+ throw new Error(`Invalid format: ${format}. Use 'pdf', 'rag', 'llm', or 'both'`);
148
+ }
149
+ options.format = format;
150
+ break;
151
+ default:
152
+ if (arg.startsWith('-')) {
153
+ throw new Error(`Unknown option: ${arg}`);
154
+ }
155
+ // Allow non-option arguments (for future extensibility)
156
+ break;
157
+ }
158
+ }
159
+
160
+ if (options.help) {
161
+ this.showHelp();
162
+ await ErrorHandler.safeExit(0, 'Help displayed');
163
+ }
164
+
165
+ if (options.version) {
166
+ this.showVersion();
167
+ await ErrorHandler.safeExit(0, 'Version displayed');
168
+ }
169
+
170
+ return options;
171
+ }
172
+
173
+ /**
174
+ * Execute the main scanning and PDF generation flow
175
+ * @param {object} options - Parsed command line options
176
+ */
177
+ async executeMainFlow(options) {
178
+ // Load or create configuration
179
+ this.config = await this.loadConfiguration();
180
+
181
+ // Initialize components
182
+ this.scanner = new Scanner(this.config);
183
+ this.pdfGenerator = new PDFGenerator(this.config);
184
+
185
+ // Determine scan path (default: current working directory)
186
+ const scanPath = process.cwd();
187
+ const projectName = path.basename(scanPath);
188
+
189
+ console.log(chalk.cyan(`CodeSummary - Scanning project: ${chalk.bold(projectName)}\n`));
190
+
191
+ // Scan directory
192
+ const spinner = ora('Scanning directory structure...').start();
193
+ const filesByExtension = await this.scanner.scanDirectory(scanPath);
194
+ spinner.succeed('Directory scan completed');
195
+
196
+ // Check if any supported files were found
197
+ if (Object.keys(filesByExtension).length === 0) {
198
+ console.log(chalk.red('ERROR: No supported files found. Nothing to document.'));
199
+ await ErrorHandler.safeExit(1, 'No supported files found');
200
+ }
201
+
202
+ // Display scan summary
203
+ this.scanner.displayScanSummary(filesByExtension);
204
+
205
+ // Let user select extensions to include (skip prompt in non-interactive mode)
206
+ const selectedExtensions = (options.noInteractive || !process.stdin.isTTY)
207
+ ? Object.keys(filesByExtension)
208
+ : await this.selectExtensions(filesByExtension);
209
+
210
+ if (selectedExtensions.length === 0) {
211
+ console.log(chalk.yellow('WARNING: No extensions selected. Exiting.'));
212
+ await ErrorHandler.safeExit(0, 'No extensions selected');
213
+ }
214
+
215
+ // Check file count threshold
216
+ const totalFiles = this.calculateTotalFiles(filesByExtension, selectedExtensions);
217
+ await this.checkFileCountThreshold(totalFiles, options.noInteractive);
218
+
219
+ // Generate output based on format (--both runs pdf+rag; errors are collected)
220
+ const runPdf = options.format === 'pdf' || options.format === 'both';
221
+ const runRag = options.format === 'rag' || options.format === 'both';
222
+ const runLlm = options.format === 'llm';
223
+ const generationErrors = [];
224
+
225
+ if (runPdf) {
226
+ try {
227
+ const outputPath = this.determineOutputPath(options.output, projectName);
228
+ await PDFGenerator.ensureOutputDirectory(path.dirname(outputPath));
229
+ const pdfSpinner = ora('Generating PDF document...').start();
230
+ const result = await this.pdfGenerator.generatePDF(
231
+ filesByExtension,
232
+ selectedExtensions,
233
+ outputPath,
234
+ projectName
235
+ );
236
+ pdfSpinner.succeed('PDF generation completed');
237
+ await this.displayCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.pageCount);
238
+ } catch (error) {
239
+ generationErrors.push(`PDF: ${error.message}`);
240
+ console.error(chalk.red(`ERROR generating PDF: ${error.message}`));
241
+ }
242
+ }
243
+
244
+ if (runRag) {
245
+ try {
246
+ const ragOutputPath = this.determineRagOutputPath(options.output, projectName);
247
+ await fs.ensureDir(path.dirname(ragOutputPath));
248
+ const ragSpinner = ora('Generating RAG-optimized output...').start();
249
+ const ragGenerator = new RagGenerator();
250
+ const result = await ragGenerator.generateRagOutput(
251
+ filesByExtension,
252
+ selectedExtensions,
253
+ ragOutputPath,
254
+ projectName,
255
+ scanPath
256
+ );
257
+ ragSpinner.succeed('RAG output generation completed');
258
+ await this.displayRagCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.totalChunks);
259
+ } catch (error) {
260
+ generationErrors.push(`RAG: ${error.message}`);
261
+ console.error(chalk.red(`ERROR generating RAG output: ${error.message}`));
262
+ }
263
+ }
264
+
265
+ if (runLlm) {
266
+ try {
267
+ const llmOutputPath = this.determineLlmOutputPath(options.output, projectName);
268
+ await fs.ensureDir(path.dirname(llmOutputPath));
269
+ const llmSpinner = ora('Generating LLM-optimised Markdown...').start();
270
+ const llmGenerator = new LlmGenerator();
271
+ const result = await llmGenerator.generateLlmOutput(
272
+ filesByExtension,
273
+ selectedExtensions,
274
+ llmOutputPath,
275
+ projectName
276
+ );
277
+ llmSpinner.succeed('LLM output generation completed');
278
+ await this.displayLlmCompletionSummary(result.outputPath, selectedExtensions, totalFiles);
279
+ } catch (error) {
280
+ generationErrors.push(`LLM: ${error.message}`);
281
+ console.error(chalk.red(`ERROR generating LLM output: ${error.message}`));
282
+ }
283
+ }
284
+
285
+ if (generationErrors.length > 0) {
286
+ console.error(chalk.red(`\n${generationErrors.length} output(s) failed.`));
287
+ await ErrorHandler.safeExit(1, 'Generation errors');
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Load configuration (with first-run setup if needed)
293
+ * @returns {object} Configuration object
294
+ */
295
+ async loadConfiguration() {
296
+ let config = await this.configManager.loadConfig();
297
+
298
+ if (!config) {
299
+ // First run - trigger setup wizard
300
+ config = await this.configManager.runFirstTimeSetup();
301
+ } else {
302
+ console.log(chalk.gray(`Using configuration from ${this.configManager.configPath}`));
303
+ }
304
+
305
+ return config;
306
+ }
307
+
308
+ /**
309
+ * Let user select which extensions to include
310
+ * @param {object} filesByExtension - Available files by extension
311
+ * @returns {Array} Selected extensions
312
+ */
313
+ async selectExtensions(filesByExtension) {
314
+ const extensionInfo = this.scanner.getExtensionInfo(filesByExtension);
315
+
316
+ const choices = extensionInfo.map(info => ({
317
+ name: `${info.extension} → ${info.description} (${info.count} files)`,
318
+ value: info.extension,
319
+ checked: true // Pre-select all detected extensions
320
+ }));
321
+
322
+ const { selectedExtensions } = await inquirer.prompt([{
323
+ type: 'checkbox',
324
+ name: 'selectedExtensions',
325
+ message: 'Select file extensions to include:',
326
+ choices,
327
+ validate: (answer) => {
328
+ if (answer.length === 0) {
329
+ return 'You must select at least one extension.';
330
+ }
331
+ return true;
332
+ }
333
+ }]);
334
+
335
+ return selectedExtensions;
336
+ }
337
+
338
+ /**
339
+ * Calculate total files for selected extensions
340
+ * @param {object} filesByExtension - Files by extension
341
+ * @param {Array} selectedExtensions - Selected extensions
342
+ * @returns {number} Total file count
343
+ */
344
+ calculateTotalFiles(filesByExtension, selectedExtensions) {
345
+ return selectedExtensions.reduce((total, ext) => {
346
+ return total + (filesByExtension[ext]?.length || 0);
347
+ }, 0);
348
+ }
349
+
350
+ /**
351
+ * Check if file count exceeds threshold and prompt user
352
+ * @param {number} totalFiles - Total file count
353
+ */
354
+ async checkFileCountThreshold(totalFiles, noInteractive = false) {
355
+ if (totalFiles > this.config.settings.maxFilesBeforePrompt) {
356
+ console.log(chalk.yellow(`WARNING: Found ${totalFiles} files. Generation may take a while.`));
357
+
358
+ if (noInteractive || !process.stdin.isTTY) return;
359
+
360
+ const { shouldContinue } = await inquirer.prompt([{
361
+ type: 'confirm',
362
+ name: 'shouldContinue',
363
+ message: 'Do you want to continue?',
364
+ default: true
365
+ }]);
366
+
367
+ if (!shouldContinue) {
368
+ console.log(chalk.gray('Operation cancelled by user.'));
369
+ await ErrorHandler.safeExit(0, 'Operation cancelled by user');
370
+ }
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Determine final output path for RAG format
376
+ * @param {string} overridePath - Optional override path from CLI
377
+ * @param {string} projectName - Project name
378
+ * @returns {string} Final output path
379
+ */
380
+ determineRagOutputPath(overridePath, projectName) {
381
+ let outputDir;
382
+
383
+ if (overridePath) {
384
+ const sanitizedPath = ErrorHandler.sanitizeInput(overridePath, { allowPath: true });
385
+ ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
386
+ outputDir = path.resolve(sanitizedPath);
387
+ } else {
388
+ if (this.config.output.mode === 'relative') {
389
+ outputDir = process.cwd();
390
+ } else {
391
+ outputDir = path.resolve(this.config.output.fixedPath);
392
+ }
393
+ }
394
+
395
+ const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
396
+ return resolveVersionedPath(path.join(outputDir, `${sanitizedProjectName}_rag.json`));
397
+ }
398
+
399
+ /**
400
+ * Determine final output path for PDF
401
+ * @param {string} overridePath - Optional override path from CLI
402
+ * @param {string} projectName - Project name
403
+ * @returns {string} Final output path
404
+ */
405
+ determineOutputPath(overridePath, projectName) {
406
+ let outputDir;
407
+
408
+ if (overridePath) {
409
+ // Validate and sanitize override path from CLI
410
+ const sanitizedPath = ErrorHandler.sanitizeInput(overridePath, { allowPath: true });
411
+ ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
412
+
413
+ outputDir = path.resolve(sanitizedPath);
414
+ console.log(chalk.gray(`PDF will be saved to: ${outputDir}`));
415
+ } else {
416
+ // Use config settings
417
+ if (this.config.output.mode === 'relative') {
418
+ outputDir = process.cwd();
419
+ } else {
420
+ outputDir = path.resolve(this.config.output.fixedPath);
421
+ }
422
+ }
423
+
424
+ // Sanitize project name for filename
425
+ const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
426
+
427
+ return PDFGenerator.generateOutputPath(sanitizedProjectName, outputDir);
428
+ }
429
+
430
+ /**
431
+ * Determine final output path for LLM Markdown format
432
+ * @param {string} overridePath - Optional override path from CLI
433
+ * @param {string} projectName - Project name
434
+ * @returns {string} Final output path
435
+ */
436
+ determineLlmOutputPath(overridePath, projectName) {
437
+ let outputDir;
438
+
439
+ if (overridePath) {
440
+ const sanitizedPath = ErrorHandler.sanitizeInput(overridePath, { allowPath: true });
441
+ ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
442
+ outputDir = path.resolve(sanitizedPath);
443
+ } else {
444
+ if (this.config.output.mode === 'relative') {
445
+ outputDir = process.cwd();
446
+ } else {
447
+ outputDir = path.resolve(this.config.output.fixedPath);
448
+ }
449
+ }
450
+
451
+ const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
452
+ return resolveVersionedPath(path.join(outputDir, `${sanitizedProjectName}_llm.md`));
453
+ }
454
+
455
+ /**
456
+ * Display LLM completion summary
457
+ * @param {string} outputPath - Generated Markdown path
458
+ * @param {Array} selectedExtensions - Selected extensions
459
+ * @param {number} totalFiles - Total files processed
460
+ */
461
+ async displayLlmCompletionSummary(outputPath, selectedExtensions, totalFiles) {
462
+ const stats = await fs.stat(outputPath);
463
+ const fileSizeFormatted = formatFileSize(stats.size);
464
+
465
+ console.log(chalk.green('\nSUCCESS: LLM-optimised Markdown generated successfully!\n'));
466
+ console.log(chalk.cyan('Summary:'));
467
+ console.log(chalk.gray(` Output: ${outputPath}`));
468
+ console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`));
469
+ console.log(chalk.gray(` Total files: ${totalFiles}`));
470
+ console.log(chalk.gray(` File size: ${fileSizeFormatted}`));
471
+ console.log(chalk.gray(` Ready to paste into any LLM chat interface`));
472
+ console.log();
473
+ }
474
+
475
+ /**
476
+ * Display RAG completion summary
477
+ * @param {string} outputPath - Generated RAG JSON path
478
+ * @param {Array} selectedExtensions - Selected extensions
479
+ * @param {number} totalFiles - Total files processed
480
+ * @param {number} totalChunks - Number of chunks generated
481
+ */
482
+ async displayRagCompletionSummary(outputPath, selectedExtensions, totalFiles, totalChunks) {
483
+ const stats = await fs.stat(outputPath);
484
+ const fileSizeFormatted = formatFileSize(stats.size);
485
+
486
+ console.log(chalk.green('\nSUCCESS: RAG-optimized output generated successfully!\n'));
487
+ console.log(chalk.cyan('Summary:'));
488
+ console.log(chalk.gray(` Output: ${outputPath}`));
489
+ console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`));
490
+ console.log(chalk.gray(` Total files: ${totalFiles}`));
491
+ console.log(chalk.gray(` Total chunks: ${totalChunks}`));
492
+ console.log(chalk.gray(` JSON size: ${fileSizeFormatted}`));
493
+ console.log(chalk.gray(` Ready for RAG/LLM ingestion`));
494
+ console.log();
495
+ }
496
+
497
+ /**
498
+ * Display completion summary
499
+ * @param {string} outputPath - Generated PDF path
500
+ * @param {Array} selectedExtensions - Selected extensions
501
+ * @param {number} totalFiles - Total files processed
502
+ * @param {number|string} pageCount - Number of pages in PDF or 'N/A'
503
+ */
504
+ async displayCompletionSummary(outputPath, selectedExtensions, totalFiles, pageCount) {
505
+ // Get PDF stats
506
+ const stats = await fs.stat(outputPath);
507
+ const fileSizeFormatted = formatFileSize(stats.size);
508
+
509
+ console.log(chalk.green('\nSUCCESS: PDF generation completed successfully!\n'));
510
+ console.log(chalk.cyan('Summary:'));
511
+ console.log(chalk.gray(` Output: ${outputPath}`));
512
+ console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`));
513
+ console.log(chalk.gray(` Total files: ${totalFiles}`));
514
+ if (pageCount !== 'N/A') {
515
+ console.log(chalk.gray(` Total pages: ${pageCount}`));
516
+ }
517
+ console.log(chalk.gray(` PDF size: ${fileSizeFormatted}`));
518
+ console.log();
519
+ }
520
+
521
+ /**
522
+ * Show current configuration
523
+ */
524
+ async showConfig() {
525
+ const config = await this.configManager.loadConfig();
526
+ if (config) {
527
+ this.configManager.displayConfig(config);
528
+ } else {
529
+ console.log(chalk.yellow('WARNING: No configuration found. Run codesummary to set up.'));
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Reset configuration
535
+ */
536
+ async resetConfig() {
537
+ await this.configManager.resetConfig();
538
+ console.log(chalk.green('SUCCESS: Configuration reset. Run codesummary to set up again.'));
539
+ }
540
+
541
+ /**
542
+ * Edit configuration interactively
543
+ */
544
+ async editConfig() {
545
+ let config = await this.configManager.loadConfig();
546
+
547
+ if (!config) {
548
+ console.log(chalk.yellow('WARNING: No configuration found. Running first-time setup...'));
549
+ config = await this.configManager.runFirstTimeSetup();
550
+ } else {
551
+ config = await this.configManager.editConfig(config);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Show version information
557
+ */
558
+ showVersion() {
559
+ try {
560
+ const { version } = _require('../package.json');
561
+ console.log(`CodeSummary v${version}`);
562
+ } catch (error) {
563
+ console.log('CodeSummary version unknown');
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Show help information
569
+ */
570
+ showHelp() {
571
+ console.log(chalk.cyan('\nCodeSummary - Generate PDF documentation from source code\n'));
572
+
573
+ console.log(chalk.white('Usage:'));
574
+ console.log(' codesummary [options] Scan current directory and generate PDF');
575
+ console.log(' codesummary config Edit configuration settings');
576
+ console.log();
577
+
578
+ console.log(chalk.white('Options:'));
579
+ console.log(' -o, --output <path> Override output directory');
580
+ console.log(' -f, --format <format> Output format: pdf (default), rag, llm, or both (pdf+rag)');
581
+ console.log(' --show-config Display current configuration');
582
+ console.log(' --reset-config Reset configuration to defaults');
583
+ console.log(' -h, --help Show this help message');
584
+ console.log(' -v, --version Show version information');
585
+ console.log();
586
+
587
+ console.log(chalk.white('Examples:'));
588
+ console.log(' codesummary Scan current project (PDF)');
589
+ console.log(' codesummary --format rag Generate RAG-optimised JSON');
590
+ console.log(' codesummary --format llm Generate LLM-optimised Markdown');
591
+ console.log(' codesummary --output ./docs Save output to ./docs folder');
592
+ console.log(' codesummary config Edit settings');
593
+ console.log(' codesummary --show-config View current settings');
594
+ console.log();
595
+
596
+ console.log(chalk.gray('For more information, visit: https://github.com/skamoll/CodeSummary'));
597
+ }
598
+ }
599
+
541
600
  export default CLI;