UNPKG

sca-tool

Version:

Universal static code analysis tool for calculating API operation costs across multiple frameworks (NestJS, Express, Fastify)

479 lines • 21.5 kB
#!/usr/bin/env bun "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.program = void 0; const commander_1 = require("commander"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const analyzer_1 = require("./analyzer"); const reports_1 = require("./reports"); const config_1 = require("./config"); const editor_detector_1 = require("./editor-detector"); const config_loader_1 = require("./config-loader"); const program = new commander_1.Command(); exports.program = program; program .name('static-code-analyzer') .description('šŸ” SCA-Tool - Static Code Analyzer for API Cost Analysis\n\nAnalyze and optimize API operation costs in TypeScript/JavaScript applications.\nSupports NestJS, Express, Fastify, Prisma, TypeORM, Mongoose, Supabase, and more.') .version('1.0.3'); // Analyze single file program .command('file <filePath>') .description('šŸ“„ Analyze a single TypeScript/JavaScript file for API operation costs') .option('-f, --framework <framework>', 'Framework preset (nestjs, express, fastify)', 'nestjs') .option('-o, --output <file>', 'Output file path') .option('--format <format>', 'Output format (text, json, markdown, html)', 'text') .option('-c, --config <file>', 'Custom configuration file') .option('-v, --verbose', 'Verbose logging') .action(async (filePath, options) => { try { // Load configuration with fallbacks const analyzer = options.config ? analyzer_1.ApiCostAnalyzer.fromConfigFile(options.config) : new analyzer_1.ApiCostAnalyzer(); // Set framework (this will override config if specified) if (options.framework && config_1.FRAMEWORK_PRESETS[options.framework]) { analyzer.setFramework(options.framework); if (options.verbose) { console.log(`šŸ“‹ Using ${config_1.FRAMEWORK_PRESETS[options.framework].name} preset`); } } // Load custom config if provided if (options.config) { const rawConfig = JSON.parse(fs.readFileSync(options.config, 'utf8')); const customConfig = (0, config_1.normalizePatterns)(rawConfig); analyzer.updateConfig(customConfig); if (options.verbose) { console.log(`āš™ļø Loaded custom config from ${options.config}`); } } console.log(`šŸ” Analyzing file ${filePath}...`); const result = analyzer.analyzeFile(filePath); const report = reports_1.ReportGenerator.generateReport(result, options.format); if (options.output) { reports_1.ReportGenerator.saveReport(report, options.output); console.log(`āœ… Report saved to ${options.output}`); } else { console.log(report); } } catch (error) { console.error('āŒ Error:', error instanceof Error ? error.message : error); process.exit(1); } }); // Analyze project directory program .command('project <projectPath>') .description('šŸ“ Analyze an entire project directory for API operation costs') .option('-f, --framework <framework>', 'Framework preset (nestjs, express, fastify)', 'nestjs') .option('-o, --output <file>', 'Output file path') .option('--format <format>', 'Output format (text, json, markdown, html)', 'text') .option('-c, --config <file>', 'Custom configuration file') .option('-p, --patterns <patterns>', 'File patterns to analyze (comma-separated)', '') .option('-v, --verbose', 'Verbose logging') .option('--exclude <patterns>', 'Patterns to exclude (comma-separated)', '') .action(async (projectPath, options) => { try { // Load configuration with fallbacks const analyzer = options.config ? analyzer_1.ApiCostAnalyzer.fromConfigFile(options.config) : new analyzer_1.ApiCostAnalyzer(); // Set framework (this will override config if specified) if (options.framework && config_1.FRAMEWORK_PRESETS[options.framework]) { analyzer.setFramework(options.framework); if (options.verbose) { console.log(`šŸ“‹ Using ${config_1.FRAMEWORK_PRESETS[options.framework].name} preset`); } } // Load custom config if provided if (options.config) { const rawConfig = JSON.parse(fs.readFileSync(options.config, 'utf8')); const customConfig = (0, config_1.normalizePatterns)(rawConfig); analyzer.updateConfig(customConfig); if (options.verbose) { console.log(`āš™ļø Loaded custom config from ${options.config}`); } } // Parse file patterns let patterns = ['**/*.ts', '**/*.js']; if (options.patterns) { patterns = options.patterns.split(',').map((p) => p.trim()); } else if (options.framework === 'nestjs') { patterns = ['**/*.service.ts', '**/*.controller.ts', '**/*.gateway.ts', '**/*.resolver.ts']; } else if (options.framework === 'express') { patterns = ['**/routes/**/*.js', '**/routes/**/*.ts', '**/controllers/**/*.js', '**/controllers/**/*.ts']; } console.log(`šŸ” Analyzing project ${projectPath}...`); if (options.verbose) { console.log(`šŸ“‚ Patterns: ${patterns.join(', ')}`); } const result = analyzer.analyzeProject(projectPath, patterns); console.log(`āœ… Found ${result.files.length} files with ${result.summary.totalMethods} methods`); const report = reports_1.ReportGenerator.generateProjectReport(result.files, options.format); if (options.output) { reports_1.ReportGenerator.saveReport(report, options.output); console.log(`āœ… Report saved to ${options.output}`); } else { console.log(report); } } catch (error) { console.error('āŒ Analysis failed:', error instanceof Error ? error.message : error); process.exit(1); } }); // Quick NestJS commands program .command('nestjs <path>') .description('šŸš€ Quick analysis optimized for NestJS projects (services, controllers, gateways, resolvers)') .option('-o, --output <file>', 'Output file path') .option('--format <format>', 'Output format (text, json, markdown, html)', 'text') .option('-v, --verbose', 'Verbose logging') .action(async (projectPath, options) => { try { console.log(`šŸ” Analyzing NestJS project ${projectPath}...`); const result = (0, analyzer_1.analyzeNestJSProject)(projectPath); const report = reports_1.ReportGenerator.generateProjectReport(result.files, options.format); console.log(`āœ… Found ${result.files.length} files with ${result.summary.totalMethods} methods`); if (options.output) { reports_1.ReportGenerator.saveReport(report, options.output); console.log(`āœ… Report saved to ${options.output}`); } else { console.log(report); } } catch (error) { console.error('āŒ Analysis failed:', error instanceof Error ? error.message : error); process.exit(1); } }); // Configuration commands program .command('config') .description('āš™ļø Show current configuration and available framework presets') .action(() => { console.log('šŸ“‹ Available framework presets:'); Object.entries(config_1.FRAMEWORK_PRESETS).forEach(([key, preset]) => { console.log(` ${key}: ${preset.description}`); }); console.log('\nšŸ“‹ Example configurations:'); console.log(' --framework nestjs # Use NestJS preset'); console.log(' --config custom.json # Use custom config file'); console.log('\nšŸ“‹ Custom config file example:'); console.log(JSON.stringify({ operations: { 'customMethod': 2, 'expensiveOperation': 5 }, patterns: { 'custom_framework': { pattern: /\.customMethod\(/, description: 'Custom framework operations' } } }, null, 2)); }); // Generate sample config program .command('init') .description('šŸ”§ Generate optimized configuration file for your project') .option('-f, --framework <framework>', 'Framework preset', 'nestjs') .option('-o, --output <dir>', 'Output directory', '.') .action((options) => { try { const outputDir = options.output; // Create configs directory if it doesn't exist const configsDir = path.join(outputDir, 'configs'); if (!fs.existsSync(configsDir)) { fs.mkdirSync(configsDir, { recursive: true }); console.log(`šŸ“ Created configs directory: ${configsDir}`); } // Create config file in configs/ folder const config = (0, config_1.getFrameworkConfig)(options.framework); const configPath = path.join(configsDir, 'sca.config.json'); // Convert regex patterns to strings for JSON serialization and add comprehensive config const jsonConfig = { ...config, patterns: Object.fromEntries(Object.entries(config.patterns).map(([key, value]) => [ key, { ...value, pattern: value.pattern instanceof RegExp ? value.pattern.source : value.pattern } ])), // Add comprehensive configuration options output: { defaultFormat: 'text', defaultOutputDir: './reports', fileNaming: { pattern: '{filename}-analysis-{timestamp}', includeTimestamp: true }, html: { theme: 'light', includeCharts: true, title: `${options.framework.toUpperCase()} API Cost Analysis Report`, footer: 'Generated by SCA-Tool - Static Code Analyzer', openInBrowser: false } }, analysis: { costThresholds: { low: 2, medium: 5, high: 10 }, recommendations: { enabled: true, customRules: [] }, parallel: { detectParallelOperations: true, parallelThreshold: 2 }, methodFilters: { minCost: 0, maxCost: 1000, excludePatterns: [] } }, paths: { configFile: 'configs/sca.config.json', outputDir: './reports', tempDir: './temp', cacheDir: './.sca-cache', excludePatterns: [ 'node_modules/**', 'dist/**', 'build/**', '**/*.test.ts', '**/*.spec.ts', '**/*.d.ts' ], includePatterns: [ '**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx' ], watch: { debounceMs: 500, excludePatterns: [ 'node_modules/**', 'dist/**', 'build/**' ] } } }; fs.writeFileSync(configPath, JSON.stringify(jsonConfig, null, 2)); console.log(`āœ… Generated config file: ${configPath}`); // Create package.json scripts const packageJsonPath = path.join(outputDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (!packageJson.scripts) packageJson.scripts = {}; packageJson.scripts['sca:project'] = 'sca project ./src --config configs/sca.config.json'; packageJson.scripts['sca:file'] = 'sca file --config configs/sca.config.json'; packageJson.scripts['sca:report'] = 'sca project ./src --config configs/sca.config.json --format html --output reports/api-cost-report.html'; packageJson.scripts['sca:watch'] = 'sca watch ./src --config configs/sca.config.json'; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); console.log(`āœ… Added scripts to package.json`); console.log(' šŸ“‹ npm run sca:project # Analyze entire project'); console.log(' šŸ“‹ npm run sca:file <path> # Analyze single file'); console.log(' šŸ“‹ npm run sca:report # Generate HTML report'); console.log(' šŸ“‹ npm run sca:watch <path> # Watch mode for development'); } // Create .gitignore entries const gitignorePath = path.join(outputDir, '.gitignore'); const gitignoreEntry = '\n# API Cost Analysis Reports\nreports/\n*.cost-report.*\n'; if (fs.existsSync(gitignorePath)) { const gitignore = fs.readFileSync(gitignorePath, 'utf8'); if (!gitignore.includes('# API Cost Analysis Reports')) { fs.appendFileSync(gitignorePath, gitignoreEntry); console.log(`āœ… Added entries to .gitignore`); } } else { fs.writeFileSync(gitignorePath, gitignoreEntry.trim()); console.log(`āœ… Created .gitignore with report entries`); } console.log('\nšŸŽ‰ Setup complete! Quick start:'); console.log(` sca project ./src --config configs/sca.config.json`); console.log(` npm run sca:project # Or use npm script`); } catch (error) { console.error('āŒ Init failed:', error instanceof Error ? error.message : error); process.exit(1); } }); // Editor commands program .command('editor') .description('šŸ“ Editor integration commands') .option('--detect', 'Detect available code editors') .option('--setup-vscode', 'Generate VS Code tasks.json for SCA integration') .option('--open <file>', 'Open file in detected editor') .option('--line <number>', 'Line number to open (use with --open)', parseInt) .action((options) => { try { if (options.detect) { const editorConfig = editor_detector_1.EditorDetector.detectEditor(); console.log('\nšŸ“ Editor Detection Results:\n'); console.log(`Default Editor: ${editorConfig.defaultEditor}`); console.log(`Detected Editor: ${editorConfig.detectedEditor || 'None detected'}`); console.log('\nAvailable Editors:'); Object.entries(editorConfig.editorCommands || {}).forEach(([name, info]) => { console.log(` - ${name}: ${info.name}`); }); } if (options.setupVscode) { editor_detector_1.EditorDetector.generateVSCodeTasks(); console.log('āœ… VS Code tasks.json generated successfully!'); } if (options.open) { const config = config_loader_1.ConfigLoader.loadConfig(); const editorConfig = config_loader_1.ConfigLoader.getEditorConfig(config); editor_detector_1.EditorDetector.openInEditor(options.open, options.line, editorConfig); } if (!options.detect && !options.setupVscode && !options.open) { console.log('\nšŸ“ Editor Integration Help:\n'); console.log('Available commands:'); console.log(' sca editor --detect Detect available editors'); console.log(' sca editor --setup-vscode Generate VS Code integration'); console.log(' sca editor --open <file> Open file in editor'); console.log(' sca editor --open <file> --line <num> Open file at specific line'); } } catch (error) { console.error('āŒ Editor command failed:', error); process.exit(1); } }); // Watch mode program .command('watch <path>') .description('šŸ‘€ Watch files for changes and automatically re-analyze (development mode)') .option('-f, --framework <framework>', 'Framework preset', 'nestjs') .option('--format <format>', 'Output format', 'text') .action((filePath, options) => { // Load configuration with fallbacks const analyzer = options.config ? analyzer_1.ApiCostAnalyzer.fromConfigFile(options.config) : new analyzer_1.ApiCostAnalyzer(); if (options.framework && config_1.FRAMEWORK_PRESETS[options.framework]) { analyzer.setFramework(options.framework); } console.log(`šŸ‘€ Watching ${filePath} for changes...`); const analyzeAndReport = () => { try { console.log('\nšŸ”„ File changed, re-analyzing...'); const result = analyzer.analyzeFile(filePath); const report = reports_1.ReportGenerator.generateReport(result, options.format); console.log(report); } catch (error) { console.error('āŒ Analysis failed:', error instanceof Error ? error.message : error); } }; // Initial analysis analyzeAndReport(); // Watch for changes fs.watchFile(filePath, analyzeAndReport); // Keep process alive process.on('SIGINT', () => { console.log('\nšŸ‘‹ Stopping watcher...'); fs.unwatchFile(filePath); process.exit(0); }); }); // Demo command program .command('demo') .description('šŸŽ® Show demo with examples and supported patterns') .action(() => { console.log('šŸŽ® API Cost Analyzer Demo'); console.log('='.repeat(50)); console.log('\nšŸ“‹ Available Frameworks:'); Object.entries(config_1.FRAMEWORK_PRESETS).forEach(([key, preset]) => { console.log(` ${key.padEnd(10)} - ${preset.description}`); }); console.log('\nšŸ“‹ Example Commands:'); console.log(' sca file ./src/users/users.service.ts --framework nestjs'); console.log(' sca project ./src --framework nestjs --format html --output report.html'); console.log(' sca nestjs ./src --output nestjs-analysis.md --format markdown'); console.log(' sca watch ./src/auth/auth.service.ts --framework nestjs'); console.log('\nšŸ“‹ Supported Patterns:'); const nestjsConfig = (0, config_1.getFrameworkConfig)('nestjs'); Object.entries(nestjsConfig.patterns).forEach(([name, config]) => { console.log(` ${name}: ${config.description}`); }); console.log('\nšŸš€ Get started with: sca init'); }); // Help command enhancement program.on('--help', () => { console.log(''); console.log('šŸ“š Examples:'); console.log(' $ sca init --framework nestjs # Initialize configuration'); console.log(' $ sca file ./src/users.service.ts # Analyze single file'); console.log(' $ sca project ./src # Analyze entire project'); console.log(' $ sca nestjs ./src --format html --output report.html # NestJS with HTML report'); console.log(' $ sca watch ./src/auth.service.ts # Watch mode for development'); console.log(' $ sca demo # Show examples and patterns'); console.log(''); console.log('šŸš€ Supported Frameworks:'); Object.entries(config_1.FRAMEWORK_PRESETS).forEach(([key, preset]) => { console.log(` ${key.padEnd(10)} - ${preset.description}`); }); console.log(''); console.log('šŸ’” Quick Start:'); console.log(' 1. bun install -g sca-tool'); console.log(' 2. sca init --framework nestjs'); console.log(' 3. sca project ./src'); console.log(''); }); // Parse command line arguments if (require.main === module) { program.parse(); } //# sourceMappingURL=cli.js.map