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
9 changes: 5 additions & 4 deletions src/ast-analysis/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Shared utilities for AST analysis modules (complexity, CFG, dataflow, AST nodes).
*/

import { ConfigError } from '../errors.js';
import { LANGUAGE_REGISTRY } from '../parser.js';

// ─── Generic Rule Factory ─────────────────────────────────────────────────
Expand All @@ -18,7 +19,7 @@ export function makeRules(defaults, overrides, label) {
const validKeys = new Set(Object.keys(defaults));
for (const key of Object.keys(overrides)) {
if (!validKeys.has(key)) {
throw new Error(`${label} rules: unknown key "${key}"`);
throw new ConfigError(`${label} rules: unknown key "${key}"`);
}
}
return { ...defaults, ...overrides };
Expand Down Expand Up @@ -61,10 +62,10 @@ export const CFG_DEFAULTS = {
export function makeCfgRules(overrides) {
const rules = makeRules(CFG_DEFAULTS, overrides, 'CFG');
if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
throw new Error('CFG rules: functionNodes must be a non-empty Set');
throw new ConfigError('CFG rules: functionNodes must be a non-empty Set');
}
if (!(rules.forNodes instanceof Set)) {
throw new Error('CFG rules: forNodes must be a Set');
throw new ConfigError('CFG rules: forNodes must be a Set');
}
return rules;
}
Expand Down Expand Up @@ -136,7 +137,7 @@ export const DATAFLOW_DEFAULTS = {
export function makeDataflowRules(overrides) {
const rules = makeRules(DATAFLOW_DEFAULTS, overrides, 'Dataflow');
if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
throw new Error('Dataflow rules: functionNodes must be a non-empty Set');
throw new ConfigError('Dataflow rules: functionNodes must be a non-empty Set');
}
return rules;
}
Expand Down
3 changes: 2 additions & 1 deletion src/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { complexityData } from './complexity.js';
import { dataflowData } from './dataflow.js';
import { ConfigError } from './errors.js';
import { flowData } from './flow.js';
import {
contextData,
Expand Down Expand Up @@ -53,7 +54,7 @@ export const BATCH_COMMANDS = {
export function batchData(command, targets, customDbPath, opts = {}) {
const entry = BATCH_COMMANDS[command];
if (!entry) {
throw new Error(
throw new ConfigError(
`Unknown batch command "${command}". Valid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
#!/usr/bin/env node

import { run } from './cli/index.js';
import { CodegraphError } from './errors.js';

run().catch((err) => {
console.error(`codegraph: fatal error — ${err.message || err}`);
if (err instanceof CodegraphError) {
console.error(`codegraph [${err.code}]: ${err.message}`);
if (err.file) console.error(` file: ${err.file}`);
} else {
console.error(`codegraph: fatal error — ${err.message || err}`);
}
process.exit(1);
});
5 changes: 3 additions & 2 deletions src/cli/commands/ast.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ConfigError } from '../../errors.js';

export const command = {
name: 'ast [pattern]',
description: 'Search stored AST nodes (calls, new, string, regex, throw, await) by pattern',
Expand All @@ -9,8 +11,7 @@ export const command = {
async execute([pattern], opts, ctx) {
const { AST_NODE_KINDS, astQuery } = await import('../../ast.js');
if (opts.kind && !AST_NODE_KINDS.includes(opts.kind)) {
console.error(`Invalid AST kind "${opts.kind}". Valid: ${AST_NODE_KINDS.join(', ')}`);
process.exit(1);
throw new ConfigError(`Invalid AST kind "${opts.kind}". Valid: ${AST_NODE_KINDS.join(', ')}`);
}
astQuery(pattern, opts.db, {
kind: opts.kind,
Expand Down
9 changes: 5 additions & 4 deletions src/cli/commands/batch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import { BATCH_COMMANDS, multiBatchData, splitTargets } from '../../batch.js';
import { batch } from '../../commands/batch.js';
import { ConfigError } from '../../errors.js';
import { EVERY_SYMBOL_KIND } from '../../queries.js';

export const command = {
Expand Down Expand Up @@ -40,13 +41,13 @@ export const command = {
targets = splitTargets(positionalTargets);
}
} catch (err) {
console.error(`Failed to parse targets: ${err.message}`);
process.exit(1);
throw new ConfigError(`Failed to parse targets: ${err.message}`, { cause: err });
}

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

const batchOpts = {
Expand Down
11 changes: 7 additions & 4 deletions src/cli/commands/check.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConfigError } from '../../errors.js';
import { EVERY_SYMBOL_KIND } from '../../queries.js';

export const command = {
Expand Down Expand Up @@ -27,8 +28,9 @@ export const command = {

if (!isDiffMode && !opts.rules) {
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
process.exit(1);
throw new ConfigError(
`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`,
);
}
const { manifesto } = await import('../../commands/manifesto.js');
manifesto(opts.db, {
Expand Down Expand Up @@ -58,8 +60,9 @@ export const command = {

if (opts.rules) {
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
process.exit(1);
throw new ConfigError(
`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`,
);
}
const { manifesto } = await import('../../commands/manifesto.js');
manifesto(opts.db, {
Expand Down
5 changes: 3 additions & 2 deletions src/cli/commands/co-change.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { AnalysisError } from '../../errors.js';

export const command = {
name: 'co-change [file]',
description:
Expand Down Expand Up @@ -32,8 +34,7 @@ export const command = {
if (opts.json) {
console.log(JSON.stringify(result, null, 2));
} else if (result.error) {
console.error(result.error);
process.exit(1);
throw new AnalysisError(result.error);
} else {
console.log(
`\nCo-change analysis complete: ${result.pairsFound} pairs from ${result.commitsScanned} commits (since: ${result.since})\n`,
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/registry.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import { ConfigError } from '../../errors.js';
import {
listRepos,
pruneRegistry,
Expand Down Expand Up @@ -54,8 +55,7 @@ export const command = {
if (removed) {
console.log(`Removed "${name}" from registry.`);
} else {
console.error(`Repository "${name}" not found in registry.`);
process.exit(1);
throw new ConfigError(`Repository "${name}" not found in registry.`);
}
},
},
Expand Down
54 changes: 17 additions & 37 deletions src/cli/commands/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,17 @@ export const command = {
['--force', 'Overwrite existing snapshot'],
],
execute([name], opts, ctx) {
try {
const result = snapshotSave(name, { dbPath: opts.db, force: opts.force });
console.log(`Snapshot saved: ${result.name} (${ctx.formatSize(result.size)})`);
} catch (err) {
console.error(err.message);
process.exit(1);
}
const result = snapshotSave(name, { dbPath: opts.db, force: opts.force });
console.log(`Snapshot saved: ${result.name} (${ctx.formatSize(result.size)})`);
},
},
{
name: 'restore <name>',
description: 'Restore a snapshot over the current graph database',
options: [['-d, --db <path>', 'Path to graph.db']],
execute([name], opts) {
try {
snapshotRestore(name, { dbPath: opts.db });
console.log(`Snapshot "${name}" restored.`);
} catch (err) {
console.error(err.message);
process.exit(1);
}
snapshotRestore(name, { dbPath: opts.db });
console.log(`Snapshot "${name}" restored.`);
},
},
{
Expand All @@ -43,23 +33,18 @@ export const command = {
['-j, --json', 'Output as JSON'],
],
execute(_args, opts, ctx) {
try {
const snapshots = snapshotList({ dbPath: opts.db });
if (opts.json) {
console.log(JSON.stringify(snapshots, null, 2));
} else if (snapshots.length === 0) {
console.log('No snapshots found.');
} else {
console.log(`Snapshots (${snapshots.length}):\n`);
for (const s of snapshots) {
console.log(
` ${s.name.padEnd(30)} ${ctx.formatSize(s.size).padStart(10)} ${s.createdAt.toISOString()}`,
);
}
const snapshots = snapshotList({ dbPath: opts.db });
if (opts.json) {
console.log(JSON.stringify(snapshots, null, 2));
} else if (snapshots.length === 0) {
console.log('No snapshots found.');
} else {
console.log(`Snapshots (${snapshots.length}):\n`);
for (const s of snapshots) {
console.log(
` ${s.name.padEnd(30)} ${ctx.formatSize(s.size).padStart(10)} ${s.createdAt.toISOString()}`,
);
}
} catch (err) {
console.error(err.message);
process.exit(1);
}
},
},
Expand All @@ -68,13 +53,8 @@ export const command = {
description: 'Delete a saved snapshot',
options: [['-d, --db <path>', 'Path to graph.db']],
execute([name], opts) {
try {
snapshotDelete(name, { dbPath: opts.db });
console.log(`Snapshot "${name}" deleted.`);
} catch (err) {
console.error(err.message);
process.exit(1);
}
snapshotDelete(name, { dbPath: opts.db });
console.log(`Snapshot "${name}" deleted.`);
},
},
],
Expand Down
12 changes: 5 additions & 7 deletions src/cli/commands/triage.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ConfigError } from '../../errors.js';
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../queries.js';

export const command = {
Expand Down Expand Up @@ -46,20 +47,17 @@ export const command = {
}

if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
process.exit(1);
throw new ConfigError(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
}
if (opts.role && !VALID_ROLES.includes(opts.role)) {
console.error(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
process.exit(1);
throw new ConfigError(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
}
let weights;
if (opts.weights) {
try {
weights = JSON.parse(opts.weights);
} catch {
console.error('Invalid --weights JSON');
process.exit(1);
} catch (err) {
throw new ConfigError('Invalid --weights JSON', { cause: err });
}
}
const { triage } = await import('../../commands/triage.js');
Expand Down
6 changes: 3 additions & 3 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { Command } from 'commander';
import { ConfigError } from '../errors.js';
import { setVerbose } from '../logger.js';
import { checkForUpdates, printUpdateNotification } from '../update-check.js';
import { applyQueryOpts, config, formatSize, resolveNoTests } from './shared/options.js';
Expand Down Expand Up @@ -68,8 +69,7 @@ function registerCommand(parent, def) {
if (def.validate) {
const err = def.validate(args, opts, ctx);
if (err) {
console.error(err);
process.exit(1);
throw new ConfigError(err);
}
}

Expand Down Expand Up @@ -112,7 +112,7 @@ async function discoverCommands() {

export async function run() {
await discoverCommands();
program.parse();
await program.parseAsync();
}

export { program, registerCommand, ctx };
10 changes: 5 additions & 5 deletions src/commands/check.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { checkData } from '../check.js';
import { AnalysisError } from '../errors.js';
import { outputResult } from '../infrastructure/result-formatter.js';

/**
* CLI formatter — prints check results and exits with code 1 on failure.
* CLI formatter — prints check results and sets exitCode 1 on failure.
*/
export function check(customDbPath, opts = {}) {
const data = checkData(customDbPath, opts);

if (data.error) {
console.error(data.error);
process.exit(1);
throw new AnalysisError(data.error);
}

if (outputResult(data, null, opts)) {
if (!data.passed) process.exit(1);
if (!data.passed) process.exitCode = 1;
return;
}

Expand Down Expand Up @@ -77,6 +77,6 @@ export function check(customDbPath, opts = {}) {
console.log(`\n ${s.total} predicates | ${s.passed} passed | ${s.failed} failed\n`);

if (!data.passed) {
process.exit(1);
process.exitCode = 1;
}
}
6 changes: 3 additions & 3 deletions src/commands/manifesto.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { outputResult } from '../infrastructure/result-formatter.js';
import { manifestoData } from '../manifesto.js';

/**
* CLI formatter — prints manifesto results and exits with code 1 on failure.
* CLI formatter — prints manifesto results and sets exitCode 1 on failure.
*/
export function manifesto(customDbPath, opts = {}) {
const data = manifestoData(customDbPath, opts);

if (outputResult(data, 'violations', opts)) {
if (!data.passed) process.exit(1);
if (!data.passed) process.exitCode = 1;
return;
}

Expand Down Expand Up @@ -72,6 +72,6 @@ export function manifesto(customDbPath, opts = {}) {
console.log();

if (!data.passed) {
process.exit(1);
process.exitCode = 1;
}
}
8 changes: 4 additions & 4 deletions src/db/connection.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import Database from 'better-sqlite3';
import { DbError } from '../errors.js';
import { warn } from '../logger.js';

function isProcessAlive(pid) {
Expand Down Expand Up @@ -78,11 +79,10 @@ export function findDbPath(customPath) {
export function openReadonlyOrFail(customPath) {
const dbPath = findDbPath(customPath);
if (!fs.existsSync(dbPath)) {
console.error(
`No codegraph database found at ${dbPath}.\n` +
`Run "codegraph build" first to analyze your codebase.`,
throw new DbError(
`No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
{ file: dbPath },
);
process.exit(1);
}
return new Database(dbPath, { readonly: true });
}
Loading
Loading