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,095 lines 93.6 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'; import { execSync } from 'child_process'; // 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 }; } }, }; 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 targetPath = resolve(ctx.flags.path || '.'); const analysisType = ctx.flags.type || 'quality'; const formatJson = ctx.flags.format === 'json'; output.writeln(); output.writeln(output.bold('Code Analysis')); output.writeln(output.dim('-'.repeat(50))); const spinner = output.createSpinner({ text: `Analyzing ${targetPath}...`, spinner: 'dots' }); spinner.start(); try { const files = await scanSourceFiles(targetPath); if (files.length === 0) { spinner.stop(); output.printWarning('No source files found'); return { success: true }; } const fileStats = []; for (const filePath of files) { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); const nonEmpty = lines.filter(l => l.trim().length > 0 && !/^\s*(\/\/|\/\*|\*\s|#)/.test(l)).length; const todos = (content.match(/\b(TODO|FIXME|HACK|XXX)\b/gi) || []).length; const fns = (content.match(/(?:export\s+)?(?:async\s+)?function\s+\w+|(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/g) || []).length; const imps = (content.match(/^import\s+/gm) || []).length + (content.match(/require\s*\(/g) || []).length; let maxNesting = 0; let nesting = 0; for (const line of lines) { nesting += (line.match(/\{/g) || []).length; nesting -= (line.match(/\}/g) || []).length; if (nesting > maxNesting) maxNesting = nesting; } const securityIssues = []; if (/\beval\s*\(/.test(content)) securityIssues.push('eval()'); if (/\bexec\s*\(/.test(content)) securityIssues.push('exec()'); if (/\.innerHTML\s*=/.test(content)) securityIssues.push('innerHTML'); if (/dangerouslySetInnerHTML/.test(content)) securityIssues.push('dangerouslySetInnerHTML'); if (/['"](?:password|secret|api[_-]?key|token)\s*[:=]\s*['"][^'"]{3,}['"]/i.test(content)) securityIssues.push('hardcoded secret'); if (/new\s+Function\s*\(/.test(content)) securityIssues.push('new Function()'); fileStats.push({ file: filePath, loc: nonEmpty, todos, functions: fns, imports: imps, maxNesting, securityIssues, }); } spinner.stop(); const totalLoc = fileStats.reduce((s, f) => s + f.loc, 0); const totalTodos = fileStats.reduce((s, f) => s + f.todos, 0); const totalFunctions = fileStats.reduce((s, f) => s + f.functions, 0); const totalImports = fileStats.reduce((s, f) => s + f.imports, 0); const avgFileSize = Math.round(totalLoc / files.length); const longestFile = fileStats.reduce((a, b) => a.loc > b.loc ? a : b); const avgFnPerFile = (totalFunctions / files.length).toFixed(1); const deepestNesting = fileStats.reduce((a, b) => a.maxNesting > b.maxNesting ? a : b); const allSecurityIssues = fileStats.filter(f => f.securityIssues.length > 0); if (formatJson) { const jsonData = { type: analysisType, path: targetPath, files: files.length, totalLoc, totalTodos, totalFunctions, totalImports, avgFileSize, fileStats: fileStats.map(f => ({ relativePath: path.relative(targetPath, f.file), loc: f.loc, todos: f.todos, functions: f.functions, imports: f.imports, maxNesting: f.maxNesting, securityIssues: f.securityIssues })) }; output.printJson(jsonData); return { success: true, data: jsonData }; } if (analysisType === 'quality') { output.printBox([`Files: ${files.length}`, `Lines of Code: ${totalLoc.toLocaleString()}`, `Avg File Size: ${avgFileSize} LOC`, `TODO/FIXME: ${totalTodos}`, `Functions: ${totalFunctions}`, `Imports: ${totalImports}`].join('\n'), 'Quality Summary'); output.writeln(); output.writeln(output.bold('Largest Files')); output.writeln(output.dim('-'.repeat(60))); const top10 = [...fileStats].sort((a, b) => b.loc - a.loc).slice(0, 10); output.printTable({ columns: [ { key: 'file', header: 'File', width: 45 }, { key: 'loc', header: 'LOC', width: 8, align: 'right' }, { key: 'fns', header: 'Fns', width: 6, align: 'right' }, { key: 'todos', header: 'TODOs', width: 7, align: 'right' }, ], data: top10.map(f => ({ file: path.relative(targetPath, f.file), loc: f.loc, fns: f.functions, todos: f.todos })), }); if (totalTodos > 0) { output.writeln(); output.printWarning(`${totalTodos} TODO/FIXME comments found across ${fileStats.filter(f => f.todos > 0).length} files`); } } else if (analysisType === 'complexity') { output.printBox([`Files: ${files.length}`, `Total Functions: ${totalFunctions}`, `Avg Functions/File: ${avgFnPerFile}`, `Deepest Nesting: ${deepestNesting.maxNesting} levels (${path.relative(targetPath, deepestNesting.file)})`, `Longest File: ${longestFile.loc} LOC (${path.relative(targetPath, longestFile.file)})`].join('\n'), 'Complexity Summary'); output.writeln(); output.writeln(output.bold('High Complexity Files (nesting > 5)')); output.writeln(output.dim('-'.repeat(60))); const complex = fileStats.filter(f => f.maxNesting > 5).sort((a, b) => b.maxNesting - a.maxNesting); if (complex.length === 0) { output.printSuccess('No files with excessive nesting detected'); } else { output.printTable({ columns: [ { key: 'file', header: 'File', width: 45 }, { key: 'nesting', header: 'Max Nest', width: 10, align: 'right' }, { key: 'fns', header: 'Fns', width: 6, align: 'right' }, { key: 'loc', header: 'LOC', width: 8, align: 'right' }, ], data: complex.slice(0, 15).map(f => ({ file: path.relative(targetPath, f.file), nesting: f.maxNesting, fns: f.functions, loc: f.loc })), }); } } else if (analysisType === 'security') { output.printBox([`Files Scanned: ${files.length}`, `Files with Issues: ${allSecurityIssues.length}`, `Total Issues: ${allSecurityIssues.reduce((s, f) => s + f.securityIssues.length, 0)}`].join('\n'), 'Security Summary'); if (allSecurityIssues.length === 0) { output.writeln(); output.printSuccess('No common security patterns detected'); } else { output.writeln(); output.writeln(output.bold('Security Concerns')); output.writeln(output.dim('-'.repeat(60))); output.printTable({ columns: [ { key: 'file', header: 'File', width: 40 }, { key: 'issues', header: 'Issues', width: 35 }, ], data: allSecurityIssues.map(f => ({ file: path.relative(targetPath, f.file), issues: f.securityIssues.join(', ') })), }); } } else { output.printWarning(`Unknown analysis type: ${analysisType}. Use quality, complexity, or security.`); } return { success: true }; } catch (error) { spinner.stop(); const message = error instanceof Error ? error.message : String(error); output.printError(`Code analysis failed: ${message}`); return { success: false, exitCode: 1 }; } }, }; // ============================================================================ // 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: