@ddt-tools/cli 0.2.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/index.cjs ADDED
@@ -0,0 +1,1288 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ catalogCommand: () => catalogCommand,
34
+ compareProfilesCommand: () => compareProfilesCommand,
35
+ connectionCommand: () => connectionCommand,
36
+ execCommand: () => execCommand,
37
+ explorerCommand: () => explorerCommand,
38
+ queryLogCommand: () => queryLogCommand,
39
+ saferAlternativeCommand: () => saferAlternativeCommand,
40
+ safetyCommand: () => safetyCommand,
41
+ scaffoldCommand: () => scaffoldCommand,
42
+ searchCommand: () => searchCommand,
43
+ sketchCommand: () => sketchCommand
44
+ });
45
+ module.exports = __toCommonJS(index_exports);
46
+
47
+ // src/commands/connection.ts
48
+ var import_commander = require("commander");
49
+ var import_core = require("@ddt-tools/core");
50
+ function connectionCommand() {
51
+ const cmd = new import_commander.Command("connection").description("Manage Databricks connection profiles.");
52
+ cmd.command("add <name>").description("Store a new connection profile.").option("--host <host>", "Workspace host, e.g. dbc-12345-abcd.cloud.databricks.com").option("--auth <method>", "One of: pat | oauth-m2m | oauth-u2m | azure-ad | google-idc", "pat").option(
53
+ "--token <token>",
54
+ "PAT token \u2014 use env:DATABRICKS_TOKEN to reference an env var, never inline a secret"
55
+ ).option("--client-id <id>", "OAuth M2M / Azure AD client ID").option("--client-secret <secret>", "OAuth M2M / Azure AD client secret").option("--tenant-id <id>", "Azure AD tenant ID").option(
56
+ "--service-account-key-path <path>",
57
+ "Path to a GCP service-account key JSON file (for google-idc auth)"
58
+ ).requiredOption("--warehouse-id <id>", "SQL warehouse ID for DDL operations").option("--catalog <catalog>", "Default Unity Catalog catalog").option("--schema <schema>", "Default schema").action(async (name, opts) => {
59
+ if (!opts.host) throw new Error("--host is required");
60
+ const auth = buildAuth(opts);
61
+ const profile = {
62
+ name,
63
+ platform: "Databricks",
64
+ auth,
65
+ warehouseId: String(opts.warehouseId),
66
+ ...opts.catalog ? { catalog: String(opts.catalog) } : {},
67
+ ...opts.schema ? { schema: String(opts.schema) } : {}
68
+ };
69
+ await (0, import_core.upsertProfile)(profile);
70
+ console.log(`Saved profile "${name}" \u2192 ${(0, import_core.defaultProfilesPath)()}`);
71
+ });
72
+ cmd.command("list").description("List configured Databricks profiles.").action(async () => {
73
+ const profiles = await (0, import_core.loadProfiles)();
74
+ if (profiles.length === 0) {
75
+ console.log(`(no profiles configured at ${(0, import_core.defaultProfilesPath)()})`);
76
+ return;
77
+ }
78
+ for (const p of profiles) {
79
+ console.log(`${p.name} ${p.auth.method} ${p.auth.host} warehouse=${p.warehouseId}`);
80
+ }
81
+ });
82
+ cmd.command("get <name>").description("Print a profile (secrets are redacted).").action(async (name) => {
83
+ const p = await (0, import_core.getProfile)(name);
84
+ const redacted = {
85
+ ...p,
86
+ auth: redactAuth(p.auth)
87
+ };
88
+ console.log(JSON.stringify(redacted, null, 2));
89
+ });
90
+ cmd.command("remove <name>").description("Delete a connection profile.").action(async (name) => {
91
+ const removed = await (0, import_core.removeProfile)(name);
92
+ console.log(removed ? `Removed profile "${name}".` : `No profile "${name}" found.`);
93
+ if (!removed) process.exitCode = 1;
94
+ });
95
+ cmd.command("test <name>").description(
96
+ "Probe a connection profile via the workspace REST API (`/api/2.0/preview/scim/v2/Me`)."
97
+ ).action(async (name) => {
98
+ const profile = await (0, import_core.getProfile)(name);
99
+ const conn = (0, import_core.createConnection)(profile);
100
+ try {
101
+ await conn.connect();
102
+ const info = await conn.test();
103
+ console.log(`OK ${name}`);
104
+ console.log(` host: ${profile.auth.host}`);
105
+ console.log(` account: ${info.account}`);
106
+ console.log(` version: ${info.version}`);
107
+ } catch (err) {
108
+ console.error(`FAIL ${name}: ${err instanceof Error ? err.message : String(err)}`);
109
+ process.exitCode = 1;
110
+ } finally {
111
+ await conn.disconnect();
112
+ }
113
+ });
114
+ return cmd;
115
+ }
116
+ function buildAuth(opts) {
117
+ const host = String(opts.host);
118
+ const method = String(opts.auth ?? "pat").toLowerCase();
119
+ switch (method) {
120
+ case "pat":
121
+ case "personal_access_token":
122
+ if (!opts.token) throw new Error("--token is required for PAT auth");
123
+ return { method: "PERSONAL_ACCESS_TOKEN", host, token: String(opts.token) };
124
+ case "oauth-m2m":
125
+ case "oauth_m2m":
126
+ if (!opts.clientId || !opts.clientSecret) {
127
+ throw new Error("--client-id and --client-secret are required for OAuth M2M");
128
+ }
129
+ return {
130
+ method: "OAUTH_M2M",
131
+ host,
132
+ clientId: String(opts.clientId),
133
+ clientSecret: String(opts.clientSecret)
134
+ };
135
+ case "oauth-u2m":
136
+ case "oauth_u2m":
137
+ return { method: "OAUTH_U2M", host };
138
+ case "azure-ad":
139
+ case "azure_ad":
140
+ if (!opts.tenantId || !opts.clientId) {
141
+ throw new Error("--tenant-id and --client-id are required for Azure AD");
142
+ }
143
+ return {
144
+ method: "AZURE_AD",
145
+ host,
146
+ tenantId: String(opts.tenantId),
147
+ clientId: String(opts.clientId),
148
+ ...opts.clientSecret ? { clientSecret: String(opts.clientSecret) } : {}
149
+ };
150
+ case "google-idc":
151
+ case "google_idc":
152
+ if (!opts.serviceAccountKeyPath) {
153
+ throw new Error("--service-account-key-path is required for google-idc auth");
154
+ }
155
+ return {
156
+ method: "GOOGLE_IDC",
157
+ host,
158
+ serviceAccountKeyPath: String(opts.serviceAccountKeyPath)
159
+ };
160
+ default:
161
+ throw new Error(`Unknown --auth value: ${method}`);
162
+ }
163
+ }
164
+ function redactAuth(auth) {
165
+ if (auth.method === "PERSONAL_ACCESS_TOKEN" && auth.token && !auth.token.startsWith("env:")) {
166
+ return { ...auth, token: "<redacted>" };
167
+ }
168
+ if (auth.method === "OAUTH_M2M" && auth.clientSecret && !auth.clientSecret.startsWith("env:")) {
169
+ return { ...auth, clientSecret: "<redacted>" };
170
+ }
171
+ if (auth.method === "AZURE_AD" && auth.clientSecret && !auth.clientSecret.startsWith("env:")) {
172
+ return { ...auth, clientSecret: "<redacted>" };
173
+ }
174
+ return auth;
175
+ }
176
+
177
+ // src/commands/scaffold.ts
178
+ var import_commander2 = require("commander");
179
+ function scaffoldCommand(name) {
180
+ return new import_commander2.Command(name).description(`${name} \u2014 Databricks (scaffold; later-release deliverable).`).action(() => {
181
+ console.error(
182
+ `ddt ${name}: scaffold \u2014 the Databricks engine for this command is a later deliverable.
183
+ Track progress at Databricks/docs/ROADMAP.md.`
184
+ );
185
+ process.exitCode = 2;
186
+ });
187
+ }
188
+
189
+ // src/commands/safety.ts
190
+ var import_commander3 = require("commander");
191
+ var import_core2 = require("@ddt-tools/core");
192
+ function safetyCommand() {
193
+ const cmd = new import_commander3.Command("safety");
194
+ cmd.description(
195
+ "Inspect the safety-finding catalog. See `ddt safety list` and `ddt safety explain <code>`."
196
+ );
197
+ cmd.command("list").description("List every known finding code with category + one-line summary.").option("--format <format>", "Output format: table | json", "table").option(
198
+ "--category <kind>",
199
+ "Filter to one category: unrecoverable | destructive | expensive | warning. Default: all."
200
+ ).action((opts) => {
201
+ const codes = import_core2.safety.listFindingCodes();
202
+ let entries = codes.map((c) => import_core2.safety.explainFinding(c)).filter((e) => e !== void 0);
203
+ if (opts.category) {
204
+ const want = opts.category.toUpperCase();
205
+ const valid = ["UNRECOVERABLE", "DESTRUCTIVE", "EXPENSIVE", "WARNING"];
206
+ if (!valid.includes(want)) {
207
+ console.error(
208
+ `Unknown --category "${opts.category}". Use one of: ${valid.join(" | ").toLowerCase()}.`
209
+ );
210
+ process.exitCode = 1;
211
+ return;
212
+ }
213
+ entries = entries.filter((e) => e.category === want);
214
+ }
215
+ if ((opts.format ?? "table") === "json") {
216
+ process.stdout.write(JSON.stringify(entries, null, 2) + "\n");
217
+ return;
218
+ }
219
+ if (entries.length === 0) {
220
+ console.log("(no entries match the filter)");
221
+ return;
222
+ }
223
+ printList(entries);
224
+ });
225
+ cmd.command("explain").description('Expand a finding code into a deep "why this is dangerous" page.').argument("<code>", "The finding code (e.g. DROP_UNRECOVERABLE, COLUMN_TYPE_CHANGE)").option("--format <format>", "Output format: text | json | markdown", "text").action((codeArg, opts) => {
226
+ const code = codeArg.toUpperCase();
227
+ const entry = import_core2.safety.explainFinding(code);
228
+ if (!entry) {
229
+ console.error(`Unknown safety finding code: "${codeArg}"`);
230
+ console.error(" Try: ddt safety list");
231
+ process.exitCode = 1;
232
+ return;
233
+ }
234
+ const format = (opts.format ?? "text").toLowerCase();
235
+ if (format === "json") {
236
+ process.stdout.write(JSON.stringify(entry, null, 2) + "\n");
237
+ return;
238
+ }
239
+ if (format === "markdown") {
240
+ process.stdout.write(renderMarkdown(entry) + "\n");
241
+ return;
242
+ }
243
+ printDeep(entry);
244
+ });
245
+ return cmd;
246
+ }
247
+ function printList(entries) {
248
+ const byCategory = /* @__PURE__ */ new Map();
249
+ for (const e of entries) {
250
+ const arr = byCategory.get(e.category) ?? [];
251
+ arr.push(e);
252
+ byCategory.set(e.category, arr);
253
+ }
254
+ const order = [
255
+ "UNRECOVERABLE",
256
+ "DESTRUCTIVE",
257
+ "EXPENSIVE",
258
+ "WARNING"
259
+ ];
260
+ for (const cat of order) {
261
+ const arr = byCategory.get(cat);
262
+ if (!arr || arr.length === 0) continue;
263
+ console.log(`# ${cat} (${arr.length})`);
264
+ for (const e of arr) {
265
+ console.log(` ${e.code}`);
266
+ console.log(` ${e.title}`);
267
+ console.log(` ${e.summary}`);
268
+ }
269
+ console.log("");
270
+ }
271
+ console.log("Use `ddt safety explain <code>` for the full per-finding write-up.");
272
+ }
273
+ function printDeep(entry) {
274
+ console.log(`${entry.code} [${entry.category}]`);
275
+ console.log(entry.title);
276
+ console.log("");
277
+ console.log("Summary");
278
+ console.log(` ${entry.summary}`);
279
+ console.log("");
280
+ console.log("Why this is dangerous");
281
+ for (const line of wrap(entry.whyDangerous, 76)) {
282
+ console.log(` ${line}`);
283
+ }
284
+ console.log("");
285
+ console.log("What cannot be reversed");
286
+ for (const item of entry.cannotBeReversed) console.log(` - ${item}`);
287
+ console.log("");
288
+ console.log("Safer alternatives");
289
+ for (const item of entry.saferAlternatives) console.log(` - ${item}`);
290
+ console.log("");
291
+ if (entry.requiredGates.length > 0) {
292
+ console.log("Required gates to allow this finding through");
293
+ for (const g of entry.requiredGates) console.log(` - ${g}`);
294
+ console.log("");
295
+ }
296
+ if (entry.example) {
297
+ console.log("Example");
298
+ console.log(` ${entry.example}`);
299
+ }
300
+ }
301
+ function renderMarkdown(entry) {
302
+ const lines = [];
303
+ lines.push(`## ${entry.code} \u2014 ${entry.title}`);
304
+ lines.push("");
305
+ lines.push(`**Category:** ${entry.category}`);
306
+ lines.push("");
307
+ lines.push(`**Summary:** ${entry.summary}`);
308
+ lines.push("");
309
+ lines.push("### Why this is dangerous");
310
+ lines.push(entry.whyDangerous);
311
+ lines.push("");
312
+ lines.push("### What cannot be reversed");
313
+ for (const item of entry.cannotBeReversed) lines.push(`- ${item}`);
314
+ lines.push("");
315
+ lines.push("### Safer alternatives");
316
+ for (const item of entry.saferAlternatives) lines.push(`- ${item}`);
317
+ if (entry.requiredGates.length > 0) {
318
+ lines.push("");
319
+ lines.push("### Required gates");
320
+ for (const g of entry.requiredGates) lines.push(`- \`${g}\``);
321
+ }
322
+ if (entry.example) {
323
+ lines.push("");
324
+ lines.push("### Example");
325
+ lines.push("```");
326
+ lines.push(entry.example);
327
+ lines.push("```");
328
+ }
329
+ return lines.join("\n");
330
+ }
331
+ function wrap(text, width) {
332
+ const words = text.split(/\s+/);
333
+ const lines = [];
334
+ let current = "";
335
+ for (const word of words) {
336
+ if (current.length === 0) {
337
+ current = word;
338
+ } else if (current.length + 1 + word.length <= width) {
339
+ current += " " + word;
340
+ } else {
341
+ lines.push(current);
342
+ current = word;
343
+ }
344
+ }
345
+ if (current.length > 0) lines.push(current);
346
+ return lines;
347
+ }
348
+
349
+ // src/commands/safer-alternative.ts
350
+ var import_node_fs = require("fs");
351
+ var import_commander4 = require("commander");
352
+ var import_core3 = require("@ddt-tools/core");
353
+ function saferAlternativeCommand() {
354
+ const cmd = new import_commander4.Command("safer-alternative");
355
+ cmd.description(
356
+ "AI-assist: propose a safer DDL alternative for a safety finding. Requires a configured AI provider (ddt ai status)."
357
+ ).requiredOption("--code <code>", "SafetyFindingCode (e.g. COLUMN_DROP, DROP_UNRECOVERABLE).").requiredOption("--fqn <fqn>", "Object FQN the finding refers to.").requiredOption("--object-type <type>", "Object type (e.g. MANAGED_TABLE, VIEW, FUNCTION).").requiredOption("--reason <text>", "Short reason text from the safety classifier.").option(
358
+ "--gate <gate>",
359
+ "Optional SafetyGate the finding raised (e.g. REQUIRE_ALLOW_DROP_COLUMN)."
360
+ ).option("--sql <path>", 'Path to a file with the dangerous DDL. Use "-" to read from stdin.').option("--context <path>", "Optional path to the surrounding source DDL.").option("--intent <text>", "Optional human-authored intent notes.").option("--format <fmt>", "Output format: text | json. Default text.", "text").option(
361
+ "--ai-max-spend <usd>",
362
+ "Refuse the call if today's estimated spend \u2265 this (USD). 0 = no cap.",
363
+ "0"
364
+ ).action(async (opts) => {
365
+ const dangerousSql = await readInput(
366
+ opts.sql,
367
+ '--sql is required (use a path or "-" for stdin).'
368
+ );
369
+ const contextSql = opts.context ? await import_node_fs.promises.readFile(String(opts.context), "utf8") : void 0;
370
+ const finding = {
371
+ code: String(opts.code),
372
+ category: "DESTRUCTIVE",
373
+ fqn: String(opts.fqn),
374
+ objectType: String(opts.objectType),
375
+ reason: String(opts.reason),
376
+ gate: opts.gate ? String(opts.gate) : void 0
377
+ };
378
+ const result = await import_core3.saferAlternative.suggestSaferAlternative(
379
+ {
380
+ finding,
381
+ dangerousSql,
382
+ contextSql,
383
+ intentNotes: opts.intent ? String(opts.intent) : void 0
384
+ },
385
+ {
386
+ completeFn: async (prompt) => {
387
+ const r = await import_core3.ai.complete([{ role: "user", content: prompt }], {
388
+ feature: "safer-alternative",
389
+ maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
390
+ });
391
+ return r.text;
392
+ }
393
+ }
394
+ );
395
+ if (String(opts.format).toLowerCase() === "json") {
396
+ const { rawModelText: _omit, ...keep } = result;
397
+ console.log(JSON.stringify(keep, null, 2));
398
+ return;
399
+ }
400
+ console.log(`Confidence: ${result.confidence}${result.parseFailed ? " (parse failed)" : ""}`);
401
+ console.log(`Reasoning: ${result.reasoning}`);
402
+ if (result.alternativeSql) {
403
+ console.log("");
404
+ console.log("--- Proposed safer DDL ---");
405
+ console.log(result.alternativeSql);
406
+ } else {
407
+ console.warn("No safer alternative SQL was returned.");
408
+ }
409
+ if (result.requiredGates.length > 0) {
410
+ console.log(`Required gates: ${result.requiredGates.join(", ")}`);
411
+ }
412
+ });
413
+ return cmd;
414
+ }
415
+ async function readInput(pathOrDash, missingMessage) {
416
+ if (!pathOrDash) throw new Error(missingMessage);
417
+ const p = String(pathOrDash);
418
+ if (p === "-") {
419
+ const chunks = [];
420
+ for await (const chunk of process.stdin) {
421
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
422
+ }
423
+ return Buffer.concat(chunks).toString("utf8");
424
+ }
425
+ return import_node_fs.promises.readFile(p, "utf8");
426
+ }
427
+
428
+ // src/commands/sketch.ts
429
+ var import_node_fs2 = require("fs");
430
+ var import_commander5 = require("commander");
431
+ var import_core4 = require("@ddt-tools/core");
432
+ var KINDS = [
433
+ "managed-table",
434
+ "external-table",
435
+ "streaming-table",
436
+ "materialized-view",
437
+ "view",
438
+ "function",
439
+ "volume"
440
+ ];
441
+ function sketchCommand() {
442
+ const cmd = new import_commander5.Command("sketch");
443
+ cmd.description(
444
+ "AI-assist: scaffold idiomatic Databricks UC DDL from a prose description. Output always carries a REVIEW BEFORE DEPLOY header."
445
+ ).argument("<kind>", `Object kind: ${KINDS.join(" | ")}`).requiredOption("--description <text>", 'Free-form description. Use "-" to read from stdin.').option("--target <fqn>", "Target FQN (e.g. analytics.public.orders).").option("--context <text>", 'Optional additional context (e.g. "use SQL warehouse small").').option("--out <path>", "Output file. Default stdout.").option("--format <fmt>", "Output format: text | json. Default text.", "text").option(
446
+ "--ai-max-spend <usd>",
447
+ "Refuse the call if today's estimated spend \u2265 this (USD). 0 = no cap.",
448
+ "0"
449
+ ).action(async (kindArg, opts) => {
450
+ const kind = String(kindArg).toLowerCase();
451
+ if (!KINDS.includes(kind)) {
452
+ throw new Error(`Unknown kind "${kindArg}". Use one of: ${KINDS.join(" | ")}`);
453
+ }
454
+ const description = String(opts.description) === "-" ? await readStdin() : String(opts.description);
455
+ const targetFqn = opts.target ? splitFqn(String(opts.target)) : void 0;
456
+ const result = await import_core4.objectSketch.sketchToDdl(
457
+ {
458
+ description,
459
+ objectKind: kind,
460
+ targetFqn,
461
+ additionalContext: opts.context ? String(opts.context) : void 0
462
+ },
463
+ {
464
+ completeFn: async (prompt) => {
465
+ const r = await import_core4.ai.complete([{ role: "user", content: prompt }], {
466
+ feature: "object-sketch",
467
+ maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
468
+ });
469
+ return r.text;
470
+ }
471
+ }
472
+ );
473
+ const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify({ ...result, rawModelText: void 0 }, null, 2) : result.generatedSql;
474
+ if (opts.out) {
475
+ await import_node_fs2.promises.writeFile(String(opts.out), output, "utf8");
476
+ console.log(`Wrote ${String(opts.out)} (${output.length} bytes)`);
477
+ } else {
478
+ console.log(output);
479
+ }
480
+ if (result.assumptions.length > 0) {
481
+ console.error("");
482
+ console.error("Model assumptions:");
483
+ for (const a of result.assumptions) console.error(` - ${a}`);
484
+ }
485
+ if (result.parseFailed) {
486
+ console.warn("Model output could not be parsed \u2014 see the raw text via --format json.");
487
+ }
488
+ });
489
+ return cmd;
490
+ }
491
+ function splitFqn(fqn) {
492
+ const parts = fqn.split(".");
493
+ if (parts.length === 1) return { name: parts[0] };
494
+ if (parts.length === 2) return { schema: parts[0], name: parts[1] };
495
+ if (parts.length === 3) return { catalog: parts[0], schema: parts[1], name: parts[2] };
496
+ throw new Error(`Invalid --target "${fqn}": expected 1, 2, or 3 dot-separated parts.`);
497
+ }
498
+ async function readStdin() {
499
+ const chunks = [];
500
+ for await (const chunk of process.stdin) {
501
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
502
+ }
503
+ return Buffer.concat(chunks).toString("utf8");
504
+ }
505
+
506
+ // src/commands/compare-profiles.ts
507
+ var import_node_fs3 = require("fs");
508
+ var import_commander6 = require("commander");
509
+ var import_core5 = require("@ddt-tools/core");
510
+ function compareProfilesCommand() {
511
+ const cmd = new import_commander6.Command("compare-profiles");
512
+ cmd.description("Manage saved compare profiles (.ddt/compare-profiles.json).");
513
+ cmd.command("list").description("List every saved profile.").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--json", "Emit JSON instead of a human table.").action(async (opts) => {
514
+ const store = new import_core5.compareProfiles.CompareProfilesStore({ root: String(opts.root) });
515
+ const all = await store.list();
516
+ if (opts.json) {
517
+ console.log(JSON.stringify(all, null, 2));
518
+ return;
519
+ }
520
+ if (all.length === 0) {
521
+ console.log("(no compare profiles saved yet)");
522
+ console.log(` store: ${store.path}`);
523
+ return;
524
+ }
525
+ console.log(`${all.length} compare profile(s):`);
526
+ for (const p of all) {
527
+ console.log(` ${p.id.padEnd(24)} ${p.name}`);
528
+ console.log(` source: ${p.source.kind}=${p.source.reference}`);
529
+ console.log(` target: ${p.target.kind}=${p.target.reference}`);
530
+ console.log(` mappings: ${p.mappings.length} updated: ${p.updatedAt}`);
531
+ }
532
+ });
533
+ cmd.command("show").description("Show one profile by id.").argument("<id>", "Profile id (from `compare-profiles list`).").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--json", "Emit JSON instead of a human table.").action(async (id, opts) => {
534
+ const store = new import_core5.compareProfiles.CompareProfilesStore({ root: String(opts.root) });
535
+ const p = await store.get(String(id));
536
+ if (!p) {
537
+ console.error(
538
+ `No profile with id "${id}". Run \`ddt compare-profiles list\` to enumerate.`
539
+ );
540
+ process.exitCode = 1;
541
+ return;
542
+ }
543
+ if (opts.json) {
544
+ console.log(JSON.stringify(p, null, 2));
545
+ return;
546
+ }
547
+ console.log(`Profile: ${p.name} (${p.id})`);
548
+ if (p.description) console.log(` ${p.description}`);
549
+ console.log(
550
+ ` source: ${p.source.kind}=${p.source.reference}${p.source.catalog ? ` cat=${p.source.catalog}` : ""}${p.source.schema ? ` schema=${p.source.schema}` : ""}`
551
+ );
552
+ console.log(
553
+ ` target: ${p.target.kind}=${p.target.reference}${p.target.catalog ? ` cat=${p.target.catalog}` : ""}${p.target.schema ? ` schema=${p.target.schema}` : ""}`
554
+ );
555
+ console.log(` case-sensitive: ${p.caseSensitive ? "yes" : "no"}`);
556
+ console.log(` rewrite-inside-strings: ${p.rewriteInsideStrings ? "yes" : "no"}`);
557
+ console.log(` updatedAt: ${p.updatedAt}`);
558
+ if (p.mappings.length === 0) {
559
+ console.log(" mappings: (none \u2014 identity)");
560
+ } else {
561
+ console.log(` mappings (${p.mappings.length}):`);
562
+ for (const m of p.mappings) console.log(` ${m.source} => ${m.target}`);
563
+ }
564
+ });
565
+ cmd.command("save").description("Upsert a profile from a JSON file or stdin.").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--json-file <path>", 'Path to a JSON file containing one profile. Use "-" for stdin.').action(async (opts) => {
566
+ if (!opts.jsonFile) {
567
+ throw new Error('--json-file is required (use a path or "-" for stdin).');
568
+ }
569
+ const raw = String(opts.jsonFile) === "-" ? await readStdin2() : await import_node_fs3.promises.readFile(String(opts.jsonFile), "utf8");
570
+ const parsed = JSON.parse(raw);
571
+ if (!parsed?.id || !parsed?.name || !parsed?.source || !parsed?.target) {
572
+ throw new Error(
573
+ "Profile JSON must contain at minimum: id, name, source, target. mappings defaults to []."
574
+ );
575
+ }
576
+ const store = new import_core5.compareProfiles.CompareProfilesStore({ root: String(opts.root) });
577
+ const stamped = await store.upsert({ ...parsed, mappings: parsed.mappings ?? [] });
578
+ console.log(`Saved profile "${stamped.name}" (${stamped.id})`);
579
+ console.log(` store: ${store.path}`);
580
+ });
581
+ cmd.command("remove").description("Delete one profile by id.").argument("<id>", "Profile id (from `compare-profiles list`).").option("--root <path>", "Project root. Default cwd.", process.cwd()).action(async (id, opts) => {
582
+ const store = new import_core5.compareProfiles.CompareProfilesStore({ root: String(opts.root) });
583
+ const removed = await store.remove(String(id));
584
+ if (removed) {
585
+ console.log(`Removed profile "${id}".`);
586
+ } else {
587
+ console.warn(`No profile with id "${id}" \u2014 nothing to remove.`);
588
+ }
589
+ });
590
+ cmd.command("preview").description(
591
+ "Preview which FQNs match between the profile's source and target. Local-only \u2014 no workspace round-trip."
592
+ ).argument("<id>", "Profile id (from `compare-profiles list`).").option("--root <path>", "Project root. Default cwd.", process.cwd()).option(
593
+ "--examples <n>",
594
+ "Max example FQNs to show per bucket in human output. Default 5.",
595
+ (v) => parseInt(v, 10),
596
+ 5
597
+ ).option("--json", "Emit the full PreviewSummary as JSON.").action(async (id, opts) => {
598
+ const store = new import_core5.compareProfiles.CompareProfilesStore({ root: String(opts.root) });
599
+ const profile = await store.get(String(id));
600
+ if (!profile) {
601
+ console.error(
602
+ `No profile with id "${id}". Run \`ddt compare-profiles list\` to enumerate.`
603
+ );
604
+ process.exitCode = 1;
605
+ return;
606
+ }
607
+ const root = String(opts.root);
608
+ const source = await resolveEndpointFqns(profile.source, root);
609
+ const target = await resolveEndpointFqns(profile.target, root);
610
+ const summary = import_core5.compareProfiles.previewMatch({
611
+ source: source.fqns,
612
+ target: target.fqns,
613
+ mappings: profile.mappings,
614
+ ...profile.caseSensitive ? { caseSensitive: true } : {}
615
+ });
616
+ if (opts.json) {
617
+ console.log(
618
+ JSON.stringify(
619
+ {
620
+ profile: { id: profile.id, name: profile.name },
621
+ source: {
622
+ kind: profile.source.kind,
623
+ reference: profile.source.reference,
624
+ count: source.fqns.length
625
+ },
626
+ target: {
627
+ kind: profile.target.kind,
628
+ reference: profile.target.reference,
629
+ count: target.fqns.length
630
+ },
631
+ summary
632
+ },
633
+ null,
634
+ 2
635
+ )
636
+ );
637
+ return;
638
+ }
639
+ console.log(`Preview: ${profile.name} (${profile.id})`);
640
+ console.log(
641
+ ` source (${profile.source.kind}=${profile.source.reference}): ${source.fqns.length} object(s)${source.note ? ` \u2014 ${source.note}` : ""}`
642
+ );
643
+ console.log(
644
+ ` target (${profile.target.kind}=${profile.target.reference}): ${target.fqns.length} object(s)${target.note ? ` \u2014 ${target.note}` : ""}`
645
+ );
646
+ console.log("");
647
+ console.log(` matched: ${summary.matchedCount}`);
648
+ console.log(` source-only: ${summary.sourceOnlyCount}`);
649
+ console.log(` target-only: ${summary.targetOnlyCount}`);
650
+ const exN = Math.max(0, Number(opts.examples));
651
+ printBucket("matched", summary.matched, exN);
652
+ printBucket("source-only", summary.sourceOnly, exN);
653
+ printBucket("target-only", summary.targetOnly, exN);
654
+ if (summary.matchedCount === 0 && (summary.sourceOnlyCount > 0 || summary.targetOnlyCount > 0)) {
655
+ console.warn("No FQNs matched \u2014 check the profile's scope and mapping rules.");
656
+ }
657
+ });
658
+ return cmd;
659
+ }
660
+ async function resolveEndpointFqns(endpoint, root) {
661
+ if (endpoint.kind === "connection") {
662
+ const cache = new import_core5.catalog.CatalogCache({ root, connection: endpoint.reference });
663
+ const snapshot = await cache.get();
664
+ if (snapshot.catalogs.length === 0) {
665
+ return {
666
+ fqns: [],
667
+ note: `empty catalog cache at ${cache.path} \u2014 run \`ddt catalog refresh --connection ${endpoint.reference}\` first`
668
+ };
669
+ }
670
+ return { fqns: fqnsFromSnapshot(snapshot, endpoint.catalog, endpoint.schema) };
671
+ }
672
+ if (endpoint.kind === "pac") {
673
+ const contents = await import_core5.pac.readPac(endpoint.reference);
674
+ return { fqns: fqnsFromObjects(contents.model, endpoint.catalog, endpoint.schema) };
675
+ }
676
+ const loaded = await (0, import_core5.loadProject)(endpoint.reference);
677
+ const model = await (0, import_core5.parseProjectModel)(loaded);
678
+ return { fqns: fqnsFromObjects(model, endpoint.catalog, endpoint.schema) };
679
+ }
680
+ function fqnsFromSnapshot(snapshot, catalogScope, schemaScope) {
681
+ const sameId = (a, b) => !!a && !!b && a.toUpperCase() === b.toUpperCase();
682
+ const out = [];
683
+ for (const cat of snapshot.catalogs) {
684
+ if (catalogScope && !sameId(cat.catalog, catalogScope)) continue;
685
+ for (const sc of cat.schemas) {
686
+ if (schemaScope && !sameId(sc.schema, schemaScope)) continue;
687
+ for (const obj of sc.objects) {
688
+ out.push({ database: obj.catalog, schema: obj.schema, name: obj.name });
689
+ }
690
+ }
691
+ }
692
+ return out;
693
+ }
694
+ function fqnsFromObjects(model, catalogScope, schemaScope) {
695
+ const sameId = (a, b) => !!a && !!b && a.toUpperCase() === b.toUpperCase();
696
+ const out = [];
697
+ for (const obj of model) {
698
+ if (catalogScope && !sameId(obj.fqn.database, catalogScope)) continue;
699
+ if (schemaScope && !sameId(obj.fqn.schema, schemaScope)) continue;
700
+ out.push(obj.fqn);
701
+ }
702
+ return out;
703
+ }
704
+ function printBucket(label, items, exampleN) {
705
+ if (items.length === 0) return;
706
+ console.log("");
707
+ console.log(
708
+ ` ${label} examples (showing ${Math.min(exampleN, items.length)} of ${items.length}):`
709
+ );
710
+ for (const fqn of items.slice(0, exampleN)) console.log(` ${fqn}`);
711
+ }
712
+ async function readStdin2() {
713
+ const chunks = [];
714
+ for await (const chunk of process.stdin) {
715
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
716
+ }
717
+ return Buffer.concat(chunks).toString("utf8");
718
+ }
719
+
720
+ // src/commands/explorer.ts
721
+ var import_commander7 = require("commander");
722
+ var import_core6 = require("@ddt-tools/core");
723
+ function explorerCommand() {
724
+ const cmd = new import_commander7.Command("explorer");
725
+ cmd.description(
726
+ "ASCII tree dump of the cached catalog for a connection. Run `ddt catalog refresh` first to populate."
727
+ ).requiredOption("--connection <name>", "Connection profile name (the cache subfolder).").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--filter <query>", "Typeahead substring filter (case-insensitive).").option(
728
+ "--depth <n>",
729
+ "Truncate tree at depth N (0 = root only, 1 = +catalogs, 2 = +schemas, 3 = +object-groups, 4 = +objects). Default unlimited.",
730
+ (v) => parseInt(v, 10)
731
+ ).option("--json", "Emit tree as JSON instead of ASCII.").action(async (opts) => {
732
+ const cache = new import_core6.catalog.CatalogCache({
733
+ root: String(opts.root),
734
+ connection: String(opts.connection)
735
+ });
736
+ const snapshot = await cache.get();
737
+ let tree = import_core6.objectExplorer.treeForSnapshot(snapshot);
738
+ if (opts.filter) tree = import_core6.objectExplorer.filterTree(tree, String(opts.filter));
739
+ if (typeof opts.depth === "number") tree = import_core6.objectExplorer.truncateTree(tree, opts.depth);
740
+ if (snapshot.catalogs.length === 0) {
741
+ console.warn(`Catalog cache is empty for connection "${opts.connection}".`);
742
+ console.warn(` Cache file: ${cache.path}`);
743
+ console.warn(` Run \`ddt catalog refresh --connection ${opts.connection}\` to populate.`);
744
+ return;
745
+ }
746
+ if (opts.json) {
747
+ console.log(JSON.stringify(tree, null, 2));
748
+ return;
749
+ }
750
+ renderAsciiTree(tree);
751
+ });
752
+ return cmd;
753
+ }
754
+ function renderAsciiTree(root) {
755
+ const desc = root.description ? ` (${root.description})` : "";
756
+ console.log(`${root.label}${desc}`);
757
+ const children = root.children ?? [];
758
+ for (let i = 0; i < children.length; i++) {
759
+ renderChild(children[i], "", i === children.length - 1);
760
+ }
761
+ }
762
+ function renderChild(node, prefix, isLast) {
763
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
764
+ const desc = node.description ? ` (${node.description})` : "";
765
+ console.log(`${prefix}${connector}${node.label}${desc}`);
766
+ const children = node.children ?? [];
767
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
768
+ for (let i = 0; i < children.length; i++) {
769
+ renderChild(children[i], childPrefix, i === children.length - 1);
770
+ }
771
+ }
772
+
773
+ // src/commands/catalog.ts
774
+ var import_commander8 = require("commander");
775
+ var import_core7 = require("@ddt-tools/core");
776
+ var BUILTIN_CATALOGS = /* @__PURE__ */ new Set(["system", "samples", "__databricks_internal"]);
777
+ function catalogCommand() {
778
+ const cmd = new import_commander8.Command("catalog");
779
+ cmd.description(
780
+ "Manage the per-connection catalog cache used by the Object Explorer + EE2 intellisense."
781
+ );
782
+ cmd.command("refresh").description("Open the connection, scan every (non-builtin) catalog, write the catalog cache.").requiredOption("-c, --connection <name>", "Connection profile name.").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--catalogs <csv>", "Comma-separated list. Default: every non-builtin catalog.").option("--include-builtins", "Include system / samples / __databricks_internal.", false).option(
783
+ "--concurrency <n>",
784
+ "Bounded-concurrency cap for the bulk-scan pool. Default 10.",
785
+ "10"
786
+ ).option("--no-fingerprint-skip", "Force a full scan even when fingerprints are unchanged.").action(async (opts) => {
787
+ const profile = await (0, import_core7.getProfile)(String(opts.connection));
788
+ const conn = (0, import_core7.createConnection)(profile);
789
+ const cache = new import_core7.catalog.CatalogCache({
790
+ root: String(opts.root),
791
+ connection: profile.name
792
+ });
793
+ console.log(`Connecting to ${profile.auth.host}\u2026`);
794
+ await conn.connect();
795
+ try {
796
+ const cats = await resolveCatalogs(conn, opts);
797
+ if (cats.length === 0) {
798
+ console.warn("No catalogs to scan. Pass --catalogs or --include-builtins.");
799
+ return;
800
+ }
801
+ console.log(
802
+ `Scanning ${cats.length} catalog(s) with concurrency ${Number(opts.concurrency)}.`
803
+ );
804
+ const cached = await cache.get();
805
+ const cachedByCat = new Map(cached.catalogs.map((c) => [c.catalog, c]));
806
+ const concurrency = Math.max(1, Number(opts.concurrency) || 10);
807
+ const fingerprintSkip = opts.fingerprintSkip !== false;
808
+ const { results, errors } = await import_core7.catalog.mapPool(
809
+ cats,
810
+ async (cat) => {
811
+ let freshFingerprint = null;
812
+ try {
813
+ const fpRes = await conn.query(
814
+ import_core7.catalog.fingerprintSqlForCatalog(cat)
815
+ );
816
+ freshFingerprint = import_core7.catalog.parseFingerprintRow(fpRes.rows);
817
+ } catch (err) {
818
+ console.warn(
819
+ `fingerprint(${cat}) failed; falling through to full scan: ${err.message}`
820
+ );
821
+ }
822
+ const cachedCat = cachedByCat.get(cat);
823
+ if (fingerprintSkip && cachedCat && cachedCat.fingerprint != null && freshFingerprint != null && import_core7.catalog.isFresh(cachedCat.fingerprint, freshFingerprint)) {
824
+ console.log(` ${cat}: fresh; reusing cache`);
825
+ return {
826
+ catalog: cat,
827
+ fingerprint: cachedCat.fingerprint,
828
+ schemas: cachedCat.schemas
829
+ };
830
+ }
831
+ const scanRes = await conn.query(import_core7.catalog.bulkScanSqlForCatalog(cat));
832
+ const schemas = import_core7.catalog.parseBulkScanRows(cat, scanRes.rows);
833
+ const fingerprint = freshFingerprint ?? null;
834
+ console.log(
835
+ ` ${cat}: ${schemas.length} schemas / ${schemas.reduce((sum, s) => sum + s.objects.length, 0)} objects`
836
+ );
837
+ return { catalog: cat, fingerprint, schemas };
838
+ },
839
+ { concurrency }
840
+ );
841
+ for (const e of errors) {
842
+ console.warn(`scan failed for "${cats[e.index]}": ${e.error.message}`);
843
+ }
844
+ const snapshot = import_core7.catalog.refreshFingerprints({
845
+ version: import_core7.catalog.CATALOG_SNAPSHOT_VERSION,
846
+ connection: profile.name,
847
+ snapshotAt: (/* @__PURE__ */ new Date()).toISOString(),
848
+ fingerprint: null,
849
+ catalogs: results.filter((r) => !!r)
850
+ });
851
+ await cache.set(snapshot);
852
+ console.log(`Wrote ${snapshot.catalogs.length} catalogs to ${cache.path}`);
853
+ } finally {
854
+ await conn.disconnect();
855
+ }
856
+ });
857
+ cmd.command("show").description("Pretty-print or JSON-dump the cached snapshot for a connection.").requiredOption("-c, --connection <name>", "Connection profile name.").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--json", "Emit JSON instead of summary.").action(async (opts) => {
858
+ const cache = new import_core7.catalog.CatalogCache({
859
+ root: String(opts.root),
860
+ connection: String(opts.connection)
861
+ });
862
+ const snapshot = await cache.get();
863
+ if (opts.json) {
864
+ console.log(JSON.stringify(snapshot, null, 2));
865
+ return;
866
+ }
867
+ if (snapshot.catalogs.length === 0) {
868
+ console.warn(`Catalog cache is empty for "${opts.connection}".`);
869
+ console.warn(` Cache file: ${cache.path}`);
870
+ console.warn(` Run \`ddt catalog refresh --connection ${opts.connection}\` to populate.`);
871
+ return;
872
+ }
873
+ console.log(`Snapshot for "${snapshot.connection}" \u2014 ${snapshot.snapshotAt}`);
874
+ console.log(
875
+ ` Top-level fingerprint: ${snapshot.fingerprint != null ? new Date(snapshot.fingerprint).toISOString() : "(none)"}`
876
+ );
877
+ for (const cat of snapshot.catalogs) {
878
+ const objCount = cat.schemas.reduce((sum, s) => sum + s.objects.length, 0);
879
+ console.log(
880
+ ` ${cat.catalog.padEnd(32)} ${cat.schemas.length} schemas / ${objCount} objects`
881
+ );
882
+ }
883
+ });
884
+ cmd.command("clear").description("Delete the cached snapshot for a connection.").requiredOption("-c, --connection <name>", "Connection profile name.").option("--root <path>", "Project root. Default cwd.", process.cwd()).action(async (opts) => {
885
+ const cache = new import_core7.catalog.CatalogCache({
886
+ root: String(opts.root),
887
+ connection: String(opts.connection)
888
+ });
889
+ await cache.clear();
890
+ console.log(`Cleared cache for "${opts.connection}".`);
891
+ console.log(` was: ${cache.path}`);
892
+ });
893
+ return cmd;
894
+ }
895
+ async function resolveCatalogs(conn, opts) {
896
+ if (opts.catalogs) {
897
+ return String(opts.catalogs).split(",").map((s) => s.trim()).filter(Boolean);
898
+ }
899
+ const res = await conn.query("SHOW CATALOGS");
900
+ const names = res.rows.map((r) => String(r.catalog ?? r.catalog_name ?? r.CATALOG ?? r.CATALOG_NAME ?? "")).filter(Boolean);
901
+ if (opts.includeBuiltins) return names;
902
+ return names.filter((n) => !BUILTIN_CATALOGS.has(n.toLowerCase()));
903
+ }
904
+
905
+ // src/commands/exec.ts
906
+ var import_commander9 = require("commander");
907
+ var import_node_fs4 = require("fs");
908
+ var import_connection = require("@ddt-tools/core/connection");
909
+ var import_core8 = require("@ddt-tools/core");
910
+ var PROD_PATTERN = /\bprod(uction)?\b/i;
911
+ function isProductionProfile(name) {
912
+ return PROD_PATTERN.test(name);
913
+ }
914
+ async function runOnProfile(execFn, profile, sql, timeoutMs) {
915
+ const start = Date.now();
916
+ try {
917
+ const res = await execFn(profile, sql, timeoutMs);
918
+ return { ...res, profile, durationMs: Date.now() - start };
919
+ } catch (err) {
920
+ return {
921
+ profile,
922
+ status: "error",
923
+ durationMs: Date.now() - start,
924
+ error: err instanceof Error ? err.message : String(err)
925
+ };
926
+ }
927
+ }
928
+ function renderText(results) {
929
+ const lines = [];
930
+ for (const r of results) {
931
+ if (r.status === "success") {
932
+ const rows = r.rowsAffected !== void 0 ? ` (${r.rowsAffected} rows)` : "";
933
+ lines.push(` ${r.profile}: \u2713${rows} \u2014 ${(r.durationMs / 1e3).toFixed(1)}s`);
934
+ } else {
935
+ lines.push(` ${r.profile}: \u2717 ${r.error ?? "unknown error"}`);
936
+ }
937
+ }
938
+ const total = results.length;
939
+ const succeeded = results.filter((r) => r.status === "success").length;
940
+ lines.push(`
941
+ ${succeeded}/${total} profiles succeeded.`);
942
+ return lines.join("\n");
943
+ }
944
+ async function defaultExecFn(profile, sql, timeoutMs) {
945
+ const profileObj = await (0, import_connection.getProfile)(profile);
946
+ const conn = (0, import_connection.createConnection)(profileObj);
947
+ await conn.connect();
948
+ try {
949
+ const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1e3));
950
+ const runner = {
951
+ executeStatement: async (statement) => {
952
+ const result2 = await conn.query(statement, { timeoutSeconds });
953
+ return {
954
+ rows: result2.rows,
955
+ durationMs: result2.durationMs
956
+ };
957
+ }
958
+ };
959
+ const result = await import_core8.queryExecution.executeStatements(sql, runner, {
960
+ failFast: false,
961
+ splitOptions: { dialect: "databricks" },
962
+ toolName: "ddt"
963
+ });
964
+ const firstError = result.statements.find((s) => s.error);
965
+ if (firstError && firstError.error) {
966
+ throw new Error(firstError.error.message);
967
+ }
968
+ const rowsAffected = result.statements.reduce((acc, s) => acc + (s.rowCount ?? 0), 0);
969
+ return { status: "success", rowsAffected };
970
+ } finally {
971
+ await conn.disconnect().catch(() => {
972
+ });
973
+ }
974
+ }
975
+ function execCommand(execFn = defaultExecFn) {
976
+ const cmd = new import_commander9.Command("exec");
977
+ cmd.description("Run a SQL script on one or more connection profiles in parallel.").argument("<file>", "Path to the .sql file to execute.").requiredOption("--profiles <list>", "Comma-separated connection profile names.").option("--yes", "Confirm execution against production profiles without prompting.").option("--format <fmt>", "text | json (default text).", "text").option("--timeout <ms>", "Per-profile timeout in milliseconds.", "30000").action(async (file, opts) => {
978
+ const profiles = opts.profiles.split(",").map((p) => p.trim()).filter(Boolean);
979
+ if (profiles.length === 0) throw new Error("--profiles must list at least one profile.");
980
+ const prodProfiles = profiles.filter(isProductionProfile);
981
+ if (prodProfiles.length > 0 && !opts.yes) {
982
+ throw new Error(
983
+ `Profile(s) look like production: ${prodProfiles.join(", ")}. Pass --yes to confirm.`
984
+ );
985
+ }
986
+ let sql;
987
+ try {
988
+ sql = await import_node_fs4.promises.readFile(file, "utf8");
989
+ } catch {
990
+ throw new Error(`Cannot read file: ${file}`);
991
+ }
992
+ if (!sql.trim()) throw new Error(`File is empty: ${file}`);
993
+ const timeoutMs = parseInt(String(opts.timeout ?? "30000"), 10);
994
+ const results = await Promise.all(
995
+ profiles.map((p) => runOnProfile(execFn, p, sql, timeoutMs))
996
+ );
997
+ const fmt = String(opts.format ?? "text").toLowerCase();
998
+ if (fmt === "json") {
999
+ const failed = results.filter((r) => r.status === "error").length;
1000
+ process.stdout.write(
1001
+ JSON.stringify(
1002
+ {
1003
+ results,
1004
+ summary: {
1005
+ total: results.length,
1006
+ succeeded: results.length - failed,
1007
+ failed
1008
+ }
1009
+ },
1010
+ null,
1011
+ 2
1012
+ ) + "\n"
1013
+ );
1014
+ return;
1015
+ }
1016
+ if (fmt !== "text") throw new Error(`Unknown --format: ${opts.format}. Use text | json.`);
1017
+ process.stdout.write(renderText(results) + "\n");
1018
+ });
1019
+ return cmd;
1020
+ }
1021
+
1022
+ // src/commands/search.ts
1023
+ var import_commander10 = require("commander");
1024
+ var import_node_fs5 = require("fs");
1025
+ var import_node_os = __toESM(require("os"), 1);
1026
+ var import_node_path = __toESM(require("path"), 1);
1027
+ var import_core9 = require("@ddt-tools/core");
1028
+ function buildDriftReport(results) {
1029
+ const allFqns = /* @__PURE__ */ new Set();
1030
+ const byFqn = /* @__PURE__ */ new Map();
1031
+ for (const r of results) {
1032
+ if (r.error) continue;
1033
+ for (const m of r.matches) {
1034
+ allFqns.add(m.fqn);
1035
+ if (!byFqn.has(m.fqn)) byFqn.set(m.fqn, /* @__PURE__ */ new Set());
1036
+ byFqn.get(m.fqn).add(r.profile);
1037
+ }
1038
+ }
1039
+ const successProfiles = results.filter((r) => !r.error).map((r) => r.profile);
1040
+ const driftLines = [];
1041
+ for (const fqn of allFqns) {
1042
+ const present = byFqn.get(fqn);
1043
+ if (present.size < successProfiles.length) {
1044
+ const absent = successProfiles.filter((p) => !present.has(p));
1045
+ driftLines.push(
1046
+ ` DRIFT ${fqn} [present: ${[...present].join(", ")} absent: ${absent.join(", ")}]`
1047
+ );
1048
+ }
1049
+ }
1050
+ return driftLines.join("\n");
1051
+ }
1052
+ function renderText2(results, showDrift) {
1053
+ const lines = [];
1054
+ for (const r of results) {
1055
+ if (r.error) {
1056
+ lines.push(`[${r.profile}] ERROR: ${r.error}`);
1057
+ continue;
1058
+ }
1059
+ if (r.matches.length === 0) {
1060
+ lines.push(`[${r.profile}] no matches`);
1061
+ } else {
1062
+ for (const m of r.matches) {
1063
+ lines.push(`[${r.profile}] ${m.fqn} (${m.objectType})`);
1064
+ }
1065
+ }
1066
+ }
1067
+ if (showDrift) {
1068
+ const drift = buildDriftReport(results);
1069
+ if (drift) {
1070
+ lines.push("");
1071
+ lines.push("--- Drift detected ---");
1072
+ lines.push(drift);
1073
+ }
1074
+ }
1075
+ return lines.join("\n");
1076
+ }
1077
+ async function loadProfileNames(profilesOpt, profilesDir) {
1078
+ if (profilesOpt.toLowerCase() !== "all") {
1079
+ return profilesOpt.split(",").map((p) => p.trim()).filter(Boolean);
1080
+ }
1081
+ try {
1082
+ const entries = await import_node_fs5.promises.readdir(profilesDir);
1083
+ return entries.filter((e) => e.endsWith(".json") || e.endsWith(".toml")).map((e) => import_node_path.default.basename(e, import_node_path.default.extname(e)));
1084
+ } catch {
1085
+ return [];
1086
+ }
1087
+ }
1088
+ async function defaultSearchFn(profile, pattern, opts) {
1089
+ const root = opts.root ?? process.cwd();
1090
+ const cache = new import_core9.catalog.CatalogCache({ root, connection: profile });
1091
+ const snapshot = await cache.get();
1092
+ if (snapshot.catalogs.length === 0) {
1093
+ throw new Error(
1094
+ `No catalog cache for profile "${profile}" at ${cache.path}. Run \`ddt catalog refresh --connection ${profile} --root ${root}\` or \`ddt extract --connection ${profile} --output ${root} --write-catalog-cache\` first.`
1095
+ );
1096
+ }
1097
+ const needle = pattern.toLowerCase();
1098
+ const typeFilter = opts.objectType ? opts.objectType.toUpperCase() : void 0;
1099
+ const matches = [];
1100
+ for (const cat of snapshot.catalogs) {
1101
+ for (const schema of cat.schemas) {
1102
+ for (const obj of schema.objects) {
1103
+ if (typeFilter && obj.objectType !== typeFilter) continue;
1104
+ const name = obj.name.toLowerCase();
1105
+ const hit = opts.exact ? name === needle : name.includes(needle);
1106
+ if (hit) {
1107
+ matches.push({
1108
+ profile,
1109
+ fqn: `${obj.catalog}.${obj.schema}.${obj.name}`,
1110
+ objectType: obj.objectType
1111
+ });
1112
+ }
1113
+ }
1114
+ }
1115
+ }
1116
+ return matches;
1117
+ }
1118
+ function searchCommand(searchFn = defaultSearchFn) {
1119
+ const cmd = new import_commander10.Command("search");
1120
+ cmd.description("Find objects matching a name pattern across one or more connection profiles.").argument("<pattern>", "Name pattern to search for (case-insensitive substring).").requiredOption(
1121
+ "--profiles <list|all>",
1122
+ 'Comma-separated profile names, or "all" to scan every configured profile.'
1123
+ ).option("--exact", "Exact name match instead of substring.", false).option("--type <objectType>", "Filter results to a specific object type (e.g. TABLE, VIEW).").option("--format <fmt>", "text | json (default text).", "text").option(
1124
+ "--profiles-dir <dir>",
1125
+ "Directory to scan when --profiles all is used.",
1126
+ import_node_path.default.join(import_node_os.default.homedir(), ".ddt", "connections")
1127
+ ).option(
1128
+ "--root <dir>",
1129
+ "Project / cache root containing the .ddt/cache directory. Default: cwd.",
1130
+ process.cwd()
1131
+ ).option("--no-drift", "Suppress the drift section in text output.").action(async (pattern, opts) => {
1132
+ const profiles = await loadProfileNames(opts.profiles, opts.profilesDir);
1133
+ if (profiles.length === 0) {
1134
+ throw new Error(
1135
+ opts.profiles.toLowerCase() === "all" ? `No profiles found in ${opts.profilesDir}. Run \`ddt connection add\` first.` : "--profiles must list at least one profile."
1136
+ );
1137
+ }
1138
+ const results = await Promise.all(
1139
+ profiles.map(async (p) => {
1140
+ try {
1141
+ const matches = await searchFn(p, pattern, {
1142
+ exact: opts.exact,
1143
+ objectType: opts.type,
1144
+ root: opts.root
1145
+ });
1146
+ return { profile: p, matches };
1147
+ } catch (err) {
1148
+ return {
1149
+ profile: p,
1150
+ matches: [],
1151
+ error: err instanceof Error ? err.message : String(err)
1152
+ };
1153
+ }
1154
+ })
1155
+ );
1156
+ const fmt = String(opts.format ?? "text").toLowerCase();
1157
+ if (fmt === "json") {
1158
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
1159
+ return;
1160
+ }
1161
+ if (fmt !== "text") throw new Error(`Unknown --format: ${opts.format}. Use text | json.`);
1162
+ const text = renderText2(results, opts.drift !== false);
1163
+ process.stdout.write(text ? text + "\n" : "No results.\n");
1164
+ });
1165
+ return cmd;
1166
+ }
1167
+
1168
+ // src/commands/query-log.ts
1169
+ var import_commander11 = require("commander");
1170
+ var import_node_fs6 = require("fs");
1171
+ var import_node_os2 = __toESM(require("os"), 1);
1172
+ var import_node_path2 = __toESM(require("path"), 1);
1173
+ var import_core10 = require("@ddt-tools/core");
1174
+ var DEFAULT_STORE = import_node_path2.default.join(import_node_os2.default.homedir(), ".ddt", "query-log.json");
1175
+ async function loadStore(storePath) {
1176
+ try {
1177
+ const raw = await import_node_fs6.promises.readFile(storePath, "utf8");
1178
+ return import_core10.queryLog.QueryLogStore.deserialize(JSON.parse(raw));
1179
+ } catch {
1180
+ return new import_core10.queryLog.QueryLogStore();
1181
+ }
1182
+ }
1183
+ async function saveStore(storePath, store) {
1184
+ await import_node_fs6.promises.mkdir(import_node_path2.default.dirname(storePath), { recursive: true });
1185
+ await import_node_fs6.promises.writeFile(storePath, JSON.stringify(store.serialize(), null, 2), "utf8");
1186
+ }
1187
+ function queryLogCommand() {
1188
+ const cmd = new import_commander11.Command("query-log");
1189
+ cmd.description("Browse and manage the local query log from the EE3 query window (AUTH.5).");
1190
+ cmd.command("list").description("List recent query log entries (newest first).").option("--profile <p>", "Filter to a specific connection profile.").option("--limit <n>", "Maximum entries to show (default 100).", "100").option("--format <fmt>", "text | json (default text).", "text").option("--store <path>", "Path to the query log JSON file.", DEFAULT_STORE).action(async (opts) => {
1191
+ const store = await loadStore(opts.store);
1192
+ const entries = store.listEntries({
1193
+ profile: opts.profile,
1194
+ limit: parseInt(String(opts.limit ?? "100"), 10)
1195
+ });
1196
+ const fmt = String(opts.format ?? "text").toLowerCase();
1197
+ if (fmt === "json") {
1198
+ process.stdout.write(JSON.stringify(entries, null, 2) + "\n");
1199
+ return;
1200
+ }
1201
+ if (entries.length === 0) {
1202
+ process.stdout.write("No query log entries.\n");
1203
+ return;
1204
+ }
1205
+ for (const e of entries) {
1206
+ const when = e.executedAt.slice(0, 19).replace("T", " ");
1207
+ const dur = e.durationMs !== void 0 ? ` ${e.durationMs}ms` : "";
1208
+ const rows = e.rowCount !== void 0 ? ` ${e.rowCount} rows` : "";
1209
+ const status = e.status === "error" ? " \u2717" : " \u2713";
1210
+ const sql = e.sql.replace(/\s+/g, " ").slice(0, 80);
1211
+ process.stdout.write(`[${e.id}] ${when} [${e.profile}]${status}${dur}${rows} ${sql}
1212
+ `);
1213
+ }
1214
+ });
1215
+ cmd.command("show <id>").description("Show a single query log entry by id.").option("--format <fmt>", "text | json | sql (default text).", "text").option("--store <path>", "Path to the query log JSON file.", DEFAULT_STORE).action(async (id, opts) => {
1216
+ const store = await loadStore(opts.store);
1217
+ const entry = store.getEntry(id);
1218
+ if (!entry) throw new Error(`No entry with id '${id}'.`);
1219
+ const fmt = String(opts.format ?? "text").toLowerCase();
1220
+ if (fmt === "sql") {
1221
+ process.stdout.write(entry.sql + "\n");
1222
+ return;
1223
+ }
1224
+ if (fmt === "json") {
1225
+ process.stdout.write(JSON.stringify(entry, null, 2) + "\n");
1226
+ return;
1227
+ }
1228
+ process.stdout.write(`id: ${entry.id}
1229
+ `);
1230
+ process.stdout.write(`profile: ${entry.profile}
1231
+ `);
1232
+ process.stdout.write(`executed: ${entry.executedAt}
1233
+ `);
1234
+ process.stdout.write(`status: ${entry.status}
1235
+ `);
1236
+ if (entry.durationMs !== void 0) process.stdout.write(`duration: ${entry.durationMs}ms
1237
+ `);
1238
+ if (entry.rowCount !== void 0) process.stdout.write(`rows: ${entry.rowCount}
1239
+ `);
1240
+ if (entry.error) process.stdout.write(`error: ${entry.error}
1241
+ `);
1242
+ process.stdout.write(`
1243
+ ${entry.sql}
1244
+ `);
1245
+ });
1246
+ cmd.command("add <sql>").description("Manually append a query to the log (used by EE3 and for testing).").requiredOption("--profile <p>", "Connection profile the query was run against.").option("--status <s>", "success | error (default success).", "success").option("--store <path>", "Path to the query log JSON file.", DEFAULT_STORE).action(async (sql, opts) => {
1247
+ if (opts.status !== "success" && opts.status !== "error") {
1248
+ throw new Error(`--status must be success or error, got: ${opts.status}`);
1249
+ }
1250
+ const store = await loadStore(opts.store);
1251
+ const entry = store.appendEntry({
1252
+ profile: opts.profile,
1253
+ sql,
1254
+ executedAt: (/* @__PURE__ */ new Date()).toISOString(),
1255
+ status: opts.status
1256
+ });
1257
+ await saveStore(opts.store, store);
1258
+ process.stdout.write(`Added entry ${entry.id}.
1259
+ `);
1260
+ });
1261
+ cmd.command("clear").description("Remove query log entries.").option("--profile <p>", "Remove only entries for this profile (omit for all).").option("--yes", "Skip confirmation prompt.").option("--store <path>", "Path to the query log JSON file.", DEFAULT_STORE).action(async (opts) => {
1262
+ if (!opts.yes) {
1263
+ const target = opts.profile ? `profile '${opts.profile}'` : "all profiles";
1264
+ throw new Error(`Pass --yes to confirm clearing the query log for ${target}.`);
1265
+ }
1266
+ const store = await loadStore(opts.store);
1267
+ const removed = store.clearEntries(opts.profile);
1268
+ await saveStore(opts.store, store);
1269
+ process.stdout.write(`Cleared ${removed} entries.
1270
+ `);
1271
+ });
1272
+ return cmd;
1273
+ }
1274
+ // Annotate the CommonJS export names for ESM import in node:
1275
+ 0 && (module.exports = {
1276
+ catalogCommand,
1277
+ compareProfilesCommand,
1278
+ connectionCommand,
1279
+ execCommand,
1280
+ explorerCommand,
1281
+ queryLogCommand,
1282
+ saferAlternativeCommand,
1283
+ safetyCommand,
1284
+ scaffoldCommand,
1285
+ searchCommand,
1286
+ sketchCommand
1287
+ });
1288
+ //# sourceMappingURL=index.cjs.map