UNPKG

@paulohenriquevn/m2js

Version:

Transform TypeScript/JavaScript code into LLM-friendly Markdown summaries + Smart Dead Code Detection + Graph-Deep Diff Analysis. Extract exported functions, classes, and JSDoc comments for better AI context with 60%+ token reduction. Intelligent dead cod

552 lines 26.2 kB
#!/usr/bin/env node "use strict"; /* eslint-disable max-lines */ /* eslint-disable max-lines-per-function */ /* eslint-disable complexity */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const chalk_1 = __importDefault(require("chalk")); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const parser_1 = require("./parser"); const generator_1 = require("./generator"); const file_scanner_1 = require("./file-scanner"); const batch_processor_1 = require("./batch-processor"); const dependency_analyzer_1 = require("./dependency-analyzer"); const dead_code_cli_1 = require("./dead-code-cli"); const duplicate_code_cli_1 = require("./duplicate-code-cli"); const graph_diff_cli_1 = require("./graph-diff-cli"); const config_loader_1 = require("./config-loader"); // Version info const packageJson = { version: '1.3.0' }; const program = new commander_1.Command(); program .name('m2js') .description('Transform TypeScript/JavaScript code into LLM-friendly Markdown summaries') .version(packageJson.version) .argument('[path]', 'TypeScript/JavaScript file or directory to convert (optional for some commands)') .option('-o, --output <file>', 'specify output file (only for single files)') .option('--no-comments', 'skip comment extraction') .option('--graph', 'generate dependency graph analysis instead of code extraction') .option('--mermaid', 'include Mermaid diagrams in graph output (use with --graph)') .option('--usage-examples', 'include usage examples and patterns found in the code') .option('--business-context', 'analyze and include business domain context') .option('--architecture-insights', 'analyze and include architectural patterns and decisions') .option('--semantic-analysis', 'analyze business entities, relationships, and workflows') .option('--ai-enhanced', 'enable all AI-friendly analysis features (business context, usage examples, architecture, semantic)') .option('--detect-unused', 'detect unused exports and imports (dead code analysis)') .option('--detect-duplicates', 'detect duplicate code blocks using jscpd integration') .option('--min-lines <number>', 'minimum lines to consider as duplicate (default: 5)', parseInt) .option('--min-tokens <number>', 'minimum tokens to consider as duplicate (default: 50)', parseInt) .option('--format <type>', 'output format for analysis: table, json (default: table)', 'table') .option('--init-config', 'generate example .m2jsrc configuration file') .option('--help-dead-code', 'show detailed help for dead code analysis') .option('--help-duplicates', 'show detailed help for duplicate code analysis') .option('--graph-diff', 'analyze architectural changes between git references') .option('--baseline <ref>', 'baseline git reference for comparison (branch, commit, tag)') .option('--current <ref>', 'current git reference for comparison (default: working directory)') .option('--min-severity <level>', 'minimum severity to report: low, medium, high, critical') .option('--include-details', 'include detailed change analysis (default: true)') .option('--include-impact', 'include impact scoring (default: true)') .option('--include-suggestions', 'include improvement suggestions (default: true)') .option('--help-graph-diff', 'show detailed help for graph diff analysis') .action(async (inputPath, options) => { try { // Handle special commands first if (options.initConfig) { await generateConfigFile(); return; } if (options.helpDeadCode) { console.log((0, dead_code_cli_1.getDeadCodeHelpText)()); return; } if (options.helpDuplicates) { console.log((0, duplicate_code_cli_1.getDuplicateCodeHelpText)()); return; } if (options.helpGraphDiff) { console.log((0, graph_diff_cli_1.getGraphDiffHelpText)()); return; } // Check if path is required for the command if (!inputPath) { console.error(chalk_1.default.red('Error: path argument is required')); console.log(chalk_1.default.blue('Use --init-config to generate configuration, --help-dead-code, --help-duplicates, or --help-graph-diff for help')); process.exit(1); } await processInput(inputPath, options); } catch (error) { console.error(chalk_1.default.red(`Error: ${error.message}`)); process.exit(1); } }); async function processInput(inputPath, options) { const resolvedPath = path_1.default.resolve(inputPath); // Determine if input is a file or directory const isDir = await (0, file_scanner_1.isDirectory)(resolvedPath); const isFileTarget = await (0, file_scanner_1.isFile)(resolvedPath); if (!isDir && !isFileTarget) { throw new Error(`Path not found: ${resolvedPath}`); } // Route to dead code analysis if --detect-unused option is used if (options.detectUnused) { await processDeadCodeAnalysis(resolvedPath, options); return; } // Route to duplicate code analysis if --detect-duplicates option is used if (options.detectDuplicates) { await processDuplicateCodeAnalysis(resolvedPath, options); return; } // Route to graph diff analysis if --graph-diff option is used if (options.graphDiff) { await processGraphDiffAnalysis(resolvedPath, options); return; } // Route to graph analysis if --graph option is used if (options.graph) { await processGraphAnalysis(resolvedPath, options); return; } if (isDir) { // Process directory await processDirectory(resolvedPath, options); } else { // Process single file await processSingleFile(resolvedPath, options); } } async function processSingleFile(inputPath, options) { const resolvedPath = path_1.default.resolve(inputPath); console.log(chalk_1.default.blue(`Reading ${path_1.default.basename(resolvedPath)}...`)); console.log(chalk_1.default.blue(`Resolving file path...`)); try { await fs_1.promises.access(resolvedPath); } catch { throw new Error(`File not found: ${resolvedPath}`); } if (!resolvedPath.match(/\.(ts|tsx|js|jsx)$/)) { throw new Error(`Unsupported file type: ${resolvedPath}. Only .ts, .tsx, .js, .jsx are supported.`); } console.log(chalk_1.default.blue('Parsing with Babel...')); const content = await fs_1.promises.readFile(resolvedPath, 'utf-8'); const parsedFile = (0, parser_1.parseFile)(resolvedPath, content); // Enhanced messages with export counting const { exportMetadata } = parsedFile; if (exportMetadata.totalFunctions > 0) { console.log(chalk_1.default.blue(`Extracting exported functions (${exportMetadata.totalFunctions} found)...`)); } if (exportMetadata.totalClasses > 0) { console.log(chalk_1.default.blue(`Extracting exported classes (${exportMetadata.totalClasses} found)...`)); } if (parsedFile.functions.length > 0 || parsedFile.classes.length > 0) { console.log(chalk_1.default.blue('Extracting JSDoc comments...')); } if (parsedFile.functions.length === 0 && parsedFile.classes.length === 0) { console.log(chalk_1.default.yellow('No exported functions or classes found')); return; } console.log(chalk_1.default.blue('Organizing hierarchical structure...')); console.log(chalk_1.default.blue('Generating enhanced markdown...')); // Check if AI enhancements are requested const needsEnhancement = options.businessContext || options.usageExamples || options.architectureInsights || options.semanticAnalysis || options.aiEnhanced; let markdown; if (needsEnhancement) { // Enhanced generation temporarily disabled due to build issues console.log(chalk_1.default.yellow('Enhanced analysis features temporarily disabled')); console.log(chalk_1.default.blue('Generating standard markdown...')); markdown = (0, generator_1.generateMarkdown)(parsedFile); } else { markdown = (0, generator_1.generateMarkdown)(parsedFile); } const outputPath = (0, generator_1.getOutputPath)(resolvedPath, options.output); await fs_1.promises.writeFile(outputPath, markdown, 'utf-8'); console.log(chalk_1.default.green(`Created ${path_1.default.basename(outputPath)} with enhanced context`)); // Enhanced statistics with path information // Show path information const cwd = process.cwd(); let displayPath = parsedFile.filePath; if (parsedFile.filePath.startsWith(cwd)) { displayPath = path_1.default.relative(cwd, parsedFile.filePath); if (!displayPath.startsWith('.')) { displayPath = `./${displayPath}`; } } console.log(chalk_1.default.cyan(`Source: ${displayPath}`)); // Show export metadata const exportStats = []; if (exportMetadata.totalFunctions > 0) { exportStats.push(`${exportMetadata.totalFunctions} function${exportMetadata.totalFunctions === 1 ? '' : 's'}`); } if (exportMetadata.totalClasses > 0) { exportStats.push(`${exportMetadata.totalClasses} class${exportMetadata.totalClasses === 1 ? '' : 'es'}`); } if (exportMetadata.hasDefaultExport) { exportStats.push(`1 default ${exportMetadata.defaultExportType}`); } console.log(chalk_1.default.cyan(`Exports: ${exportStats.join(', ')}`)); // Generate structure preview console.log(chalk_1.default.cyan('Generated enhanced structure:')); if (parsedFile.functions.length > 0) { console.log(chalk_1.default.cyan(` Functions (${parsedFile.functions.length})`)); } if (parsedFile.classes.length > 0) { console.log(chalk_1.default.cyan(` Classes (${parsedFile.classes.length})`)); parsedFile.classes.forEach(cls => { const publicMethodsCount = cls.methods.filter(m => !m.isPrivate).length; console.log(chalk_1.default.cyan(` └── ${cls.name} (${publicMethodsCount} methods)`)); }); } const inputStats = await fs_1.promises.stat(resolvedPath); const outputStats = await fs_1.promises.stat(outputPath); const reduction = Math.round((1 - outputStats.size / inputStats.size) * 100); console.log(chalk_1.default.cyan(`Saved to ${path_1.default.basename(outputPath)} (${inputStats.size}${outputStats.size} bytes, ${reduction}% reduction)`)); } async function processDirectory(directoryPath, options) { console.log(chalk_1.default.blue(`Scanning directory ${path_1.default.basename(directoryPath)}...`)); // Warn about --output option for directories if (options.output) { console.log(chalk_1.default.yellow('Note: --output option is ignored for directory processing')); } const batchOptions = { sourceDirectory: directoryPath, includeComments: !options.noComments, onProgress: (current, total, fileName) => { console.log(chalk_1.default.blue(`Processing files (${current}/${total}): ${fileName}`)); }, onFileProcessed: (filePath, success, error) => { if (success) { console.log(chalk_1.default.green(`Generated ${path_1.default.basename(filePath, path_1.default.extname(filePath))}.md`)); } else { console.log(chalk_1.default.red(`Failed ${path_1.default.basename(filePath)}: ${error}`)); } }, }; const result = await (0, batch_processor_1.processBatch)(batchOptions); // Display final summary console.log(chalk_1.default.cyan('Batch processing complete:')); console.log(chalk_1.default.cyan(`Total files: ${result.totalFiles}`)); console.log(chalk_1.default.green(`Successful: ${result.successCount}`)); if (result.failureCount > 0) { console.log(chalk_1.default.red(`Failed: ${result.failureCount}`)); // List failed files const failedFiles = result.processedFiles .filter(f => !f.success) .map(f => ` • ${path_1.default.basename(f.filePath)}: ${f.error}`) .join('\n'); if (failedFiles) { console.log(chalk_1.default.red('Failed files:')); console.log(chalk_1.default.red(failedFiles)); } } console.log(chalk_1.default.cyan(`Generated ${result.successCount} markdown files in the same directory as source files`)); } async function processDeadCodeAnalysis(inputPath, options) { const resolvedPath = path_1.default.resolve(inputPath); // Determine input type and collect files let files = []; const isDir = await (0, file_scanner_1.isDirectory)(resolvedPath); const isFileTarget = await (0, file_scanner_1.isFile)(resolvedPath); if (isDir) { console.log(chalk_1.default.blue(`Scanning directory: ${path_1.default.basename(resolvedPath)}`)); const scanResult = await (0, file_scanner_1.scanDirectory)(resolvedPath); files = scanResult.files; if (files.length === 0) { throw new Error(`No TypeScript/JavaScript files found in directory: ${resolvedPath}`); } console.log(chalk_1.default.blue(`Found ${files.length} TypeScript/JavaScript files`)); } else if (isFileTarget) { if (!resolvedPath.match(/\.(ts|tsx|js|jsx)$/)) { throw new Error(`Unsupported file type: ${resolvedPath}. Only .ts, .tsx, .js, .jsx are supported.`); } files = [resolvedPath]; } else { throw new Error(`Path not found: ${resolvedPath}`); } // Execute dead code analysis const deadCodeOptions = { format: options.format, includeMetrics: true, }; await (0, dead_code_cli_1.executeDeadCodeAnalysis)(files, deadCodeOptions); } async function processDuplicateCodeAnalysis(inputPath, options) { const resolvedPath = path_1.default.resolve(inputPath); // Determine input type and collect files let files = []; const isDir = await (0, file_scanner_1.isDirectory)(resolvedPath); const isFileTarget = await (0, file_scanner_1.isFile)(resolvedPath); if (isDir) { console.log(chalk_1.default.blue(`Scanning directory: ${path_1.default.basename(resolvedPath)}`)); const scanResult = await (0, file_scanner_1.scanDirectory)(resolvedPath); files = scanResult.files; if (files.length === 0) { throw new Error(`No TypeScript/JavaScript files found in directory: ${resolvedPath}`); } console.log(chalk_1.default.blue(`Found ${files.length} TypeScript/JavaScript files`)); } else if (isFileTarget) { if (!resolvedPath.match(/\.(ts|tsx|js|jsx)$/)) { throw new Error(`Unsupported file type: ${resolvedPath}. Only .ts, .tsx, .js, .jsx are supported.`); } files = [resolvedPath]; } else { throw new Error(`Path not found: ${resolvedPath}`); } // Execute duplicate code analysis const duplicateOptions = { format: options.format, minLines: options.minLines, minTokens: options.minTokens, includeContext: true, includeSuggestions: true, }; await (0, duplicate_code_cli_1.executeDuplicateCodeAnalysis)(files, duplicateOptions); } /** * Process graph diff analysis */ async function processGraphDiffAnalysis(inputPath, options) { const resolvedPath = path_1.default.resolve(inputPath); // Validate that baseline is provided if (!options.baseline) { throw new Error('Graph diff analysis requires --baseline option. Use --help-graph-diff for more information.'); } // Validate that path exists and is a directory (git repo) const isDir = await (0, file_scanner_1.isDirectory)(resolvedPath); if (!isDir) { throw new Error('Graph diff analysis requires a directory path (project root with git repository)'); } console.log(chalk_1.default.blue(`Analyzing project: ${path_1.default.basename(resolvedPath)}`)); // Build graph diff options const graphDiffOptions = { baseline: options.baseline, current: options.current, format: options.format, includeDetails: options.includeDetails !== false, includeImpact: options.includeImpact !== false, includeSuggestions: options.includeSuggestions !== false, minSeverity: options.minSeverity, }; await (0, graph_diff_cli_1.executeGraphDiffAnalysis)(resolvedPath, graphDiffOptions); } /** * Generate example configuration file */ async function generateConfigFile() { const configPath = path_1.default.join(process.cwd(), '.m2jsrc'); try { // Check if config already exists await fs_1.promises.access(configPath); console.log(chalk_1.default.yellow('.m2jsrc already exists in current directory')); console.log(chalk_1.default.blue('Delete it first if you want to regenerate')); return; } catch { // File doesn't exist, proceed with creation } const exampleConfig = config_loader_1.ConfigLoader.generateExampleConfig(); try { await fs_1.promises.writeFile(configPath, exampleConfig, 'utf-8'); console.log(chalk_1.default.green('Generated .m2jsrc configuration file')); console.log(chalk_1.default.blue('Edit the file to customize M2JS behavior')); console.log(chalk_1.default.dim(`Location: ${configPath}`)); console.log(chalk_1.default.cyan('\nKey configuration options:')); console.log(chalk_1.default.cyan('• deadCode.enableCache - Enable file parsing cache')); console.log(chalk_1.default.cyan('• deadCode.showProgress - Show progress bar')); console.log(chalk_1.default.cyan('• files.ignorePatterns - Files/folders to skip')); console.log(chalk_1.default.cyan('• deadCode.format - Default output format')); console.log(chalk_1.default.blue('\nYou can also use environment variables:')); console.log(chalk_1.default.blue('• M2JS_CACHE_ENABLED=false m2js --detect-unused')); console.log(chalk_1.default.blue('• M2JS_SHOW_PROGRESS=true m2js src --detect-unused')); } catch (error) { console.error(chalk_1.default.red(`Failed to create config file: ${error.message}`)); process.exit(1); } } async function processGraphAnalysis(inputPath, options) { const resolvedPath = path_1.default.resolve(inputPath); console.log(chalk_1.default.blue('Starting dependency graph analysis...')); // Check if --mermaid option was used without --graph if (options.mermaid && !options.graph) { console.log(chalk_1.default.yellow('--mermaid option requires --graph. Ignoring --mermaid.')); } // Determine input type and scan for files let files = []; const isDir = await (0, file_scanner_1.isDirectory)(resolvedPath); const isFileTarget = await (0, file_scanner_1.isFile)(resolvedPath); if (isDir) { console.log(chalk_1.default.blue(`Scanning directory: ${path_1.default.basename(resolvedPath)}`)); const scanResult = await (0, file_scanner_1.scanDirectory)(resolvedPath); files = scanResult.files; if (files.length === 0) { throw new Error(`No TypeScript/JavaScript files found in directory: ${resolvedPath}`); } console.log(chalk_1.default.blue(`Found ${files.length} TypeScript/JavaScript files`)); } else if (isFileTarget) { // Single file - warn that graph analysis works better with directories console.log(chalk_1.default.yellow('Graph analysis works best with directories. Analyzing single file...')); files = [resolvedPath]; } else { throw new Error(`Path not found: ${resolvedPath}`); } // Analyze dependencies console.log(chalk_1.default.blue('Analyzing dependencies...')); const graphOptions = { includeMermaid: options.mermaid || false, includeExternalDeps: true, detectCircular: true, }; try { const dependencyGraph = (0, dependency_analyzer_1.analyzeDependencies)(files, graphOptions); console.log(chalk_1.default.blue('Generating dependency graph markdown...')); const markdown = (0, generator_1.generateDependencyMarkdown)(dependencyGraph, graphOptions); // Determine output path let outputPath; if (options.output) { outputPath = options.output; } else { const baseName = isDir ? path_1.default.basename(resolvedPath) : path_1.default.basename(resolvedPath, path_1.default.extname(resolvedPath)); outputPath = path_1.default.join(isDir ? resolvedPath : path_1.default.dirname(resolvedPath), `${baseName}-dependencies.md`); } // Write output await fs_1.promises.writeFile(outputPath, markdown, 'utf-8'); console.log(chalk_1.default.green(`Dependency graph generated successfully!`)); // Display summary const { metrics } = dependencyGraph; console.log(chalk_1.default.cyan('Analysis Summary:')); console.log(chalk_1.default.cyan(`Total modules analyzed: ${metrics.totalNodes}`)); console.log(chalk_1.default.cyan(`Total dependencies: ${metrics.totalEdges}`)); console.log(chalk_1.default.cyan(`Internal dependencies: ${metrics.internalDependencies}`)); console.log(chalk_1.default.cyan(`External dependencies: ${metrics.externalDependencies}`)); if (metrics.circularDependencies.length > 0) { console.log(chalk_1.default.yellow(`Circular dependencies detected: ${metrics.circularDependencies.length}`)); } else { console.log(chalk_1.default.green('No circular dependencies found')); } console.log(chalk_1.default.cyan(`Output saved to: ${path_1.default.relative(process.cwd(), outputPath)}`)); } catch (error) { throw new Error(`Dependency analysis failed: ${error.message}`); } } // === TEMPLATE GENERATION COMMANDS === // Template generation command - temporarily disabled // program // .command('template') // .description( // 'Generate LLM-friendly specification templates for guided development' // ) // .argument( // '<domain>', // `Domain template to use (${availableDomains.join(', ')})` // ) // .argument( // '<component>', // 'Component name to generate (e.g., User, OrderService, ProductController)' // ) // .option('-o, --output <file>', 'specify output file') // .option('--no-examples', 'skip usage examples') // .option('--no-business-context', 'skip business context section') // .option('--no-architecture-guide', 'skip architecture implementation guide') // .option('--no-testing-guide', 'skip testing guide section') // .option('--interactive', 'interactive template customization') // .action(async (domain: string, component: string, options: Record<string, unknown>) => { // try { // await processTemplateGeneration(domain, component, options); // } catch (error) { // console.error( // chalk.red(`❌ Template generation failed: ${(error as Error).message}`) // ); // process.exit(1); // } // }); // List domains command - temporarily disabled // program // .command('domains') // .description('List available domain templates with descriptions') // .action(() => { // console.log(listAvailableDomains()); // }); // Template generation function - temporarily disabled // async function processTemplateGeneration( // domain: string, // component: string, // options: Record<string, unknown> // ): Promise<void> { // console.log(chalk.blue('🎯 M2JS Template Generator')); // console.log(chalk.blue(`📋 Domain: ${domain}`)); // console.log(chalk.blue(`🔧 Component: ${component}`)); // if (!availableDomains.includes(domain)) { // console.log( // chalk.yellow(`⚠️ Available domains: ${availableDomains.join(', ')}`) // ); // throw new Error(`Domain '${domain}' not supported`); // } // const templateOptions: TemplateOptions = { // domain, // component, // output: options.output as string | undefined, // interactive: (options.interactive as boolean) || false, // examples: options.examples !== false, // businessContext: options.businessContext !== false, // architectureGuide: options.architectureGuide !== false, // testingGuide: options.testingGuide !== false, // }; // console.log(chalk.blue('🧠 Generating LLM specification template...')); // const template = generateTemplate(templateOptions); // // Determine output path // const outputPath = (options.output as string) || `./${component}.spec.md`; // // Write template // await fs.writeFile(outputPath, template, 'utf-8'); // console.log(chalk.green(`✅ Template generated successfully!`)); // console.log(chalk.cyan(`📁 Output: ${outputPath}`)); // console.log(''); // console.log(chalk.cyan('🤖 Next steps:')); // console.log(chalk.cyan('1. Review the generated specification template')); // console.log( // chalk.cyan('2. Provide the template to your LLM coding assistant') // ); // console.log( // chalk.cyan( // '3. Ask the LLM to implement the component following the specification' // ) // ); // console.log( // chalk.cyan('4. Use the business rules and examples to guide implementation') // ); // console.log(''); // console.log(chalk.blue('💡 Example LLM prompt:')); // console.log( // chalk.blue( // '"Please implement the component described in this specification template, following all business rules and architectural guidelines."' // ) // ); // } if (require.main === module) { program.parse(); } //# sourceMappingURL=cli.js.map