UNPKG

codesummary

Version:

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

541 lines (467 loc) 18.4 kB
import inquirer from 'inquirer'; import chalk from 'chalk'; import path from 'path'; import fs from 'fs-extra'; import ora from 'ora'; import ConfigManager from './configManager.js'; import Scanner from './scanner.js'; import PDFGenerator from './pdfGenerator.js'; import ErrorHandler from './errorHandler.js'; /** * Command Line Interface for CodeSummary * Handles user interaction and orchestrates the scanning and PDF generation process */ export class CLI { constructor() { this.configManager = new ConfigManager(); this.config = null; this.scanner = null; this.pdfGenerator = null; } /** * Main entry point for CLI execution * @param {Array} args - Command line arguments */ async run(args = []) { try { // Parse command line arguments const options = await this.parseArguments(args); // Handle special commands if (options.showConfig) { await this.showConfig(); return; } if (options.resetConfig) { await this.resetConfig(); return; } if (options.config) { await this.editConfig(); return; } // Main scanning and PDF generation flow await this.executeMainFlow(options); } catch (error) { ErrorHandler.handleError(error, 'CLI Operation'); } } /** * Parse command line arguments * @param {Array} args - Raw arguments * @returns {object} Parsed options */ async parseArguments(args) { const options = { output: null, showConfig: false, resetConfig: false, config: false, help: false, version: false, noInteractive: false, format: 'pdf' }; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '--output': case '-o': if (i + 1 >= args.length) { throw new Error(`Option ${arg} requires a value`); } i++; // Move to next argument const outputPath = args[i]; // Validate output path if (!outputPath || outputPath.trim().length === 0) { throw new Error(`Option ${arg} requires a non-empty path`); } // Sanitize and validate path const sanitizedPath = ErrorHandler.sanitizeInput(outputPath, { allowPath: true, maxLength: 500, strictMode: true }); if (sanitizedPath !== outputPath) { console.warn(chalk.yellow(`WARNING: Output path was sanitized: ${outputPath} -> ${sanitizedPath}`)); } try { ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true, mustBeAbsolute: false }); } catch (error) { throw new Error(`Invalid output path: ${error.message}`); } options.output = sanitizedPath; break; case '--show-config': options.showConfig = true; break; case '--reset-config': options.resetConfig = true; break; case 'config': options.config = true; break; case '--help': case '-h': options.help = true; break; case '--version': case '-v': options.version = true; break; case '--no-interactive': options.noInteractive = true; break; case '--format': case '-f': if (i + 1 >= args.length) { throw new Error(`Option ${arg} requires a value (pdf or rag)`); } i++; const format = args[i].toLowerCase(); if (!['pdf', 'rag'].includes(format)) { throw new Error(`Invalid format: ${format}. Use 'pdf' or 'rag'`); } options.format = format; break; default: if (arg.startsWith('-')) { throw new Error(`Unknown option: ${arg}`); } // Allow non-option arguments (for future extensibility) break; } } if (options.help) { this.showHelp(); await ErrorHandler.safeExit(0, 'Help displayed'); } if (options.version) { await this.showVersion(); await ErrorHandler.safeExit(0, 'Version displayed'); } return options; } /** * Execute the main scanning and PDF generation flow * @param {object} options - Parsed command line options */ async executeMainFlow(options) { // Load or create configuration this.config = await this.loadConfiguration(); // Initialize components this.scanner = new Scanner(this.config); this.pdfGenerator = new PDFGenerator(this.config); // Determine scan path (default: current working directory) const scanPath = process.cwd(); const projectName = path.basename(scanPath); console.log(chalk.cyan(`CodeSummary - Scanning project: ${chalk.bold(projectName)}\n`)); // Scan directory const spinner = ora('Scanning directory structure...').start(); const filesByExtension = await this.scanner.scanDirectory(scanPath); spinner.succeed('Directory scan completed'); // Check if any supported files were found if (Object.keys(filesByExtension).length === 0) { console.log(chalk.red('ERROR: No supported files found. Nothing to document.')); await ErrorHandler.safeExit(1, 'No supported files found'); } // Display scan summary this.scanner.displayScanSummary(filesByExtension); // Let user select extensions to include const selectedExtensions = await this.selectExtensions(filesByExtension); if (selectedExtensions.length === 0) { console.log(chalk.yellow('WARNING: No extensions selected. Exiting.')); await ErrorHandler.safeExit(0, 'No extensions selected'); } // Check file count threshold const totalFiles = this.calculateTotalFiles(filesByExtension, selectedExtensions); await this.checkFileCountThreshold(totalFiles); // Generate output based on format if (options.format === 'rag') { // Generate RAG-optimized output const ragGenerator = await import('./ragGenerator.js'); const ragOutputPath = this.determineRagOutputPath(options.output, projectName); // Ensure output directory exists await fs.ensureDir(path.dirname(ragOutputPath)); const generationSpinner = ora('Generating RAG-optimized output...').start(); const result = await ragGenerator.default.generateRagOutput( filesByExtension, selectedExtensions, ragOutputPath, projectName, scanPath ); generationSpinner.succeed('RAG output generation completed'); // Display RAG success summary await this.displayRagCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.totalChunks); } else { // Generate PDF (default behavior) const outputPath = this.determineOutputPath(options.output, projectName); // Ensure output directory exists await PDFGenerator.ensureOutputDirectory(path.dirname(outputPath)); // Generate PDF const generationSpinner = ora('Generating PDF document...').start(); const result = await this.pdfGenerator.generatePDF( filesByExtension, selectedExtensions, outputPath, projectName ); generationSpinner.succeed('PDF generation completed'); // Display success summary await this.displayCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.pageCount); } } /** * Load configuration (with first-run setup if needed) * @returns {object} Configuration object */ async loadConfiguration() { let config = await this.configManager.loadConfig(); if (!config) { // First run - trigger setup wizard config = await this.configManager.runFirstTimeSetup(); } else { console.log(chalk.gray(`Using configuration from ${this.configManager.configPath}`)); } return config; } /** * Let user select which extensions to include * @param {object} filesByExtension - Available files by extension * @returns {Array} Selected extensions */ async selectExtensions(filesByExtension) { const extensionInfo = this.scanner.getExtensionInfo(filesByExtension); const choices = extensionInfo.map(info => ({ name: `${info.extension}${info.description} (${info.count} files)`, value: info.extension, checked: true // Pre-select all detected extensions })); const { selectedExtensions } = await inquirer.prompt([{ type: 'checkbox', name: 'selectedExtensions', message: 'Select file extensions to include:', choices, validate: (answer) => { if (answer.length === 0) { return 'You must select at least one extension.'; } return true; } }]); return selectedExtensions; } /** * Calculate total files for selected extensions * @param {object} filesByExtension - Files by extension * @param {Array} selectedExtensions - Selected extensions * @returns {number} Total file count */ calculateTotalFiles(filesByExtension, selectedExtensions) { return selectedExtensions.reduce((total, ext) => { return total + (filesByExtension[ext]?.length || 0); }, 0); } /** * Check if file count exceeds threshold and prompt user * @param {number} totalFiles - Total file count */ async checkFileCountThreshold(totalFiles) { if (totalFiles > this.config.settings.maxFilesBeforePrompt) { console.log(chalk.yellow(`WARNING: Found ${totalFiles} files. Generating the PDF may take a while.`)); const { shouldContinue } = await inquirer.prompt([{ type: 'confirm', name: 'shouldContinue', message: 'Do you want to continue?', default: true }]); if (!shouldContinue) { console.log(chalk.gray('Operation cancelled by user.')); await ErrorHandler.safeExit(0, 'Operation cancelled by user'); } } } /** * Determine final output path for RAG format * @param {string} overridePath - Optional override path from CLI * @param {string} projectName - Project name * @returns {string} Final output path */ determineRagOutputPath(overridePath, projectName) { let outputDir; if (overridePath) { const sanitizedPath = ErrorHandler.sanitizeInput(overridePath); ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true }); outputDir = path.resolve(sanitizedPath); } else { if (this.config.output.mode === 'relative') { outputDir = process.cwd(); } else { outputDir = path.resolve(this.config.output.fixedPath); } } const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName); return path.join(outputDir, `${sanitizedProjectName}_rag.json`); } /** * Determine final output path for PDF * @param {string} overridePath - Optional override path from CLI * @param {string} projectName - Project name * @returns {string} Final output path */ determineOutputPath(overridePath, projectName) { let outputDir; if (overridePath) { // Validate and sanitize override path from CLI const sanitizedPath = ErrorHandler.sanitizeInput(overridePath); ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true }); outputDir = path.resolve(sanitizedPath); console.log(chalk.gray(`PDF will be saved to: ${outputDir}`)); } else { // Use config settings if (this.config.output.mode === 'relative') { outputDir = process.cwd(); } else { outputDir = path.resolve(this.config.output.fixedPath); } } // Sanitize project name for filename const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName); return PDFGenerator.generateOutputPath(sanitizedProjectName, outputDir); } /** * Display RAG completion summary * @param {string} outputPath - Generated RAG JSON path * @param {Array} selectedExtensions - Selected extensions * @param {number} totalFiles - Total files processed * @param {number} totalChunks - Number of chunks generated */ async displayRagCompletionSummary(outputPath, selectedExtensions, totalFiles, totalChunks) { const stats = await fs.stat(outputPath); const fileSizeFormatted = this.formatFileSize(stats.size); console.log(chalk.green('\nSUCCESS: RAG-optimized output generated successfully!\n')); console.log(chalk.cyan('Summary:')); console.log(chalk.gray(` Output: ${outputPath}`)); console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`)); console.log(chalk.gray(` Total files: ${totalFiles}`)); console.log(chalk.gray(` Total chunks: ${totalChunks}`)); console.log(chalk.gray(` JSON size: ${fileSizeFormatted}`)); console.log(chalk.gray(` Ready for RAG/LLM ingestion`)); console.log(); } /** * Display completion summary * @param {string} outputPath - Generated PDF path * @param {Array} selectedExtensions - Selected extensions * @param {number} totalFiles - Total files processed * @param {number|string} pageCount - Number of pages in PDF or 'N/A' */ async displayCompletionSummary(outputPath, selectedExtensions, totalFiles, pageCount) { // Get PDF stats const stats = await fs.stat(outputPath); const fileSizeFormatted = this.formatFileSize(stats.size); console.log(chalk.green('\nSUCCESS: PDF generation completed successfully!\n')); console.log(chalk.cyan('Summary:')); console.log(chalk.gray(` Output: ${outputPath}`)); console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`)); console.log(chalk.gray(` Total files: ${totalFiles}`)); if (pageCount !== 'N/A') { console.log(chalk.gray(` Total pages: ${pageCount}`)); } console.log(chalk.gray(` PDF size: ${fileSizeFormatted}`)); console.log(); } /** * Show current configuration */ async showConfig() { const config = await this.configManager.loadConfig(); if (config) { this.configManager.displayConfig(config); } else { console.log(chalk.yellow('WARNING: No configuration found. Run codesummary to set up.')); } } /** * Reset configuration */ async resetConfig() { await this.configManager.resetConfig(); console.log(chalk.green('SUCCESS: Configuration reset. Run codesummary to set up again.')); } /** * Edit configuration interactively */ async editConfig() { let config = await this.configManager.loadConfig(); if (!config) { console.log(chalk.yellow('WARNING: No configuration found. Running first-time setup...')); config = await this.configManager.runFirstTimeSetup(); } else { config = await this.configManager.editConfig(config); } } /** * Format file size in human readable format * @param {number} bytes - Size in bytes * @returns {string} Formatted size */ formatFileSize(bytes) { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } /** * Show version information */ async showVersion() { try { // Get the current module directory and resolve package.json const currentDir = path.dirname(new URL(import.meta.url).pathname); // Handle Windows paths by removing leading slash if present const normalizedDir = process.platform === 'win32' && currentDir.startsWith('/') ? currentDir.slice(1) : currentDir; const packageJsonPath = path.resolve(normalizedDir, '..', 'package.json'); const packageJson = await fs.readJson(packageJsonPath); console.log(`CodeSummary v${packageJson.version}`); } catch (error) { console.log('CodeSummary version unknown'); } } /** * Show help information */ showHelp() { console.log(chalk.cyan('\nCodeSummary - Generate PDF documentation from source code\n')); console.log(chalk.white('Usage:')); console.log(' codesummary [options] Scan current directory and generate PDF'); console.log(' codesummary config Edit configuration settings'); console.log(); console.log(chalk.white('Options:')); console.log(' -o, --output <path> Override output directory'); console.log(' -f, --format <format> Output format: pdf (default) or rag'); console.log(' --show-config Display current configuration'); console.log(' --reset-config Reset configuration to defaults'); console.log(' -h, --help Show this help message'); console.log(' -v, --version Show version information'); console.log(); console.log(chalk.white('Examples:')); console.log(' codesummary Scan current project (PDF)'); console.log(' codesummary --format rag Generate RAG-optimized JSON'); console.log(' codesummary --output ./docs Save output to ./docs folder'); console.log(' codesummary config Edit settings'); console.log(' codesummary --show-config View current settings'); console.log(); console.log(chalk.gray('For more information, visit: https://github.com/skamoll/CodeSummary')); } } export default CLI;