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
JavaScript
/**
* 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: