Skip to content

Commit a25a544

Browse files
authored
feat: extract CLI wrappers into src/commands/ directory (Phase 3.2) (#393)
* feat: extract CLI wrappers into src/commands/ directory (Phase 3.2) Separate CLI display logic from data functions across 15 analysis modules. Each command now lives in src/commands/<name>.js while *Data() functions remain in their original modules (preserving MCP dynamic imports). - Create 16 command files in src/commands/ (audit, batch, cfg, check, cochange, communities, complexity, dataflow, flow, branch-compare, manifesto, owners, sequence, structure, triage, query barrel) - Add shared CommandRunner lifecycle in src/infrastructure/command-runner.js - Move result-formatter.js and test-filter.js to src/infrastructure/ - Update all imports in cli.js, index.js, queries-cli.js, and 7 other modules - Remove ~1,059 lines of CLI wrapper code from original analysis modules Impact: 33 functions changed, 19 affected * fix: remove unused command-runner.js, correct ROADMAP CommandRunner was created but never adopted by any command file — the 16 commands vary too much (async, multi-mode, process.exit) for a single lifecycle helper today. Remove dead code and mark as future work in the ROADMAP rather than claiming it as done.
1 parent 698b509 commit a25a544

43 files changed

Lines changed: 1079 additions & 1059 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/roadmap/ROADMAP.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -612,9 +612,10 @@ Rewrite the CFG algorithm as a node-level visitor that builds basic blocks and e
612612
-`queries.js` CLI wrappers → `queries-cli.js` (15 functions)
613613
- ✅ Shared `result-formatter.js` (`outputResult` for JSON/NDJSON dispatch)
614614
- ✅ Shared `test-filter.js` (`isTestFile` predicate)
615-
- 🔲 Extract CLI wrappers from remaining modules (audit, batch, check, cochange, communities, complexity, cfg, dataflow, flow, manifesto, owners, structure, triage, branch-compare)
616-
- 🔲 Introduce `CommandRunner` shared lifecycle
617-
- 🔲 Per-command `src/commands/` directory structure
615+
- ✅ Extract CLI wrappers from remaining modules (audit, batch, check, cochange, communities, complexity, cfg, dataflow, flow, manifesto, owners, structure, triage, branch-compare, sequence)
616+
- ✅ Per-command `src/commands/` directory structure (16 command files)
617+
- ✅ Move shared utilities to `src/infrastructure/` (result-formatter.js, test-filter.js)
618+
- 🔲 Introduce `CommandRunner` shared lifecycle (command files vary too much for a single pattern today — revisit once commands stabilize)
618619

619620
Eliminate the `*Data()` / `*()` dual-function pattern replicated across 19 modules. Every analysis module (queries, audit, batch, check, cochange, communities, complexity, cfg, dataflow, ast, flow, manifesto, owners, structure, triage, branch-compare, viewer) currently implements both data extraction AND CLI formatting.
620621

src/ast.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ import { buildExtensionSet } from './ast-analysis/shared.js';
1212
import { walkWithVisitors } from './ast-analysis/visitor.js';
1313
import { createAstStoreVisitor } from './ast-analysis/visitors/ast-store-visitor.js';
1414
import { openReadonlyOrFail } from './db.js';
15+
import { outputResult } from './infrastructure/result-formatter.js';
1516
import { debug } from './logger.js';
1617
import { paginateResult } from './paginate.js';
1718

18-
import { outputResult } from './result-formatter.js';
19-
2019
// ─── Constants ────────────────────────────────────────────────────────
2120

2221
export const AST_NODE_KINDS = ['call', 'new', 'string', 'regex', 'throw', 'await'];

src/audit.js

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
import path from 'node:path';
1010
import { loadConfig } from './config.js';
1111
import { openReadonlyOrFail } from './db.js';
12+
import { isTestFile } from './infrastructure/test-filter.js';
1213
import { RULE_DEFS } from './manifesto.js';
13-
import { explainData, kindIcon } from './queries.js';
14-
import { outputResult } from './result-formatter.js';
15-
import { isTestFile } from './test-filter.js';
14+
import { explainData } from './queries.js';
1615

1716
// ─── Threshold resolution ───────────────────────────────────────────
1817

@@ -336,87 +335,3 @@ function defaultHealth() {
336335
thresholdBreaches: [],
337336
};
338337
}
339-
340-
// ─── CLI formatter ──────────────────────────────────────────────────
341-
342-
export function audit(target, customDbPath, opts = {}) {
343-
const data = auditData(target, customDbPath, opts);
344-
345-
if (outputResult(data, null, opts)) return;
346-
347-
if (data.functions.length === 0) {
348-
console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
349-
return;
350-
}
351-
352-
console.log(`\n# Audit: ${target} (${data.kind})`);
353-
console.log(` ${data.functions.length} function(s) analyzed\n`);
354-
355-
for (const fn of data.functions) {
356-
const lineRange = fn.endLine ? `${fn.line}-${fn.endLine}` : `${fn.line}`;
357-
const roleTag = fn.role ? ` [${fn.role}]` : '';
358-
console.log(`## ${kindIcon(fn.kind)} ${fn.name} (${fn.kind})${roleTag}`);
359-
console.log(` ${fn.file}:${lineRange}${fn.lineCount ? ` (${fn.lineCount} lines)` : ''}`);
360-
if (fn.summary) console.log(` ${fn.summary}`);
361-
if (fn.signature) {
362-
if (fn.signature.params != null) console.log(` Parameters: (${fn.signature.params})`);
363-
if (fn.signature.returnType) console.log(` Returns: ${fn.signature.returnType}`);
364-
}
365-
366-
// Health metrics
367-
if (fn.health.cognitive != null) {
368-
console.log(`\n Health:`);
369-
console.log(
370-
` Cognitive: ${fn.health.cognitive} Cyclomatic: ${fn.health.cyclomatic} Nesting: ${fn.health.maxNesting}`,
371-
);
372-
console.log(` MI: ${fn.health.maintainabilityIndex}`);
373-
if (fn.health.halstead.volume) {
374-
console.log(
375-
` Halstead: vol=${fn.health.halstead.volume} diff=${fn.health.halstead.difficulty} effort=${fn.health.halstead.effort} bugs=${fn.health.halstead.bugs}`,
376-
);
377-
}
378-
if (fn.health.loc) {
379-
console.log(
380-
` LOC: ${fn.health.loc} SLOC: ${fn.health.sloc} Comments: ${fn.health.commentLines}`,
381-
);
382-
}
383-
}
384-
385-
// Threshold breaches
386-
if (fn.health.thresholdBreaches.length > 0) {
387-
console.log(`\n Threshold Breaches:`);
388-
for (const b of fn.health.thresholdBreaches) {
389-
const icon = b.level === 'fail' ? 'FAIL' : 'WARN';
390-
console.log(` [${icon}] ${b.metric}: ${b.value} >= ${b.threshold}`);
391-
}
392-
}
393-
394-
// Impact
395-
console.log(`\n Impact: ${fn.impact.totalDependents} transitive dependent(s)`);
396-
for (const [level, nodes] of Object.entries(fn.impact.levels)) {
397-
console.log(` Level ${level}: ${nodes.map((n) => n.name).join(', ')}`);
398-
}
399-
400-
// Call edges
401-
if (fn.callees.length > 0) {
402-
console.log(`\n Calls (${fn.callees.length}):`);
403-
for (const c of fn.callees) {
404-
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
405-
}
406-
}
407-
if (fn.callers.length > 0) {
408-
console.log(`\n Called by (${fn.callers.length}):`);
409-
for (const c of fn.callers) {
410-
console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
411-
}
412-
}
413-
if (fn.relatedTests.length > 0) {
414-
console.log(`\n Tests (${fn.relatedTests.length}):`);
415-
for (const t of fn.relatedTests) {
416-
console.log(` ${t.file}`);
417-
}
418-
}
419-
420-
console.log();
421-
}
422-
}

