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