Skip to content

Commit ee2bb45

Browse files
authored
feat: CLI composability helpers and universal output formatter (#461)
* feat: add cli composability helpers — openGraph helper, resolveQueryOpts, universal output formatter Add openGraph(opts) helper to eliminate DB-open/close boilerplate in cycles, export, and plot commands. Add resolveQueryOpts(opts) to extract the 5 repeated option fields (noTests, json, ndjson, limit, offset) plus new table/csv into one call. Refactor 20 command files to use the spread pattern. Extend outputResult() with --table (auto-column aligned table) and --csv (RFC 4180 with nested object flattening) output formats. Add --table and --csv options to applyQueryOpts(). Impact: 31 functions changed, 71 affected * fix: escapeCsv RFC 4180 compliance and column detection from flattened items Impact: 3 functions changed, 2 affected * fix: use reduce instead of spread for column width to avoid stack overflow on large sets Impact: 1 functions changed, 1 affected * fix: remove redundant double-flattening and tone down RFC 4180 claim Impact: 2 functions changed, 1 affected * fix: guard flattenObject against non-POJO objects like Date and RegExp Impact: 1 functions changed, 3 affected * fix: exclude booleans from numeric column detection and add try/finally for close() - result-formatter: skip boolean values in numeric column heuristic (Number(true) === 1 was misclassifying boolean columns as numeric) - cycles/export/plot commands: wrap DB operations in try/finally to ensure close() is called even if an export function throws Impact: 4 functions changed, 1 affected * fix: guard top-level arrays in flattenObject and emit CSV header for empty sets Impact: 2 functions changed, 1 affected * fix: emit table header for empty result sets consistent with CSV path Impact: 1 functions changed, 1 affected * fix: address Greptile review feedback on PR #461 - Make printCsv/printAutoTable return false when items is not an array, and propagate that in outputResult so callers see the failure - Remove config from resolveQueryOpts spread; audit and check now import config directly, other consumers use their existing loadConfig fallback - Restructure export neo4j branch to avoid return inside try/finally; use output===undefined guard after the block instead * fix: guard vacuous isNumeric on empty result sets * fix: document dot-notation key collision assumption in flattenObject * fix: add defensive guard for undefined plot HTML output * refactor: extract prepareFlatItems helper to deduplicate CSV/table logic Extracts the identical flat-items mapping and column-derivation block from printCsv and printAutoTable into a shared prepareFlatItems helper, so future format additions or bug fixes only need one code path.
1 parent 74aefd1 commit ee2bb45

26 files changed

Lines changed: 283 additions & 178 deletions

src/cli/commands/ast.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ export const command = {
1616
astQuery(pattern, opts.db, {
1717
kind: opts.kind,
1818
file: opts.file,
19-
noTests: ctx.resolveNoTests(opts),
20-
json: opts.json,
21-
ndjson: opts.ndjson,
22-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
23-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
19+
...ctx.resolveQueryOpts(opts),
2420
});
2521
},
2622
};

src/cli/commands/audit.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
22
import { audit } from '../../presentation/audit.js';
33
import { explain } from '../../presentation/queries-cli.js';
4+
import { config } from '../shared/options.js';
45

56
export const command = {
67
name: 'audit <target>',
@@ -24,24 +25,21 @@ export const command = {
2425
}
2526
},
2627
execute([target], opts, ctx) {
28+
const qOpts = ctx.resolveQueryOpts(opts);
2729
if (opts.quick) {
2830
explain(target, opts.db, {
2931
depth: parseInt(opts.depth, 10),
30-
noTests: ctx.resolveNoTests(opts),
31-
json: opts.json,
32-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
33-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
34-
ndjson: opts.ndjson,
32+
...qOpts,
3533
});
3634
return;
3735
}
3836
audit(target, opts.db, {
3937
depth: parseInt(opts.depth, 10),
4038
file: opts.file,
4139
kind: opts.kind,
42-
noTests: ctx.resolveNoTests(opts),
43-
json: opts.json,
44-
config: ctx.config,
40+
noTests: qOpts.noTests,
41+
json: qOpts.json,
42+
config,
4543
});
4644
},
4745
};

src/cli/commands/cfg.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ export const command = {
2020
format: opts.format,
2121
file: opts.file,
2222
kind: opts.kind,
23-
noTests: ctx.resolveNoTests(opts),
24-
json: opts.json,
25-
ndjson: opts.ndjson,
26-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
27-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
23+
...ctx.resolveQueryOpts(opts),
2824
});
2925
},
3026
};

src/cli/commands/check.js

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
22
import { ConfigError } from '../../shared/errors.js';
3+
import { config } from '../shared/options.js';
34

