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,402 lines (1,251 loc) • 84.6 kB
text/typescript
/**
* 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 type { Command, CommandContext, CommandResult } from '../types.js';
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: Command = {
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: CommandContext): Promise<CommandResult> => {
const ref = ctx.args[0] || 'HEAD';
const showRisk = ctx.flags.risk as boolean;
const showClassify = ctx.flags.classify as boolean;
const showReviewers = ctx.flags.reviewers as boolean;
const formatType = ctx.flags.format as string || 'text';
const verbose = ctx.flags.verbose as boolean;
// 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<{
ref: string;
timestamp: string;
files: Array<{
path: string;
status: string;
additions: number;
deletions: number;
binary: boolean;
}>;
risk: {
overall: string;
score: number;
breakdown: {
fileCount: number;
totalChanges: number;
highRiskFiles: string[];
securityConcerns: string[];
breakingChanges: string[];
testCoverage: string;
};
};
classification: {
category: string;
subcategory?: string;
confidence: number;
reasoning: string;
};
fileRisks: Array<{
path: string;
risk: string;
score: number;
reasons: string[];
}>;
recommendedReviewers: string[];
summary: string;
}>('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 as string[];
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: Command = {
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: CommandContext): Promise<CommandResult> => {
const targetPath = resolve(ctx.flags.path as string || '.');
const analysisType = ctx.flags.type as string || 'quality';
const formatJson = (ctx.flags.format as string) === '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: Array<{ file: string; loc: number; todos: number; functions: number; imports: number; maxNesting: number; securityIssues: string[] }> = [];
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: string[] = [];
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' as const },
{ key: 'fns', header: 'Fns', width: 6, align: 'right' as const },
{ key: 'todos', header: 'TODOs', width: 7, align: 'right' as const },
],
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' as const },
{ key: 'fns', header: 'Fns', width: 6, align: 'right' as const },
{ key: 'loc', header: 'LOC', width: 8, align: 'right' as const },
],
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: string, maxLen: number = 45): string {
if (filePath.length <= maxLen) return filePath;
return '...' + filePath.slice(-(maxLen - 3));
}
/**
* Helper: Format complexity value with color coding
*/
function formatComplexityValueAst(value: number): string {
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: string): string {
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: number): string {
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: Command = {
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: CommandContext): Promise<CommandResult> => {
const targetPath = ctx.args[0] || ctx.cwd;
const showComplexity = ctx.flags.complexity as boolean;
const showSymbols = ctx.flags.symbols as boolean;
const formatType = (ctx.flags.format as string) || 'text';
const outputFile = ctx.flags.output as string | undefined;
const verbose = ctx.flags.verbose as boolean;
// 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: Array<{
filePath: string;
language: string;
functions: Array<{ name: string; startLine: number; endLine: number }>;
classes: Array<{ name: string; startLine: number; endLine: number }>;
imports: string[];
exports: string[];
complexity: { cyclomatic: number; cognitive: number; loc: number; commentDensity: number };
}> = [];
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 as number) },
{ 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: Array<{ name: string; type: string; file: string; line: number }> = [];
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 as string) },
{ 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: Map<string, number> = 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: Command = {
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: CommandContext): Promise<CommandResult> => {
const targetPath = ctx.args[0] || ctx.cwd;
const threshold = (ctx.flags.threshold as number) || 10;
const formatType = (ctx.flags.format as string) || 'text';
const outputFile = ctx.flags.output as string | undefined;
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: Array<{
file: string;
cyclomatic: number;
cognitive: number;
loc: number;
commentDensity: number;
rating: string;
flagged: boolean;
}> = [];
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 as string) },
{ 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 as string) },
{ key: 'cyclomatic', header: 'Cyclo', width: 8, align: 'right', format: (v) => formatComplexityValueAst(v as number) },
{ 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: Command = {
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: CommandContext): Promise<CommandResult> => {
const targetPath = ctx.args[0] || ctx.cwd;
const symbolType = (ctx.flags.type as string) || 'all';
const formatType = (ctx.flags.format as string) || 'text';
const outputFile = ctx.flags.output as string | undefined;
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: Array<{
name: string;
type: string;
file: string;
startLine: number;
endLine: number;
}> = [];
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 as string) },
{ key: 'name', header: 'Name', width: 30 },
{ key: 'file', header: 'File', width: 35, format: (v) => truncatePathAst(v as string, 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: Command = {
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: CommandContext): Promise<CommandResult> => {
const targetPath = ctx.args[0] || ctx.cwd;
const formatType = (ctx.flags.format as string) || 'text';
const outputFile = ctx.flags.output as string | undefined;
const externalOnly = ctx.flags.external as boolean;
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: Map<string, { count: number; files: string[] }> = new Map();
const fileImports: Map<string, string[]> = 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: string, maxDepth: number = 10): Promise<string[]> {
const files: string[] = [];
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
const excludeDirs = ['node_modules', 'dist', 'build', '.git', 'coverage', '__pycache__'];
async function scan(currentDir: string, depth: number): Promise<void> {
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: string, filePath: string) {
const lines = code.split('\n');
const functions: Array<{ name: string; startLine: number; endLine: number }> = [];
const classes: Array<{ name: string; startLine: number; endLine: number }> = [];
const imports: string[] = [];
const exports: string[] = [];
// 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.leng