Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,91 @@ export function batch(command, targets, customDbPath, opts = {}) {
const data = batchData(command, targets, customDbPath, opts);
console.log(JSON.stringify(data, null, 2));
}

/**
* Expand comma-separated positional args into individual entries.
* `['a,b', 'c']` → `['a', 'b', 'c']`.
* Trims whitespace, filters empties. Passes through object items unchanged.
*
* @param {Array<string|object>} targets
* @returns {Array<string|object>}
*/
export function splitTargets(targets) {
const out = [];
for (const item of targets) {
if (typeof item !== 'string') {
out.push(item);
continue;
}
for (const part of item.split(',')) {
const trimmed = part.trim();
if (trimmed) out.push(trimmed);
}
}
return out;
}

/**
* Multi-command batch orchestration — run different commands per target.
*
* @param {Array<{command: string, target: string, opts?: object}>} items
* @param {string} [customDbPath]
* @param {object} [sharedOpts] - Default opts merged under per-item opts
* @returns {{ mode: 'multi', total: number, succeeded: number, failed: number, results: object[] }}
*/
export function multiBatchData(items, customDbPath, sharedOpts = {}) {
const results = [];
let succeeded = 0;
let failed = 0;

for (const item of items) {
const { command, target, opts: itemOpts } = item;
const entry = BATCH_COMMANDS[command];

if (!entry) {
results.push({
command,
target,
ok: false,
error: `Unknown batch command "${command}". Valid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
});
failed++;
continue;
}

const merged = { ...sharedOpts, ...itemOpts };

try {
let data;
if (entry.sig === 'dbOnly') {
data = entry.fn(customDbPath, { ...merged, target });
} else {
data = entry.fn(target, customDbPath, merged);
}
results.push({ command, target, ok: true, data });
succeeded++;
} catch (err) {
results.push({ command, target, ok: false, error: err.message });
failed++;
}
}

return { mode: 'multi', total: items.length, succeeded, failed, results };
}

/**
* CLI wrapper for batch-query — detects multi-command mode (objects with .command)
* or falls back to single-command batchData (default: 'where').
*/
export function batchQuery(targets, customDbPath, opts = {}) {
const { command: defaultCommand = 'where', ...rest } = opts;
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;

let data;
if (isMulti) {
data = multiBatchData(targets, customDbPath, rest);
} else {
data = batchData(defaultCommand, targets, customDbPath, rest);
}
console.log(JSON.stringify(data, null, 2));
}
96 changes: 83 additions & 13 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { Command } from 'commander';
import { audit } from './audit.js';
import { BATCH_COMMANDS, batch } from './batch.js';
import { BATCH_COMMANDS, batch, batchQuery, multiBatchData, splitTargets } from './batch.js';
import { buildGraph } from './builder.js';
import { loadConfig } from './config.js';
import { findCycles, formatCycles } from './cycles.js';
Expand Down Expand Up @@ -1287,20 +1287,89 @@ program
}

let targets;
if (opts.fromFile) {
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
if (raw.startsWith('[')) {
targets = JSON.parse(raw);
try {
if (opts.fromFile) {
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
if (raw.startsWith('[')) {
targets = JSON.parse(raw);
} else {
targets = raw.split(/\r?\n/).filter(Boolean);
}
} else if (opts.stdin) {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8').trim();
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
} else {
targets = raw.split(/\r?\n/).filter(Boolean);
targets = splitTargets(positionalTargets);
}
} else if (opts.stdin) {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8').trim();
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
} catch (err) {
console.error(`Failed to parse targets: ${err.message}`);
process.exit(1);
}

if (!targets || targets.length === 0) {
console.error('No targets provided. Pass targets as arguments, --from-file, or --stdin.');
process.exit(1);
}

const batchOpts = {
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
file: opts.file,
kind: opts.kind,
noTests: resolveNoTests(opts),
};

// Multi-command mode: items from --from-file / --stdin may be objects with { command, target }
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
if (isMulti) {
const data = multiBatchData(targets, opts.db, batchOpts);
console.log(JSON.stringify(data, null, 2));
} else {
targets = positionalTargets;
batch(command, targets, opts.db, batchOpts);
}
});

program
.command('batch-query [targets...]')
.description(
`Batch symbol lookup — resolve multiple references in one call.\nDefaults to 'where' command. Accepts comma-separated targets.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
)
.option('-d, --db <path>', 'Path to graph.db')
.option('-c, --command <cmd>', 'Query command to run (default: where)', 'where')
.option('--from-file <path>', 'Read targets from file (JSON array or newline-delimited)')
.option('--stdin', 'Read targets from stdin (JSON array)')
.option('--depth <n>', 'Traversal depth passed to underlying command')
.option('-f, --file <path>', 'Scope to file (partial match)')
.option('-k, --kind <kind>', 'Filter by symbol kind')
.option('-T, --no-tests', 'Exclude test/spec files from results')
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
.action(async (positionalTargets, opts) => {
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
process.exit(1);
}

let targets;
try {
if (opts.fromFile) {
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
if (raw.startsWith('[')) {
targets = JSON.parse(raw);
} else {
targets = raw.split(/\r?\n/).filter(Boolean);
}
} else if (opts.stdin) {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const raw = Buffer.concat(chunks).toString('utf-8').trim();
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
} else {
targets = splitTargets(positionalTargets);
}
} catch (err) {
console.error(`Failed to parse targets: ${err.message}`);
process.exit(1);
}

if (!targets || targets.length === 0) {
Expand All @@ -1309,13 +1378,14 @@ program
}

const batchOpts = {
command: opts.command,
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
file: opts.file,
kind: opts.kind,
noTests: resolveNoTests(opts),
};

batch(command, targets, opts.db, batchOpts);
batchQuery(targets, opts.db, batchOpts);
});

program.parse();
9 changes: 8 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
// Audit (composite report)
export { audit, auditData } from './audit.js';
// Batch querying
export { BATCH_COMMANDS, batch, batchData } from './batch.js';
export {
BATCH_COMMANDS,
batch,
batchData,
batchQuery,
multiBatchData,
splitTargets,
} from './batch.js';
// Architecture boundary rules
export { evaluateBoundaries, PRESETS, validateBoundaryConfig } from './boundaries.js';
// Branch comparison
Expand Down
Loading