src/batch.js

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,6 @@ export function batchData(command, targets, customDbPath, opts = {}) {
8383
return { command, total: targets.length, succeeded, failed, results };
8484
}
8585

86-
/**
87-
* CLI wrapper — calls batchData and prints JSON to stdout.
88-
*/
89-
export function batch(command, targets, customDbPath, opts = {}) {
90-
const data = batchData(command, targets, customDbPath, opts);
91-
console.log(JSON.stringify(data, null, 2));
92-
}
93-
9486
/**
9587
* Expand comma-separated positional args into individual entries.
9688
* `['a,b', 'c']` → `['a', 'b', 'c']`.
@@ -161,20 +153,3 @@ export function multiBatchData(items, customDbPath, sharedOpts = {}) {
161153

162154
return { mode: 'multi', total: items.length, succeeded, failed, results };
163155
}
164-
165-
/**
166-
* CLI wrapper for batch-query — detects multi-command mode (objects with .command)
167-
* or falls back to single-command batchData (default: 'where').
168-
*/
169-
export function batchQuery(targets, customDbPath, opts = {}) {
170-
const { command: defaultCommand = 'where', ...rest } = opts;
171-
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
172-
173-
let data;
174-
if (isMulti) {
175-
data = multiBatchData(targets, customDbPath, rest);
176-
} else {
177-
data = batchData(defaultCommand, targets, customDbPath, rest);
178-
}
179-
console.log(JSON.stringify(data, null, 2));
180-
}

