@elkindev/dbgraph 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1827 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DBGRAPH_VERSION,
4
+ NODE_KINDS,
5
+ capabilitiesFor,
6
+ formatDoctor,
7
+ formatExplore,
8
+ formatObject,
9
+ formatOutcome,
10
+ formatPrecheck,
11
+ getNeighbors,
12
+ noopLogger,
13
+ normalizeCatalog,
14
+ openConnections,
15
+ resolveProfile,
16
+ runPrecheck,
17
+ search
18
+ } from "./chunk-PQBJ4PR5.js";
19
+ import "./chunk-JN26WCE7.js";
20
+ import {
21
+ ConfigError,
22
+ ConnectionError,
23
+ ConnectivityUnavailableError,
24
+ DbgraphError,
25
+ NotFoundError,
26
+ PermissionError,
27
+ UnsupportedDialectError
28
+ } from "./chunk-TH4OVS4W.js";
29
+ import "./chunk-CK6XTW3E.js";
30
+ import "./chunk-2ESYSVXG.js";
31
+
32
+ // src/cli/parse/args.ts
33
+ var BOOLEAN_LONG_FLAGS = /* @__PURE__ */ new Set(["json", "full", "last", "quiet", "project"]);
34
+ function parseArgv(argv) {
35
+ if (argv.length === 0) {
36
+ return { command: "", positionals: [], flags: {} };
37
+ }
38
+ const [commandToken, ...rest] = argv;
39
+ const command = commandToken ?? "";
40
+ const positionals = [];
41
+ const flags = {};
42
+ let i = 0;
43
+ while (i < rest.length) {
44
+ const token = rest[i];
45
+ if (token === void 0) {
46
+ i++;
47
+ continue;
48
+ }
49
+ if (token.startsWith("--")) {
50
+ const withoutDashes = token.slice(2);
51
+ const eqIdx = withoutDashes.indexOf("=");
52
+ if (eqIdx !== -1) {
53
+ const key = withoutDashes.slice(0, eqIdx);
54
+ const value = withoutDashes.slice(eqIdx + 1);
55
+ flags[key] = value;
56
+ } else if (BOOLEAN_LONG_FLAGS.has(withoutDashes)) {
57
+ flags[withoutDashes] = true;
58
+ } else {
59
+ const next = rest[i + 1];
60
+ if (next !== void 0 && !next.startsWith("-")) {
61
+ flags[withoutDashes] = next;
62
+ i++;
63
+ } else {
64
+ flags[withoutDashes] = true;
65
+ }
66
+ }
67
+ } else if (token.startsWith("-") && token.length === 2) {
68
+ const key = token.slice(1);
69
+ flags[key] = true;
70
+ } else {
71
+ positionals.push(token);
72
+ }
73
+ i++;
74
+ }
75
+ return { command, positionals, flags };
76
+ }
77
+
78
+ // src/cli/parse/detail.ts
79
+ function parseDetail(raw) {
80
+ if (raw === void 0) {
81
+ return "normal";
82
+ }
83
+ if (raw === "brief" || raw === "normal" || raw === "full") {
84
+ return raw;
85
+ }
86
+ throw new ConfigError(
87
+ `explore: "detail" must be one of brief|normal|full (got "${String(raw)}")`
88
+ );
89
+ }
90
+
91
+ // src/cli/commands/init.ts
92
+ import { writeFileSync, readFileSync, existsSync, appendFileSync } from "fs";
93
+ import { join } from "path";
94
+
95
+ // src/cli/config/build-config.ts
96
+ var ENV_REF_RE = /^\$\{env:[A-Z_][A-Z0-9_]*\}$/;
97
+ function isEnvRef(value) {
98
+ return ENV_REF_RE.test(value);
99
+ }
100
+ function requireEnvRef(field, value) {
101
+ if (!isEnvRef(value)) {
102
+ throw new ConfigError(
103
+ `Config: field "${field}" must be a \${env:VAR} reference, not a literal value. Use a generic variable name such as DBGRAPH_DB_${field.toUpperCase()}.`
104
+ );
105
+ }
106
+ }
107
+ function buildConfig(input) {
108
+ switch (input.dialect) {
109
+ case "sqlite": {
110
+ const source = { file: input.file };
111
+ const base = input.driver !== void 0 ? { dialect: "sqlite", source, driver: input.driver } : { dialect: "sqlite", source };
112
+ return base;
113
+ }
114
+ case "mssql": {
115
+ requireEnvRef("server", input.server);
116
+ requireEnvRef("database", input.database);
117
+ requireEnvRef("user", input.user);
118
+ requireEnvRef("password", input.password);
119
+ if (input.port !== void 0) requireEnvRef("port", input.port);
120
+ if (input.domain !== void 0) requireEnvRef("domain", input.domain);
121
+ const source = {
122
+ server: input.server,
123
+ database: input.database,
124
+ user: input.user,
125
+ password: input.password
126
+ };
127
+ if (input.port !== void 0) source.port = input.port;
128
+ if (input.domain !== void 0) source.domain = input.domain;
129
+ const cfg = { dialect: "mssql", source };
130
+ return cfg;
131
+ }
132
+ }
133
+ }
134
+ function writeConfig(cfg) {
135
+ const ordered = {
136
+ dialect: cfg.dialect,
137
+ source: buildOrderedSource(cfg)
138
+ };
139
+ if (cfg.levels !== void 0) {
140
+ ordered["levels"] = cfg.levels;
141
+ }
142
+ if (cfg.dialect === "sqlite" && cfg.driver !== void 0) {
143
+ ordered["driver"] = cfg.driver;
144
+ }
145
+ return JSON.stringify(ordered, null, 2) + "\n";
146
+ }
147
+ function buildOrderedSource(cfg) {
148
+ if (cfg.dialect === "sqlite") {
149
+ return { file: cfg.source.file };
150
+ }
151
+ if (cfg.dialect === "pg") {
152
+ const src2 = cfg.source;
153
+ const ordered2 = {
154
+ host: src2.host,
155
+ database: src2.database,
156
+ user: src2.user,
157
+ password: src2.password
158
+ };
159
+ if (src2.port !== void 0) ordered2["port"] = src2.port;
160
+ if (src2.ssl !== void 0) ordered2["ssl"] = src2.ssl;
161
+ if (src2.schema !== void 0) ordered2["schema"] = src2.schema;
162
+ return ordered2;
163
+ }
164
+ if (cfg.dialect === "mysql") {
165
+ const src2 = cfg.source;
166
+ const ordered2 = {
167
+ host: src2.host,
168
+ database: src2.database,
169
+ user: src2.user,
170
+ password: src2.password
171
+ };
172
+ if (src2.port !== void 0) ordered2["port"] = src2.port;
173
+ if (src2.ssl !== void 0) ordered2["ssl"] = src2.ssl;
174
+ return ordered2;
175
+ }
176
+ if (cfg.dialect === "mongodb") {
177
+ const src2 = cfg.source;
178
+ const ordered2 = {
179
+ uri: src2.uri,
180
+ database: src2.database
181
+ };
182
+ if (src2.sampleSize !== void 0) ordered2["sampleSize"] = src2.sampleSize;
183
+ if (src2.tls !== void 0) ordered2["tls"] = src2.tls;
184
+ return ordered2;
185
+ }
186
+ const src = cfg.source;
187
+ const ordered = {
188
+ server: src.server,
189
+ database: src.database,
190
+ user: src.user,
191
+ password: src.password
192
+ };
193
+ if (src.port !== void 0) ordered["port"] = src.port;
194
+ if (src.domain !== void 0) ordered["domain"] = src.domain;
195
+ return ordered;
196
+ }
197
+
198
+ // src/cli/init/wizard.ts
199
+ import * as readline from "readline";
200
+ var ENV_REF_RE2 = /^\$\{env:[A-Z_][A-Z0-9_]*\}$/;
201
+ function isEnvRef2(value) {
202
+ return ENV_REF_RE2.test(value);
203
+ }
204
+ var LineReader = class {
205
+ _iter;
206
+ _out;
207
+ constructor(rl, out) {
208
+ this._iter = rl[Symbol.asyncIterator]();
209
+ this._out = out;
210
+ }
211
+ /** Write a prompt to the output writable */
212
+ prompt(text) {
213
+ this._out.write(text);
214
+ }
215
+ /**
216
+ * Read the next line from the underlying stream.
217
+ * Returns null when the stream ends (EOF).
218
+ */
219
+ async nextLine() {
220
+ const { value, done } = await this._iter.next();
221
+ if (done === true || value === void 0) {
222
+ return null;
223
+ }
224
+ return value.trim();
225
+ }
226
+ /**
227
+ * Ask a question: write the prompt label then read the next line.
228
+ */
229
+ async ask(label) {
230
+ this.prompt(label);
231
+ const line = await this.nextLine();
232
+ return line ?? "";
233
+ }
234
+ /**
235
+ * Ask for a connection-identity field that MUST be a ${env:VAR} reference.
236
+ * Re-prompts on literal values with a rejection message.
237
+ * When isSecret is true, the prompt label is written but NO echo happens
238
+ * (readline was created with terminal:false, so readline itself never echoes).
239
+ */
240
+ async askEnvRef(field, exampleVar, isSecret = false) {
241
+ while (true) {
242
+ const label = isSecret ? ` ${field} [hidden, enter \${env:VAR}]: ` : ` ${field} (e.g. \${env:${exampleVar}}): `;
243
+ this.prompt(label);
244
+ const line = await this.nextLine();
245
+ const value = line ?? "";
246
+ if (isEnvRef2(value)) {
247
+ if (isSecret) {
248
+ this.prompt("\n");
249
+ }
250
+ return value;
251
+ }
252
+ this.prompt(
253
+ ` [!] "${field}" must be a \${env:VAR} reference, not a literal value.
254
+ Example: \${env:${exampleVar}}
255
+ `
256
+ );
257
+ }
258
+ }
259
+ /**
260
+ * Ask whether to include an object type and at what level.
261
+ * Returns null when user declines. Defaults to 'full' on empty/invalid input.
262
+ */
263
+ async askLevel(kind) {
264
+ const include = await this.ask(` Include "${kind}" objects? [y/n]: `);
265
+ if (include.toLowerCase().startsWith("n")) {
266
+ return null;
267
+ }
268
+ const levelRaw = await this.ask(
269
+ ` Level for "${kind}" [off/metadata/full, default: full]: `
270
+ );
271
+ const level = levelRaw === "" ? "full" : levelRaw;
272
+ if (level === "off" || level === "metadata" || level === "full") {
273
+ return level;
274
+ }
275
+ return "full";
276
+ }
277
+ };
278
+ async function runWizard(options = {}) {
279
+ const inputStream = options.input ?? process.stdin;
280
+ const outputStream = options.output ?? process.stdout;
281
+ const rl = readline.createInterface({
282
+ input: inputStream,
283
+ output: outputStream,
284
+ terminal: false
285
+ });
286
+ const reader = new LineReader(rl, outputStream);
287
+ try {
288
+ reader.prompt("dbgraph interactive init\n\n");
289
+ const dialectRaw = await reader.ask("Dialect [sqlite/mssql]: ");
290
+ const dialect = dialectRaw.toLowerCase();
291
+ if (dialect !== "sqlite" && dialect !== "mssql") {
292
+ throw new Error(`Unknown dialect: "${dialectRaw}". Supported: sqlite, mssql.`);
293
+ }
294
+ const matrix = options.capabilitiesOverride ?? capabilitiesFor(dialect);
295
+ const supportedKinds = [...matrix.supported];
296
+ if (dialect === "sqlite") {
297
+ const file = await reader.ask("SQLite file path: ");
298
+ reader.prompt("\nObject types (based on SQLite capabilities):\n");
299
+ const levels = {};
300
+ for (const kind of supportedKinds) {
301
+ const level = await reader.askLevel(kind);
302
+ if (level !== null) {
303
+ levels[kind] = level;
304
+ }
305
+ }
306
+ return {
307
+ dialect: "sqlite",
308
+ file,
309
+ offeredKinds: supportedKinds,
310
+ levels
311
+ };
312
+ } else {
313
+ reader.prompt("\nMSSQL connection (all fields must be ${env:VAR} references):\n");
314
+ const server = await reader.askEnvRef("server", "DBGRAPH_DB_SERVER");
315
+ const database = await reader.askEnvRef("database", "DBGRAPH_DB_NAME");
316
+ const user = await reader.askEnvRef("user", "DBGRAPH_DB_USER");
317
+ const password = await reader.askEnvRef("password", "DBGRAPH_DB_PASSWORD", true);
318
+ const portRaw = await reader.ask(" port (leave empty to skip): ");
319
+ const port = portRaw === "" ? void 0 : portRaw;
320
+ const domainRaw = await reader.ask(" domain (leave empty to skip): ");
321
+ const domain = domainRaw === "" ? void 0 : domainRaw;
322
+ reader.prompt("\nObject types (based on MSSQL capabilities):\n");
323
+ const levels = {};
324
+ for (const kind of supportedKinds) {
325
+ const level = await reader.askLevel(kind);
326
+ if (level !== null) {
327
+ levels[kind] = level;
328
+ }
329
+ }
330
+ const result = {
331
+ dialect: "mssql",
332
+ server,
333
+ database,
334
+ user,
335
+ password,
336
+ offeredKinds: supportedKinds,
337
+ levels
338
+ };
339
+ if (port !== void 0) result.port = port;
340
+ if (domain !== void 0) result.domain = domain;
341
+ return result;
342
+ }
343
+ } finally {
344
+ rl.close();
345
+ }
346
+ }
347
+
348
+ // src/cli/sync/incremental.ts
349
+ function computeDelta(existing, fresh) {
350
+ const existingById = /* @__PURE__ */ new Map();
351
+ for (const node of existing) {
352
+ existingById.set(node.id, node.bodyHash);
353
+ }
354
+ const freshIds = /* @__PURE__ */ new Set();
355
+ const toUpsert = [];
356
+ for (const node of fresh) {
357
+ freshIds.add(node.id);
358
+ const existingHash = existingById.get(node.id);
359
+ if (existingHash === void 0) {
360
+ toUpsert.push(node);
361
+ } else if (existingHash !== node.bodyHash) {
362
+ toUpsert.push(node);
363
+ }
364
+ }
365
+ const toDelete = [];
366
+ for (const [id] of existingById) {
367
+ if (!freshIds.has(id)) {
368
+ toDelete.push(id);
369
+ }
370
+ }
371
+ return { toDelete, toUpsert };
372
+ }
373
+
374
+ // src/cli/commands/sync.ts
375
+ import { randomUUID } from "crypto";
376
+ async function runSync(options) {
377
+ const { adapter, store, full } = options;
378
+ const logger = options.logger ?? noopLogger;
379
+ const liveFingerprint = await adapter.fingerprint();
380
+ const snapshots = await store.listSnapshots();
381
+ const lastSnapshot = snapshots.length > 0 ? snapshots[0] : null;
382
+ const storedFingerprint = lastSnapshot?.fingerprint ?? null;
383
+ if (!full && storedFingerprint !== null && storedFingerprint === liveFingerprint) {
384
+ logger.info("extraction skipped");
385
+ return {
386
+ mode: "skipped",
387
+ counts: {},
388
+ upserted: 0,
389
+ deleted: 0,
390
+ hasDrift: false,
391
+ snapshotId: "",
392
+ fingerprint: liveFingerprint
393
+ };
394
+ }
395
+ const hasDrift = storedFingerprint !== null && storedFingerprint !== liveFingerprint;
396
+ logger.info("extract started");
397
+ const scope = {
398
+ levels: adapter.capabilities.defaultLevels
399
+ };
400
+ const rawCatalog = await adapter.extract(scope);
401
+ const normResult = normalizeCatalog(rawCatalog, scope);
402
+ const freshNodes = normResult.graph.nodes;
403
+ const existingNodes = await getAllNodes(store);
404
+ const delta = computeDelta(existingNodes, freshNodes);
405
+ const upserted = delta.toUpsert.length;
406
+ const deleted = delta.toDelete.length;
407
+ if (delta.toDelete.length > 0) {
408
+ await store.deleteNodes(delta.toDelete);
409
+ }
410
+ if (delta.toUpsert.length > 0) {
411
+ await store.upsertGraph({
412
+ nodes: delta.toUpsert,
413
+ edges: normResult.graph.edges
414
+ });
415
+ }
416
+ logger.info("delta computed", { upserted, deleted });
417
+ const counts = buildCounts(freshNodes);
418
+ const snapshot = {
419
+ id: randomUUID(),
420
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
421
+ engine: adapter.dialect,
422
+ fingerprint: liveFingerprint,
423
+ counts
424
+ };
425
+ await store.putSnapshot(snapshot);
426
+ logger.info("snapshot written", { id: snapshot.id });
427
+ return {
428
+ mode: full ? "full" : "incremental",
429
+ counts,
430
+ upserted,
431
+ deleted,
432
+ hasDrift,
433
+ snapshotId: snapshot.id,
434
+ fingerprint: liveFingerprint
435
+ };
436
+ }
437
+ async function getAllNodes(store) {
438
+ const results = await Promise.all(
439
+ NODE_KINDS.map((kind) => store.getNodesByKind(kind))
440
+ );
441
+ return results.flatMap((arr) => arr);
442
+ }
443
+ function buildCounts(nodes) {
444
+ const counts = {};
445
+ for (const node of nodes) {
446
+ counts[node.kind] = (counts[node.kind] ?? 0) + 1;
447
+ }
448
+ return counts;
449
+ }
450
+
451
+ // src/cli/log/console-logger.ts
452
+ var LEVEL_RANK = {
453
+ debug: 0,
454
+ info: 1,
455
+ warn: 2,
456
+ error: 3
457
+ };
458
+ function createConsoleLogger(opts) {
459
+ const write = opts?.write ?? ((t) => {
460
+ process.stderr.write(t);
461
+ });
462
+ const minLevel = opts?.level ?? "info";
463
+ const minRank = LEVEL_RANK[minLevel];
464
+ function emit(level, msg, meta) {
465
+ if (LEVEL_RANK[level] < minRank) return;
466
+ const metaPart = meta !== void 0 ? ` ${JSON.stringify(meta)}` : "";
467
+ write(`[${level}] ${msg}${metaPart}
468
+ `);
469
+ }
470
+ return {
471
+ debug: (msg, meta) => emit("debug", msg, meta),
472
+ info: (msg, meta) => emit("info", msg, meta),
473
+ warn: (msg, meta) => emit("warn", msg, meta),
474
+ error: (msg, meta) => emit("error", msg, meta)
475
+ };
476
+ }
477
+
478
+ // src/cli/format/sync.ts
479
+ var LABEL_WIDTH = 13;
480
+ var KIND_WIDTH = 12;
481
+ function formatSyncSummary(view) {
482
+ const lines = [];
483
+ lines.push("Sync Summary");
484
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
485
+ if (view.mode === "skipped") {
486
+ lines.push(" already up to date \u2014 no changes since last snapshot");
487
+ lines.push(` ${"fingerprint".padEnd(LABEL_WIDTH)}${view.fingerprint}`);
488
+ lines.push("");
489
+ return lines.join("\n") + "\n";
490
+ }
491
+ lines.push(` ${"mode".padEnd(LABEL_WIDTH)}${view.mode}`);
492
+ lines.push(" counts:");
493
+ const kinds = Object.keys(view.counts).sort();
494
+ if (kinds.length === 0) {
495
+ lines.push(" (none)");
496
+ } else {
497
+ for (const kind of kinds) {
498
+ const count = view.counts[kind] ?? 0;
499
+ lines.push(` ${kind.padEnd(KIND_WIDTH)} ${count}`);
500
+ }
501
+ }
502
+ lines.push(` ${"upserted".padEnd(LABEL_WIDTH)}${view.upserted}`);
503
+ lines.push(` ${"deleted".padEnd(LABEL_WIDTH)}${view.deleted}`);
504
+ lines.push(` ${"snapshot".padEnd(LABEL_WIDTH)}${view.snapshotId}`);
505
+ lines.push(` ${"fingerprint".padEnd(LABEL_WIDTH)}${view.fingerprint}`);
506
+ lines.push("");
507
+ if (view.hasDrift) {
508
+ lines.push("DRIFT RESOLVED \u2014 source schema had changed since the previous snapshot.");
509
+ lines.push("");
510
+ }
511
+ return lines.join("\n") + "\n";
512
+ }
513
+
514
+ // src/cli/commands/init.ts
515
+ var GITIGNORE_ENTRY = ".dbgraph/";
516
+ function ensureGitignored(projectRoot) {
517
+ const gitignorePath = join(projectRoot, ".gitignore");
518
+ if (existsSync(gitignorePath)) {
519
+ const content = readFileSync(gitignorePath, "utf-8");
520
+ const lines = content.split("\n").map((l) => l.trim());
521
+ if (lines.includes(GITIGNORE_ENTRY)) {
522
+ return;
523
+ }
524
+ const suffix = content.endsWith("\n") ? "" : "\n";
525
+ appendFileSync(gitignorePath, `${suffix}${GITIGNORE_ENTRY}
526
+ `, "utf-8");
527
+ } else {
528
+ writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
529
+ `, "utf-8");
530
+ }
531
+ }
532
+ async function syncAfterInit(projectRoot) {
533
+ const logger = createConsoleLogger({ level: "info" });
534
+ const { adapter, store } = await openConnections(projectRoot, logger);
535
+ try {
536
+ const summary = await runSync({ adapter, store, full: false, logger });
537
+ process.stdout.write(formatSyncSummary(summary));
538
+ } finally {
539
+ await adapter.close();
540
+ await store.close();
541
+ }
542
+ }
543
+ function wizardResultToBuildInput(result) {
544
+ if (result.dialect === "sqlite") {
545
+ return { dialect: "sqlite", file: result.file };
546
+ }
547
+ const base = {
548
+ dialect: "mssql",
549
+ server: result.server,
550
+ database: result.database,
551
+ user: result.user,
552
+ password: result.password
553
+ };
554
+ const mssql = base;
555
+ if (result.port !== void 0) mssql.port = result.port;
556
+ if (result.domain !== void 0) mssql.domain = result.domain;
557
+ return mssql;
558
+ }
559
+ async function runInit(options) {
560
+ const { projectRoot } = options;
561
+ let buildInput;
562
+ if ("interactive" in options && options.interactive === true) {
563
+ const wizardOpts = {};
564
+ if (options.wizardInput !== void 0) wizardOpts.input = options.wizardInput;
565
+ if (options.wizardOutput !== void 0) wizardOpts.output = options.wizardOutput;
566
+ if (options.capabilitiesOverride !== void 0) {
567
+ wizardOpts.capabilitiesOverride = options.capabilitiesOverride;
568
+ }
569
+ const wizardResult = await runWizard(wizardOpts);
570
+ buildInput = wizardResultToBuildInput(wizardResult);
571
+ } else {
572
+ const opts = options;
573
+ if (opts.dialect === "sqlite") {
574
+ buildInput = {
575
+ dialect: "sqlite",
576
+ file: opts.file,
577
+ ...opts.driver !== void 0 ? { driver: opts.driver } : {}
578
+ };
579
+ } else {
580
+ buildInput = {
581
+ dialect: "mssql",
582
+ server: opts.server,
583
+ database: opts.database,
584
+ user: opts.user,
585
+ password: opts.password,
586
+ ...opts.port !== void 0 ? { port: opts.port } : {},
587
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {}
588
+ };
589
+ }
590
+ }
591
+ const config = buildConfig(buildInput);
592
+ const configString = writeConfig(config);
593
+ const configPath = join(projectRoot, "dbgraph.config.json");
594
+ writeFileSync(configPath, configString, "utf-8");
595
+ ensureGitignored(projectRoot);
596
+ const syncFn = options._syncFn ?? syncAfterInit;
597
+ await syncFn(projectRoot);
598
+ return { type: "success" };
599
+ }
600
+
601
+ // src/cli/format/status.ts
602
+ function formatStatus(view) {
603
+ const lines = [];
604
+ lines.push("Graph");
605
+ lines.push("\u2500\u2500\u2500\u2500\u2500");
606
+ const kinds = Object.keys(view.kindCounts).sort();
607
+ if (kinds.length === 0) {
608
+ lines.push(" (empty \u2014 run sync first)");
609
+ } else {
610
+ for (const kind of kinds) {
611
+ const count = view.kindCounts[kind] ?? 0;
612
+ lines.push(` ${kind.padEnd(14)} ${count}`);
613
+ }
614
+ }
615
+ if (view.excludedCount > 0) {
616
+ lines.push(` excluded ${view.excludedCount}`);
617
+ }
618
+ lines.push("");
619
+ lines.push("Last Snapshot");
620
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
621
+ if (view.lastSnapshot === null) {
622
+ lines.push(" never synced \u2014 run: dbgraph sync");
623
+ } else {
624
+ const snap = view.lastSnapshot;
625
+ lines.push(` taken ${snap.takenAt}`);
626
+ lines.push(` engine ${snap.engine}`);
627
+ lines.push(` fingerprint ${snap.fingerprint}`);
628
+ const snapKinds = Object.keys(snap.counts).sort();
629
+ if (snapKinds.length > 0) {
630
+ lines.push(" counts:");
631
+ for (const kind of snapKinds) {
632
+ const count = snap.counts[kind] ?? 0;
633
+ lines.push(` ${kind.padEnd(12)} ${count}`);
634
+ }
635
+ }
636
+ }
637
+ lines.push("");
638
+ if (view.hasDrift) {
639
+ lines.push("DRIFT DETECTED \u2014 source schema has changed since last sync.");
640
+ lines.push(" Run: dbgraph sync");
641
+ lines.push("");
642
+ }
643
+ return lines.join("\n") + "\n";
644
+ }
645
+
646
+ // src/cli/commands/status.ts
647
+ async function runStatus(options) {
648
+ const { adapter, store } = options;
649
+ const allNodeArrays = await Promise.all(
650
+ NODE_KINDS.map((kind) => store.getNodesByKind(kind))
651
+ );
652
+ const allNodes = allNodeArrays.flatMap((arr) => arr);
653
+ const kindCounts = {};
654
+ let excludedCount = 0;
655
+ for (const node of allNodes) {
656
+ if (node.missing || node.excluded) {
657
+ excludedCount++;
658
+ } else {
659
+ kindCounts[node.kind] = (kindCounts[node.kind] ?? 0) + 1;
660
+ }
661
+ }
662
+ const snapshots = await store.listSnapshots();
663
+ const lastSnapshot = snapshots.length > 0 ? snapshots[0] ?? null : null;
664
+ const liveFp = await adapter.fingerprint();
665
+ const hasDrift = lastSnapshot !== null && liveFp !== lastSnapshot.fingerprint;
666
+ const view = {
667
+ kindCounts,
668
+ lastSnapshot,
669
+ excludedCount,
670
+ hasDrift
671
+ };
672
+ const output = formatStatus(view);
673
+ return { type: "success", output };
674
+ }
675
+
676
+ // src/cli/format/query.ts
677
+ function formatQueryText(view) {
678
+ const lines = [];
679
+ if (view.hits.length === 0) {
680
+ lines.push(`No results for "${view.term}".`);
681
+ } else {
682
+ lines.push(`Results for "${view.term}" (${view.total} found):`);
683
+ lines.push("");
684
+ for (const hit of view.hits) {
685
+ lines.push(` ${hit.kind.padEnd(14)} ${hit.qname}`);
686
+ }
687
+ }
688
+ lines.push("");
689
+ return lines.join("\n") + "\n";
690
+ }
691
+ function formatQueryJson(view) {
692
+ const payload = {
693
+ term: view.term,
694
+ total: view.total,
695
+ hits: view.hits.map((h) => ({
696
+ kind: h.kind,
697
+ qname: h.qname,
698
+ id: h.id,
699
+ score: h.score
700
+ }))
701
+ };
702
+ return JSON.stringify(payload, null, 2) + "\n";
703
+ }
704
+
705
+ // src/cli/commands/query.ts
706
+ async function runQuery(options) {
707
+ const { store, term, json } = options;
708
+ const result = await search(store, { term });
709
+ const view = {
710
+ term,
711
+ hits: result.hits,
712
+ total: result.total
713
+ };
714
+ const output = json ? formatQueryJson(view) : formatQueryText(view);
715
+ if (result.hits.length === 0) {
716
+ return { type: "negative", output };
717
+ }
718
+ return { type: "success", output };
719
+ }
720
+
721
+ // src/cli/commands/explore.ts
722
+ async function runExplore(options) {
723
+ const { store, qname, detail } = options;
724
+ const matches = [];
725
+ for (const kind of NODE_KINDS) {
726
+ const found = await store.getNodeByQName(kind, qname);
727
+ if (found !== null) matches.push(found);
728
+ }
729
+ const real = matches.filter((n) => !n.missing);
730
+ const effective = real.length > 0 ? real : matches;
731
+ const node = effective[0] ?? null;
732
+ if (node === null) {
733
+ throw new NotFoundError("entity", qname);
734
+ }
735
+ const neighbors = await getNeighbors(store, { nodeId: node.id });
736
+ const view = { node, neighbors };
737
+ const output = formatExplore(view, detail);
738
+ return { type: "success", output };
739
+ }
740
+
741
+ // src/cli/commands/object.ts
742
+ async function runObject(options) {
743
+ const { store, qname, detail } = options;
744
+ const matches = [];
745
+ for (const kind of NODE_KINDS) {
746
+ const found = await store.getNodeByQName(kind, qname);
747
+ if (found !== null) matches.push(found);
748
+ }
749
+ const real = matches.filter((n) => !n.missing);
750
+ const effective = real.length > 0 ? real : matches;
751
+ const node = effective[0] ?? null;
752
+ if (node === null) {
753
+ throw new NotFoundError("entity", qname);
754
+ }
755
+ const neighbors = await getNeighbors(store, { nodeId: node.id });
756
+ const view = { node, neighbors };
757
+ const output = formatObject(view, detail);
758
+ return { type: "success", output };
759
+ }
760
+
761
+ // src/cli/diff/engine.ts
762
+ function diffManifests(a, b) {
763
+ const mapA = /* @__PURE__ */ new Map();
764
+ for (const row of a) {
765
+ mapA.set(row.nodeId, row);
766
+ }
767
+ const mapB = /* @__PURE__ */ new Map();
768
+ for (const row of b) {
769
+ mapB.set(row.nodeId, row);
770
+ }
771
+ const added = [];
772
+ const removed = [];
773
+ const changed = [];
774
+ for (const [nodeId, rowB] of mapB) {
775
+ const rowA = mapA.get(nodeId);
776
+ if (rowA === void 0) {
777
+ added.push({
778
+ nodeId,
779
+ kind: rowB.kind,
780
+ qname: rowB.qname,
781
+ bodyHash: rowB.bodyHash
782
+ });
783
+ } else if (rowA.bodyHash !== rowB.bodyHash) {
784
+ changed.push({
785
+ nodeId,
786
+ kind: rowB.kind,
787
+ qname: rowB.qname,
788
+ oldBodyHash: rowA.bodyHash,
789
+ newBodyHash: rowB.bodyHash
790
+ });
791
+ }
792
+ }
793
+ for (const [nodeId, rowA] of mapA) {
794
+ if (!mapB.has(nodeId)) {
795
+ removed.push({
796
+ nodeId,
797
+ kind: rowA.kind,
798
+ qname: rowA.qname,
799
+ bodyHash: rowA.bodyHash
800
+ });
801
+ }
802
+ }
803
+ return { added, removed, changed };
804
+ }
805
+
806
+ // src/cli/format/diff.ts
807
+ function formatDiff(result) {
808
+ const { added, removed, changed } = result;
809
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
810
+ return "No changes detected.\n";
811
+ }
812
+ const lines = [];
813
+ if (added.length > 0) {
814
+ lines.push("ADDED");
815
+ lines.push("\u2500\u2500\u2500\u2500\u2500");
816
+ const sorted = [...added].sort(compareByKindThenQname);
817
+ for (const row of sorted) {
818
+ lines.push(` + ${row.kind.padEnd(14)} ${row.qname}`);
819
+ }
820
+ lines.push("");
821
+ }
822
+ if (removed.length > 0) {
823
+ lines.push("REMOVED");
824
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
825
+ const sorted = [...removed].sort(compareByKindThenQname);
826
+ for (const row of sorted) {
827
+ lines.push(` - ${row.kind.padEnd(14)} ${row.qname}`);
828
+ }
829
+ lines.push("");
830
+ }
831
+ if (changed.length > 0) {
832
+ lines.push("MODIFIED");
833
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
834
+ const sorted = [...changed].sort(compareByKindThenQname);
835
+ for (const row of sorted) {
836
+ const what = describeChange(row);
837
+ lines.push(` ~ ${row.kind.padEnd(14)} ${row.qname}`);
838
+ lines.push(` ${what}`);
839
+ }
840
+ lines.push("");
841
+ }
842
+ return lines.join("\n") + "\n";
843
+ }
844
+ function formatDiffNoManifest(snapA, snapB) {
845
+ return `No per-object manifest found for snapshots "${snapA}" / "${snapB}".
846
+ These snapshots were recorded before the v2 schema index.
847
+ Run: dbgraph sync to record a new snapshot, then diff again.
848
+ `;
849
+ }
850
+ function compareByKindThenQname(a, b) {
851
+ const kindCmp = a.kind.localeCompare(b.kind);
852
+ return kindCmp !== 0 ? kindCmp : a.qname.localeCompare(b.qname);
853
+ }
854
+ function describeChange(row) {
855
+ const oldHash = row.oldBodyHash ?? "(null)";
856
+ const newHash = row.newBodyHash ?? "(null)";
857
+ return `definition changed (hash: ${oldHash.slice(0, 8)}\u2026 \u2192 ${newHash.slice(0, 8)}\u2026)`;
858
+ }
859
+
860
+ // src/cli/commands/diff.ts
861
+ async function runDiff(options) {
862
+ const { store, last } = options;
863
+ let snapAId;
864
+ let snapBId;
865
+ if (last === true) {
866
+ const snapshots = await store.listSnapshots();
867
+ if (snapshots.length < 2) {
868
+ return {
869
+ type: "negative",
870
+ output: "Not enough snapshots for diff. Need at least two syncs.\n"
871
+ };
872
+ }
873
+ const len = snapshots.length;
874
+ snapAId = snapshots[len - 2].id;
875
+ snapBId = snapshots[len - 1].id;
876
+ } else {
877
+ if (options.snapA === void 0 || options.snapB === void 0) {
878
+ return {
879
+ type: "negative",
880
+ output: "Usage: dbgraph diff <snapA> <snapB> | dbgraph diff --last\n"
881
+ };
882
+ }
883
+ snapAId = options.snapA;
884
+ snapBId = options.snapB;
885
+ }
886
+ const [manifestA, manifestB] = await Promise.all([
887
+ store.getSnapshotObjects(snapAId),
888
+ store.getSnapshotObjects(snapBId)
889
+ ]);
890
+ if (manifestA.length === 0 || manifestB.length === 0) {
891
+ return {
892
+ type: "negative",
893
+ output: formatDiffNoManifest(snapAId, snapBId)
894
+ };
895
+ }
896
+ const diffResult = diffManifests(manifestA, manifestB);
897
+ const hasChanges = diffResult.added.length > 0 || diffResult.removed.length > 0 || diffResult.changed.length > 0;
898
+ const output = formatDiff(diffResult);
899
+ return {
900
+ type: hasChanges ? "negative" : "success",
901
+ output
902
+ };
903
+ }
904
+
905
+ // src/cli/commands/affected.ts
906
+ import { readFileSync as readFileSync2 } from "fs";
907
+ async function runAffected(options) {
908
+ const { store, sqlFile, json = false } = options;
909
+ const detail = options.detail ?? "normal";
910
+ const ddl = readFileSync2(sqlFile, "utf-8");
911
+ const view = await runPrecheck(store, ddl);
912
+ const hasImpact = view.matchedObjects.length > 0;
913
+ const outcomeType = hasImpact ? "negative" : "success";
914
+ let output;
915
+ if (json) {
916
+ output = JSON.stringify(view, null, 2) + "\n";
917
+ } else {
918
+ output = formatPrecheck(view, detail);
919
+ }
920
+ return { type: outcomeType, output };
921
+ }
922
+
923
+ // src/cli/commands/install.ts
924
+ import { win32 as pathWin32, posix as pathPosix } from "path";
925
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
926
+ import { dirname } from "path";
927
+ var realFsSeam = {
928
+ readFile(path) {
929
+ return readFileSync3(path, "utf-8");
930
+ },
931
+ writeFile(path, content) {
932
+ const dir = dirname(path);
933
+ mkdirSync(dir, { recursive: true });
934
+ writeFileSync2(path, content, "utf-8");
935
+ },
936
+ exists(path) {
937
+ return existsSync2(path);
938
+ }
939
+ };
940
+ var MCP_ENTRY_NAME = "dbgraph-mcp";
941
+ var CLAUDE_CONFIG_FILE = "claude_desktop_config.json";
942
+ function resolveConfigPath(platform, env) {
943
+ if (platform === "win32") {
944
+ const appData = env["APPDATA"];
945
+ if (appData === void 0 || appData === "") return void 0;
946
+ return pathWin32.join(appData, "Claude", CLAUDE_CONFIG_FILE);
947
+ }
948
+ const home = env["HOME"];
949
+ if (home === void 0 || home === "") return void 0;
950
+ return pathPosix.join(home, ".config", "Claude", CLAUDE_CONFIG_FILE);
951
+ }
952
+ function resolveProjectConfigPath(platform, cwd, segs) {
953
+ return platform === "win32" ? pathWin32.join(cwd, ...segs) : pathPosix.join(cwd, ...segs);
954
+ }
955
+ function mergeMcpConfig(config, entry) {
956
+ const existing = config;
957
+ const servers = existing.mcpServers ?? {};
958
+ const current = servers[MCP_ENTRY_NAME];
959
+ if (current !== void 0 && current.command === entry.command && JSON.stringify(current.args) === JSON.stringify(entry.args)) {
960
+ return config;
961
+ }
962
+ return {
963
+ ...existing,
964
+ mcpServers: {
965
+ ...servers,
966
+ [MCP_ENTRY_NAME]: entry
967
+ }
968
+ };
969
+ }
970
+ function removeMcpConfig(config) {
971
+ const existing = config;
972
+ const servers = existing.mcpServers;
973
+ if (servers === void 0 || servers[MCP_ENTRY_NAME] === void 0) {
974
+ return config;
975
+ }
976
+ const { [MCP_ENTRY_NAME]: _removed, ...remaining } = servers;
977
+ void _removed;
978
+ return {
979
+ ...existing,
980
+ mcpServers: Object.keys(remaining).length === 0 ? void 0 : remaining
981
+ };
982
+ }
983
+ function homeRoot(platform, env) {
984
+ if (platform === "win32") {
985
+ const up = env["USERPROFILE"];
986
+ if (up === void 0 || up === "") return void 0;
987
+ return up;
988
+ }
989
+ const home = env["HOME"];
990
+ if (home === void 0 || home === "") return void 0;
991
+ return home;
992
+ }
993
+ function mergeMcpText(content, entry) {
994
+ let config;
995
+ try {
996
+ config = JSON.parse(content);
997
+ } catch {
998
+ config = {};
999
+ }
1000
+ const updated = mergeMcpConfig(config, entry);
1001
+ if (updated === config) return content;
1002
+ return JSON.stringify(updated, null, 2) + "\n";
1003
+ }
1004
+ function removeMcpText(content) {
1005
+ let config;
1006
+ try {
1007
+ config = JSON.parse(content);
1008
+ } catch {
1009
+ config = {};
1010
+ }
1011
+ const updated = removeMcpConfig(config);
1012
+ if (updated === config) return content;
1013
+ return JSON.stringify(updated, null, 2) + "\n";
1014
+ }
1015
+ function mergeVsCodeConfig(config, entry) {
1016
+ const existing = config;
1017
+ const servers = existing.servers ?? {};
1018
+ const current = servers[MCP_ENTRY_NAME];
1019
+ if (current !== void 0 && current.type === "stdio" && current.command === entry.command && JSON.stringify(current.args) === JSON.stringify(entry.args)) {
1020
+ return config;
1021
+ }
1022
+ return {
1023
+ ...existing,
1024
+ servers: {
1025
+ ...servers,
1026
+ [MCP_ENTRY_NAME]: { type: "stdio", command: entry.command, args: entry.args }
1027
+ }
1028
+ };
1029
+ }
1030
+ function removeVsCodeConfig(config) {
1031
+ const existing = config;
1032
+ const servers = existing.servers;
1033
+ if (servers === void 0 || servers[MCP_ENTRY_NAME] === void 0) {
1034
+ return config;
1035
+ }
1036
+ const { [MCP_ENTRY_NAME]: _removed, ...remaining } = servers;
1037
+ void _removed;
1038
+ return {
1039
+ ...existing,
1040
+ servers: Object.keys(remaining).length === 0 ? void 0 : remaining
1041
+ };
1042
+ }
1043
+ function mergeVsCodeText(content, entry) {
1044
+ let config;
1045
+ try {
1046
+ config = JSON.parse(content);
1047
+ } catch {
1048
+ config = {};
1049
+ }
1050
+ const updated = mergeVsCodeConfig(config, entry);
1051
+ if (updated === config) return content;
1052
+ return JSON.stringify(updated, null, 2) + "\n";
1053
+ }
1054
+ function removeVsCodeText(content) {
1055
+ let config;
1056
+ try {
1057
+ config = JSON.parse(content);
1058
+ } catch {
1059
+ config = {};
1060
+ }
1061
+ const updated = removeVsCodeConfig(config);
1062
+ if (updated === config) return content;
1063
+ return JSON.stringify(updated, null, 2) + "\n";
1064
+ }
1065
+ function mergeOpenCodeConfig(config, entry) {
1066
+ const existing = config;
1067
+ const mcp = existing.mcp ?? {};
1068
+ const derivedCommand = [entry.command, ...entry.args];
1069
+ const current = mcp[MCP_ENTRY_NAME];
1070
+ if (current !== void 0 && current.type === "local" && JSON.stringify(current.command) === JSON.stringify(derivedCommand)) {
1071
+ return config;
1072
+ }
1073
+ return {
1074
+ ...existing,
1075
+ mcp: {
1076
+ ...mcp,
1077
+ [MCP_ENTRY_NAME]: { type: "local", command: derivedCommand }
1078
+ }
1079
+ };
1080
+ }
1081
+ function removeOpenCodeConfig(config) {
1082
+ const existing = config;
1083
+ const mcp = existing.mcp;
1084
+ if (mcp === void 0 || mcp[MCP_ENTRY_NAME] === void 0) {
1085
+ return config;
1086
+ }
1087
+ const { [MCP_ENTRY_NAME]: _removed, ...remaining } = mcp;
1088
+ void _removed;
1089
+ return {
1090
+ ...existing,
1091
+ mcp: Object.keys(remaining).length === 0 ? void 0 : remaining
1092
+ };
1093
+ }
1094
+ function mergeOpenCodeText(content, entry) {
1095
+ let config;
1096
+ try {
1097
+ config = JSON.parse(content);
1098
+ } catch {
1099
+ config = {};
1100
+ }
1101
+ const updated = mergeOpenCodeConfig(config, entry);
1102
+ if (updated === config) return content;
1103
+ return JSON.stringify(updated, null, 2) + "\n";
1104
+ }
1105
+ function removeOpenCodeText(content) {
1106
+ let config;
1107
+ try {
1108
+ config = JSON.parse(content);
1109
+ } catch {
1110
+ config = {};
1111
+ }
1112
+ const updated = removeOpenCodeConfig(config);
1113
+ if (updated === config) return content;
1114
+ return JSON.stringify(updated, null, 2) + "\n";
1115
+ }
1116
+ var CODEX_RENDER = '[mcp_servers.dbgraph-mcp]\ncommand = "npx"\nargs = ["-y", "dbgraph-mcp"]';
1117
+ var CODEX_HEADER = "[mcp_servers.dbgraph-mcp]";
1118
+ function findCodexHeaderLine(lines) {
1119
+ return lines.findIndex((l) => l === CODEX_HEADER);
1120
+ }
1121
+ function findCodexBlockEnd(lines, headerIdx) {
1122
+ for (let i = headerIdx + 1; i < lines.length; i++) {
1123
+ const line = lines[i] ?? "";
1124
+ if (/^\s*\[/.test(line)) {
1125
+ return i;
1126
+ }
1127
+ }
1128
+ return lines.length;
1129
+ }
1130
+ function mergeCodexToml(content) {
1131
+ const lines = content.split("\n");
1132
+ const hadTrailingNewline = content.endsWith("\n") || content === "";
1133
+ const workLines = hadTrailingNewline && lines[lines.length - 1] === "" ? lines.slice(0, -1) : [...lines];
1134
+ const headerIdx = findCodexHeaderLine(workLines);
1135
+ if (headerIdx === -1) {
1136
+ const renderLines2 = CODEX_RENDER.split("\n");
1137
+ if (workLines.length === 0 || workLines.length === 1 && workLines[0] === "") {
1138
+ return CODEX_RENDER + "\n";
1139
+ }
1140
+ const result2 = [...workLines, "", ...renderLines2, ""];
1141
+ return result2.join("\n");
1142
+ }
1143
+ const blockEndIdx = findCodexBlockEnd(workLines, headerIdx);
1144
+ const blockLines = workLines.slice(headerIdx, blockEndIdx);
1145
+ const currentBlock = blockLines.join("\n");
1146
+ if (currentBlock === CODEX_RENDER) {
1147
+ return content;
1148
+ }
1149
+ const renderLines = CODEX_RENDER.split("\n");
1150
+ const before = workLines.slice(0, headerIdx);
1151
+ const after = workLines.slice(blockEndIdx);
1152
+ const result = [...before, ...renderLines, ...after, ""];
1153
+ return result.join("\n");
1154
+ }
1155
+ function removeCodexToml(content) {
1156
+ if (content === "") return "";
1157
+ const lines = content.split("\n");
1158
+ const hadTrailingNewline = content.endsWith("\n");
1159
+ const workLines = hadTrailingNewline && lines[lines.length - 1] === "" ? lines.slice(0, -1) : [...lines];
1160
+ const headerIdx = findCodexHeaderLine(workLines);
1161
+ if (headerIdx === -1) {
1162
+ return content;
1163
+ }
1164
+ const blockEndIdx = findCodexBlockEnd(workLines, headerIdx);
1165
+ const before = workLines.slice(0, headerIdx);
1166
+ const after = workLines.slice(blockEndIdx);
1167
+ const combined = [...before, ...after];
1168
+ const collapsed = [];
1169
+ let prevBlank = false;
1170
+ for (const line of combined) {
1171
+ const isBlank = line.trim() === "";
1172
+ if (isBlank && prevBlank) continue;
1173
+ collapsed.push(line);
1174
+ prevBlank = isBlank;
1175
+ }
1176
+ while (collapsed.length > 0 && (collapsed[0] ?? "").trim() === "") {
1177
+ collapsed.shift();
1178
+ }
1179
+ if (collapsed.length === 0) {
1180
+ return "\n";
1181
+ }
1182
+ while (collapsed.length > 0 && (collapsed[collapsed.length - 1] ?? "").trim() === "") {
1183
+ collapsed.pop();
1184
+ }
1185
+ return collapsed.join("\n") + "\n";
1186
+ }
1187
+ var AGENT_TABLE = [
1188
+ {
1189
+ id: "claude-code",
1190
+ displayName: "Claude Code",
1191
+ format: "mcpServers",
1192
+ // Project scope (US-038): <cwd>/.mcp.json — LIVE-verified 2026-07-06.
1193
+ projectPath: [".mcp.json"],
1194
+ resolvePath(platform, env) {
1195
+ return resolveConfigPath(platform, env);
1196
+ },
1197
+ merge(content, entry) {
1198
+ return mergeMcpText(content, entry);
1199
+ },
1200
+ remove(content) {
1201
+ return removeMcpText(content);
1202
+ }
1203
+ },
1204
+ {
1205
+ id: "cursor",
1206
+ displayName: "Cursor",
1207
+ format: "mcpServers",
1208
+ // Project scope (US-038): <cwd>/.cursor/mcp.json — LIVE-verified 2026-07-06.
1209
+ projectPath: [".cursor", "mcp.json"],
1210
+ resolvePath(platform, env) {
1211
+ const root = homeRoot(platform, env);
1212
+ if (root === void 0) return void 0;
1213
+ return platform === "win32" ? pathWin32.join(root, ".cursor", "mcp.json") : pathPosix.join(root, ".cursor", "mcp.json");
1214
+ },
1215
+ merge(content, entry) {
1216
+ return mergeMcpText(content, entry);
1217
+ },
1218
+ remove(content) {
1219
+ return removeMcpText(content);
1220
+ }
1221
+ },
1222
+ {
1223
+ id: "gemini",
1224
+ displayName: "Gemini CLI",
1225
+ format: "mcpServers",
1226
+ // Project scope (US-038): <cwd>/.gemini/settings.json — LIVE-verified 2026-07-06.
1227
+ projectPath: [".gemini", "settings.json"],
1228
+ resolvePath(platform, env) {
1229
+ const root = homeRoot(platform, env);
1230
+ if (root === void 0) return void 0;
1231
+ return platform === "win32" ? pathWin32.join(root, ".gemini", "settings.json") : pathPosix.join(root, ".gemini", "settings.json");
1232
+ },
1233
+ merge(content, entry) {
1234
+ return mergeMcpText(content, entry);
1235
+ },
1236
+ remove(content) {
1237
+ return removeMcpText(content);
1238
+ }
1239
+ },
1240
+ {
1241
+ id: "vscode",
1242
+ displayName: "VS Code",
1243
+ format: "vscode",
1244
+ // Project scope (US-038): <cwd>/.vscode/mcp.json — LIVE-verified 2026-07-06.
1245
+ projectPath: [".vscode", "mcp.json"],
1246
+ resolvePath(platform, env) {
1247
+ const root = homeRoot(platform, env);
1248
+ if (root === void 0) return void 0;
1249
+ return platform === "win32" ? pathWin32.join(root, ".vscode", "mcp.json") : pathPosix.join(root, ".vscode", "mcp.json");
1250
+ },
1251
+ merge(content, entry) {
1252
+ return mergeVsCodeText(content, entry);
1253
+ },
1254
+ remove(content) {
1255
+ return removeVsCodeText(content);
1256
+ }
1257
+ },
1258
+ {
1259
+ id: "opencode",
1260
+ displayName: "opencode",
1261
+ format: "opencode",
1262
+ // Project scope (US-038): <cwd>/opencode.json — LIVE-verified 2026-07-06.
1263
+ // NOTE: project location is the repo-root opencode.json (NOT ~/.config/opencode).
1264
+ projectPath: ["opencode.json"],
1265
+ resolvePath(platform, env) {
1266
+ const root = homeRoot(platform, env);
1267
+ if (root === void 0) return void 0;
1268
+ return platform === "win32" ? pathWin32.join(root, ".config", "opencode", "opencode.json") : pathPosix.join(root, ".config", "opencode", "opencode.json");
1269
+ },
1270
+ merge(content, entry) {
1271
+ return mergeOpenCodeText(content, entry);
1272
+ },
1273
+ remove(content) {
1274
+ return removeOpenCodeText(content);
1275
+ }
1276
+ },
1277
+ {
1278
+ id: "codex",
1279
+ displayName: "Codex CLI",
1280
+ format: "codex-toml",
1281
+ // Project scope (US-038, Decision #5): <cwd>/.codex/config.toml — LIVE-verified
1282
+ // 2026-07-06. Reuses the SAME mergeCodexToml writer as global; the project file is
1283
+ // TRUST-GATED (the project must be trusted in ~/.codex/config.toml), so the codex
1284
+ // summary line carries a trust-caveat suffix (see runInstall project branch).
1285
+ projectPath: [".codex", "config.toml"],
1286
+ resolvePath(platform, env) {
1287
+ const root = homeRoot(platform, env);
1288
+ if (root === void 0) return void 0;
1289
+ return platform === "win32" ? pathWin32.join(root, ".codex", "config.toml") : pathPosix.join(root, ".codex", "config.toml");
1290
+ },
1291
+ merge(content, entry) {
1292
+ void entry;
1293
+ return mergeCodexToml(content);
1294
+ },
1295
+ remove(content) {
1296
+ return removeCodexToml(content);
1297
+ }
1298
+ }
1299
+ ];
1300
+ var MANUAL_SNIPPET = `No supported MCP agent config was detected.
1301
+
1302
+ Supported agents: Claude Code, Cursor, Gemini CLI, VS Code, opencode, Codex CLI.
1303
+
1304
+ To install dbgraph manually, add the following to your agent's MCP configuration:
1305
+
1306
+ For Claude Code, Cursor, and Gemini CLI (mcpServers JSON):
1307
+ {
1308
+ "mcpServers": {
1309
+ "dbgraph-mcp": {
1310
+ "command": "npx",
1311
+ "args": ["-y", "dbgraph-mcp"]
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ For VS Code (servers JSON):
1317
+ {
1318
+ "servers": {
1319
+ "dbgraph-mcp": {
1320
+ "type": "stdio",
1321
+ "command": "npx",
1322
+ "args": ["-y", "dbgraph-mcp"]
1323
+ }
1324
+ }
1325
+ }
1326
+
1327
+ For opencode (mcp JSON):
1328
+ {
1329
+ "mcp": {
1330
+ "dbgraph-mcp": {
1331
+ "type": "local",
1332
+ "command": ["npx", "-y", "dbgraph-mcp"]
1333
+ }
1334
+ }
1335
+ }
1336
+
1337
+ For Codex CLI (TOML, ~/.codex/config.toml):
1338
+ [mcp_servers.dbgraph-mcp]
1339
+ command = "npx"
1340
+ args = ["-y", "dbgraph-mcp"]
1341
+
1342
+ Config file locations:
1343
+ Claude Code \u2014 Windows: %APPDATA%\\Claude\\claude_desktop_config.json
1344
+ Linux/macOS: ~/.config/Claude/claude_desktop_config.json
1345
+ Cursor \u2014 ~/.cursor/mcp.json
1346
+ Gemini CLI \u2014 ~/.gemini/settings.json
1347
+ VS Code \u2014 ~/.vscode/mcp.json
1348
+ opencode \u2014 ~/.config/opencode/opencode.json
1349
+ Codex CLI \u2014 ~/.codex/config.toml
1350
+ `;
1351
+ var DEFAULT_MCP_ENTRY = {
1352
+ command: "npx",
1353
+ args: ["-y", "dbgraph-mcp"]
1354
+ };
1355
+ var CODEX_PROJECT_TRUST_SUFFIX = " (requires trusted project: set trust_level in ~/.codex/config.toml)";
1356
+ function projectSummaryLine(id, action) {
1357
+ if (action === "unsupported") {
1358
+ return `${id} \u2192 not supported with --project
1359
+ `;
1360
+ }
1361
+ const verb = action === "installed" ? "written" : action === "removed" ? "removed" : action === "already" ? "already present" : "absent";
1362
+ const suffix = id === "codex" && action === "installed" ? CODEX_PROJECT_TRUST_SUFFIX : "";
1363
+ return `${id} \u2192 ${verb}${suffix}
1364
+ `;
1365
+ }
1366
+ async function runInstall(options = {}) {
1367
+ const {
1368
+ remove = false,
1369
+ project = false,
1370
+ cwd = process.cwd(),
1371
+ fs: fsSeam = realFsSeam,
1372
+ platform = process.platform,
1373
+ env = process.env,
1374
+ write = (text) => process.stdout.write(text),
1375
+ agents = AGENT_TABLE
1376
+ } = options;
1377
+ const results = [];
1378
+ for (const row of agents) {
1379
+ if (project) {
1380
+ if (row.projectPath === void 0) {
1381
+ results.push({ agent: row.id, action: "unsupported" });
1382
+ continue;
1383
+ }
1384
+ const configPath = resolveProjectConfigPath(platform, cwd, row.projectPath);
1385
+ const fileExists = fsSeam.exists(configPath);
1386
+ if (!fileExists && remove) {
1387
+ results.push({ agent: row.id, action: "absent", path: configPath });
1388
+ continue;
1389
+ }
1390
+ let raw;
1391
+ if (!fileExists) {
1392
+ raw = "";
1393
+ } else {
1394
+ try {
1395
+ raw = fsSeam.readFile(configPath);
1396
+ } catch {
1397
+ raw = row.format === "codex-toml" ? "" : "{}";
1398
+ }
1399
+ }
1400
+ const next = remove ? row.remove(raw) : row.merge(raw, DEFAULT_MCP_ENTRY);
1401
+ if (next === raw) {
1402
+ results.push({ agent: row.id, action: remove ? "absent" : "already", path: configPath });
1403
+ } else {
1404
+ fsSeam.writeFile(configPath, next);
1405
+ results.push({ agent: row.id, action: remove ? "removed" : "installed", path: configPath });
1406
+ }
1407
+ } else {
1408
+ const configPath = row.resolvePath(platform, env);
1409
+ if (configPath === void 0) {
1410
+ results.push({ agent: row.id, action: "skipped" });
1411
+ continue;
1412
+ }
1413
+ if (!fsSeam.exists(configPath)) {
1414
+ results.push({ agent: row.id, action: "absent" });
1415
+ continue;
1416
+ }
1417
+ let raw;
1418
+ try {
1419
+ raw = fsSeam.readFile(configPath);
1420
+ } catch {
1421
+ raw = row.format === "codex-toml" ? "" : "{}";
1422
+ }
1423
+ const next = remove ? row.remove(raw) : row.merge(raw, DEFAULT_MCP_ENTRY);
1424
+ if (next === raw) {
1425
+ results.push({ agent: row.id, action: remove ? "absent" : "already", path: configPath });
1426
+ } else {
1427
+ fsSeam.writeFile(configPath, next);
1428
+ results.push({ agent: row.id, action: remove ? "removed" : "installed", path: configPath });
1429
+ }
1430
+ }
1431
+ }
1432
+ if (project) {
1433
+ for (const r of results) {
1434
+ write(projectSummaryLine(r.agent, r.action));
1435
+ }
1436
+ } else {
1437
+ const allSkippedOrAbsent = results.every(
1438
+ (r) => r.action === "skipped" || r.action === "absent"
1439
+ );
1440
+ if (allSkippedOrAbsent) {
1441
+ write(MANUAL_SNIPPET);
1442
+ } else {
1443
+ for (const r of results) {
1444
+ if (r.action !== "skipped" && r.action !== "absent") {
1445
+ const row = agents.find((t) => t.id === r.agent);
1446
+ const name = row?.displayName ?? r.agent;
1447
+ write(`${name} \u2192 ${r.action}${r.path !== void 0 ? ` (${r.path})` : ""}
1448
+ `);
1449
+ }
1450
+ }
1451
+ }
1452
+ }
1453
+ return { type: "success" };
1454
+ }
1455
+
1456
+ // src/cli/commands/doctor.ts
1457
+ var UNAVAILABLE_PROBE = {
1458
+ nativeDriver: false,
1459
+ cliTools: [],
1460
+ odbc: false
1461
+ };
1462
+ async function runDoctor(deps) {
1463
+ const { engine, probe } = deps;
1464
+ let probeResult;
1465
+ try {
1466
+ probeResult = await probe();
1467
+ } catch {
1468
+ probeResult = UNAVAILABLE_PROBE;
1469
+ }
1470
+ const { resolvedProfile, chosenStrategy } = resolveProfileAndStrategy(engine, probeResult);
1471
+ const view = {
1472
+ engine,
1473
+ nativeDriver: probeResult.nativeDriver,
1474
+ cliTools: probeResult.cliTools,
1475
+ odbc: probeResult.odbc,
1476
+ resolvedProfile,
1477
+ chosenStrategy
1478
+ };
1479
+ return formatDoctor(view);
1480
+ }
1481
+ function resolveProfileAndStrategy(engine, probe) {
1482
+ if (engine === "mssql") {
1483
+ const profile = resolveProfile(probe);
1484
+ const profileName = `${profile.variant}@${profile.versionRange}`;
1485
+ let chosenStrategy;
1486
+ const sqlcmdTool = probe.cliTools.find((t) => t.tool === "sqlcmd");
1487
+ if (sqlcmdTool?.path !== null && sqlcmdTool?.path !== void 0) {
1488
+ chosenStrategy = "sqlcmd";
1489
+ } else if (probe.odbc) {
1490
+ chosenStrategy = "odbc";
1491
+ } else if (probe.nativeDriver) {
1492
+ chosenStrategy = "native-tedious";
1493
+ } else {
1494
+ chosenStrategy = "unavailable";
1495
+ }
1496
+ return { resolvedProfile: profileName, chosenStrategy };
1497
+ }
1498
+ if (engine === "pg") {
1499
+ const chosenStrategy = probe.nativeDriver ? "native-pg" : "unavailable";
1500
+ return { resolvedProfile: "n/a", chosenStrategy };
1501
+ }
1502
+ if (engine === "mysql") {
1503
+ const chosenStrategy = probe.nativeDriver ? "native-mysql2" : "unavailable";
1504
+ return { resolvedProfile: "n/a", chosenStrategy };
1505
+ }
1506
+ if (engine === "sqlite") {
1507
+ const chosenStrategy = probe.nativeDriver ? "native-sqlite" : "unavailable";
1508
+ return { resolvedProfile: "n/a", chosenStrategy };
1509
+ }
1510
+ return { resolvedProfile: "unknown@any", chosenStrategy: "unavailable" };
1511
+ }
1512
+
1513
+ // src/cli/dispatch.ts
1514
+ function buildLogger(args) {
1515
+ const quiet = args.flags["quiet"] === true || args.flags["q"] === true;
1516
+ return createConsoleLogger({ level: quiet ? "warn" : "info" });
1517
+ }
1518
+ async function handleInit(args) {
1519
+ const projectRoot = process.cwd();
1520
+ if (args.flags["i"] === true || args.flags["interactive"] === true) {
1521
+ return runInit({ projectRoot, interactive: true });
1522
+ }
1523
+ const dialect = typeof args.flags["dialect"] === "string" ? args.flags["dialect"] : "";
1524
+ if (dialect === "sqlite") {
1525
+ const file = typeof args.flags["file"] === "string" ? args.flags["file"] : "";
1526
+ const driver = args.flags["driver"];
1527
+ return runInit({
1528
+ projectRoot,
1529
+ dialect: "sqlite",
1530
+ file,
1531
+ ...driver === "better-sqlite3" || driver === "node:sqlite" ? { driver } : {}
1532
+ });
1533
+ }
1534
+ if (dialect === "mssql") {
1535
+ const server = typeof args.flags["server"] === "string" ? args.flags["server"] : "";
1536
+ const database = typeof args.flags["database"] === "string" ? args.flags["database"] : "";
1537
+ const user = typeof args.flags["user"] === "string" ? args.flags["user"] : "";
1538
+ const password = typeof args.flags["password"] === "string" ? args.flags["password"] : "";
1539
+ const port = typeof args.flags["port"] === "string" ? args.flags["port"] : void 0;
1540
+ const domain = typeof args.flags["domain"] === "string" ? args.flags["domain"] : void 0;
1541
+ return runInit({
1542
+ projectRoot,
1543
+ dialect: "mssql",
1544
+ server,
1545
+ database,
1546
+ user,
1547
+ password,
1548
+ ...port !== void 0 ? { port } : {},
1549
+ ...domain !== void 0 ? { domain } : {}
1550
+ });
1551
+ }
1552
+ return runInit({ projectRoot, interactive: true });
1553
+ }
1554
+ async function handleSync(args) {
1555
+ const full = args.flags["full"] === true;
1556
+ const projectRoot = process.cwd();
1557
+ const logger = buildLogger(args);
1558
+ const { adapter, store } = await openConnections(projectRoot, logger);
1559
+ try {
1560
+ const summary = await runSync({ adapter, store, full, logger });
1561
+ process.stdout.write(formatSyncSummary(summary));
1562
+ return { type: "success" };
1563
+ } finally {
1564
+ await adapter.close();
1565
+ await store.close();
1566
+ }
1567
+ }
1568
+ async function handleStatus(args) {
1569
+ const projectRoot = process.cwd();
1570
+ const logger = buildLogger(args);
1571
+ const { adapter, store } = await openConnections(projectRoot, logger);
1572
+ try {
1573
+ const result = await runStatus({ adapter, store });
1574
+ process.stdout.write(result.output);
1575
+ return { type: "success" };
1576
+ } finally {
1577
+ await adapter.close();
1578
+ await store.close();
1579
+ }
1580
+ }
1581
+ async function handleQuery(args) {
1582
+ const term = args.positionals[0] ?? "";
1583
+ const json = args.flags["json"] === true;
1584
+ const projectRoot = process.cwd();
1585
+ const logger = buildLogger(args);
1586
+ const { adapter, store } = await openConnections(projectRoot, logger);
1587
+ try {
1588
+ const result = await runQuery({ store, term, json });
1589
+ process.stdout.write(result.output);
1590
+ return result;
1591
+ } finally {
1592
+ await adapter.close();
1593
+ await store.close();
1594
+ }
1595
+ }
1596
+ async function handleExplore(args) {
1597
+ const qname = args.positionals[0] ?? "";
1598
+ const detail = parseDetail(args.flags["detail"]);
1599
+ const projectRoot = process.cwd();
1600
+ const logger = buildLogger(args);
1601
+ const { adapter, store } = await openConnections(projectRoot, logger);
1602
+ try {
1603
+ const result = await runExplore({ store, qname, detail });
1604
+ process.stdout.write(result.output);
1605
+ return { type: "success" };
1606
+ } finally {
1607
+ await adapter.close();
1608
+ await store.close();
1609
+ }
1610
+ }
1611
+ async function handleObject(args) {
1612
+ const qname = args.positionals[0] ?? "";
1613
+ const detail = parseDetail(args.flags["detail"]);
1614
+ const projectRoot = process.cwd();
1615
+ const logger = buildLogger(args);
1616
+ const { adapter, store } = await openConnections(projectRoot, logger);
1617
+ try {
1618
+ const result = await runObject({ store, qname, detail });
1619
+ process.stdout.write(result.output);
1620
+ return { type: "success" };
1621
+ } finally {
1622
+ await adapter.close();
1623
+ await store.close();
1624
+ }
1625
+ }
1626
+ async function handleAffected(args) {
1627
+ const sqlFile = args.positionals[0] ?? "";
1628
+ if (sqlFile === "") {
1629
+ throw new Error("dbgraph affected: <script.sql> argument is required");
1630
+ }
1631
+ const json = args.flags["json"] === true;
1632
+ const detail = parseDetail(args.flags["detail"]);
1633
+ const projectRoot = process.cwd();
1634
+ const logger = buildLogger(args);
1635
+ const { adapter, store } = await openConnections(projectRoot, logger);
1636
+ try {
1637
+ const result = await runAffected({ store, sqlFile, json, detail });
1638
+ process.stdout.write(result.output);
1639
+ return result;
1640
+ } finally {
1641
+ await adapter.close();
1642
+ await store.close();
1643
+ }
1644
+ }
1645
+ async function handleInstall(args) {
1646
+ const remove = args.flags["remove"] === true;
1647
+ const project = args.flags["project"] === true;
1648
+ await runInstall({ remove, project, cwd: process.cwd(), fs: realFsSeam });
1649
+ return { type: "success" };
1650
+ }
1651
+ async function handleDoctor(_args) {
1652
+ const projectRoot = process.cwd();
1653
+ let engine = "mssql";
1654
+ try {
1655
+ const { readFileSync: readFileSync4 } = await import("fs");
1656
+ const { join: join2 } = await import("path");
1657
+ const { parseConfig } = await import("./parse-config-5TLAZCR3.js");
1658
+ const configPath = join2(projectRoot, "dbgraph.config.json");
1659
+ const rawJson = JSON.parse(readFileSync4(configPath, "utf-8"));
1660
+ const cfg = parseConfig(rawJson);
1661
+ engine = cfg.dialect;
1662
+ } catch {
1663
+ }
1664
+ const probe = await buildProbe(engine);
1665
+ const output = await runDoctor({ engine, probe });
1666
+ process.stdout.write(output);
1667
+ return { type: "success" };
1668
+ }
1669
+ async function buildProbe(engine) {
1670
+ try {
1671
+ const barrel = await import("./src-5YCYYWGV.js");
1672
+ if (engine === "mssql") {
1673
+ const probe = new barrel.MssqlCapabilityProbe();
1674
+ return () => probe.probe();
1675
+ }
1676
+ if (engine === "pg") {
1677
+ const probe = new barrel.PgCapabilityProbe();
1678
+ return () => probe.probe();
1679
+ }
1680
+ if (engine === "mysql") {
1681
+ const probe = new barrel.MysqlCapabilityProbe();
1682
+ return () => probe.probe();
1683
+ }
1684
+ if (engine === "sqlite") {
1685
+ const probe = new barrel.SqliteCapabilityProbe();
1686
+ return () => probe.probe();
1687
+ }
1688
+ } catch {
1689
+ }
1690
+ return async () => ({
1691
+ nativeDriver: false,
1692
+ cliTools: [],
1693
+ odbc: false
1694
+ });
1695
+ }
1696
+ async function handleDiff(args) {
1697
+ const last = args.flags["last"] === true;
1698
+ const projectRoot = process.cwd();
1699
+ const logger = buildLogger(args);
1700
+ const { adapter, store } = await openConnections(projectRoot, logger);
1701
+ try {
1702
+ let result;
1703
+ if (last) {
1704
+ result = await runDiff({ store, last: true });
1705
+ } else {
1706
+ const snapA = args.positionals[0] ?? "";
1707
+ const snapB = args.positionals[1] ?? "";
1708
+ result = await runDiff({ store, snapA, snapB });
1709
+ }
1710
+ process.stdout.write(result.output);
1711
+ return result;
1712
+ } finally {
1713
+ await adapter.close();
1714
+ await store.close();
1715
+ }
1716
+ }
1717
+ var COMMAND_TABLE = {
1718
+ init: handleInit,
1719
+ sync: handleSync,
1720
+ status: handleStatus,
1721
+ query: handleQuery,
1722
+ explore: handleExplore,
1723
+ object: handleObject,
1724
+ diff: handleDiff,
1725
+ affected: handleAffected,
1726
+ install: handleInstall,
1727
+ doctor: handleDoctor
1728
+ };
1729
+ function dispatch(command) {
1730
+ const handler = COMMAND_TABLE[command];
1731
+ if (handler !== void 0) {
1732
+ return { type: "handler", handler };
1733
+ }
1734
+ return { type: "unknown", command };
1735
+ }
1736
+
1737
+ // src/cli/exit-code.ts
1738
+ function exitCodeFor(input) {
1739
+ if (input instanceof DbgraphError) {
1740
+ if (input instanceof PermissionError) return 3;
1741
+ if (input instanceof UnsupportedDialectError) return 4;
1742
+ if (input instanceof ConnectionError) return 2;
1743
+ return 2;
1744
+ }
1745
+ const typed = input;
1746
+ switch (typed.type) {
1747
+ case "success":
1748
+ return 0;
1749
+ case "negative":
1750
+ return 1;
1751
+ case "unknownCommand":
1752
+ return 2;
1753
+ }
1754
+ }
1755
+
1756
+ // src/cli/cli.ts
1757
+ import { pathToFileURL } from "url";
1758
+ var USAGE_TEXT = `
1759
+ dbgraph \u2014 database schema graph indexer
1760
+
1761
+ Usage: dbgraph <command> [options]
1762
+
1763
+ Commands:
1764
+ init Initialize the graph index for a database
1765
+ sync Synchronize the graph index with the database
1766
+ status Show the current state of the graph index
1767
+ query Search the graph index for a term
1768
+ explore Explore a node and its neighbors in the graph
1769
+ object Show one object in full (columns, constraints, indexes, triggers)
1770
+ diff Compare two snapshots of the graph index
1771
+ affected Analyze DDL to show impacted objects (--json for machine output)
1772
+ install Wire dbgraph-mcp into supported MCP agents (--project for project scope, --remove to undo)
1773
+ doctor Run a content-free connectivity self-test (safe to share)
1774
+ mcp Serve the MCP tools over stdio (default) or Streamable HTTP (--http)
1775
+
1776
+ Options:
1777
+ --help, -h Show this help text
1778
+ --version, -v Print the dbgraph version and exit
1779
+
1780
+ Run "dbgraph <command> --help" for command-specific options.
1781
+ `.trim();
1782
+ async function runCli(argv) {
1783
+ const parsed = parseArgv(argv);
1784
+ if (parsed.command === "--help" || parsed.command === "-h" || parsed.flags["help"] === true || parsed.flags["h"] === true) {
1785
+ process.stdout.write(USAGE_TEXT + "\n");
1786
+ return 0;
1787
+ }
1788
+ if (parsed.command === "--version" || parsed.command === "-v" || parsed.flags["version"] === true || parsed.flags["v"] === true) {
1789
+ process.stdout.write((process.env["DBGRAPH_BUILD_VERSION"] ?? DBGRAPH_VERSION) + "\n");
1790
+ return 0;
1791
+ }
1792
+ const dispatchResult = dispatch(parsed.command);
1793
+ if (dispatchResult.type === "unknown") {
1794
+ process.stderr.write(`Unknown command: "${parsed.command}"
1795
+
1796
+ ${USAGE_TEXT}
1797
+ `);
1798
+ return exitCodeFor({ type: "unknownCommand", command: parsed.command });
1799
+ }
1800
+ try {
1801
+ const outcome = await dispatchResult.handler(parsed);
1802
+ return exitCodeFor(outcome);
1803
+ } catch (err) {
1804
+ if (err instanceof ConnectivityUnavailableError) {
1805
+ process.stderr.write(formatOutcome(err.outcome) + "\n");
1806
+ return exitCodeFor(err);
1807
+ }
1808
+ if (err instanceof DbgraphError) {
1809
+ process.stderr.write(`Error: ${err.message}
1810
+ `);
1811
+ return exitCodeFor(err);
1812
+ }
1813
+ throw err;
1814
+ }
1815
+ }
1816
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
1817
+ runCli(process.argv.slice(2)).then((code) => {
1818
+ process.exit(code);
1819
+ }).catch((err) => {
1820
+ console.error("Unexpected error:", err);
1821
+ process.exit(2);
1822
+ });
1823
+ }
1824
+ export {
1825
+ USAGE_TEXT,
1826
+ runCli
1827
+ };