@ddt-tools/cli 0.2.0 → 0.2.4

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.
Files changed (203) hide show
  1. package/dist/advise-tests-YNMKVJCD.js +87 -0
  2. package/dist/advise-tests-YNMKVJCD.js.map +1 -0
  3. package/dist/ai-NTNPYEKZ.js +86 -0
  4. package/dist/ai-NTNPYEKZ.js.map +1 -0
  5. package/dist/anonymize-LERTWUQO.js +139 -0
  6. package/dist/anonymize-LERTWUQO.js.map +1 -0
  7. package/dist/approval-GGZGKIU4.js +73 -0
  8. package/dist/approval-GGZGKIU4.js.map +1 -0
  9. package/dist/approval-chain-GWJKZHVU.js +118 -0
  10. package/dist/approval-chain-GWJKZHVU.js.map +1 -0
  11. package/dist/audit-log-2PH55BU4.js +159 -0
  12. package/dist/audit-log-2PH55BU4.js.map +1 -0
  13. package/dist/backlog-QNXGOUF4.js +76 -0
  14. package/dist/backlog-QNXGOUF4.js.map +1 -0
  15. package/dist/bisect-W3XKKRWG.js +111 -0
  16. package/dist/bisect-W3XKKRWG.js.map +1 -0
  17. package/dist/bookmarks-XVOGXGMC.js +107 -0
  18. package/dist/bookmarks-XVOGXGMC.js.map +1 -0
  19. package/dist/branch-S3I2IJGQ.js +103 -0
  20. package/dist/branch-S3I2IJGQ.js.map +1 -0
  21. package/dist/build-MP3JQEFO.js +20 -0
  22. package/dist/build-MP3JQEFO.js.map +1 -0
  23. package/dist/catalog-3J3NFNXP.js +137 -0
  24. package/dist/catalog-3J3NFNXP.js.map +1 -0
  25. package/dist/changelog-ZQAH3ULB.js +216 -0
  26. package/dist/changelog-ZQAH3ULB.js.map +1 -0
  27. package/dist/chunk-2FT6HXKS.js +55 -0
  28. package/dist/chunk-2FT6HXKS.js.map +1 -0
  29. package/dist/chunk-DGUM43GV.js +11 -0
  30. package/dist/chunk-DGUM43GV.js.map +1 -0
  31. package/dist/chunk-DL3V7UJ2.js +25 -0
  32. package/dist/chunk-DL3V7UJ2.js.map +1 -0
  33. package/dist/chunk-VM2H4LAO.js +15 -0
  34. package/dist/chunk-VM2H4LAO.js.map +1 -0
  35. package/dist/chunk-XFXG347C.js +40 -0
  36. package/dist/chunk-XFXG347C.js.map +1 -0
  37. package/dist/cli.js +499 -19402
  38. package/dist/cli.js.map +1 -1
  39. package/dist/compare-P7JOV76O.js +379 -0
  40. package/dist/compare-P7JOV76O.js.map +1 -0
  41. package/dist/compare-profiles-H33CXZPD.js +219 -0
  42. package/dist/compare-profiles-H33CXZPD.js.map +1 -0
  43. package/dist/completion-ZSNCQKJ2.js +89 -0
  44. package/dist/completion-ZSNCQKJ2.js.map +1 -0
  45. package/dist/connection-CDGVEFUC.js +148 -0
  46. package/dist/connection-CDGVEFUC.js.map +1 -0
  47. package/dist/cost-estimate-S2MKHT2H.js +321 -0
  48. package/dist/cost-estimate-S2MKHT2H.js.map +1 -0
  49. package/dist/data-compare-46ZI7KHL.js +128 -0
  50. package/dist/data-compare-46ZI7KHL.js.map +1 -0
  51. package/dist/data-fit-WGEPLD5S.js +127 -0
  52. package/dist/data-fit-WGEPLD5S.js.map +1 -0
  53. package/dist/deploy-status-4H5KJFRC.js +58 -0
  54. package/dist/deploy-status-4H5KJFRC.js.map +1 -0
  55. package/dist/design-ILX3ZSWW.js +135 -0
  56. package/dist/design-ILX3ZSWW.js.map +1 -0
  57. package/dist/diagnose-WPUL67E4.js +150 -0
  58. package/dist/diagnose-WPUL67E4.js.map +1 -0
  59. package/dist/discover-DEO2R5T6.js +78 -0
  60. package/dist/discover-DEO2R5T6.js.map +1 -0
  61. package/dist/docs-QNY3MUVO.js +183 -0
  62. package/dist/docs-QNY3MUVO.js.map +1 -0
  63. package/dist/drift-FDRNPWQA.js +233 -0
  64. package/dist/drift-FDRNPWQA.js.map +1 -0
  65. package/dist/drift-gate-6BWWWMHW.js +103 -0
  66. package/dist/drift-gate-6BWWWMHW.js.map +1 -0
  67. package/dist/error-lookup-4R3Y4RBC.js +56 -0
  68. package/dist/error-lookup-4R3Y4RBC.js.map +1 -0
  69. package/dist/errorReporting-3LPE2IJY.js +109 -0
  70. package/dist/errorReporting-3LPE2IJY.js.map +1 -0
  71. package/dist/exec-JOLH5LPT.js +122 -0
  72. package/dist/exec-JOLH5LPT.js.map +1 -0
  73. package/dist/explain-NS26WE2Y.js +189 -0
  74. package/dist/explain-NS26WE2Y.js.map +1 -0
  75. package/dist/explorer-GSYYYOAL.js +58 -0
  76. package/dist/explorer-GSYYYOAL.js.map +1 -0
  77. package/dist/extract-4LWEZG4O.js +152 -0
  78. package/dist/extract-4LWEZG4O.js.map +1 -0
  79. package/dist/features-KQV4OFIZ.js +54 -0
  80. package/dist/features-KQV4OFIZ.js.map +1 -0
  81. package/dist/feedback-CBLGXUEG.js +158 -0
  82. package/dist/feedback-CBLGXUEG.js.map +1 -0
  83. package/dist/find-SMXRCZ76.js +176 -0
  84. package/dist/find-SMXRCZ76.js.map +1 -0
  85. package/dist/format-HMGG6MY3.js +277 -0
  86. package/dist/format-HMGG6MY3.js.map +1 -0
  87. package/dist/generate-W7VLBDLI.js +160 -0
  88. package/dist/generate-W7VLBDLI.js.map +1 -0
  89. package/dist/graph-YYL5UYCJ.js +168 -0
  90. package/dist/graph-YYL5UYCJ.js.map +1 -0
  91. package/dist/history-GDRFP4PG.js +184 -0
  92. package/dist/history-GDRFP4PG.js.map +1 -0
  93. package/dist/hosts-DRFZTMIJ.js +45 -0
  94. package/dist/hosts-DRFZTMIJ.js.map +1 -0
  95. package/dist/impact-A4NU6CB2.js +63 -0
  96. package/dist/impact-A4NU6CB2.js.map +1 -0
  97. package/dist/import-2RNYDL4E.js +79 -0
  98. package/dist/import-2RNYDL4E.js.map +1 -0
  99. package/dist/index.cjs +11 -5
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.js +8 -2
  102. package/dist/index.js.map +1 -1
  103. package/dist/init-EAOGNGXI.js +54 -0
  104. package/dist/init-EAOGNGXI.js.map +1 -0
  105. package/dist/install-hooks-G3Y5LVXK.js +109 -0
  106. package/dist/install-hooks-G3Y5LVXK.js.map +1 -0
  107. package/dist/license-Z5YSC7XQ.js +43 -0
  108. package/dist/license-Z5YSC7XQ.js.map +1 -0
  109. package/dist/lineage-C5CGVP36.js +555 -0
  110. package/dist/lineage-C5CGVP36.js.map +1 -0
  111. package/dist/lint-AQFPZ3WG.js +144 -0
  112. package/dist/lint-AQFPZ3WG.js.map +1 -0
  113. package/dist/mcp-F7FND5X7.js +343 -0
  114. package/dist/mcp-F7FND5X7.js.map +1 -0
  115. package/dist/migrate-from-dbt-K4ELOWUD.js +156 -0
  116. package/dist/migrate-from-dbt-K4ELOWUD.js.map +1 -0
  117. package/dist/migrate-platform-E7VZFPO5.js +91 -0
  118. package/dist/migrate-platform-E7VZFPO5.js.map +1 -0
  119. package/dist/optimize-WUJ5ZN5Y.js +109 -0
  120. package/dist/optimize-WUJ5ZN5Y.js.map +1 -0
  121. package/dist/perf-UULZSREY.js +200 -0
  122. package/dist/perf-UULZSREY.js.map +1 -0
  123. package/dist/pii-QHU32VML.js +146 -0
  124. package/dist/pii-QHU32VML.js.map +1 -0
  125. package/dist/pilot-BR6GVK32.js +29 -0
  126. package/dist/pilot-BR6GVK32.js.map +1 -0
  127. package/dist/pr-comment-2FOA3EXG.js +81 -0
  128. package/dist/pr-comment-2FOA3EXG.js.map +1 -0
  129. package/dist/preview-XNY422OU.js +46 -0
  130. package/dist/preview-XNY422OU.js.map +1 -0
  131. package/dist/profile-SQTBNKYS.js +98 -0
  132. package/dist/profile-SQTBNKYS.js.map +1 -0
  133. package/dist/promote-FSGUPIPD.js +417 -0
  134. package/dist/promote-FSGUPIPD.js.map +1 -0
  135. package/dist/publish-AYCRMCE2.js +739 -0
  136. package/dist/publish-AYCRMCE2.js.map +1 -0
  137. package/dist/purge-Y5IOTXKA.js +56 -0
  138. package/dist/purge-Y5IOTXKA.js.map +1 -0
  139. package/dist/query-log-SDDGMJLJ.js +112 -0
  140. package/dist/query-log-SDDGMJLJ.js.map +1 -0
  141. package/dist/refactor-TC7S43F2.js +5809 -0
  142. package/dist/refactor-TC7S43F2.js.map +1 -0
  143. package/dist/refresh-MDJYOYV5.js +39 -0
  144. package/dist/refresh-MDJYOYV5.js.map +1 -0
  145. package/dist/replay-E4664A5K.js +118 -0
  146. package/dist/replay-E4664A5K.js.map +1 -0
  147. package/dist/revert-QWQWCJJB.js +111 -0
  148. package/dist/revert-QWQWCJJB.js.map +1 -0
  149. package/dist/review-7CAVLD67.js +164 -0
  150. package/dist/review-7CAVLD67.js.map +1 -0
  151. package/dist/rollback-suggest-C6D5YFCA.js +79 -0
  152. package/dist/rollback-suggest-C6D5YFCA.js.map +1 -0
  153. package/dist/safer-alternative-QR4QEFUV.js +84 -0
  154. package/dist/safer-alternative-QR4QEFUV.js.map +1 -0
  155. package/dist/safety-OFWUFLK4.js +165 -0
  156. package/dist/safety-OFWUFLK4.js.map +1 -0
  157. package/dist/savings-MEBE4TXI.js +95 -0
  158. package/dist/savings-MEBE4TXI.js.map +1 -0
  159. package/dist/scan-secrets-XCUBMLHL.js +54 -0
  160. package/dist/scan-secrets-XCUBMLHL.js.map +1 -0
  161. package/dist/schema-7JZIG6QR.js +447 -0
  162. package/dist/schema-7JZIG6QR.js.map +1 -0
  163. package/dist/script-BMYVBHFR.js +167 -0
  164. package/dist/script-BMYVBHFR.js.map +1 -0
  165. package/dist/search-TA3C3AZT.js +151 -0
  166. package/dist/search-TA3C3AZT.js.map +1 -0
  167. package/dist/seed-W4Q3L2IU.js +101 -0
  168. package/dist/seed-W4Q3L2IU.js.map +1 -0
  169. package/dist/sketch-6B2V6FJV.js +83 -0
  170. package/dist/sketch-6B2V6FJV.js.map +1 -0
  171. package/dist/snapshot-YMVS322L.js +171 -0
  172. package/dist/snapshot-YMVS322L.js.map +1 -0
  173. package/dist/snippets-EVTN63OU.js +74 -0
  174. package/dist/snippets-EVTN63OU.js.map +1 -0
  175. package/dist/standards-FGJW3CQL.js +238 -0
  176. package/dist/standards-FGJW3CQL.js.map +1 -0
  177. package/dist/suggest-V3LVIFZ5.js +44 -0
  178. package/dist/suggest-V3LVIFZ5.js.map +1 -0
  179. package/dist/suggest-constraints-EX2FCWOQ.js +154 -0
  180. package/dist/suggest-constraints-EX2FCWOQ.js.map +1 -0
  181. package/dist/suite-YTQ3CNX5.js +85 -0
  182. package/dist/suite-YTQ3CNX5.js.map +1 -0
  183. package/dist/telemetry-KOIY3NEQ.js +90 -0
  184. package/dist/telemetry-KOIY3NEQ.js.map +1 -0
  185. package/dist/template-MUJ6X6LN.js +396 -0
  186. package/dist/template-MUJ6X6LN.js.map +1 -0
  187. package/dist/test-XFSQHR2S.js +169 -0
  188. package/dist/test-XFSQHR2S.js.map +1 -0
  189. package/dist/trial-GFTGYCR3.js +31 -0
  190. package/dist/trial-GFTGYCR3.js.map +1 -0
  191. package/dist/validate-LFDEZFFH.js +107 -0
  192. package/dist/validate-LFDEZFFH.js.map +1 -0
  193. package/dist/verify-KRDYOJCR.js +76 -0
  194. package/dist/verify-KRDYOJCR.js.map +1 -0
  195. package/dist/watch-FSG23RR3.js +80 -0
  196. package/dist/watch-FSG23RR3.js.map +1 -0
  197. package/dist/xcompare-U4TXTTIR.js +87 -0
  198. package/dist/xcompare-U4TXTTIR.js.map +1 -0
  199. package/package.json +2 -2
  200. package/dist/cli.cjs +0 -19298
  201. package/dist/cli.cjs.map +0 -1
  202. package/dist/cli.d.cts +0 -1
  203. package/dist/cli.d.ts +0 -1