src/boundaries.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { isTestFile } from './infrastructure/test-filter.js';
12
import { debug } from './logger.js';
2-
import { isTestFile } from './test-filter.js';
33

44
// ─── Glob-to-Regex ───────────────────────────────────────────────────
55

src/branch-compare.js

Lines changed: 1 addition & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import os from 'node:os';
1212
import path from 'node:path';
1313
import Database from 'better-sqlite3';
1414
import { buildGraph } from './builder.js';
15+
import { isTestFile } from './infrastructure/test-filter.js';
1516
import { kindIcon } from './queries.js';
16-
import { outputResult } from './result-formatter.js';
17-
import { isTestFile } from './test-filter.js';
1817

1918
// ─── Git Helpers ────────────────────────────────────────────────────────
2019

@@ -477,97 +476,3 @@ export function branchCompareMermaid(data) {
477476

478477
return lines.join('\n');
479478
}
480-
481-
// ─── Text Formatting ────────────────────────────────────────────────────
482-
483-
function formatText(data) {
484-
if (data.error) return `Error: ${data.error}`;
485-
486-
const lines = [];
487-
const shortBase = data.baseSha.slice(0, 7);
488-
const shortTarget = data.targetSha.slice(0, 7);
489-
490-
lines.push(`branch-compare: ${data.baseRef}..${data.targetRef}`);
491-
lines.push(` Base: ${data.baseRef} (${shortBase})`);
492-
lines.push(` Target: ${data.targetRef} (${shortTarget})`);
493-
lines.push(` Files changed: ${data.changedFiles.length}`);
494-
495-
if (data.added.length > 0) {
496-
lines.push('');
497-
lines.push(` + Added (${data.added.length} symbol${data.added.length !== 1 ? 's' : ''}):`);
498-
for (const sym of data.added) {
499-
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
500-
}
501-
}
502-
503-
if (data.removed.length > 0) {
504-
lines.push('');
505-
lines.push(
506-
` - Removed (${data.removed.length} symbol${data.removed.length !== 1 ? 's' : ''}):`,
507-
);
508-
for (const sym of data.removed) {
509-
lines.push(` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.line}`);
510-
if (sym.impact && sym.impact.length > 0) {
511-
lines.push(
512-
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
513-
);
514-
}
515-
}
516-
}
517-
518-
if (data.changed.length > 0) {
519-
lines.push('');
520-
lines.push(
521-
` ~ Changed (${data.changed.length} symbol${data.changed.length !== 1 ? 's' : ''}):`,
522-
);
523-
for (const sym of data.changed) {
524-
const parts = [];
525-
if (sym.changes.lineCount !== 0) {
526-
parts.push(`lines: ${sym.base.lineCount} -> ${sym.target.lineCount}`);
527-
}
528-
if (sym.changes.fanIn !== 0) {
529-
parts.push(`fan_in: ${sym.base.fanIn} -> ${sym.target.fanIn}`);
530-
}
531-
if (sym.changes.fanOut !== 0) {
532-
parts.push(`fan_out: ${sym.base.fanOut} -> ${sym.target.fanOut}`);
533-
}
534-
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
535-
lines.push(
536-
` [${kindIcon(sym.kind)}] ${sym.name} -- ${sym.file}:${sym.base.line}${detail}`,
537-
);
538-
if (sym.impact && sym.impact.length > 0) {
539-
lines.push(
540-
` ^ ${sym.impact.length} transitive caller${sym.impact.length !== 1 ? 's' : ''} affected`,
541-
);
542-
}
543-
}
544-
}
545-
546-
const s = data.summary;
547-
lines.push('');
548-
lines.push(
549-
` Summary: +${s.added} added, -${s.removed} removed, ~${s.changed} changed` +
550-
` -> ${s.totalImpacted} caller${s.totalImpacted !== 1 ? 's' : ''} impacted` +
551-
(s.filesAffected > 0
552-
? ` across ${s.filesAffected} file${s.filesAffected !== 1 ? 's' : ''}`
553-
: ''),
554-
);
555-
556-
return lines.join('\n');
557-
}
558-
559-
// ─── CLI Display Function ───────────────────────────────────────────────
560-
561-
export async function branchCompare(baseRef, targetRef, opts = {}) {
562-
const data = await branchCompareData(baseRef, targetRef, opts);
563-
564-
if (opts.format === 'json') opts = { ...opts, json: true };
565-
if (outputResult(data, null, opts)) return;
566-
567-
if (opts.format === 'mermaid') {
568-
console.log(branchCompareMermaid(data));
569-
return;
570-
}
571-
572-
console.log(formatText(data));
573-
}

