@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
JavaScript
;
/* 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