@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/LICENSE +21 -0
- package/README.md +317 -0
- package/dist/chunk-2ESYSVXG.js +48 -0
- package/dist/chunk-CK6XTW3E.js +31 -0
- package/dist/chunk-JN26WCE7.js +231 -0
- package/dist/chunk-PQBJ4PR5.js +7994 -0
- package/dist/chunk-PXNV4PMV.js +3790 -0
- package/dist/chunk-TH4OVS4W.js +119 -0
- package/dist/cli.js +1827 -0
- package/dist/connectivity-outcome-GLCSH4UL.js +7 -0
- package/dist/errors-IVGCGSZR.js +31 -0
- package/dist/esm-WVQHRQ3J.js +5195 -0
- package/dist/index.cjs +125968 -0
- package/dist/index.d.cts +2692 -0
- package/dist/index.d.ts +2692 -0
- package/dist/index.js +8230 -0
- package/dist/lib-HSEC5IHY.js +32422 -0
- package/dist/mcp.js +7886 -0
- package/dist/mysql2-JRR5EBJC.js +15624 -0
- package/dist/open-WLBPYZLW.js +602 -0
- package/dist/parse-config-5TLAZCR3.js +8 -0
- package/dist/src-5YCYYWGV.js +147 -0
- package/dist/tedious-3WZBQM6C.js +59836 -0
- package/package.json +72 -0
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
|
+
};
|