src/cfg.js

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ import {
1616
import { walkWithVisitors } from './ast-analysis/visitor.js';
1717
import { createCfgVisitor } from './ast-analysis/visitors/cfg-visitor.js';
1818
import { openReadonlyOrFail } from './db.js';
19+
import { isTestFile } from './infrastructure/test-filter.js';
1920
import { info } from './logger.js';
2021
import { paginateResult } from './paginate.js';
2122

22-
import { outputResult } from './result-formatter.js';
23-
import { isTestFile } from './test-filter.js';
24-
2523
// Re-export for backward compatibility
2624
export { CFG_RULES };
2725
export { _makeCfgRules as makeCfgRules };
@@ -472,58 +470,3 @@ function edgeStyle(kind) {
472470
if (kind === 'continue') return ', color=blue, style=dashed';
473471
return '';
474472
}
475-
476-
// ─── CLI Printer ────────────────────────────────────────────────────────
477-
478-
/**
479-
* CLI display for cfg command.
480-
*/
481-
export function cfg(name, customDbPath, opts = {}) {
482-
const data = cfgData(name, customDbPath, opts);
483-
484-
if (outputResult(data, 'results', opts)) return;
485-
486-
if (data.warning) {
487-
console.log(`\u26A0 ${data.warning}`);
488-
return;
489-
}
490-
if (data.results.length === 0) {
491-
console.log(`No symbols matching "${name}".`);
492-
return;
493-
}
494-
495-
const format = opts.format || 'text';
496-
if (format === 'dot') {
497-
console.log(cfgToDOT(data));
498-
return;
499-
}
500-
if (format === 'mermaid') {
501-
console.log(cfgToMermaid(data));
502-
return;
503-
}
504-
505-
// Text format
506-
for (const r of data.results) {
507-
console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
508-
console.log('\u2500'.repeat(60));
509-
console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
510-
511-
if (r.blocks.length > 0) {
512-
console.log('\n Blocks:');
513-
for (const b of r.blocks) {
514-
const loc = b.startLine
515-
? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
516-
: '';
517-
const label = b.label ? ` (${b.label})` : '';
518-
console.log(` [${b.index}] ${b.type}${label}${loc}`);
519-
}
520-
}
521-
522-
if (r.edges.length > 0) {
523-
console.log('\n Edges:');
524-
for (const e of r.edges) {
525-
console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
526-
}
527-
}
528-
}
529-
}

0 commit comments

Comments
 (0)