45
export const command = {
56
name: 'check [ref]',
@@ -25,6 +26,7 @@ export const command = {
2526
],
2627
async execute([ref], opts, ctx) {
2728
const isDiffMode = ref || opts.staged;
29+
const qOpts = ctx.resolveQueryOpts(opts);
2830

2931
if (!isDiffMode && !opts.rules) {
3032
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
@@ -36,12 +38,7 @@ export const command = {
3638
manifesto(opts.db, {
3739
file: opts.file,
3840
kind: opts.kind,
39-
noTests: ctx.resolveNoTests(opts),
40-
json: opts.json,
41-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
42-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
43-
ndjson: opts.ndjson,
44-
config: ctx.config,
41+
...qOpts,
4542
});
4643
return;
4744
}
@@ -55,9 +52,9 @@ export const command = {
5552
signatures: opts.signatures || undefined,
5653
boundaries: opts.boundaries || undefined,
5754
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
58-
noTests: ctx.resolveNoTests(opts),
59-
json: opts.json,
60-
config: ctx.config,
55+
noTests: qOpts.noTests,
56+
json: qOpts.json,
57+
config,
6158
});
6259

6360
if (opts.rules) {
@@ -70,12 +67,7 @@ export const command = {
7067
manifesto(opts.db, {
7168
file: opts.file,
7269
kind: opts.kind,
73-
noTests: ctx.resolveNoTests(opts),
74-
json: opts.json,
75-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
76-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
77-
ndjson: opts.ndjson,
78-
config: ctx.config,
70+
...qOpts,
7971
});
8072
}
8173
},

src/cli/commands/children.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ export const command = {
2222
children(name, opts.db, {
2323
file: opts.file,
2424
kind: opts.kind,
25-
noTests: ctx.resolveNoTests(opts),
26-
json: opts.json,
27-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
28-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
25+
...ctx.resolveQueryOpts(opts),
2926
});
3027
},
3128
};

src/cli/commands/communities.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ export const command = {
1313
functions: opts.functions,
1414
resolution: parseFloat(opts.resolution),
1515
drift: opts.drift,
16-
noTests: ctx.resolveNoTests(opts),
17-
json: opts.json,
18-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
19-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
20-
ndjson: opts.ndjson,
16+
...ctx.resolveQueryOpts(opts),
2117
});
2218
},
2319
};

src/cli/commands/context.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,8 @@ export const command = {
2323
file: opts.file,
2424
kind: opts.kind,
2525
noSource: !opts.source,
26-
noTests: ctx.resolveNoTests(opts),
2726
includeTests: opts.withTestSource,
28-
json: opts.json,
29-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
30-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
31-
ndjson: opts.ndjson,
27+
...ctx.resolveQueryOpts(opts),
3228
});
3329
},
3430
};

src/cli/commands/cycles.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { openReadonlyOrFail } from '../../db/index.js';
21
import { findCycles, formatCycles } from '../../domain/graph/cycles.js';
2+
import { openGraph } from '../shared/open-graph.js';
33

44
export const command = {
55
name: 'cycles',
@@ -12,12 +12,16 @@ export const command = {
1212
['-j, --json', 'Output as JSON'],
1313
],
1414
execute(_args, opts, ctx) {
15-
const db = openReadonlyOrFail(opts.db);
16-
const cycles = findCycles(db, {
17-
fileLevel: !opts.functions,
18-
noTests: ctx.resolveNoTests(opts),
19-
});
20-
db.close();
15+
const { db, close } = openGraph(opts);
16+
let cycles;
17+
try {
18+
cycles = findCycles(db, {
19+
fileLevel: !opts.functions,
20+
noTests: ctx.resolveNoTests(opts),
21+
});
22+
} finally {
23+
close();
24+
}
2125

2226
if (opts.json) {
2327
console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2));

src/cli/commands/dataflow.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,9 @@ export const command = {
2020
dataflow(name, opts.db, {
2121
file: opts.file,
2222
kind: opts.kind,
23-
noTests: ctx.resolveNoTests(opts),
24-
json: opts.json,
25-
ndjson: opts.ndjson,
26-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
27-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
2823
impact: opts.impact,
2924
depth: parseInt(opts.depth, 10),
25+
...ctx.resolveQueryOpts(opts),
3026
});
3127
},
3228
};

src/cli/commands/deps.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ export const command = {
66
queryOpts: true,
77
execute([file], opts, ctx) {
88
fileDeps(file, opts.db, {
9-
noTests: ctx.resolveNoTests(opts),
10-
json: opts.json,
11-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
12-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
13-
ndjson: opts.ndjson,
9+
...ctx.resolveQueryOpts(opts),
1410
});
1511
},
1612
};

0 commit comments

Comments
 (0)