UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

1,167 lines 80 kB
/** * V3 CLI Analyze Command * Code analysis, diff classification, AST analysis, and change risk assessment * * Features: * - AST analysis using ruvector (tree-sitter) with graceful fallback * - Symbol extraction (functions, classes, variables, types) * - Cyclomatic complexity scoring * - Diff classification and risk assessment * - Graph boundaries using MinCut algorithm * - Module communities using Louvain algorithm * - Circular dependency detection * * Created with ruv.io */ import { output } from '../output.js'; import { callMCPTool, MCPClientError } from '../mcp-client.js'; import * as path from 'path'; import * as fs from 'fs/promises'; import { writeFile } from 'fs/promises'; import { resolve } from 'path'; // Dynamic import for AST analyzer async function getASTAnalyzer() { try { return await import('../ruvector/ast-analyzer.js'); } catch { return null; } } // Dynamic import for graph analyzer async function getGraphAnalyzer() { try { return await import('../ruvector/graph-analyzer.js'); } catch { return null; } } // Diff subcommand const diffCommand = { name: 'diff', description: 'Analyze git diff for change risk assessment and classification', options: [ { name: 'risk', short: 'r', description: 'Show risk assessment', type: 'boolean', default: false, }, { name: 'classify', short: 'c', description: 'Classify change type', type: 'boolean', default: false, }, { name: 'reviewers', description: 'Show recommended reviewers', type: 'boolean', default: false, }, { name: 'format', short: 'f', description: 'Output format: text, json, table', type: 'string', default: 'text', choices: ['text', 'json', 'table'], }, { name: 'verbose', short: 'v', description: 'Show detailed file-level analysis', type: 'boolean', default: false, }, ], examples: [ { command: 'claude-flow analyze diff --risk', description: 'Analyze current diff with risk assessment' }, { command: 'claude-flow analyze diff HEAD~1 --classify', description: 'Classify changes from last commit' }, { command: 'claude-flow analyze diff main..feature --format json', description: 'Compare branches with JSON output' }, { command: 'claude-flow analyze diff --reviewers', description: 'Get recommended reviewers for changes' }, ], action: async (ctx) => { const ref = ctx.args[0] || 'HEAD'; const showRisk = ctx.flags.risk; const showClassify = ctx.flags.classify; const showReviewers = ctx.flags.reviewers; const formatType = ctx.flags.format || 'text'; const verbose = ctx.flags.verbose; // If no specific flag, show all const showAll = !showRisk && !showClassify && !showReviewers; output.printInfo(`Analyzing diff: ${output.highlight(ref)}`); try { // Call MCP tool for diff analysis const result = await callMCPTool('analyze_diff', { ref, includeFileRisks: verbose, includeReviewers: showReviewers || showAll, }); // JSON output if (formatType === 'json') { output.printJson(result); return { success: true, data: result }; } output.writeln(); // Summary box const files = result.files || []; const risk = result.risk || { overall: 'unknown', score: 0, breakdown: { fileCount: 0, totalChanges: 0, highRiskFiles: [], securityConcerns: [], breakingChanges: [], testCoverage: 'unknown' } }; const classification = result.classification || { category: 'unknown', confidence: 0, reasoning: '' }; output.printBox([ `Ref: ${result.ref || 'HEAD'}`, `Files: ${files.length}`, `Risk: ${getRiskDisplay(risk.overall)} (${risk.score}/100)`, `Type: ${classification.category}${classification.subcategory ? ` (${classification.subcategory})` : ''}`, ``, result.summary || 'No summary available', ].join('\n'), 'Diff Analysis'); // Risk assessment if (showRisk || showAll) { output.writeln(); output.writeln(output.bold('Risk Assessment')); output.writeln(output.dim('-'.repeat(50))); output.printTable({ columns: [ { key: 'metric', header: 'Metric', width: 25 }, { key: 'value', header: 'Value', width: 30 }, ], data: [ { metric: 'Overall Risk', value: getRiskDisplay(risk.overall) }, { metric: 'Risk Score', value: `${risk.score}/100` }, { metric: 'Files Changed', value: risk.breakdown.fileCount }, { metric: 'Total Lines Changed', value: risk.breakdown.totalChanges }, { metric: 'Test Coverage', value: risk.breakdown.testCoverage }, ], }); // Security concerns if (risk.breakdown.securityConcerns.length > 0) { output.writeln(); output.writeln(output.bold(output.warning('Security Concerns'))); output.printList(risk.breakdown.securityConcerns.map(c => output.warning(c))); } // Breaking changes if (risk.breakdown.breakingChanges.length > 0) { output.writeln(); output.writeln(output.bold(output.error('Potential Breaking Changes'))); output.printList(risk.breakdown.breakingChanges.map(c => output.error(c))); } // High risk files if (risk.breakdown.highRiskFiles.length > 0) { output.writeln(); output.writeln(output.bold('High Risk Files')); output.printList(risk.breakdown.highRiskFiles.map(f => output.warning(f))); } } // Classification if (showClassify || showAll) { output.writeln(); output.writeln(output.bold('Classification')); output.writeln(output.dim('-'.repeat(50))); output.printTable({ columns: [ { key: 'field', header: 'Field', width: 15 }, { key: 'value', header: 'Value', width: 40 }, ], data: [ { field: 'Category', value: classification.category }, { field: 'Subcategory', value: classification.subcategory || '-' }, { field: 'Confidence', value: `${(classification.confidence * 100).toFixed(0)}%` }, ], }); output.writeln(); output.writeln(output.dim(`Reasoning: ${classification.reasoning}`)); } // Reviewers if (showReviewers || showAll) { output.writeln(); output.writeln(output.bold('Recommended Reviewers')); output.writeln(output.dim('-'.repeat(50))); const reviewers = result.recommendedReviewers || []; if (reviewers.length > 0) { output.printNumberedList(reviewers.map(r => output.highlight(r))); } else { output.writeln(output.dim('No specific reviewers recommended')); } } // Verbose file-level details if (verbose && result.fileRisks) { output.writeln(); output.writeln(output.bold('File-Level Analysis')); output.writeln(output.dim('-'.repeat(50))); output.printTable({ columns: [ { key: 'path', header: 'File', width: 40 }, { key: 'risk', header: 'Risk', width: 12, format: (v) => getRiskDisplay(String(v)) }, { key: 'score', header: 'Score', width: 8, align: 'right' }, { key: 'reasons', header: 'Reasons', width: 30, format: (v) => { const reasons = v; return reasons.slice(0, 2).join('; '); } }, ], data: result.fileRisks, }); } // Files changed table if (formatType === 'table' || showAll) { output.writeln(); output.writeln(output.bold('Files Changed')); output.writeln(output.dim('-'.repeat(50))); output.printTable({ columns: [ { key: 'status', header: 'Status', width: 10, format: (v) => getStatusDisplay(String(v)) }, { key: 'path', header: 'File', width: 45 }, { key: 'additions', header: '+', width: 8, align: 'right', format: (v) => output.success(`+${v}`) }, { key: 'deletions', header: '-', width: 8, align: 'right', format: (v) => output.error(`-${v}`) }, ], data: files.slice(0, 20), }); if (files.length > 20) { output.writeln(output.dim(` ... and ${files.length - 20} more files`)); } } return { success: true, data: result }; } catch (error) { if (error instanceof MCPClientError) { output.printError(`Diff analysis failed: ${error.message}`); } else { output.printError(`Unexpected error: ${String(error)}`); } return { success: false, exitCode: 1 }; } }, }; // Code subcommand (placeholder for future code analysis) const codeCommand = { name: 'code', description: 'Static code analysis and quality assessment', options: [ { name: 'path', short: 'p', type: 'string', description: 'Path to analyze', default: '.' }, { name: 'type', short: 't', type: 'string', description: 'Analysis type: quality, complexity, security', default: 'quality' }, { name: 'format', short: 'f', type: 'string', description: 'Output format: text, json', default: 'text' }, ], examples: [ { command: 'claude-flow analyze code -p ./src', description: 'Analyze source directory' }, { command: 'claude-flow analyze code --type complexity', description: 'Run complexity analysis' }, ], action: async (ctx) => { const path = ctx.flags.path || '.'; const analysisType = ctx.flags.type || 'quality'; output.writeln(); output.writeln(output.bold('Code Analysis')); output.writeln(output.dim('-'.repeat(50))); output.printInfo(`Analyzing ${path} for ${analysisType}...`); output.writeln(); // Placeholder - would integrate with actual code analysis tools output.printBox([ `Path: ${path}`, `Type: ${analysisType}`, `Status: Feature in development`, ``, `Code analysis capabilities coming soon.`, `Use 'analyze diff' for change analysis.`, ].join('\n'), 'Code Analysis'); return { success: true }; }, }; // ============================================================================ // AST Analysis Subcommands (using ruvector tree-sitter with fallback) // ============================================================================ /** * Helper: Truncate file path for display */ function truncatePathAst(filePath, maxLen = 45) { if (filePath.length <= maxLen) return filePath; return '...' + filePath.slice(-(maxLen - 3)); } /** * Helper: Format complexity value with color coding */ function formatComplexityValueAst(value) { if (value <= 5) return output.success(String(value)); if (value <= 10) return output.warning(String(value)); return output.error(String(value)); } /** * Helper: Get type marker for symbols */ function getTypeMarkerAst(type) { switch (type) { case 'function': return output.success('fn'); case 'class': return output.info('class'); case 'variable': return output.dim('var'); case 'type': return output.highlight('type'); case 'interface': return output.highlight('iface'); default: return output.dim(type.slice(0, 5)); } } /** * Helper: Get complexity rating text */ function getComplexityRatingAst(value) { if (value <= 5) return output.success('Simple'); if (value <= 10) return output.warning('Moderate'); if (value <= 20) return output.error('Complex'); return output.error(output.bold('Very Complex')); } /** * AST analysis subcommand */ const astCommand = { name: 'ast', description: 'Analyze code using AST parsing (tree-sitter via ruvector)', options: [ { name: 'complexity', short: 'c', description: 'Include complexity metrics', type: 'boolean', default: false, }, { name: 'symbols', short: 's', description: 'Include symbol extraction', type: 'boolean', default: false, }, { name: 'format', short: 'f', description: 'Output format (text, json, table)', type: 'string', default: 'text', choices: ['text', 'json', 'table'], }, { name: 'output', short: 'o', description: 'Output file path', type: 'string', }, { name: 'verbose', short: 'v', description: 'Show detailed analysis', type: 'boolean', default: false, }, ], examples: [ { command: 'claude-flow analyze ast src/', description: 'Analyze all files in src/' }, { command: 'claude-flow analyze ast src/index.ts --complexity', description: 'Analyze with complexity' }, { command: 'claude-flow analyze ast . --format json', description: 'JSON output' }, { command: 'claude-flow analyze ast src/ --symbols', description: 'Extract symbols' }, ], action: async (ctx) => { const targetPath = ctx.args[0] || ctx.cwd; const showComplexity = ctx.flags.complexity; const showSymbols = ctx.flags.symbols; const formatType = ctx.flags.format || 'text'; const outputFile = ctx.flags.output; const verbose = ctx.flags.verbose; // If no specific flags, show summary const showAll = !showComplexity && !showSymbols; output.printInfo(`Analyzing: ${output.highlight(targetPath)}`); output.writeln(); const spinner = output.createSpinner({ text: 'Parsing AST...', spinner: 'dots' }); spinner.start(); try { const astModule = await getASTAnalyzer(); if (!astModule) { spinner.stop(); output.printWarning('AST analyzer not available, using regex fallback'); } // Resolve path and check if file or directory const resolvedPath = resolve(targetPath); const stat = await fs.stat(resolvedPath); const isDirectory = stat.isDirectory(); let results = []; if (isDirectory) { // Scan directory for source files const files = await scanSourceFiles(resolvedPath); spinner.stop(); output.printInfo(`Found ${files.length} source files`); spinner.start(); for (const file of files.slice(0, 100)) { try { const content = await fs.readFile(file, 'utf-8'); if (astModule) { const analyzer = astModule.createASTAnalyzer(); const analysis = analyzer.analyze(content, file); results.push(analysis); } else { // Fallback analysis results.push(fallbackAnalyze(content, file)); } } catch { // Skip files that can't be analyzed } } } else { // Single file const content = await fs.readFile(resolvedPath, 'utf-8'); if (astModule) { const analyzer = astModule.createASTAnalyzer(); const analysis = analyzer.analyze(content, resolvedPath); results.push(analysis); } else { results.push(fallbackAnalyze(content, resolvedPath)); } } spinner.stop(); if (results.length === 0) { output.printWarning('No files analyzed'); return { success: true }; } // Calculate totals const totals = { files: results.length, functions: results.reduce((sum, r) => sum + r.functions.length, 0), classes: results.reduce((sum, r) => sum + r.classes.length, 0), imports: results.reduce((sum, r) => sum + r.imports.length, 0), avgComplexity: results.reduce((sum, r) => sum + r.complexity.cyclomatic, 0) / results.length, totalLoc: results.reduce((sum, r) => sum + r.complexity.loc, 0), }; // JSON output if (formatType === 'json') { const jsonOutput = { files: results, totals }; if (outputFile) { await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } else { output.printJson(jsonOutput); } return { success: true, data: jsonOutput }; } // Summary box output.printBox([ `Files analyzed: ${totals.files}`, `Functions: ${totals.functions}`, `Classes: ${totals.classes}`, `Total LOC: ${totals.totalLoc}`, `Avg Complexity: ${formatComplexityValueAst(Math.round(totals.avgComplexity))}`, ].join('\n'), 'AST Analysis Summary'); // Complexity view if (showComplexity || showAll) { output.writeln(); output.writeln(output.bold('Complexity by File')); output.writeln(output.dim('-'.repeat(60))); const complexityData = results .map(r => ({ file: truncatePathAst(r.filePath), cyclomatic: r.complexity.cyclomatic, cognitive: r.complexity.cognitive, loc: r.complexity.loc, rating: getComplexityRatingAst(r.complexity.cyclomatic), })) .sort((a, b) => b.cyclomatic - a.cyclomatic) .slice(0, 15); output.printTable({ columns: [ { key: 'file', header: 'File', width: 40 }, { key: 'cyclomatic', header: 'Cyclo', width: 8, align: 'right', format: (v) => formatComplexityValueAst(v) }, { key: 'cognitive', header: 'Cogni', width: 8, align: 'right' }, { key: 'loc', header: 'LOC', width: 8, align: 'right' }, { key: 'rating', header: 'Rating', width: 15 }, ], data: complexityData, }); if (results.length > 15) { output.writeln(output.dim(` ... and ${results.length - 15} more files`)); } } // Symbols view if (showSymbols || showAll) { output.writeln(); output.writeln(output.bold('Extracted Symbols')); output.writeln(output.dim('-'.repeat(60))); const allSymbols = []; for (const r of results) { for (const fn of r.functions) { allSymbols.push({ name: fn.name, type: 'function', file: truncatePathAst(r.filePath, 30), line: fn.startLine }); } for (const cls of r.classes) { allSymbols.push({ name: cls.name, type: 'class', file: truncatePathAst(r.filePath, 30), line: cls.startLine }); } } const displaySymbols = allSymbols.slice(0, 20); output.printTable({ columns: [ { key: 'type', header: 'Type', width: 8, format: (v) => getTypeMarkerAst(v) }, { key: 'name', header: 'Symbol', width: 30 }, { key: 'file', header: 'File', width: 35 }, { key: 'line', header: 'Line', width: 8, align: 'right' }, ], data: displaySymbols, }); if (allSymbols.length > 20) { output.writeln(output.dim(` ... and ${allSymbols.length - 20} more symbols`)); } } // Verbose output if (verbose) { output.writeln(); output.writeln(output.bold('Import Analysis')); output.writeln(output.dim('-'.repeat(60))); const importCounts = new Map(); for (const r of results) { for (const imp of r.imports) { importCounts.set(imp, (importCounts.get(imp) || 0) + 1); } } const topImports = Array.from(importCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10); for (const [imp, count] of topImports) { output.writeln(` ${output.highlight(count.toString().padStart(3))} ${imp}`); } } if (outputFile) { await writeFile(outputFile, JSON.stringify({ files: results, totals }, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } return { success: true, data: { files: results, totals } }; } catch (error) { spinner.stop(); const message = error instanceof Error ? error.message : String(error); output.printError(`AST analysis failed: ${message}`); return { success: false, exitCode: 1 }; } }, }; /** * Complexity analysis subcommand */ const complexityAstCommand = { name: 'complexity', aliases: ['cx'], description: 'Analyze code complexity metrics', options: [ { name: 'threshold', short: 't', description: 'Complexity threshold to flag (default: 10)', type: 'number', default: 10, }, { name: 'format', short: 'f', description: 'Output format (text, json)', type: 'string', default: 'text', choices: ['text', 'json'], }, { name: 'output', short: 'o', description: 'Output file path', type: 'string', }, ], examples: [ { command: 'claude-flow analyze complexity src/', description: 'Analyze complexity' }, { command: 'claude-flow analyze complexity src/ --threshold 15', description: 'Flag high complexity' }, ], action: async (ctx) => { const targetPath = ctx.args[0] || ctx.cwd; const threshold = ctx.flags.threshold || 10; const formatType = ctx.flags.format || 'text'; const outputFile = ctx.flags.output; output.printInfo(`Analyzing complexity: ${output.highlight(targetPath)}`); output.writeln(); const spinner = output.createSpinner({ text: 'Calculating complexity...', spinner: 'dots' }); spinner.start(); try { const astModule = await getASTAnalyzer(); const resolvedPath = resolve(targetPath); const stat = await fs.stat(resolvedPath); const files = stat.isDirectory() ? await scanSourceFiles(resolvedPath) : [resolvedPath]; const results = []; for (const file of files.slice(0, 100)) { try { const content = await fs.readFile(file, 'utf-8'); let analysis; if (astModule) { const analyzer = astModule.createASTAnalyzer(); analysis = analyzer.analyze(content, file); } else { analysis = fallbackAnalyze(content, file); } const flagged = analysis.complexity.cyclomatic > threshold; const rating = analysis.complexity.cyclomatic <= 5 ? 'Simple' : analysis.complexity.cyclomatic <= 10 ? 'Moderate' : analysis.complexity.cyclomatic <= 20 ? 'Complex' : 'Very Complex'; results.push({ file: file, cyclomatic: analysis.complexity.cyclomatic, cognitive: analysis.complexity.cognitive, loc: analysis.complexity.loc, commentDensity: analysis.complexity.commentDensity, rating, flagged, }); } catch { // Skip files that can't be analyzed } } spinner.stop(); // Sort by complexity descending results.sort((a, b) => b.cyclomatic - a.cyclomatic); const flaggedCount = results.filter(r => r.flagged).length; const avgComplexity = results.length > 0 ? results.reduce((sum, r) => sum + r.cyclomatic, 0) / results.length : 0; if (formatType === 'json') { const jsonOutput = { files: results, summary: { total: results.length, flagged: flaggedCount, avgComplexity, threshold } }; if (outputFile) { await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } else { output.printJson(jsonOutput); } return { success: true, data: jsonOutput }; } // Summary output.printBox([ `Files analyzed: ${results.length}`, `Threshold: ${threshold}`, `Flagged files: ${flaggedCount > 0 ? output.error(String(flaggedCount)) : output.success('0')}`, `Average complexity: ${formatComplexityValueAst(Math.round(avgComplexity))}`, ].join('\n'), 'Complexity Analysis'); // Show flagged files first if (flaggedCount > 0) { output.writeln(); output.writeln(output.bold(output.warning(`High Complexity Files (>${threshold})`))); output.writeln(output.dim('-'.repeat(60))); const flaggedFiles = results.filter(r => r.flagged).slice(0, 10); output.printTable({ columns: [ { key: 'file', header: 'File', width: 40, format: (v) => truncatePathAst(v) }, { key: 'cyclomatic', header: 'Cyclo', width: 8, align: 'right', format: (v) => output.error(String(v)) }, { key: 'cognitive', header: 'Cogni', width: 8, align: 'right' }, { key: 'loc', header: 'LOC', width: 8, align: 'right' }, { key: 'rating', header: 'Rating', width: 15 }, ], data: flaggedFiles, }); } // Show all files in table format output.writeln(); output.writeln(output.bold('All Files')); output.writeln(output.dim('-'.repeat(60))); const displayFiles = results.slice(0, 15); output.printTable({ columns: [ { key: 'file', header: 'File', width: 40, format: (v) => truncatePathAst(v) }, { key: 'cyclomatic', header: 'Cyclo', width: 8, align: 'right', format: (v) => formatComplexityValueAst(v) }, { key: 'cognitive', header: 'Cogni', width: 8, align: 'right' }, { key: 'loc', header: 'LOC', width: 8, align: 'right' }, ], data: displayFiles, }); if (results.length > 15) { output.writeln(output.dim(` ... and ${results.length - 15} more files`)); } if (outputFile) { await writeFile(outputFile, JSON.stringify({ files: results, summary: { total: results.length, flagged: flaggedCount, avgComplexity, threshold } }, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } return { success: true, data: { files: results, flaggedCount } }; } catch (error) { spinner.stop(); const message = error instanceof Error ? error.message : String(error); output.printError(`Complexity analysis failed: ${message}`); return { success: false, exitCode: 1 }; } }, }; /** * Symbol extraction subcommand */ const symbolsCommand = { name: 'symbols', aliases: ['sym'], description: 'Extract and list code symbols (functions, classes, types)', options: [ { name: 'type', short: 't', description: 'Filter by symbol type (function, class, all)', type: 'string', default: 'all', choices: ['function', 'class', 'all'], }, { name: 'format', short: 'f', description: 'Output format (text, json)', type: 'string', default: 'text', choices: ['text', 'json'], }, { name: 'output', short: 'o', description: 'Output file path', type: 'string', }, ], examples: [ { command: 'claude-flow analyze symbols src/', description: 'Extract all symbols' }, { command: 'claude-flow analyze symbols src/ --type function', description: 'Only functions' }, { command: 'claude-flow analyze symbols src/ --format json', description: 'JSON output' }, ], action: async (ctx) => { const targetPath = ctx.args[0] || ctx.cwd; const symbolType = ctx.flags.type || 'all'; const formatType = ctx.flags.format || 'text'; const outputFile = ctx.flags.output; output.printInfo(`Extracting symbols: ${output.highlight(targetPath)}`); output.writeln(); const spinner = output.createSpinner({ text: 'Parsing code...', spinner: 'dots' }); spinner.start(); try { const astModule = await getASTAnalyzer(); const resolvedPath = resolve(targetPath); const stat = await fs.stat(resolvedPath); const files = stat.isDirectory() ? await scanSourceFiles(resolvedPath) : [resolvedPath]; const symbols = []; for (const file of files.slice(0, 100)) { try { const content = await fs.readFile(file, 'utf-8'); let analysis; if (astModule) { const analyzer = astModule.createASTAnalyzer(); analysis = analyzer.analyze(content, file); } else { analysis = fallbackAnalyze(content, file); } if (symbolType === 'all' || symbolType === 'function') { for (const fn of analysis.functions) { symbols.push({ name: fn.name, type: 'function', file, startLine: fn.startLine, endLine: fn.endLine, }); } } if (symbolType === 'all' || symbolType === 'class') { for (const cls of analysis.classes) { symbols.push({ name: cls.name, type: 'class', file, startLine: cls.startLine, endLine: cls.endLine, }); } } } catch { // Skip files that can't be parsed } } spinner.stop(); // Sort by file then name symbols.sort((a, b) => a.file.localeCompare(b.file) || a.name.localeCompare(b.name)); if (formatType === 'json') { if (outputFile) { await writeFile(outputFile, JSON.stringify(symbols, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } else { output.printJson(symbols); } return { success: true, data: symbols }; } // Summary const functionCount = symbols.filter(s => s.type === 'function').length; const classCount = symbols.filter(s => s.type === 'class').length; output.printBox([ `Total symbols: ${symbols.length}`, `Functions: ${functionCount}`, `Classes: ${classCount}`, `Files: ${files.length}`, ].join('\n'), 'Symbol Extraction'); output.writeln(); output.writeln(output.bold('Symbols')); output.writeln(output.dim('-'.repeat(60))); const displaySymbols = symbols.slice(0, 30); output.printTable({ columns: [ { key: 'type', header: 'Type', width: 10, format: (v) => getTypeMarkerAst(v) }, { key: 'name', header: 'Name', width: 30 }, { key: 'file', header: 'File', width: 35, format: (v) => truncatePathAst(v, 33) }, { key: 'startLine', header: 'Line', width: 8, align: 'right' }, ], data: displaySymbols, }); if (symbols.length > 30) { output.writeln(output.dim(` ... and ${symbols.length - 30} more symbols`)); } if (outputFile) { await writeFile(outputFile, JSON.stringify(symbols, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } return { success: true, data: symbols }; } catch (error) { spinner.stop(); const message = error instanceof Error ? error.message : String(error); output.printError(`Symbol extraction failed: ${message}`); return { success: false, exitCode: 1 }; } }, }; /** * Imports analysis subcommand */ const importsCommand = { name: 'imports', aliases: ['imp'], description: 'Analyze import dependencies across files', options: [ { name: 'format', short: 'f', description: 'Output format (text, json)', type: 'string', default: 'text', choices: ['text', 'json'], }, { name: 'output', short: 'o', description: 'Output file path', type: 'string', }, { name: 'external', short: 'e', description: 'Show only external (npm) imports', type: 'boolean', default: false, }, ], examples: [ { command: 'claude-flow analyze imports src/', description: 'Analyze all imports' }, { command: 'claude-flow analyze imports src/ --external', description: 'Only npm packages' }, ], action: async (ctx) => { const targetPath = ctx.args[0] || ctx.cwd; const formatType = ctx.flags.format || 'text'; const outputFile = ctx.flags.output; const externalOnly = ctx.flags.external; output.printInfo(`Analyzing imports: ${output.highlight(targetPath)}`); output.writeln(); const spinner = output.createSpinner({ text: 'Scanning imports...', spinner: 'dots' }); spinner.start(); try { const astModule = await getASTAnalyzer(); const resolvedPath = resolve(targetPath); const stat = await fs.stat(resolvedPath); const files = stat.isDirectory() ? await scanSourceFiles(resolvedPath) : [resolvedPath]; const importCounts = new Map(); const fileImports = new Map(); for (const file of files.slice(0, 100)) { try { const content = await fs.readFile(file, 'utf-8'); let analysis; if (astModule) { const analyzer = astModule.createASTAnalyzer(); analysis = analyzer.analyze(content, file); } else { analysis = fallbackAnalyze(content, file); } const imports = analysis.imports.filter(imp => { if (externalOnly) { return !imp.startsWith('.') && !imp.startsWith('/'); } return true; }); fileImports.set(file, imports); for (const imp of imports) { const existing = importCounts.get(imp) || { count: 0, files: [] }; existing.count++; existing.files.push(file); importCounts.set(imp, existing); } } catch { // Skip files that can't be parsed } } spinner.stop(); // Sort by count const sortedImports = Array.from(importCounts.entries()) .sort((a, b) => b[1].count - a[1].count); if (formatType === 'json') { const jsonOutput = { imports: Object.fromEntries(sortedImports), fileImports: Object.fromEntries(fileImports), }; if (outputFile) { await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } else { output.printJson(jsonOutput); } return { success: true, data: jsonOutput }; } // Summary const externalImports = sortedImports.filter(([imp]) => !imp.startsWith('.') && !imp.startsWith('/')); const localImports = sortedImports.filter(([imp]) => imp.startsWith('.') || imp.startsWith('/')); output.printBox([ `Total unique imports: ${sortedImports.length}`, `External (npm): ${externalImports.length}`, `Local (relative): ${localImports.length}`, `Files scanned: ${files.length}`, ].join('\n'), 'Import Analysis'); // Most used imports output.writeln(); output.writeln(output.bold('Most Used Imports')); output.writeln(output.dim('-'.repeat(60))); const topImports = sortedImports.slice(0, 20); output.printTable({ columns: [ { key: 'count', header: 'Uses', width: 8, align: 'right' }, { key: 'import', header: 'Import', width: 50 }, { key: 'type', header: 'Type', width: 10 }, ], data: topImports.map(([imp, data]) => ({ count: data.count, import: imp, type: imp.startsWith('.') || imp.startsWith('/') ? output.dim('local') : output.highlight('npm'), })), }); if (sortedImports.length > 20) { output.writeln(output.dim(` ... and ${sortedImports.length - 20} more imports`)); } if (outputFile) { await writeFile(outputFile, JSON.stringify({ imports: Object.fromEntries(sortedImports), fileImports: Object.fromEntries(fileImports), }, null, 2)); output.printSuccess(`Results written to ${outputFile}`); } return { success: true, data: { imports: sortedImports } }; } catch (error) { spinner.stop(); const message = error instanceof Error ? error.message : String(error); output.printError(`Import analysis failed: ${message}`); return { success: false, exitCode: 1 }; } }, }; /** * Helper: Scan directory for source files */ async function scanSourceFiles(dir, maxDepth = 10) { const files = []; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']; const excludeDirs = ['node_modules', 'dist', 'build', '.git', 'coverage', '__pycache__']; async function scan(currentDir, depth) { if (depth > maxDepth) return; try { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { if (!excludeDirs.includes(entry.name)) { await scan(fullPath, depth + 1); } } else if (entry.isFile()) { const ext = path.extname(entry.name); if (extensions.includes(ext)) { files.push(fullPath); } } } } catch { // Skip directories we can't read } } await scan(dir, 0); return files; } /** * Fallback analysis when ruvector is not available */ function fallbackAnalyze(code, filePath) { const lines = code.split('\n'); const functions = []; const classes = []; const imports = []; const exports = []; // Extract functions const funcPattern = /(?:export\s+)?(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>|^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+)?\s*\{/gm; let match; while ((match = funcPattern.exec(code)) !== null) { const name = match[1] || match[2] || match[3]; if (name && !['if', 'while', 'for', 'switch'].includes(name)) { const lineNum = code.substring(0, match.index).split('\n').length; functions.push({ name, startLine: lineNum, endLine: lineNum + 10 }); } } // Extract classes const classPattern = /(?:export\s+)?class\s+(\w+)/gm; while ((match = classPattern.exec(code)) !== null) { const lineNum = code.substring(0, match.index).split('\n').length; classes.push({ name: match[1], startLine: lineNum, endLine: lineNum + 20 }); } // Extract imports const importPattern = /import\s+(?:.*\s+from\s+)?['"]([^'"]+)['"]/gm; while ((match = importPattern.exec(code)) !== null) { imports.push(match[1]); } const requirePattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm; while ((match = requirePattern.exec(code)) !== null) { imports.push(match[1]); } // Extract exports const exportPattern = /export\s+(?:default\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/gm; while ((match = exportPattern.exec(code)) !== null) { exports.push(match[1]); } // Calculate complexity const nonEmptyLines = lines.filter(l => l.trim().length > 0).length; const commentLines = lines.filter(l => /^\s*(\/\/|\/\*|\*|#)/.test(l)).length; const decisionPoints = (code.match(/\b(if|else|for|while|switch|case|catch|&&|\|\||\?)\b/g) || []).length; let cognitive = 0; let nestingLevel = 0; for (const line of lines) { const opens = (line.match(/\{/g) || []).length; const closes = (line.match(/\}/g) || []).length; if (/\b(if|for|while|switch)\b/.test(line)) { cognitive += 1 + nestingLevel; } nestingLevel = Math.max(0, nestingLevel + opens - closes); } // Detect language const ext = path.extname(filePath).toLowerCase(); const language = ext === '.ts' || ext === '.tsx' ? 'typescript' : ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs' ? 'javascript' : ext === '.py' ? 'python' : 'unknown'; return { filePath, language, functions, classes, imports, exports, complexity: { cyclomatic: decisionPoints + 1, cognitive, loc: nonEmptyLines, commentDensity: lines.length > 0 ? commentLines / lines.length : 0, }, }; } // Dependencies subcommand const depsCommand = { name: 'deps', description: 'Analyze project dependencies', options: [ { name: 'outdated', short: 'o', type: 'boolean', description: 'Show only outdated dependencies' }, { name: 'security', short: 's', type: 'boolean', description: 'Check for security vulnerabilities' }, { name: 'format', short: 'f', type: 'string', description: 'Output format: text, json', default: 'text' }, ], examples: [ { command: 'claude-flow analyze deps --outdated', description: 'Show outdated dependencies' }, { command: 'claude-flow analyze deps --security', description: 'Check for vulnerabilities' }, ], action: async (ctx) => { const showOutdated = ctx.flags.outdated; const checkSecurity = ctx.flags.security; output.writeln(); output.writeln(output.bold('Dependency Analysis')); output.writeln(output.dim('-'.repeat(50))); output.printInfo('Analyzing dependencies...'); output.writeln(); // Placeholder - would integrate with npm/yarn audit output.printBox([ `Outdated Check: ${showOutdated ? 'Enabled' : 'Disabled'}`, `Security Check: ${checkSecurity ? 'Enabled' : 'Disabled'}`, `Status: Feature in development`, ``, `Dependency analysis capabilities coming soon.`, `Use 'security scan --type deps' for security scanning.`, ].join('\n'), 'Dependency Analysis'); return { success: true }; }, }; // ============================================================================ // Graph Analysis Subcommands (MinCut, Louvain, Circular Dependencies) // ============================================================================ /** * Analyze code boundaries using MinCut algorithm */ const boundariesCommand = { name: 'boundaries', aliases: ['boundary', 'mincut'], description: 'Find natural code boundaries using MinCut algorithm', options: [ { name: 'partitions', short: 'p', description: 'Number of partitions to find', type: 'number', default: 2, }, { name: 'output', short: 'o', description: 'Output file path', type: 'string', }, { name: 'format', short: 'f', description: 'Output format (text, json, dot)', type: 'string', default: 'text', choices: ['text', 'json', 'dot'], }, ], examples: [ { command: 'claude-flow analyze boundaries src/', description: 'Find code boundaries in src/' }, { command: 'claude-flow analyze boundaries -p 3 src/', description: 'Find 3 partitions' }, { command: 'claude-flow analyze boundaries -f dot -o graph.dot src/', description: 'Export to DOT format' }, ], action: async (ctx) => { const targetDir = ctx.args[0] || ctx.cwd; const numPartitions = ctx.flags.partitions || 2; const outputFile = ctx.flag