@@ -0,0 +1,233 @@
1
+ import {
2
+ attachRelatedOptions
3
+ } from "./chunk-DL3V7UJ2.js";
4
+ import {
5
+ addMappingFlags,
6
+ buildMappingFromOptions
7
+ } from "./chunk-2FT6HXKS.js";
8
+ import "./chunk-DGUM43GV.js";
9
+
10
+ // src/commands/drift.ts
11
+ import { Command } from "commander";
12
+ import {
13
+ CompareEngine,
14
+ DbtManifestSource,
15
+ InMemorySource,
16
+ ProjectSource,
17
+ AccountExtractor,
18
+ createConnection,
19
+ defaultExtractors,
20
+ driftAnomaly,
21
+ getProfile
22
+ } from "@ddt-tools/core";
23
+ function driftWatchSubcommand() {
24
+ const sub = new Command("watch");
25
+ sub.description(
26
+ "Poll a project against the live Unity Catalog on a fixed interval. Prints DRIFT_DETECTED events to stdout (or POSTs to --webhook) when the catalog diverges from the project."
27
+ ).requiredOption("--source <path>", "Path to .ddtproj or .ddtpac").requiredOption("--connection <name>", "Connection profile name").option("--catalog <catalog>", "Limit drift check to a single catalog.").option("--schema <schema>", "Limit drift check to a single schema (requires --catalog).").option("--interval <seconds>", "Poll interval in seconds (min 5).", "60").option(
28
+ "--webhook <url>",
29
+ "POST drift events as JSON to this URL (Slack/Teams/generic receiver)."
30
+ ).option("--format <fmt>", "Output format: text | json.", "text").option(
31
+ "--quiet",
32
+ "Suppress CLEAN status lines; emit only DRIFT_DETECTED and error events.",
33
+ false
34
+ ).action(async (opts) => {
35
+ const intervalSecs = Math.max(5, parseInt(String(opts.interval), 10) || 60);
36
+ const format = String(opts.format) === "json" ? "json" : "text";
37
+ const webhookUrl = opts.webhook ? String(opts.webhook) : void 0;
38
+ const quiet = !!opts.quiet;
39
+ const sourcePath = String(opts.source);
40
+ const scope = {
41
+ ...opts.catalog ? { catalog: String(opts.catalog) } : {},
42
+ ...opts.schema ? { schema: String(opts.schema) } : {}
43
+ };
44
+ const profile = await getProfile(String(opts.connection));
45
+ const conn = createConnection(profile);
46
+ await conn.connect();
47
+ const src = sourcePath.endsWith(".ddtpac") ? await (async () => {
48
+ const { PacSource } = await import("@ddt-tools/core");
49
+ return new PacSource(sourcePath, "source");
50
+ })() : new ProjectSource(sourcePath, "source");
51
+ if (!quiet && format === "text") {
52
+ console.log(
53
+ `drift watch: polling every ${intervalSecs}s \u2014 ${sourcePath} \u2192 ${profile.auth.host}`
54
+ );
55
+ console.log("Press Ctrl+C to stop.");
56
+ }
57
+ const postToWebhook = async (event) => {
58
+ if (!webhookUrl) return;
59
+ try {
60
+ const res = await fetch(webhookUrl, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify(event)
64
+ });
65
+ if (!res.ok && format === "text") console.warn(`Webhook POST failed: HTTP ${res.status}`);
66
+ } catch (err) {
67
+ if (format === "text")
68
+ console.warn(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ };
71
+ const pollOnce = async () => {
72
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
73
+ try {
74
+ const accountExtractor = new AccountExtractor(defaultExtractors());
75
+ const liveObjs = await accountExtractor.extract(conn, scope);
76
+ const live = new InMemorySource(liveObjs, { kind: "live", label: profile.auth.host });
77
+ const engine = new CompareEngine();
78
+ const result = await engine.compare(src, live);
79
+ const s = result.summary;
80
+ const drifted = s.added + s.removed + s.modified;
81
+ if (drifted > 0) {
82
+ const event = {
83
+ type: "DRIFT_DETECTED",
84
+ timestamp: ts,
85
+ source: sourcePath,
86
+ added: s.added,
87
+ removed: s.removed,
88
+ modified: s.modified
89
+ };
90
+ if (format === "json") process.stdout.write(JSON.stringify(event) + "\n");
91
+ else console.warn(`[${ts}] DRIFT_DETECTED +${s.added} -${s.removed} ~${s.modified}`);
92
+ await postToWebhook(event);
93
+ } else if (!quiet) {
94
+ if (format === "json")
95
+ process.stdout.write(
96
+ JSON.stringify({ type: "CLEAN", timestamp: ts, source: sourcePath }) + "\n"
97
+ );
98
+ else console.log(`[${ts}] clean`);
99
+ }
100
+ } catch (err) {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ if (format === "json")
103
+ process.stdout.write(
104
+ JSON.stringify({ type: "POLL_ERROR", timestamp: ts, error: message }) + "\n"
105
+ );
106
+ else console.error(`[${ts}] poll error: ${message}`);
107
+ }
108
+ };
109
+ await pollOnce();
110
+ const timer = setInterval(() => {
111
+ void pollOnce();
112
+ }, intervalSecs * 1e3);
113
+ process.once("SIGINT", () => {
114
+ clearInterval(timer);
115
+ void conn.disconnect().then(() => process.exit(0));
116
+ });
117
+ await new Promise(() => {
118
+ });
119
+ });
120
+ return sub;
121
+ }
122
+ function driftCommand() {
123
+ const cmd = new Command("drift");
124
+ cmd.description(
125
+ "Refuse with non-zero exit when the live target has drifted from --source or a dbt manifest."
126
+ ).option("--source <path>", "Source: .ddtproj or .ddtpac (required unless --vs-dbt-project).").option(
127
+ "--vs-dbt-project <path>",
128
+ "Compare a compiled dbt project (or target/manifest.json) against the live target. Run `dbt compile` first. When set, --source is not required."
129
+ ).requiredOption("--connection <name>", "Connection profile to extract live state from.").option("--catalog <catalog>", "Limit drift check to a single catalog.").option("--schema <schema>", "Limit drift check to a single schema (requires --catalog).").option("--ignore-case", "Compare object FQNs case-insensitively.", false).option(
130
+ "--allow-extras",
131
+ "Treat objects that exist on the target but not in source as OK (no drift).",
132
+ false
133
+ ).option(
134
+ "--anomalies",
135
+ "Also classify drift via the anomaly detector (new grants to `account users`, bypass-principal grants, owner changes, audit-column drops, mask removals, new bypass-named groups).",
136
+ false
137
+ ).option(
138
+ "--anomalies-only",
139
+ "Skip the simple drift summary and emit only the anomaly report. Implies --anomalies.",
140
+ false
141
+ ).option(
142
+ "--fail-on-anomaly <severity>",
143
+ "Exit non-zero when any anomaly at or above this severity fires. Values: critical | high | medium | low. Default: drift presence alone determines exit code."
144
+ );
145
+ addMappingFlags(cmd);
146
+ cmd.action(async (opts) => {
147
+ if (!opts.source && !opts.vsDBtProject) {
148
+ console.error("Provide either --source <path> or --vs-dbt-project <path>.");
149
+ process.exitCode = 1;
150
+ return;
151
+ }
152
+ const nameMapping = await buildMappingFromOptions(opts);
153
+ const profile = await getProfile(String(opts.connection));
154
+ const conn = createConnection(profile);
155
+ try {
156
+ await conn.connect();
157
+ const accountExtractor = new AccountExtractor(defaultExtractors());
158
+ const scope = {
159
+ ...opts.catalog ? { catalog: String(opts.catalog) } : {},
160
+ ...opts.schema ? { schema: String(opts.schema) } : {}
161
+ };
162
+ const liveObjs = await accountExtractor.extract(conn, scope);
163
+ const live = new InMemorySource(liveObjs, { kind: "live", label: profile.auth.host });
164
+ const src = opts.vsDBtProject ? new DbtManifestSource(String(opts.vsDBtProject), `dbt:${opts.vsDBtProject}`) : String(opts.source).endsWith(".ddtpac") ? (
165
+ // PacSource via inline import to keep this command self-contained.
166
+ await (async () => {
167
+ const { PacSource } = await import("@ddt-tools/core");
168
+ return new PacSource(String(opts.source), "source");
169
+ })()
170
+ ) : new ProjectSource(String(opts.source), "source");
171
+ const engine = new CompareEngine();
172
+ const result = await engine.compare(src, live, {
173
+ ignoreCase: !!opts.ignoreCase,
174
+ ...nameMapping ? { nameMapping } : {}
175
+ });
176
+ const s = result.summary;
177
+ const drifted = s.added + s.modified + (opts.allowExtras ? 0 : s.removed);
178
+ if (!opts.anomaliesOnly) {
179
+ console.log(`Source: ${result.source.kind}:${result.source.label}`);
180
+ console.log(`Target: ${result.target.kind}:${result.target.label}`);
181
+ console.log(`Summary: +${s.added} -${s.removed} ~${s.modified} =${s.unchanged}`);
182
+ console.log(`Drifted: ${drifted}` + (opts.allowExtras ? " (extras allowed)" : ""));
183
+ if (drifted > 0) {
184
+ console.log("");
185
+ console.log("Drift detected:");
186
+ for (const o of result.objects) {
187
+ if (o.kind === "unchanged") continue;
188
+ if (o.kind === "removed" && opts.allowExtras) continue;
189
+ const glyph = o.kind === "added" ? "+ source-only" : o.kind === "removed" ? "- target-only" : "~ modified";
190
+ console.log(` ${glyph} ${o.identity.objectType} ${o.identity.fqn}`);
191
+ }
192
+ process.exitCode = 1;
193
+ }
194
+ }
195
+ if (opts.anomalies || opts.anomaliesOnly) {
196
+ const report = driftAnomaly.detectAnomalies(result);
197
+ if (report.totalAnomalies === 0) {
198
+ console.log("Anomaly scan: 0 findings.");
199
+ } else {
200
+ console.log("");
201
+ console.log(`Anomaly scan: ${report.totalAnomalies} finding(s):`);
202
+ for (const a of report.anomalies) {
203
+ console.log(` [${a.severity}] ${a.category}: ${a.fqn} \u2014 ${a.reason}`);
204
+ }
205
+ const failOn = opts.failOnAnomaly?.toLowerCase();
206
+ if (failOn && ["critical", "high", "medium", "low"].includes(failOn)) {
207
+ const failRank = driftAnomaly.anomalySeverityRank(
208
+ failOn
209
+ );
210
+ const triggered = report.anomalies.some(
211
+ (a) => driftAnomaly.anomalySeverityRank(a.severity) <= failRank
212
+ );
213
+ if (triggered) process.exitCode = 1;
214
+ }
215
+ }
216
+ }
217
+ } finally {
218
+ await conn.disconnect();
219
+ }
220
+ });
221
+ attachRelatedOptions(cmd, [
222
+ "compare.ignoreCase",
223
+ "compare.ignoreComments",
224
+ "compare.ignoreFormattingDifferences",
225
+ "compare.excludeObjectTypes"
226
+ ]);
227
+ cmd.addCommand(driftWatchSubcommand());
228
+ return cmd;
229
+ }
230
+ export {
231
+ driftCommand
232
+ };
233
+ //# sourceMappingURL=drift-FDRNPWQA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/drift.ts"],"sourcesContent":["import { Command } from 'commander';\nimport {\n CompareEngine,\n DbtManifestSource,\n InMemorySource,\n ProjectSource,\n AccountExtractor,\n createConnection,\n defaultExtractors,\n driftAnomaly,\n getProfile,\n type CompareSource,\n} from '@ddt-tools/core';\nimport { addMappingFlags, buildMappingFromOptions } from '../util/mapping.js';\nimport { attachRelatedOptions } from '../util/help-catalog.js';\n\nfunction driftWatchSubcommand(): Command {\n const sub = new Command('watch');\n sub\n .description(\n 'Poll a project against the live Unity Catalog on a fixed interval. Prints DRIFT_DETECTED events to stdout (or POSTs to --webhook) when the catalog diverges from the project.',\n )\n .requiredOption('--source <path>', 'Path to .ddtproj or .ddtpac')\n .requiredOption('--connection <name>', 'Connection profile name')\n .option('--catalog <catalog>', 'Limit drift check to a single catalog.')\n .option('--schema <schema>', 'Limit drift check to a single schema (requires --catalog).')\n .option('--interval <seconds>', 'Poll interval in seconds (min 5).', '60')\n .option(\n '--webhook <url>',\n 'POST drift events as JSON to this URL (Slack/Teams/generic receiver).',\n )\n .option('--format <fmt>', 'Output format: text | json.', 'text')\n .option(\n '--quiet',\n 'Suppress CLEAN status lines; emit only DRIFT_DETECTED and error events.',\n false,\n )\n .action(async (opts) => {\n const intervalSecs = Math.max(5, parseInt(String(opts.interval), 10) || 60);\n const format = String(opts.format) === 'json' ? 'json' : 'text';\n const webhookUrl = opts.webhook ? String(opts.webhook) : undefined;\n const quiet = !!opts.quiet;\n const sourcePath = String(opts.source);\n const scope = {\n ...(opts.catalog ? { catalog: String(opts.catalog) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n\n const profile = await getProfile(String(opts.connection));\n const conn = createConnection(profile);\n await conn.connect();\n\n const src: CompareSource = sourcePath.endsWith('.ddtpac')\n ? await (async () => {\n const { PacSource } = await import('@ddt-tools/core');\n return new PacSource(sourcePath, 'source');\n })()\n : new ProjectSource(sourcePath, 'source');\n\n if (!quiet && format === 'text') {\n console.log(\n `drift watch: polling every ${intervalSecs}s — ${sourcePath} → ${profile.auth.host}`,\n );\n console.log('Press Ctrl+C to stop.');\n }\n\n const postToWebhook = async (event: Record<string, unknown>): Promise<void> => {\n if (!webhookUrl) return;\n try {\n const res = await fetch(webhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(event),\n });\n if (!res.ok && format === 'text') console.warn(`Webhook POST failed: HTTP ${res.status}`);\n } catch (err) {\n if (format === 'text')\n console.warn(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);\n }\n };\n\n const pollOnce = async (): Promise<void> => {\n const ts = new Date().toISOString();\n try {\n const accountExtractor = new AccountExtractor(defaultExtractors());\n const liveObjs = await accountExtractor.extract(conn, scope);\n const live = new InMemorySource(liveObjs, { kind: 'live', label: profile.auth.host });\n const engine = new CompareEngine();\n const result = await engine.compare(src, live);\n const s = result.summary;\n const drifted = s.added + s.removed + s.modified;\n if (drifted > 0) {\n const event = {\n type: 'DRIFT_DETECTED',\n timestamp: ts,\n source: sourcePath,\n added: s.added,\n removed: s.removed,\n modified: s.modified,\n };\n if (format === 'json') process.stdout.write(JSON.stringify(event) + '\\n');\n else console.warn(`[${ts}] DRIFT_DETECTED +${s.added} -${s.removed} ~${s.modified}`);\n await postToWebhook(event);\n } else if (!quiet) {\n if (format === 'json')\n process.stdout.write(\n JSON.stringify({ type: 'CLEAN', timestamp: ts, source: sourcePath }) + '\\n',\n );\n else console.log(`[${ts}] clean`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (format === 'json')\n process.stdout.write(\n JSON.stringify({ type: 'POLL_ERROR', timestamp: ts, error: message }) + '\\n',\n );\n else console.error(`[${ts}] poll error: ${message}`);\n }\n };\n\n await pollOnce();\n const timer = setInterval(() => {\n void pollOnce();\n }, intervalSecs * 1000);\n process.once('SIGINT', () => {\n clearInterval(timer);\n void conn.disconnect().then(() => process.exit(0));\n });\n await new Promise<void>(() => {});\n });\n return sub;\n}\n\n/**\n * `ddt drift` — refuse-on-drift CI gate. Compares a `.ddtproj` (or\n * `.ddtpac`) against a live target via the configured connection profile;\n * exits non-zero when the live state differs.\n *\n * Use it as a nightly cron / scheduled GitHub Action to catch unsanctioned\n * changes to prod.\n */\nexport function driftCommand(): Command {\n const cmd = new Command('drift');\n cmd\n .description(\n 'Refuse with non-zero exit when the live target has drifted from --source or a dbt manifest.',\n )\n .option('--source <path>', 'Source: .ddtproj or .ddtpac (required unless --vs-dbt-project).')\n .option(\n '--vs-dbt-project <path>',\n 'Compare a compiled dbt project (or target/manifest.json) against the live target. ' +\n 'Run `dbt compile` first. When set, --source is not required.',\n )\n .requiredOption('--connection <name>', 'Connection profile to extract live state from.')\n .option('--catalog <catalog>', 'Limit drift check to a single catalog.')\n .option('--schema <schema>', 'Limit drift check to a single schema (requires --catalog).')\n .option('--ignore-case', 'Compare object FQNs case-insensitively.', false)\n .option(\n '--allow-extras',\n 'Treat objects that exist on the target but not in source as OK (no drift).',\n false,\n )\n .option(\n '--anomalies',\n 'Also classify drift via the anomaly detector (new grants to `account users`, bypass-principal grants, owner changes, audit-column drops, mask removals, new bypass-named groups).',\n false,\n )\n .option(\n '--anomalies-only',\n 'Skip the simple drift summary and emit only the anomaly report. Implies --anomalies.',\n false,\n )\n .option(\n '--fail-on-anomaly <severity>',\n 'Exit non-zero when any anomaly at or above this severity fires. Values: critical | high | medium | low. Default: drift presence alone determines exit code.',\n );\n addMappingFlags(cmd);\n cmd.action(async (opts) => {\n if (!opts.source && !opts.vsDBtProject) {\n console.error('Provide either --source <path> or --vs-dbt-project <path>.');\n process.exitCode = 1;\n return;\n }\n const nameMapping = await buildMappingFromOptions(opts);\n const profile = await getProfile(String(opts.connection));\n const conn = createConnection(profile);\n try {\n await conn.connect();\n const accountExtractor = new AccountExtractor(defaultExtractors());\n const scope = {\n ...(opts.catalog ? { catalog: String(opts.catalog) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n const liveObjs = await accountExtractor.extract(conn, scope);\n const live = new InMemorySource(liveObjs, { kind: 'live', label: profile.auth.host });\n const src: CompareSource = opts.vsDBtProject\n ? new DbtManifestSource(String(opts.vsDBtProject), `dbt:${opts.vsDBtProject}`)\n : String(opts.source).endsWith('.ddtpac')\n ? // PacSource via inline import to keep this command self-contained.\n await (async () => {\n const { PacSource } = await import('@ddt-tools/core');\n return new PacSource(String(opts.source), 'source');\n })()\n : new ProjectSource(String(opts.source), 'source');\n\n const engine = new CompareEngine();\n const result = await engine.compare(src, live, {\n ignoreCase: !!opts.ignoreCase,\n ...(nameMapping ? { nameMapping } : {}),\n });\n\n const s = result.summary;\n const drifted = s.added + s.modified + (opts.allowExtras ? 0 : s.removed);\n if (!opts.anomaliesOnly) {\n console.log(`Source: ${result.source.kind}:${result.source.label}`);\n console.log(`Target: ${result.target.kind}:${result.target.label}`);\n console.log(`Summary: +${s.added} -${s.removed} ~${s.modified} =${s.unchanged}`);\n console.log(`Drifted: ${drifted}` + (opts.allowExtras ? ' (extras allowed)' : ''));\n if (drifted > 0) {\n console.log('');\n console.log('Drift detected:');\n for (const o of result.objects) {\n if (o.kind === 'unchanged') continue;\n if (o.kind === 'removed' && opts.allowExtras) continue;\n const glyph =\n o.kind === 'added'\n ? '+ source-only'\n : o.kind === 'removed'\n ? '- target-only'\n : '~ modified';\n console.log(` ${glyph} ${o.identity.objectType} ${o.identity.fqn}`);\n }\n process.exitCode = 1;\n }\n }\n\n if (opts.anomalies || opts.anomaliesOnly) {\n const report = driftAnomaly.detectAnomalies(result);\n if (report.totalAnomalies === 0) {\n console.log('Anomaly scan: 0 findings.');\n } else {\n console.log('');\n console.log(`Anomaly scan: ${report.totalAnomalies} finding(s):`);\n for (const a of report.anomalies) {\n console.log(` [${a.severity}] ${a.category}: ${a.fqn} — ${a.reason}`);\n }\n const failOn = (opts.failOnAnomaly as string | undefined)?.toLowerCase();\n if (failOn && ['critical', 'high', 'medium', 'low'].includes(failOn)) {\n const failRank = driftAnomaly.anomalySeverityRank(\n failOn as driftAnomaly.AnomalySeverity,\n );\n const triggered = report.anomalies.some(\n (a) => driftAnomaly.anomalySeverityRank(a.severity) <= failRank,\n );\n if (triggered) process.exitCode = 1;\n }\n }\n }\n } finally {\n await conn.disconnect();\n }\n });\n attachRelatedOptions(cmd, [\n 'compare.ignoreCase',\n 'compare.ignoreComments',\n 'compare.ignoreFormattingDifferences',\n 'compare.excludeObjectTypes',\n ]);\n cmd.addCommand(driftWatchSubcommand());\n return cmd;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,103 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/drift-gate.ts
4
+ import { Command } from "commander";
5
+ import {
6
+ AccountExtractor,
7
+ CompareEngine,
8
+ InMemorySource,
9
+ createConnection,
10
+ defaultExtractors,
11
+ getProfile
12
+ } from "@ddt-tools/core";
13
+ function driftGateCommand() {
14
+ const cmd = new Command("drift-gate");
15
+ cmd.description(
16
+ "Refuse-on-drift CI gate across multiple replica workspaces. Compares replicas against a primary."
17
+ ).requiredOption("--primary <profile>", "Primary workspace connection profile.").requiredOption("--replicas <list>", "Comma-separated replica connection profile names.").option("--catalog <catalog>", "Limit drift comparison to a single catalog.").option("--schema <schema>", "Limit drift comparison to a single schema (requires --catalog).").option(
18
+ "--threshold <n>",
19
+ "Max allowed differences per replica before the gate fails. Default 0.",
20
+ "0"
21
+ ).option("--format <fmt>", "Output: table | json.", "table").action(async (opts) => {
22
+ const primary = await getProfile(String(opts.primary));
23
+ const replicaNames = String(opts.replicas).split(",").map((s) => s.trim()).filter(Boolean);
24
+ if (replicaNames.length === 0) {
25
+ throw new Error("At least one replica profile is required via --replicas.");
26
+ }
27
+ const threshold = Number.parseInt(String(opts.threshold), 10);
28
+ const scope = {
29
+ ...opts.catalog ? { catalog: String(opts.catalog) } : {},
30
+ ...opts.schema ? { schema: String(opts.schema) } : {}
31
+ };
32
+ const primaryConn = createConnection(primary);
33
+ let primaryObjs;
34
+ try {
35
+ await primaryConn.connect();
36
+ primaryObjs = await new AccountExtractor(defaultExtractors()).extract(primaryConn, scope);
37
+ } finally {
38
+ await primaryConn.disconnect();
39
+ }
40
+ const primarySource = new InMemorySource(primaryObjs, {
41
+ kind: "live",
42
+ label: `primary:${primary.auth.host}`
43
+ });
44
+ const results = [];
45
+ const engine = new CompareEngine();
46
+ for (const replicaName of replicaNames) {
47
+ const replicaProfile = await getProfile(replicaName);
48
+ const replicaConn = createConnection(replicaProfile);
49
+ let replicaObjs;
50
+ try {
51
+ await replicaConn.connect();
52
+ replicaObjs = await new AccountExtractor(defaultExtractors()).extract(replicaConn, scope);
53
+ } finally {
54
+ await replicaConn.disconnect();
55
+ }
56
+ const replicaSource = new InMemorySource(replicaObjs, {
57
+ kind: "live",
58
+ label: `replica:${replicaProfile.auth.host}`
59
+ });
60
+ const result = await engine.compare(primarySource, replicaSource);
61
+ const { added, removed, modified } = result.summary;
62
+ results.push({
63
+ profile: replicaName,
64
+ workspaceHost: replicaProfile.auth.host,
65
+ added,
66
+ removed,
67
+ modified,
68
+ total: added + removed + modified
69
+ });
70
+ }
71
+ if (opts.format === "json") {
72
+ console.log(
73
+ JSON.stringify({ primary: primary.auth.host, threshold, replicas: results }, null, 2)
74
+ );
75
+ } else {
76
+ console.log(`Drift gate \u2014 primary ${primary.auth.host}`);
77
+ console.log("");
78
+ console.log(
79
+ " REPLICA-PROFILE WORKSPACE-HOST ADD RM MOD TOTAL STATUS"
80
+ );
81
+ console.log(
82
+ " ---------------------- ------------------------------ --- -- --- ----- ------"
83
+ );
84
+ for (const r of results) {
85
+ const status = r.total > threshold ? "DRIFT" : "OK";
86
+ console.log(
87
+ ` ${r.profile.padEnd(22).slice(0, 22)} ${r.workspaceHost.padEnd(30).slice(0, 30)} ${String(r.added).padStart(3)} ${String(r.removed).padStart(2)} ${String(r.modified).padStart(3)} ${String(r.total).padStart(5)} ${status}`
88
+ );
89
+ }
90
+ }
91
+ const drifting = results.filter((r) => r.total > threshold);
92
+ if (drifting.length > 0) {
93
+ console.error("");
94
+ console.error(`Drift detected in ${drifting.length} replica(s) (threshold=${threshold}).`);
95
+ process.exitCode = 1;
96
+ }
97
+ });
98
+ return cmd;
99
+ }
100
+ export {
101
+ driftGateCommand
102
+ };
103
+ //# sourceMappingURL=drift-gate-6BWWWMHW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/drift-gate.ts"],"sourcesContent":["/**\n * `ddt drift-gate` — multi-region/replica drift gate.\n *\n * Compares the live schema in N replica workspaces against a primary\n * workspace's live schema. Useful for orgs that replicate Unity Catalog\n * state across regions (Delta Sharing, multi-workspace deployments) and\n * want a CI gate that fires the moment any replica drifts from the\n * primary.\n *\n * Algorithm:\n * 1. Extract primary's live model.\n * 2. For each replica, extract live model + compare against primary.\n * 3. Aggregate per-replica diff counts.\n * 4. Exit non-zero if any replica has drift > threshold.\n *\n * Combine with the existing `ddt drift` command (project ↔ region) to\n * cover both vectors: (a) project drift (drift) and (b) inter-region\n * drift (drift-gate).\n */\nimport { Command } from 'commander';\nimport {\n AccountExtractor,\n CompareEngine,\n InMemorySource,\n createConnection,\n defaultExtractors,\n getProfile,\n} from '@ddt-tools/core';\n\ninterface RegionResult {\n profile: string;\n workspaceHost: string;\n added: number;\n removed: number;\n modified: number;\n total: number;\n}\n\nexport function driftGateCommand(): Command {\n const cmd = new Command('drift-gate');\n cmd\n .description(\n 'Refuse-on-drift CI gate across multiple replica workspaces. Compares replicas against a primary.',\n )\n .requiredOption('--primary <profile>', 'Primary workspace connection profile.')\n .requiredOption('--replicas <list>', 'Comma-separated replica connection profile names.')\n .option('--catalog <catalog>', 'Limit drift comparison to a single catalog.')\n .option('--schema <schema>', 'Limit drift comparison to a single schema (requires --catalog).')\n .option(\n '--threshold <n>',\n 'Max allowed differences per replica before the gate fails. Default 0.',\n '0',\n )\n .option('--format <fmt>', 'Output: table | json.', 'table')\n .action(async (opts) => {\n const primary = await getProfile(String(opts.primary));\n const replicaNames = String(opts.replicas)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (replicaNames.length === 0) {\n throw new Error('At least one replica profile is required via --replicas.');\n }\n const threshold = Number.parseInt(String(opts.threshold), 10);\n const scope = {\n ...(opts.catalog ? { catalog: String(opts.catalog) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n\n // 1. Extract primary.\n const primaryConn = createConnection(primary);\n let primaryObjs;\n try {\n await primaryConn.connect();\n primaryObjs = await new AccountExtractor(defaultExtractors()).extract(primaryConn, scope);\n } finally {\n await primaryConn.disconnect();\n }\n const primarySource = new InMemorySource(primaryObjs, {\n kind: 'live',\n label: `primary:${primary.auth.host}`,\n });\n\n const results: RegionResult[] = [];\n const engine = new CompareEngine();\n\n // 2. Per-replica compare.\n for (const replicaName of replicaNames) {\n const replicaProfile = await getProfile(replicaName);\n const replicaConn = createConnection(replicaProfile);\n let replicaObjs;\n try {\n await replicaConn.connect();\n replicaObjs = await new AccountExtractor(defaultExtractors()).extract(replicaConn, scope);\n } finally {\n await replicaConn.disconnect();\n }\n const replicaSource = new InMemorySource(replicaObjs, {\n kind: 'live',\n label: `replica:${replicaProfile.auth.host}`,\n });\n const result = await engine.compare(primarySource, replicaSource);\n const { added, removed, modified } = result.summary;\n results.push({\n profile: replicaName,\n workspaceHost: replicaProfile.auth.host,\n added,\n removed,\n modified,\n total: added + removed + modified,\n });\n }\n\n if (opts.format === 'json') {\n console.log(\n JSON.stringify({ primary: primary.auth.host, threshold, replicas: results }, null, 2),\n );\n } else {\n console.log(`Drift gate — primary ${primary.auth.host}`);\n console.log('');\n console.log(\n ' REPLICA-PROFILE WORKSPACE-HOST ADD RM MOD TOTAL STATUS',\n );\n console.log(\n ' ---------------------- ------------------------------ --- -- --- ----- ------',\n );\n for (const r of results) {\n const status = r.total > threshold ? 'DRIFT' : 'OK';\n console.log(\n ` ${r.profile.padEnd(22).slice(0, 22)} ${r.workspaceHost.padEnd(30).slice(0, 30)} ${String(r.added).padStart(3)} ${String(r.removed).padStart(2)} ${String(r.modified).padStart(3)} ${String(r.total).padStart(5)} ${status}`,\n );\n }\n }\n\n const drifting = results.filter((r) => r.total > threshold);\n if (drifting.length > 0) {\n console.error('');\n console.error(`Drift detected in ${drifting.length} replica(s) (threshold=${threshold}).`);\n process.exitCode = 1;\n }\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,56 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/error-lookup.ts
4
+ import { Command } from "commander";
5
+ import { errorCatalog } from "@ddt-tools/core";
6
+ function errorLookupCommand() {
7
+ const cmd = new Command("error-lookup");
8
+ cmd.description("Look up a failure in the known-error catalog by code, fingerprint, or message.").option("--code <code>", "Adapter error code (e.g. PERMISSION_DENIED).").option("--fingerprint <hex>", "16-char fingerprint from a prior failure.").option("--message <text>", "Free-text error message \u2014 falls back to regex match.").option("--list", "List every entry in the catalog (with code/key/cause).", false).option("--format <fmt>", "text | json. Default text.", "text").action(
9
+ (opts) => {
10
+ const fmt = (opts.format ?? "text").toLowerCase();
11
+ if (opts.list) {
12
+ const all = errorCatalog.DEFAULT_KNOWN_ERRORS;
13
+ if (fmt === "json") {
14
+ process.stdout.write(JSON.stringify(all, null, 2) + "\n");
15
+ } else {
16
+ for (const entry of all) {
17
+ const codeList = entry.codes?.join(", ") ?? "\u2014";
18
+ process.stdout.write(
19
+ `${entry.key}
20
+ codes: ${codeList}
21
+ cause: ${entry.causeSummary}
22
+
23
+ `
24
+ );
25
+ }
26
+ }
27
+ return;
28
+ }
29
+ if (!opts.code && !opts.fingerprint && !opts.message) {
30
+ process.stderr.write("error-lookup: pass --code, --fingerprint, --message, or --list\n");
31
+ process.exitCode = 2;
32
+ return;
33
+ }
34
+ const match = errorCatalog.lookupKnownError({
35
+ code: opts.code,
36
+ fingerprint: opts.fingerprint,
37
+ message: opts.message
38
+ });
39
+ if (!match) {
40
+ process.stderr.write("No catalog entry matched.\n");
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ if (fmt === "json") {
45
+ process.stdout.write(JSON.stringify(match, null, 2) + "\n");
46
+ } else {
47
+ process.stdout.write(errorCatalog.formatCatalogMatch(match) + "\n");
48
+ }
49
+ }
50
+ );
51
+ return cmd;
52
+ }
53
+ export {
54
+ errorLookupCommand
55
+ };
56
+ //# sourceMappingURL=error-lookup-4R3Y4RBC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/error-lookup.ts"],"sourcesContent":["/**\n * `ddt error-lookup` — look up a failure in the known-error catalog (DSR.2).\n *\n * Usage:\n * ddt error-lookup --code PERMISSION_DENIED\n * ddt error-lookup --fingerprint abcd1234deadbeef\n * ddt error-lookup --message \"Permission denied\"\n * ddt error-lookup --list\n *\n * Exit codes:\n * 0 — match found, or `--list` ran\n * 1 — no match found for the supplied input\n * 2 — usage error (no --code / --fingerprint / --message / --list passed)\n *\n * Mirrors `sdt error-lookup`.\n */\nimport { Command } from 'commander';\nimport { errorCatalog } from '@ddt-tools/core';\n\nexport function errorLookupCommand(): Command {\n const cmd = new Command('error-lookup');\n cmd\n .description('Look up a failure in the known-error catalog by code, fingerprint, or message.')\n .option('--code <code>', 'Adapter error code (e.g. PERMISSION_DENIED).')\n .option('--fingerprint <hex>', '16-char fingerprint from a prior failure.')\n .option('--message <text>', 'Free-text error message — falls back to regex match.')\n .option('--list', 'List every entry in the catalog (with code/key/cause).', false)\n .option('--format <fmt>', 'text | json. Default text.', 'text')\n .action(\n (opts: {\n code?: string;\n fingerprint?: string;\n message?: string;\n list?: boolean;\n format?: string;\n }) => {\n const fmt = (opts.format ?? 'text').toLowerCase();\n if (opts.list) {\n const all = errorCatalog.DEFAULT_KNOWN_ERRORS;\n if (fmt === 'json') {\n process.stdout.write(JSON.stringify(all, null, 2) + '\\n');\n } else {\n for (const entry of all) {\n const codeList = entry.codes?.join(', ') ?? '—';\n process.stdout.write(\n `${entry.key}\\n codes: ${codeList}\\n cause: ${entry.causeSummary}\\n\\n`,\n );\n }\n }\n return;\n }\n if (!opts.code && !opts.fingerprint && !opts.message) {\n process.stderr.write('error-lookup: pass --code, --fingerprint, --message, or --list\\n');\n process.exitCode = 2;\n return;\n }\n const match = errorCatalog.lookupKnownError({\n code: opts.code,\n fingerprint: opts.fingerprint,\n message: opts.message,\n });\n if (!match) {\n process.stderr.write('No catalog entry matched.\\n');\n process.exitCode = 1;\n return;\n }\n if (fmt === 'json') {\n process.stdout.write(JSON.stringify(match, null, 2) + '\\n');\n } else {\n process.stdout.write(errorCatalog.formatCatalogMatch(match) + '\\n');\n }\n },\n );\n return cmd;\n}\n"],"mappings":";;;AAgBA,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAEtB,SAAS,qBAA8B;AAC5C,QAAM,MAAM,IAAI,QAAQ,cAAc;AACtC,MACG,YAAY,gFAAgF,EAC5F,OAAO,iBAAiB,8CAA8C,EACtE,OAAO,uBAAuB,2CAA2C,EACzE,OAAO,oBAAoB,2DAAsD,EACjF,OAAO,UAAU,0DAA0D,KAAK,EAChF,OAAO,kBAAkB,8BAA8B,MAAM,EAC7D;AAAA,IACC,CAAC,SAMK;AACJ,YAAM,OAAO,KAAK,UAAU,QAAQ,YAAY;AAChD,UAAI,KAAK,MAAM;AACb,cAAM,MAAM,aAAa;AACzB,YAAI,QAAQ,QAAQ;AAClB,kBAAQ,OAAO,MAAM,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,IAAI;AAAA,QAC1D,OAAO;AACL,qBAAW,SAAS,KAAK;AACvB,kBAAM,WAAW,MAAM,OAAO,KAAK,IAAI,KAAK;AAC5C,oBAAQ,OAAO;AAAA,cACb,GAAG,MAAM,GAAG;AAAA,WAAc,QAAQ;AAAA,WAAc,MAAM,YAAY;AAAA;AAAA;AAAA,YACpE;AAAA,UACF;AAAA,QACF;AACA;AAAA,MACF;AACA,UAAI,CAAC,KAAK,QAAQ,CAAC,KAAK,eAAe,CAAC,KAAK,SAAS;AACpD,gBAAQ,OAAO,MAAM,kEAAkE;AACvF,gBAAQ,WAAW;AACnB;AAAA,MACF;AACA,YAAM,QAAQ,aAAa,iBAAiB;AAAA,QAC1C,MAAM,KAAK;AAAA,QACX,aAAa,KAAK;AAAA,QAClB,SAAS,KAAK;AAAA,MAChB,CAAC;AACD,UAAI,CAAC,OAAO;AACV,gBAAQ,OAAO,MAAM,6BAA6B;AAClD,gBAAQ,WAAW;AACnB;AAAA,MACF;AACA,UAAI,QAAQ,QAAQ;AAClB,gBAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,IAAI;AAAA,MAC5D,OAAO;AACL,gBAAQ,OAAO,MAAM,aAAa,mBAAmB,KAAK,IAAI,IAAI;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AACF,SAAO;AACT;","names":[]}
@@ -0,0 +1,109 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/util/errorReporting.ts
7
+ import { createInterface } from "readline";
8
+ import * as errorReport from "@ddt-tools/core/errorReport";
9
+ var EXEMPT_COMMANDS = /* @__PURE__ */ new Set(["telemetry", "feedback", "help", "completion"]);
10
+ var activeConsent = "unset";
11
+ var uninstallHooks = null;
12
+ async function setupErrorReporting(commandName) {
13
+ if (EXEMPT_COMMANDS.has(commandName)) return;
14
+ const stored = await errorReport.readConsent();
15
+ activeConsent = stored.consent;
16
+ if (shouldPromptFirstRun(stored.consent) && process.stdout.isTTY && process.stdin.isTTY && !isCi()) {
17
+ printBetaNotice();
18
+ activeConsent = await promptFirstRunConsent() ? "on" : "off";
19
+ await errorReport.writeConsent(activeConsent === "on" ? "on" : "off");
20
+ }
21
+ if (activeConsent !== "off") {
22
+ uninstallHooks = errorReport.installProcessHooks();
23
+ }
24
+ }
25
+ async function finishErrorReporting(productVersion, transport = {}) {
26
+ await errorReport.flushErrorEvents();
27
+ await errorReport.sendUsagePing({
28
+ product: "ddt",
29
+ version: productVersion,
30
+ surface: "cli",
31
+ ...transport.fetchImpl ? { fetchImpl: transport.fetchImpl } : {}
32
+ }).catch(() => void 0);
33
+ if (!errorReport.isErrorReportingEnabled(activeConsent)) return;
34
+ const spooled = await errorReport.listSpooled(transport.dir);
35
+ if (spooled.length === 0) return;
36
+ const result = await errorReport.drainSpool({ productVersion }, transport);
37
+ if (result.sent > 0) {
38
+ logger.dim(` (${result.sent} error report${result.sent === 1 ? "" : "s"} sent \u2014 thank you)`);
39
+ }
40
+ }
41
+ async function reportCliFailure(err, productVersion, transport = {}) {
42
+ errorReport.reportError(err, "handled", "cli:main");
43
+ await errorReport.flushErrorEvents(transport.dir);
44
+ if (errorReport.isErrorReportingEnabled(activeConsent)) {
45
+ await errorReport.drainSpool({ productVersion }, transport);
46
+ return;
47
+ }
48
+ if (!process.stdout.isTTY || !process.stdin.isTTY || isCi()) return;
49
+ const yes = await promptYesNo("Report this error to the DDT team? [y/N] ", false);
50
+ if (!yes) return;
51
+ const result = await errorReport.drainSpool({ productVersion }, transport);
52
+ if (result.sent > 0) logger.dim(" Report sent \u2014 thank you.");
53
+ else logger.dim(" Could not reach the error endpoint; the report is queued locally.");
54
+ }
55
+ function teardownErrorReporting() {
56
+ uninstallHooks?.();
57
+ uninstallHooks = null;
58
+ activeConsent = "unset";
59
+ }
60
+ function activeConsentForTests() {
61
+ return activeConsent;
62
+ }
63
+ var BETA_VERSION = "0.2.4";
64
+ function shouldPromptFirstRun(consent) {
65
+ return consent === "unset";
66
+ }
67
+ function printBetaNotice() {
68
+ const lines = [
69
+ "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
70
+ `\u2502 DDT ${BETA_VERSION} \u2014 Public Beta \u2502`,
71
+ "\u2502 \u2022 All features are free during the beta. \u2502",
72
+ "\u2502 \u2022 After the beta: core features stay free forever; \u2502",
73
+ "\u2502 Pro features keep working and show license notices. \u2502",
74
+ "\u2502 \u2022 AI features use your own API key (never ours). \u2502",
75
+ '\u2502 \u2022 Found a bug? Run: ddt feedback "<what happened>" \u2502',
76
+ "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
77
+ ];
78
+ for (const line of lines) logger.info(line);
79
+ }
80
+ async function promptFirstRunConsent() {
81
+ logger.info("DDT can report errors automatically (sanitized diagnostics + OS context,");
82
+ logger.info("never your SQL, identifiers, or credentials) so they get fixed fast.");
83
+ logger.info(errorReport.CONSENT_WARNING);
84
+ return promptYesNo("Enable automatic error reporting? [Y/n] ", true);
85
+ }
86
+ function promptYesNo(question, defaultYes) {
87
+ return new Promise((resolve) => {
88
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
89
+ rl.question(question, (answer) => {
90
+ rl.close();
91
+ const normalized = answer.trim().toLowerCase();
92
+ if (normalized === "") resolve(defaultYes);
93
+ else resolve(normalized === "y" || normalized === "yes");
94
+ });
95
+ });
96
+ }
97
+ function isCi() {
98
+ return process.env["CI"] === "true";
99
+ }
100
+ export {
101
+ activeConsentForTests,
102
+ finishErrorReporting,
103
+ printBetaNotice,
104
+ reportCliFailure,
105
+ setupErrorReporting,
106
+ shouldPromptFirstRun,
107
+ teardownErrorReporting
108
+ };
109
+ //# sourceMappingURL=errorReporting-3LPE2IJY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/errorReporting.ts"],"sourcesContent":["/**\n * CLI error-reporting wiring (ERR.2).\n *\n * Lazily imported from `cli.ts` hooks so the cold-start path pays zero cost\n * until a command actually runs (preAction) or fails (catch handler).\n *\n * Responsibilities:\n * - first-run consent prompt (TTY only, default Yes, one keystroke)\n * - crash-hook installation when consent allows capture\n * - end-of-run drain: spool → `POST /errors` when consent is `on`\n * - manual \"Report this error? [y/N]\" prompt when consent is NOT `on`\n * and a command fails\n *\n * @see @ddt-tools/core/errorReport (capture substrate + consent + transport)\n */\nimport { createInterface } from 'node:readline';\n// Subpath import, NOT the `@ddt-tools/core` barrel — this module runs in the\n// preAction hook of every command; the barrel would drag ~85 core modules in.\nimport * as errorReport from '@ddt-tools/core/errorReport';\nimport { logger } from './logger.js';\n\n/** Commands that must never trigger the consent prompt or auto-drain. */\nconst EXEMPT_COMMANDS = new Set(['telemetry', 'feedback', 'help', 'completion']);\n\nlet activeConsent: errorReport.ErrorReportConsent = 'unset';\nlet uninstallHooks: (() => void) | null = null;\n\n/**\n * preAction hook body. Reads (and on first run, prompts for) consent, then\n * installs crash capture unless the user opted out.\n */\nexport async function setupErrorReporting(commandName: string): Promise<void> {\n if (EXEMPT_COMMANDS.has(commandName)) return;\n const stored = await errorReport.readConsent();\n activeConsent = stored.consent;\n\n // First-run consent prompt — explicit, one keystroke, default Yes.\n // Skipped when not interactive (CI, pipes) so scripted runs never block.\n if (\n shouldPromptFirstRun(stored.consent) &&\n process.stdout.isTTY &&\n process.stdin.isTTY &&\n !isCi()\n ) {\n // Beta install-time messaging — shown ONCE, immediately before the very\n // first consent question (true first run, consent still `unset`).\n printBetaNotice();\n activeConsent = (await promptFirstRunConsent()) ? 'on' : 'off';\n await errorReport.writeConsent(activeConsent === 'on' ? 'on' : 'off');\n }\n\n // Crash capture is installed unless the user said no. Capture is local-only;\n // nothing leaves the machine without `isErrorReportingEnabled` saying so.\n if (activeConsent !== 'off') {\n uninstallHooks = errorReport.installProcessHooks();\n }\n}\n\n/**\n * postAction hook body. Flushes buffered events and, when consent is `on`,\n * drains the spool to the Worker. No-ops fast when there is nothing to send.\n * `transport` is injectable for tests.\n */\nexport async function finishErrorReporting(\n productVersion: string,\n transport: errorReport.TransportOptions = {},\n): Promise<void> {\n await errorReport.flushErrorEvents();\n\n // Anonymous usage ping (opt-in) — gated internally by the SAME consent +\n // env opt-outs as error reporting and throttled to once per 24h. It is\n // awaited here (not detached) so the postAction lifecycle has a\n // deterministic completion point: `sendUsagePing` is self-bounded (3s hard\n // timeout) and never throws, so awaiting it can block command exit by at\n // most that timeout — the same bound the spool drain below already imposes.\n // Detaching it left the send racing process teardown, which both dropped\n // pings in practice and made the wiring untestable without a sleep.\n // `transport.fetchImpl` is threaded through so tests can intercept it.\n await errorReport\n .sendUsagePing({\n product: 'ddt',\n version: productVersion,\n surface: 'cli',\n ...(transport.fetchImpl ? { fetchImpl: transport.fetchImpl } : {}),\n })\n .catch(() => undefined);\n\n if (!errorReport.isErrorReportingEnabled(activeConsent)) return;\n const spooled = await errorReport.listSpooled(transport.dir);\n if (spooled.length === 0) return;\n const result = await errorReport.drainSpool({ productVersion }, transport);\n if (result.sent > 0) {\n logger.dim(` (${result.sent} error report${result.sent === 1 ? '' : 's'} sent — thank you)`);\n }\n}\n\n/**\n * Top-level command-failure handler. Reports the error as `handled` (real\n * crashes go through `uncaughtExceptionMonitor`), then either auto-sends\n * (consent `on`) or offers a one-keystroke manual report.\n * `transport` is injectable for tests.\n */\nexport async function reportCliFailure(\n err: unknown,\n productVersion: string,\n transport: errorReport.TransportOptions = {},\n): Promise<void> {\n errorReport.reportError(err, 'handled', 'cli:main');\n await errorReport.flushErrorEvents(transport.dir);\n\n if (errorReport.isErrorReportingEnabled(activeConsent)) {\n await errorReport.drainSpool({ productVersion }, transport);\n return;\n }\n\n // Manual push path — only when interactive.\n if (!process.stdout.isTTY || !process.stdin.isTTY || isCi()) return;\n const yes = await promptYesNo('Report this error to the DDT team? [y/N] ', false);\n if (!yes) return;\n const result = await errorReport.drainSpool({ productVersion }, transport);\n if (result.sent > 0) logger.dim(' Report sent — thank you.');\n else logger.dim(' Could not reach the error endpoint; the report is queued locally.');\n}\n\n/** Uninstall crash hooks (tests). */\nexport function teardownErrorReporting(): void {\n uninstallHooks?.();\n uninstallHooks = null;\n activeConsent = 'unset';\n}\n\n/** Current consent as seen by the hooks (tests). */\nexport function activeConsentForTests(): errorReport.ErrorReportConsent {\n return activeConsent;\n}\n\n/** DDT version surfaced in the first-run beta notice. */\nconst BETA_VERSION = '0.2.4';\n\n/**\n * Whether the first-run flow (beta notice + consent prompt) should fire.\n * True ONLY on true first run — when consent has never been decided\n * (`unset`). Exported so the \"first run only\" contract is unit-testable\n * without faking a TTY.\n */\nexport function shouldPromptFirstRun(consent: errorReport.ErrorReportConsent): boolean {\n return consent === 'unset';\n}\n\n/**\n * Beta install-time messaging — printed ONCE, on true first run (consent\n * `unset`), immediately before the consent question. Tells the user, at\n * install time: it's a 30-day public beta with all features free; what\n * happens after the beta (core stays free forever, Pro features keep\n * working but show license notices); that AI features are bring-your-own\n * key; and how to report a bug. Plain ASCII box to match CLI output style.\n */\nexport function printBetaNotice(): void {\n const lines = [\n '┌─────────────────────────────────────────────────────────┐',\n `│ DDT ${BETA_VERSION} — Public Beta │`,\n '│ • All features are free during the beta. │',\n '│ • After the beta: core features stay free forever; │',\n '│ Pro features keep working and show license notices. │',\n '│ • AI features use your own API key (never ours). │',\n '│ • Found a bug? Run: ddt feedback \"<what happened>\" │',\n '└─────────────────────────────────────────────────────────┘',\n ];\n for (const line of lines) logger.info(line);\n}\n\nasync function promptFirstRunConsent(): Promise<boolean> {\n logger.info('DDT can report errors automatically (sanitized diagnostics + OS context,');\n logger.info('never your SQL, identifiers, or credentials) so they get fixed fast.');\n logger.info(errorReport.CONSENT_WARNING);\n return promptYesNo('Enable automatic error reporting? [Y/n] ', true);\n}\n\nfunction promptYesNo(question: string, defaultYes: boolean): Promise<boolean> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer) => {\n rl.close();\n const normalized = answer.trim().toLowerCase();\n if (normalized === '') resolve(defaultYes);\n else resolve(normalized === 'y' || normalized === 'yes');\n });\n });\n}\n\nfunction isCi(): boolean {\n return process.env['CI'] === 'true';\n}\n"],"mappings":"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","names":[]}