@ddt-tools/cli 0.2.0 → 0.2.5

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 (205) 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 +504 -19402
  38. package/dist/cli.js.map +1 -1
  39. package/dist/compare-IOEATL6G.js +435 -0
  40. package/dist/compare-IOEATL6G.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-LX6WT4JH.js +109 -0
  70. package/dist/errorReporting-LX6WT4JH.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-EGOVKTLX.js +29 -0
  98. package/dist/import-EGOVKTLX.js.map +1 -0
  99. package/dist/import-script-R5RXPDH6.js +79 -0
  100. package/dist/import-script-R5RXPDH6.js.map +1 -0
  101. package/dist/index.cjs +11 -5
  102. package/dist/index.cjs.map +1 -1
  103. package/dist/index.js +8 -2
  104. package/dist/index.js.map +1 -1
  105. package/dist/init-EAOGNGXI.js +54 -0
  106. package/dist/init-EAOGNGXI.js.map +1 -0
  107. package/dist/install-hooks-G3Y5LVXK.js +109 -0
  108. package/dist/install-hooks-G3Y5LVXK.js.map +1 -0
  109. package/dist/license-Z5YSC7XQ.js +43 -0
  110. package/dist/license-Z5YSC7XQ.js.map +1 -0
  111. package/dist/lineage-C5CGVP36.js +555 -0
  112. package/dist/lineage-C5CGVP36.js.map +1 -0
  113. package/dist/lint-AQFPZ3WG.js +144 -0
  114. package/dist/lint-AQFPZ3WG.js.map +1 -0
  115. package/dist/mcp-6ZXOAF7S.js +343 -0
  116. package/dist/mcp-6ZXOAF7S.js.map +1 -0
  117. package/dist/migrate-from-dbt-K4ELOWUD.js +156 -0
  118. package/dist/migrate-from-dbt-K4ELOWUD.js.map +1 -0
  119. package/dist/migrate-platform-E7VZFPO5.js +91 -0
  120. package/dist/migrate-platform-E7VZFPO5.js.map +1 -0
  121. package/dist/optimize-WUJ5ZN5Y.js +109 -0
  122. package/dist/optimize-WUJ5ZN5Y.js.map +1 -0
  123. package/dist/perf-UULZSREY.js +200 -0
  124. package/dist/perf-UULZSREY.js.map +1 -0
  125. package/dist/pii-QHU32VML.js +146 -0
  126. package/dist/pii-QHU32VML.js.map +1 -0
  127. package/dist/pilot-BR6GVK32.js +29 -0
  128. package/dist/pilot-BR6GVK32.js.map +1 -0
  129. package/dist/pr-comment-2FOA3EXG.js +81 -0
  130. package/dist/pr-comment-2FOA3EXG.js.map +1 -0
  131. package/dist/preview-XNY422OU.js +46 -0
  132. package/dist/preview-XNY422OU.js.map +1 -0
  133. package/dist/profile-SQTBNKYS.js +98 -0
  134. package/dist/profile-SQTBNKYS.js.map +1 -0
  135. package/dist/promote-FSGUPIPD.js +417 -0
  136. package/dist/promote-FSGUPIPD.js.map +1 -0
  137. package/dist/publish-HLP3XHM5.js +766 -0
  138. package/dist/publish-HLP3XHM5.js.map +1 -0
  139. package/dist/purge-Y5IOTXKA.js +56 -0
  140. package/dist/purge-Y5IOTXKA.js.map +1 -0
  141. package/dist/query-log-SDDGMJLJ.js +112 -0
  142. package/dist/query-log-SDDGMJLJ.js.map +1 -0
  143. package/dist/refactor-TC7S43F2.js +5809 -0
  144. package/dist/refactor-TC7S43F2.js.map +1 -0
  145. package/dist/refresh-MDJYOYV5.js +39 -0
  146. package/dist/refresh-MDJYOYV5.js.map +1 -0
  147. package/dist/replay-E4664A5K.js +118 -0
  148. package/dist/replay-E4664A5K.js.map +1 -0
  149. package/dist/revert-QWQWCJJB.js +111 -0
  150. package/dist/revert-QWQWCJJB.js.map +1 -0
  151. package/dist/review-7CAVLD67.js +164 -0
  152. package/dist/review-7CAVLD67.js.map +1 -0
  153. package/dist/rollback-suggest-C6D5YFCA.js +79 -0
  154. package/dist/rollback-suggest-C6D5YFCA.js.map +1 -0
  155. package/dist/safer-alternative-QR4QEFUV.js +84 -0
  156. package/dist/safer-alternative-QR4QEFUV.js.map +1 -0
  157. package/dist/safety-OFWUFLK4.js +165 -0
  158. package/dist/safety-OFWUFLK4.js.map +1 -0
  159. package/dist/savings-MEBE4TXI.js +95 -0
  160. package/dist/savings-MEBE4TXI.js.map +1 -0
  161. package/dist/scan-secrets-XCUBMLHL.js +54 -0
  162. package/dist/scan-secrets-XCUBMLHL.js.map +1 -0
  163. package/dist/schema-7JZIG6QR.js +447 -0
  164. package/dist/schema-7JZIG6QR.js.map +1 -0
  165. package/dist/script-BMYVBHFR.js +167 -0
  166. package/dist/script-BMYVBHFR.js.map +1 -0
  167. package/dist/search-TA3C3AZT.js +151 -0
  168. package/dist/search-TA3C3AZT.js.map +1 -0
  169. package/dist/seed-W4Q3L2IU.js +101 -0
  170. package/dist/seed-W4Q3L2IU.js.map +1 -0
  171. package/dist/sketch-6B2V6FJV.js +83 -0
  172. package/dist/sketch-6B2V6FJV.js.map +1 -0
  173. package/dist/snapshot-YMVS322L.js +171 -0
  174. package/dist/snapshot-YMVS322L.js.map +1 -0
  175. package/dist/snippets-EVTN63OU.js +74 -0
  176. package/dist/snippets-EVTN63OU.js.map +1 -0
  177. package/dist/standards-FGJW3CQL.js +238 -0
  178. package/dist/standards-FGJW3CQL.js.map +1 -0
  179. package/dist/suggest-V3LVIFZ5.js +44 -0
  180. package/dist/suggest-V3LVIFZ5.js.map +1 -0
  181. package/dist/suggest-constraints-EX2FCWOQ.js +154 -0
  182. package/dist/suggest-constraints-EX2FCWOQ.js.map +1 -0
  183. package/dist/suite-YTQ3CNX5.js +85 -0
  184. package/dist/suite-YTQ3CNX5.js.map +1 -0
  185. package/dist/telemetry-KOIY3NEQ.js +90 -0
  186. package/dist/telemetry-KOIY3NEQ.js.map +1 -0
  187. package/dist/template-MUJ6X6LN.js +396 -0
  188. package/dist/template-MUJ6X6LN.js.map +1 -0
  189. package/dist/test-XFSQHR2S.js +169 -0
  190. package/dist/test-XFSQHR2S.js.map +1 -0
  191. package/dist/trial-GFTGYCR3.js +31 -0
  192. package/dist/trial-GFTGYCR3.js.map +1 -0
  193. package/dist/validate-LFDEZFFH.js +107 -0
  194. package/dist/validate-LFDEZFFH.js.map +1 -0
  195. package/dist/verify-KRDYOJCR.js +76 -0
  196. package/dist/verify-KRDYOJCR.js.map +1 -0
  197. package/dist/watch-FSG23RR3.js +80 -0
  198. package/dist/watch-FSG23RR3.js.map +1 -0
  199. package/dist/xcompare-U4TXTTIR.js +87 -0
  200. package/dist/xcompare-U4TXTTIR.js.map +1 -0
  201. package/package.json +2 -2
  202. package/dist/cli.cjs +0 -19298
  203. package/dist/cli.cjs.map +0 -1
  204. package/dist/cli.d.cts +0 -1
  205. package/dist/cli.d.ts +0 -1
@@ -0,0 +1,87 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/advise-tests.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import { Command } from "commander";
7
+ import { ai, regressionAdvisor } from "@ddt-tools/core";
8
+ function adviseTestsCommand() {
9
+ const cmd = new Command("advise-tests");
10
+ cmd.description(
11
+ "PR-time AI regression-test advisor. Walks the DDT feature catalog, flags features whose surfaces match the changed files, and recommends 1\u20138 tests to add alongside the diff."
12
+ ).option(
13
+ "--changed-files <path>",
14
+ "File containing one changed-file path per line. Defaults to reading from stdin when not provided."
15
+ ).option(
16
+ "--diff-summary <text>",
17
+ 'Optional prose summary of the diff. Use "-" to read from stdin.'
18
+ ).option(
19
+ "--existing-tests <path>",
20
+ "File listing existing test file paths so the advisor avoids duplicates."
21
+ ).option("--format <fmt>", "Output format: markdown | json. Default markdown.", "markdown").option("-o, --out <path>", "Output file path. Defaults to stdout.").option(
22
+ "--ai-max-spend <usd>",
23
+ "Refuse the AI call if today's estimated spend \u2265 this (USD). 0 = no cap.",
24
+ "0"
25
+ ).action(async (opts) => {
26
+ await runAdviseTests(opts, "ddt");
27
+ });
28
+ return cmd;
29
+ }
30
+ async function runAdviseTests(opts, toolName) {
31
+ const changedFiles = opts.changedFiles ? splitLines(await fs.readFile(path.resolve(String(opts.changedFiles)), "utf8")) : splitLines(await readStdin());
32
+ if (changedFiles.length === 0) {
33
+ throw new Error(
34
+ "advise-tests: no changed files supplied. Pass --changed-files or pipe via stdin."
35
+ );
36
+ }
37
+ const diffSummary = opts.diffSummary === "-" ? await readStdin() : opts.diffSummary ? String(opts.diffSummary) : void 0;
38
+ const existingTestFiles = opts.existingTests ? splitLines(await fs.readFile(path.resolve(String(opts.existingTests)), "utf8")) : void 0;
39
+ const result = await regressionAdvisor.adviseTests(
40
+ {
41
+ changedFiles,
42
+ ...diffSummary ? { diffSummary } : {},
43
+ ...existingTestFiles ? { existingTestFiles } : {}
44
+ },
45
+ {
46
+ completeFn: async (user, system) => {
47
+ const r = await ai.complete(
48
+ [
49
+ { role: "system", content: system },
50
+ { role: "user", content: user }
51
+ ],
52
+ {
53
+ feature: "advise-tests",
54
+ maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
55
+ }
56
+ );
57
+ return r.text;
58
+ }
59
+ },
60
+ toolName
61
+ );
62
+ const format = String(opts.format ?? "markdown").toLowerCase();
63
+ const payload = format === "json" ? JSON.stringify({ ...result, rawModelText: void 0 }, null, 2) : regressionAdvisor.renderAdvisorMarkdown(result, toolName);
64
+ if (opts.out) {
65
+ const outPath = path.resolve(String(opts.out));
66
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
67
+ await fs.writeFile(outPath, payload + (payload.endsWith("\n") ? "" : "\n"), "utf8");
68
+ console.error(`Wrote ${outPath} (${payload.length} bytes).`);
69
+ } else {
70
+ process.stdout.write(payload + (payload.endsWith("\n") ? "" : "\n"));
71
+ }
72
+ }
73
+ function splitLines(s) {
74
+ return s.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
75
+ }
76
+ async function readStdin() {
77
+ const chunks = [];
78
+ for await (const chunk of process.stdin) {
79
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
80
+ }
81
+ return Buffer.concat(chunks).toString("utf8");
82
+ }
83
+ export {
84
+ adviseTestsCommand,
85
+ runAdviseTests
86
+ };
87
+ //# sourceMappingURL=advise-tests-YNMKVJCD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/advise-tests.ts"],"sourcesContent":["/**\n * `ddt advise-tests` — AI-assisted regression-test recommender\n * (Testing Infrastructure Phase 4).\n *\n * Walks a list of changed files (typically the output of\n * `git diff --name-only base..head`), runs a pre-AI heuristic against\n * `@ddt-tools/core/features.DDT_FEATURE_CATALOG`, and asks the configured AI\n * provider to recommend a minimal regression-test set.\n *\n * Mirrors `sdt advise-tests`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { ai, regressionAdvisor } from '@ddt-tools/core';\n\nexport function adviseTestsCommand(): Command {\n const cmd = new Command('advise-tests');\n cmd\n .description(\n 'PR-time AI regression-test advisor. Walks the DDT feature catalog, flags features whose surfaces match the changed files, ' +\n 'and recommends 1–8 tests to add alongside the diff.',\n )\n .option(\n '--changed-files <path>',\n 'File containing one changed-file path per line. Defaults to reading from stdin when not provided.',\n )\n .option(\n '--diff-summary <text>',\n 'Optional prose summary of the diff. Use \"-\" to read from stdin.',\n )\n .option(\n '--existing-tests <path>',\n 'File listing existing test file paths so the advisor avoids duplicates.',\n )\n .option('--format <fmt>', 'Output format: markdown | json. Default markdown.', 'markdown')\n .option('-o, --out <path>', 'Output file path. Defaults to stdout.')\n .option(\n '--ai-max-spend <usd>',\n \"Refuse the AI call if today's estimated spend ≥ this (USD). 0 = no cap.\",\n '0',\n )\n .action(async (opts) => {\n await runAdviseTests(opts, 'ddt');\n });\n return cmd;\n}\n\nexport async function runAdviseTests(\n opts: Record<string, unknown>,\n toolName: 'ddt' | 'ddt',\n): Promise<void> {\n const changedFiles = opts.changedFiles\n ? splitLines(await fs.readFile(path.resolve(String(opts.changedFiles)), 'utf8'))\n : splitLines(await readStdin());\n if (changedFiles.length === 0) {\n throw new Error(\n 'advise-tests: no changed files supplied. Pass --changed-files or pipe via stdin.',\n );\n }\n const diffSummary =\n opts.diffSummary === '-'\n ? await readStdin()\n : opts.diffSummary\n ? String(opts.diffSummary)\n : undefined;\n const existingTestFiles = opts.existingTests\n ? splitLines(await fs.readFile(path.resolve(String(opts.existingTests)), 'utf8'))\n : undefined;\n\n const result = await regressionAdvisor.adviseTests(\n {\n changedFiles,\n ...(diffSummary ? { diffSummary } : {}),\n ...(existingTestFiles ? { existingTestFiles } : {}),\n },\n {\n completeFn: async (user, system) => {\n const r = await ai.complete(\n [\n { role: 'system', content: system },\n { role: 'user', content: user },\n ],\n {\n feature: 'advise-tests',\n maxSpendUsd: Number(opts.aiMaxSpend ?? '0') || 0,\n },\n );\n return r.text;\n },\n },\n toolName,\n );\n\n const format = String(opts.format ?? 'markdown').toLowerCase();\n const payload =\n format === 'json'\n ? JSON.stringify({ ...result, rawModelText: undefined }, null, 2)\n : regressionAdvisor.renderAdvisorMarkdown(result, toolName);\n\n if (opts.out) {\n const outPath = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, payload + (payload.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${outPath} (${payload.length} bytes).`);\n } else {\n process.stdout.write(payload + (payload.endsWith('\\n') ? '' : '\\n'));\n }\n}\n\nfunction splitLines(s: string): string[] {\n return s\n .split(/\\r?\\n/)\n .map((l) => l.trim())\n .filter((l) => l.length > 0 && !l.startsWith('#'));\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer));\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,86 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/ai.ts
4
+ import { Command } from "commander";
5
+ import { ai } from "@ddt-tools/core";
6
+ function aiCommand() {
7
+ const cmd = new Command("ai");
8
+ cmd.description("Configure and test the AI provider adapter (BYO key).");
9
+ cmd.command("status").description("Show the resolved AI provider config (provider, model, key source).").action(async () => {
10
+ const config = await ai.resolveAiConfig();
11
+ const apiKey = ai.resolveApiKey(config);
12
+ console.log("AI provider config (configured):");
13
+ console.log(` provider: ${config.provider}`);
14
+ console.log(` model: ${config.model}`);
15
+ console.log(` endpoint: ${config.endpoint ?? "(provider default)"}`);
16
+ console.log(` apiKey: ${apiKey ? "\u2713 resolved" : "\u2717 not set"}`);
17
+ console.log(` maxTokens: ${config.maxTokens ?? "(provider default)"}`);
18
+ console.log(` temperature: ${config.temperature ?? "(provider default)"}`);
19
+ console.log(
20
+ ` preferLocal: ${config.preferLocal !== false ? "true (local-first)" : "false (cloud only)"}`
21
+ );
22
+ console.log(` configPath: ${ai.defaultConfigPath()}`);
23
+ if (config.preferLocal !== false) {
24
+ const probe = await ai.detectOllamaAvailability({ fetch: globalThis.fetch });
25
+ console.log("Local Ollama probe:");
26
+ if (probe.available) {
27
+ const best = ai.pickBestLocalModel(probe.models);
28
+ console.log(` status: \u2713 available at ${probe.endpoint}`);
29
+ console.log(` models: ${probe.models.join(", ") || "(none pulled)"}`);
30
+ console.log(` selected: ${best ?? "(none)"}`);
31
+ console.log("Effective routing: Ollama (local, zero cloud cost)");
32
+ } else {
33
+ console.log(` status: \u2717 unavailable (${probe.reason})`);
34
+ console.log(` fallback: ${config.provider} / ${config.model}`);
35
+ console.log(`Effective routing: ${config.provider} (cloud)`);
36
+ }
37
+ }
38
+ });
39
+ cmd.command("test").description("Round-trip a small prompt against the configured provider.").option("--prompt <text>", "Prompt to send.", "Reply with exactly the four characters: PONG").option(
40
+ "--ai-max-spend <usd>",
41
+ "Refuse the call if today's estimated spend \u2265 this (USD). 0 = no cap.",
42
+ "0"
43
+ ).action(async (opts) => {
44
+ const result = await ai.complete(
45
+ [
46
+ { role: "system", content: "You are a terse assistant. Follow instructions exactly." },
47
+ { role: "user", content: String(opts.prompt) }
48
+ ],
49
+ {
50
+ feature: "ai.test",
51
+ maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
52
+ }
53
+ );
54
+ console.log(`Response from ${result.provider} (${result.model}):`);
55
+ console.log(result.text);
56
+ console.log(
57
+ ` tokens: ${result.usage.promptTokens} in / ${result.usage.completionTokens} out`
58
+ );
59
+ });
60
+ cmd.command("usage").description("Summarize the local AI usage ledger.").action(async () => {
61
+ const records = await ai.readUsage();
62
+ const today = ai.todayRecords(records);
63
+ const todayUsd = ai.totalSpend(today);
64
+ const allUsd = ai.totalSpend(records);
65
+ console.log("AI usage (local ledger):");
66
+ console.log(` today: ${today.length} call(s), ~$${todayUsd.toFixed(4)}`);
67
+ console.log(` total: ${records.length} call(s), ~$${allUsd.toFixed(4)}`);
68
+ console.log(` ledger: ${ai.defaultUsagePath()}`);
69
+ if (records.length > 0) {
70
+ const byFeature = /* @__PURE__ */ new Map();
71
+ for (const r of records) {
72
+ const key = r.feature ?? "(unknown)";
73
+ byFeature.set(key, (byFeature.get(key) ?? 0) + 1);
74
+ }
75
+ console.log(" features:");
76
+ for (const [feature, count] of [...byFeature.entries()].sort((a, b) => b[1] - a[1])) {
77
+ console.log(` ${feature}: ${count}`);
78
+ }
79
+ }
80
+ });
81
+ return cmd;
82
+ }
83
+ export {
84
+ aiCommand
85
+ };
86
+ //# sourceMappingURL=ai-NTNPYEKZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/ai.ts"],"sourcesContent":["/**\n * `ddt ai` — configure + test the AI provider adapter.\n *\n * Subcommands:\n * ddt ai status — show current config (provider, model, key source)\n * ddt ai test — round-trip a hello-world prompt\n * ddt ai usage — summarize ai-usage.json (today + this month)\n *\n * Phase-1 of the AI rollout (see docs/AI_FEATURES.md). Mirrors\n * `sdt ai` — same subcommands, same shape, only the default config\n * path differs (`~/.config/ddt/ai.json`).\n */\nimport { Command } from 'commander';\nimport { ai } from '@ddt-tools/core';\n\nexport function aiCommand(): Command {\n const cmd = new Command('ai');\n cmd.description('Configure and test the AI provider adapter (BYO key).');\n\n cmd\n .command('status')\n .description('Show the resolved AI provider config (provider, model, key source).')\n .action(async () => {\n const config = await ai.resolveAiConfig();\n const apiKey = ai.resolveApiKey(config);\n console.log('AI provider config (configured):');\n console.log(` provider: ${config.provider}`);\n console.log(` model: ${config.model}`);\n console.log(` endpoint: ${config.endpoint ?? '(provider default)'}`);\n console.log(` apiKey: ${apiKey ? '✓ resolved' : '✗ not set'}`);\n console.log(` maxTokens: ${config.maxTokens ?? '(provider default)'}`);\n console.log(` temperature: ${config.temperature ?? '(provider default)'}`);\n console.log(\n ` preferLocal: ${config.preferLocal !== false ? 'true (local-first)' : 'false (cloud only)'}`,\n );\n console.log(` configPath: ${ai.defaultConfigPath()}`);\n\n if (config.preferLocal !== false) {\n const probe = await ai.detectOllamaAvailability({ fetch: globalThis.fetch });\n console.log('Local Ollama probe:');\n if (probe.available) {\n const best = ai.pickBestLocalModel(probe.models);\n console.log(` status: ✓ available at ${probe.endpoint}`);\n console.log(` models: ${probe.models.join(', ') || '(none pulled)'}`);\n console.log(` selected: ${best ?? '(none)'}`);\n console.log('Effective routing: Ollama (local, zero cloud cost)');\n } else {\n console.log(` status: ✗ unavailable (${probe.reason})`);\n console.log(` fallback: ${config.provider} / ${config.model}`);\n console.log(`Effective routing: ${config.provider} (cloud)`);\n }\n }\n });\n\n cmd\n .command('test')\n .description('Round-trip a small prompt against the configured provider.')\n .option('--prompt <text>', 'Prompt to send.', 'Reply with exactly the four characters: PONG')\n .option(\n '--ai-max-spend <usd>',\n \"Refuse the call if today's estimated spend ≥ this (USD). 0 = no cap.\",\n '0',\n )\n .action(async (opts) => {\n const result = await ai.complete(\n [\n { role: 'system', content: 'You are a terse assistant. Follow instructions exactly.' },\n { role: 'user', content: String(opts.prompt) },\n ],\n {\n feature: 'ai.test',\n maxSpendUsd: Number(opts.aiMaxSpend ?? '0') || 0,\n },\n );\n console.log(`Response from ${result.provider} (${result.model}):`);\n console.log(result.text);\n console.log(\n ` tokens: ${result.usage.promptTokens} in / ${result.usage.completionTokens} out`,\n );\n });\n\n cmd\n .command('usage')\n .description('Summarize the local AI usage ledger.')\n .action(async () => {\n const records = await ai.readUsage();\n const today = ai.todayRecords(records);\n const todayUsd = ai.totalSpend(today);\n const allUsd = ai.totalSpend(records);\n console.log('AI usage (local ledger):');\n console.log(` today: ${today.length} call(s), ~$${todayUsd.toFixed(4)}`);\n console.log(` total: ${records.length} call(s), ~$${allUsd.toFixed(4)}`);\n console.log(` ledger: ${ai.defaultUsagePath()}`);\n if (records.length > 0) {\n const byFeature = new Map<string, number>();\n for (const r of records) {\n const key = r.feature ?? '(unknown)';\n byFeature.set(key, (byFeature.get(key) ?? 0) + 1);\n }\n console.log(' features:');\n for (const [feature, count] of [...byFeature.entries()].sort((a, b) => b[1] - a[1])) {\n console.log(` ${feature}: ${count}`);\n }\n }\n });\n\n return cmd;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,139 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/anonymize.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import { Command } from "commander";
7
+ import { loadProject, pac, parseProjectModel } from "@ddt-tools/core";
8
+ function anonymizeCommand() {
9
+ const cmd = new Command("anonymize");
10
+ cmd.description("Generate column-mask DDL for PII columns detected in the project model.").requiredOption("--source <path>", ".ddtproj or .ddtpac to analyze.").option(
11
+ "--unmasked-user <name>",
12
+ "User / service-principal that sees unmasked values. Default 'ddt-deploy'.",
13
+ "ddt-deploy"
14
+ ).option(
15
+ "--mask-catalog <catalog>",
16
+ "Catalog the mask functions live in. Default: same catalog as the columns."
17
+ ).option("-o, --out <path>", "Output file path. Default: ./anonymize.sql in CWD.").action(async (opts) => {
18
+ const sourcePath = String(opts.source);
19
+ const model = await loadModel(sourcePath);
20
+ const unmaskedUser = String(opts.unmaskedUser);
21
+ const maskCatalog = opts.maskCatalog ? String(opts.maskCatalog) : void 0;
22
+ const candidates = collectPiiColumns(model);
23
+ if (candidates.length === 0) {
24
+ console.error("No PII columns detected (heuristic on column names).");
25
+ return;
26
+ }
27
+ const sql = renderAnonymizeSql(candidates, unmaskedUser, maskCatalog);
28
+ const outPath = opts.out ? path.resolve(String(opts.out)) : path.resolve("anonymize.sql");
29
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
30
+ await fs.writeFile(outPath, sql, "utf8");
31
+ console.error(`Wrote ${outPath} (${candidates.length} column(s), ${sql.length} bytes).`);
32
+ console.error(
33
+ "Review the user allow-list before applying. Default fully masks values from every user except the one passed in."
34
+ );
35
+ });
36
+ return cmd;
37
+ }
38
+ function collectPiiColumns(model) {
39
+ const out = [];
40
+ for (const o of model) {
41
+ if (o.objectType !== "MANAGED_TABLE" && o.objectType !== "EXTERNAL_TABLE") continue;
42
+ const rec = o;
43
+ const cols = rec.columns;
44
+ if (!Array.isArray(cols)) continue;
45
+ const catalog = o.fqn.database ?? "main";
46
+ const schema = o.fqn.schema ?? "default";
47
+ for (const c of cols) {
48
+ const category = classify(c.name);
49
+ if (category) out.push({ catalog, schema, table: o.fqn.name, column: c.name, category });
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ function classify(name) {
55
+ const lower = name.toLowerCase();
56
+ if (/(^|_)email(_|$)/.test(lower)) return "email";
57
+ if (/(^|_)(phone|mobile|cell)(_|$)/.test(lower)) return "phone";
58
+ if (/(^|_)(first_name|last_name|full_name|surname|given_name)(_|$)/.test(lower)) return "name";
59
+ if (/(^|_)(dob|birth_date|date_of_birth)(_|$)/.test(lower)) return "dob";
60
+ if (/(^|_)ssn(_|$)/.test(lower) || /(^|_)tax_id(_|$)/.test(lower)) return "ssn";
61
+ if (/(^|_)(address|street|zip_code|postal_code|postcode)(_|$)/.test(lower)) return "address";
62
+ if (/(^|_)(credit_card|card_number)(_|$)/.test(lower)) return "credit-card";
63
+ return void 0;
64
+ }
65
+ function renderAnonymizeSql(cols, unmaskedUser, maskCatalogOverride) {
66
+ const lines = [];
67
+ lines.push(`-- Generated by \`ddt anonymize\`.`);
68
+ lines.push(
69
+ `-- Edit the user allow-list before applying. Default: only ${unmaskedUser} sees unmasked values.`
70
+ );
71
+ lines.push("");
72
+ const byCatalog = /* @__PURE__ */ new Map();
73
+ for (const c of cols) {
74
+ const target = maskCatalogOverride ?? c.catalog;
75
+ const arr = byCatalog.get(target) ?? [];
76
+ arr.push(c);
77
+ byCatalog.set(target, arr);
78
+ }
79
+ for (const [catalog, catalogCols] of byCatalog) {
80
+ const cats = new Set(catalogCols.map((c) => c.category));
81
+ lines.push(`-- \u2500\u2500 Mask functions in catalog \`${catalog}\`.security \u2500\u2500`);
82
+ lines.push(`CREATE SCHEMA IF NOT EXISTS ${catalog}.security;`);
83
+ lines.push("");
84
+ for (const cat of cats) {
85
+ lines.push(
86
+ `CREATE OR REPLACE FUNCTION ${catalog}.security.mask_${cat.replace(/-/g, "_")}(val STRING)`
87
+ );
88
+ lines.push(` RETURNS STRING`);
89
+ lines.push(` RETURN CASE`);
90
+ lines.push(
91
+ ` WHEN is_member('${unmaskedUser}') OR current_user() = '${unmaskedUser}' THEN val`
92
+ );
93
+ lines.push(` ELSE ${maskExpression(cat)}`);
94
+ lines.push(` END;`);
95
+ lines.push("");
96
+ }
97
+ }
98
+ lines.push("-- Apply masks to columns");
99
+ for (const c of cols) {
100
+ const fnCatalog = maskCatalogOverride ?? c.catalog;
101
+ const fnName = `${fnCatalog}.security.mask_${c.category.replace(/-/g, "_")}`;
102
+ lines.push(
103
+ `ALTER TABLE ${c.catalog}.${c.schema}.${c.table} ALTER COLUMN ${c.column} SET MASK ${fnName};`
104
+ );
105
+ }
106
+ lines.push("");
107
+ return lines.join("\n") + "\n";
108
+ }
109
+ function maskExpression(cat) {
110
+ switch (cat) {
111
+ case "email":
112
+ return `regexp_replace(val, '^(.).*(@.*)$', '$1***$2')`;
113
+ case "phone":
114
+ return `concat('***-***-', right(val, 4))`;
115
+ case "ssn":
116
+ return `concat('***-**-', right(val, 4))`;
117
+ case "credit-card":
118
+ return `concat('****-****-****-', right(val, 4))`;
119
+ case "dob":
120
+ return `concat(left(val, 4), '-XX-XX')`;
121
+ case "name":
122
+ case "address":
123
+ case "generic":
124
+ default:
125
+ return `sha2(val, 256)`;
126
+ }
127
+ }
128
+ async function loadModel(sourcePath) {
129
+ if (sourcePath.endsWith(".ddtpac")) {
130
+ const c = await pac.readPac(sourcePath);
131
+ return c.model;
132
+ }
133
+ const loaded = await loadProject(sourcePath);
134
+ return await parseProjectModel(loaded);
135
+ }
136
+ export {
137
+ anonymizeCommand
138
+ };
139
+ //# sourceMappingURL=anonymize-LERTWUQO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/anonymize.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { loadProject, pac, parseProjectModel, type DatabricksObject } from '@ddt-tools/core';\n\n/**\n * `ddt anonymize` — generate column-mask DDL for PII columns\n * discovered in a project model. Pairs with the L012 `pii-without-mask`\n * lint rule.\n *\n * Databricks Unity Catalog uses `ALTER TABLE ... ALTER COLUMN ... SET\n * MASK <function>` (column masks via UC functions). This command\n * generates:\n * 1. A UC FUNCTION per PII category (mask_email, mask_phone, …)\n * that returns the value unchanged when current_user() is in an\n * allow-list, else returns a masked variant.\n * 2. ALTER COLUMN SET MASK statements wiring each PII column to\n * its category's function.\n *\n * Output is conservative: default allow-list is just the deploy\n * service-principal; production access should be added before\n * applying.\n */\nexport function anonymizeCommand(): Command {\n const cmd = new Command('anonymize');\n cmd\n .description('Generate column-mask DDL for PII columns detected in the project model.')\n .requiredOption('--source <path>', '.ddtproj or .ddtpac to analyze.')\n .option(\n '--unmasked-user <name>',\n \"User / service-principal that sees unmasked values. Default 'ddt-deploy'.\",\n 'ddt-deploy',\n )\n .option(\n '--mask-catalog <catalog>',\n 'Catalog the mask functions live in. Default: same catalog as the columns.',\n )\n .option('-o, --out <path>', 'Output file path. Default: ./anonymize.sql in CWD.')\n .action(async (opts) => {\n const sourcePath = String(opts.source);\n const model = await loadModel(sourcePath);\n const unmaskedUser = String(opts.unmaskedUser);\n const maskCatalog = opts.maskCatalog ? String(opts.maskCatalog) : undefined;\n const candidates = collectPiiColumns(model);\n if (candidates.length === 0) {\n console.error('No PII columns detected (heuristic on column names).');\n return;\n }\n const sql = renderAnonymizeSql(candidates, unmaskedUser, maskCatalog);\n const outPath = opts.out ? path.resolve(String(opts.out)) : path.resolve('anonymize.sql');\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, sql, 'utf8');\n console.error(`Wrote ${outPath} (${candidates.length} column(s), ${sql.length} bytes).`);\n console.error(\n 'Review the user allow-list before applying. Default fully masks values from every user except the one passed in.',\n );\n });\n return cmd;\n}\n\ninterface PiiColumn {\n catalog: string;\n schema: string;\n table: string;\n column: string;\n category: 'email' | 'phone' | 'name' | 'dob' | 'ssn' | 'address' | 'credit-card' | 'generic';\n}\n\nfunction collectPiiColumns(model: readonly DatabricksObject[]): PiiColumn[] {\n const out: PiiColumn[] = [];\n for (const o of model) {\n if (o.objectType !== 'MANAGED_TABLE' && o.objectType !== 'EXTERNAL_TABLE') continue;\n const rec = o as unknown as Record<string, unknown>;\n const cols = rec.columns as Array<{ name: string }> | undefined;\n if (!Array.isArray(cols)) continue;\n const catalog = o.fqn.database ?? 'main';\n const schema = o.fqn.schema ?? 'default';\n for (const c of cols) {\n const category = classify(c.name);\n if (category) out.push({ catalog, schema, table: o.fqn.name, column: c.name, category });\n }\n }\n return out;\n}\n\nfunction classify(name: string): PiiColumn['category'] | undefined {\n const lower = name.toLowerCase();\n if (/(^|_)email(_|$)/.test(lower)) return 'email';\n if (/(^|_)(phone|mobile|cell)(_|$)/.test(lower)) return 'phone';\n if (/(^|_)(first_name|last_name|full_name|surname|given_name)(_|$)/.test(lower)) return 'name';\n if (/(^|_)(dob|birth_date|date_of_birth)(_|$)/.test(lower)) return 'dob';\n if (/(^|_)ssn(_|$)/.test(lower) || /(^|_)tax_id(_|$)/.test(lower)) return 'ssn';\n if (/(^|_)(address|street|zip_code|postal_code|postcode)(_|$)/.test(lower)) return 'address';\n if (/(^|_)(credit_card|card_number)(_|$)/.test(lower)) return 'credit-card';\n return undefined;\n}\n\nfunction renderAnonymizeSql(\n cols: PiiColumn[],\n unmaskedUser: string,\n maskCatalogOverride: string | undefined,\n): string {\n const lines: string[] = [];\n lines.push(`-- Generated by \\`ddt anonymize\\`.`);\n lines.push(\n `-- Edit the user allow-list before applying. Default: only ${unmaskedUser} sees unmasked values.`,\n );\n lines.push('');\n // Group columns by mask-function catalog. If override, all go there;\n // else group by each column's own catalog.\n const byCatalog = new Map<string, PiiColumn[]>();\n for (const c of cols) {\n const target = maskCatalogOverride ?? c.catalog;\n const arr = byCatalog.get(target) ?? [];\n arr.push(c);\n byCatalog.set(target, arr);\n }\n // For each catalog that hosts mask functions, emit one FUNCTION per\n // category present.\n for (const [catalog, catalogCols] of byCatalog) {\n const cats = new Set(catalogCols.map((c) => c.category));\n lines.push(`-- ── Mask functions in catalog \\`${catalog}\\`.security ──`);\n lines.push(`CREATE SCHEMA IF NOT EXISTS ${catalog}.security;`);\n lines.push('');\n for (const cat of cats) {\n lines.push(\n `CREATE OR REPLACE FUNCTION ${catalog}.security.mask_${cat.replace(/-/g, '_')}(val STRING)`,\n );\n lines.push(` RETURNS STRING`);\n lines.push(` RETURN CASE`);\n lines.push(\n ` WHEN is_member('${unmaskedUser}') OR current_user() = '${unmaskedUser}' THEN val`,\n );\n lines.push(` ELSE ${maskExpression(cat)}`);\n lines.push(` END;`);\n lines.push('');\n }\n }\n // Apply masks.\n lines.push('-- Apply masks to columns');\n for (const c of cols) {\n const fnCatalog = maskCatalogOverride ?? c.catalog;\n const fnName = `${fnCatalog}.security.mask_${c.category.replace(/-/g, '_')}`;\n lines.push(\n `ALTER TABLE ${c.catalog}.${c.schema}.${c.table} ALTER COLUMN ${c.column} SET MASK ${fnName};`,\n );\n }\n lines.push('');\n return lines.join('\\n') + '\\n';\n}\n\nfunction maskExpression(cat: PiiColumn['category']): string {\n switch (cat) {\n case 'email':\n return `regexp_replace(val, '^(.).*(@.*)$', '$1***$2')`;\n case 'phone':\n return `concat('***-***-', right(val, 4))`;\n case 'ssn':\n return `concat('***-**-', right(val, 4))`;\n case 'credit-card':\n return `concat('****-****-****-', right(val, 4))`;\n case 'dob':\n return `concat(left(val, 4), '-XX-XX')`;\n case 'name':\n case 'address':\n case 'generic':\n default:\n return `sha2(val, 256)`;\n }\n}\n\nasync function loadModel(sourcePath: string): Promise<DatabricksObject[]> {\n if (sourcePath.endsWith('.ddtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await loadProject(sourcePath);\n return await parseProjectModel(loaded);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,73 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/approval.ts
4
+ import path from "path";
5
+ import { Command } from "commander";
6
+ import { approval } from "@ddt-tools/core";
7
+ var DEFAULT_ROOT = path.join(".ddt", "approvals");
8
+ function approvalCommand() {
9
+ const cmd = new Command("approval");
10
+ cmd.description("Record / list / verify multi-approver gate tokens for prod deploys.");
11
+ cmd.command("add").description("Record an approval for a deploy id.").argument("<deploy-id>", "Stable deploy identifier (slug; sanitised in filenames).").requiredOption("--as <approver>", "Approver identifier (email, username, OIDC subject).").option("--message <text>", "Optional approval message.").option("--digest <hex>", "Optional compare/safety digest the approver reviewed.").option("--root <path>", "Approvals directory.", DEFAULT_ROOT).action(async (deployId, opts) => {
12
+ const record = {
13
+ approver: String(opts.as),
14
+ signedAt: (/* @__PURE__ */ new Date()).toISOString(),
15
+ ...opts.message ? { message: String(opts.message) } : {},
16
+ ...opts.digest ? { digest: String(opts.digest) } : {}
17
+ };
18
+ const file = await approval.appendApproval(String(opts.root), deployId, record);
19
+ console.log(
20
+ `Recorded approval from ${record.approver} for ${deployId} (${file.approvals.length} total).`
21
+ );
22
+ });
23
+ cmd.command("list").description("List approvals recorded for a deploy id.").argument("<deploy-id>").option("--root <path>", "Approvals directory.", DEFAULT_ROOT).option("--json", "Emit JSON.", false).action(async (deployId, opts) => {
24
+ const file = await approval.readApprovalFile(String(opts.root), deployId);
25
+ if (opts.json) {
26
+ console.log(JSON.stringify(file, null, 2));
27
+ return;
28
+ }
29
+ if (file.approvals.length === 0) {
30
+ console.log(`No approvals recorded for "${deployId}".`);
31
+ return;
32
+ }
33
+ console.log(`Approvals for ${deployId}:`);
34
+ for (const a of file.approvals) {
35
+ const meta = [a.signedAt];
36
+ if (a.digest) meta.push(`digest=${a.digest}`);
37
+ if (a.message) meta.push(`message=${JSON.stringify(a.message)}`);
38
+ console.log(` - ${a.approver} (${meta.join("; ")})`);
39
+ }
40
+ });
41
+ cmd.command("clear").description("Remove all approvals for a deploy id.").argument("<deploy-id>").option("--root <path>", "Approvals directory.", DEFAULT_ROOT).action(async (deployId, opts) => {
42
+ await approval.clearApprovals(String(opts.root), deployId);
43
+ console.log(`Cleared approvals for ${deployId}.`);
44
+ });
45
+ cmd.command("check").description("Evaluate the gate. Exits 0 when satisfied, 2 when blocked.").argument("<deploy-id>").requiredOption("--required <n>", "Number of distinct approvals required.", "2").option("--allowed <ids>", "Comma-separated allow-list of approvers (empty = any).").option("--digest <hex>", "Current compare/safety digest; rejects stale approvals.").option("--root <path>", "Approvals directory.", DEFAULT_ROOT).option("--json", "Emit JSON.", false).action(async (deployId, opts) => {
46
+ const allowedApprovers = opts.allowed ? String(opts.allowed).split(",").map((s) => s.trim()).filter(Boolean) : [];
47
+ const file = await approval.readApprovalFile(String(opts.root), deployId);
48
+ const outcome = approval.evaluateApprovalGate(
49
+ {
50
+ deployId,
51
+ required: Number(opts.required ?? "2") || 0,
52
+ allowedApprovers,
53
+ ...opts.digest ? { currentDigest: String(opts.digest) } : {}
54
+ },
55
+ file.approvals
56
+ );
57
+ if (opts.json) {
58
+ console.log(JSON.stringify(outcome, null, 2));
59
+ } else if (outcome.satisfied) {
60
+ console.log(
61
+ `OK: ${outcome.satisfiedBy.length} approval(s) \u2014 ${outcome.satisfiedBy.join(", ")}.`
62
+ );
63
+ } else {
64
+ console.error(`BLOCKED: ${outcome.blockReason ?? "approval gate not satisfied"}.`);
65
+ }
66
+ if (!outcome.satisfied) process.exitCode = 2;
67
+ });
68
+ return cmd;
69
+ }
70
+ export {
71
+ approvalCommand
72
+ };
73
+ //# sourceMappingURL=approval-GGZGKIU4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/approval.ts"],"sourcesContent":["/**\n * `ddt approval` — manage multi-approver gate tokens (Team-tier).\n *\n * ddt approval add <deploy-id> --as <approver> [--message <text>] [--digest <hex>]\n * ddt approval list <deploy-id>\n * ddt approval clear <deploy-id>\n * ddt approval check <deploy-id> --required N --allowed <ids>\n *\n * Tokens default to `.ddt/approvals/` under the current working\n * directory; override with `--root <path>` on every subcommand.\n *\n * Mirrors `sdt approval`.\n */\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { approval } from '@ddt-tools/core';\n\nconst DEFAULT_ROOT = path.join('.ddt', 'approvals');\n\nexport function approvalCommand(): Command {\n const cmd = new Command('approval');\n cmd.description('Record / list / verify multi-approver gate tokens for prod deploys.');\n\n cmd\n .command('add')\n .description('Record an approval for a deploy id.')\n .argument('<deploy-id>', 'Stable deploy identifier (slug; sanitised in filenames).')\n .requiredOption('--as <approver>', 'Approver identifier (email, username, OIDC subject).')\n .option('--message <text>', 'Optional approval message.')\n .option('--digest <hex>', 'Optional compare/safety digest the approver reviewed.')\n .option('--root <path>', 'Approvals directory.', DEFAULT_ROOT)\n .action(async (deployId: string, opts: Record<string, unknown>) => {\n const record: approval.ApprovalRecord = {\n approver: String(opts.as),\n signedAt: new Date().toISOString(),\n ...(opts.message ? { message: String(opts.message) } : {}),\n ...(opts.digest ? { digest: String(opts.digest) } : {}),\n };\n const file = await approval.appendApproval(String(opts.root), deployId, record);\n console.log(\n `Recorded approval from ${record.approver} for ${deployId} ` +\n `(${file.approvals.length} total).`,\n );\n });\n\n cmd\n .command('list')\n .description('List approvals recorded for a deploy id.')\n .argument('<deploy-id>')\n .option('--root <path>', 'Approvals directory.', DEFAULT_ROOT)\n .option('--json', 'Emit JSON.', false)\n .action(async (deployId: string, opts: Record<string, unknown>) => {\n const file = await approval.readApprovalFile(String(opts.root), deployId);\n if (opts.json) {\n console.log(JSON.stringify(file, null, 2));\n return;\n }\n if (file.approvals.length === 0) {\n console.log(`No approvals recorded for \"${deployId}\".`);\n return;\n }\n console.log(`Approvals for ${deployId}:`);\n for (const a of file.approvals) {\n const meta = [a.signedAt];\n if (a.digest) meta.push(`digest=${a.digest}`);\n if (a.message) meta.push(`message=${JSON.stringify(a.message)}`);\n console.log(` - ${a.approver} (${meta.join('; ')})`);\n }\n });\n\n cmd\n .command('clear')\n .description('Remove all approvals for a deploy id.')\n .argument('<deploy-id>')\n .option('--root <path>', 'Approvals directory.', DEFAULT_ROOT)\n .action(async (deployId: string, opts: Record<string, unknown>) => {\n await approval.clearApprovals(String(opts.root), deployId);\n console.log(`Cleared approvals for ${deployId}.`);\n });\n\n cmd\n .command('check')\n .description('Evaluate the gate. Exits 0 when satisfied, 2 when blocked.')\n .argument('<deploy-id>')\n .requiredOption('--required <n>', 'Number of distinct approvals required.', '2')\n .option('--allowed <ids>', 'Comma-separated allow-list of approvers (empty = any).')\n .option('--digest <hex>', 'Current compare/safety digest; rejects stale approvals.')\n .option('--root <path>', 'Approvals directory.', DEFAULT_ROOT)\n .option('--json', 'Emit JSON.', false)\n .action(async (deployId: string, opts: Record<string, unknown>) => {\n const allowedApprovers = opts.allowed\n ? String(opts.allowed)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n : [];\n const file = await approval.readApprovalFile(String(opts.root), deployId);\n const outcome = approval.evaluateApprovalGate(\n {\n deployId,\n required: Number(opts.required ?? '2') || 0,\n allowedApprovers,\n ...(opts.digest ? { currentDigest: String(opts.digest) } : {}),\n },\n file.approvals,\n );\n if (opts.json) {\n console.log(JSON.stringify(outcome, null, 2));\n } else if (outcome.satisfied) {\n console.log(\n `OK: ${outcome.satisfiedBy.length} approval(s) — ${outcome.satisfiedBy.join(', ')}.`,\n );\n } else {\n console.error(`BLOCKED: ${outcome.blockReason ?? 'approval gate not satisfied'}.`);\n }\n if (!outcome.satisfied) process.exitCode = 2;\n });\n\n return cmd;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,118 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-DGUM43GV.js";
4
+
5
+ // src/commands/approval-chain.ts
6
+ import { promises as fs } from "fs";
7
+ import path from "path";
8
+ import { Command } from "commander";
9
+ import { approvalChain } from "@ddt-tools/core";
10
+ function approvalChainCommand() {
11
+ const cmd = new Command("approval-chain");
12
+ cmd.description("Signed M-of-N approval workflow (DSR.8).");
13
+ cmd.addCommand(keygenCommand());
14
+ cmd.addCommand(signCommand());
15
+ cmd.addCommand(verifyCommand());
16
+ return cmd;
17
+ }
18
+ function keygenCommand() {
19
+ const c = new Command("keygen");
20
+ c.description("Generate an Ed25519 keypair for an approver.").requiredOption("--id <approver>", "Approver id (used as the filename stem).").requiredOption("--out <dir>", "Output directory for the keypair.").action(async (opts) => {
21
+ const dir = path.resolve(opts.out);
22
+ await fs.mkdir(dir, { recursive: true });
23
+ const pair = approvalChain.generateApproverKeyPair();
24
+ const priv = path.join(dir, `${opts.id}.pem`);
25
+ const pub = path.join(dir, `${opts.id}.pub.pem`);
26
+ await fs.writeFile(priv, pair.privateKeyPem, { mode: 384 });
27
+ await fs.writeFile(pub, pair.publicKeyPem);
28
+ process.stdout.write(`Wrote ${priv} and ${pub} (kid=${pair.kid}).
29
+ `);
30
+ });
31
+ return c;
32
+ }
33
+ function signCommand() {
34
+ const c = new Command("sign");
35
+ c.description("Produce a signed approval record.").requiredOption("--deploy-id <id>", "Stable deploy id.").requiredOption("--env <env>", "Target environment (matches a key in approval.json).").requiredOption("--approver <id>", "Approver id (must appear in the env's approvers list).").requiredOption("--decision <decision>", "approve | reject", "approve").requiredOption("--key <path>", "Path to the approver's private key (PEM).").option("--digest <hex>", "Optional compare+safety digest the approver reviewed.").option("--out <path>", "Write the signed record JSON to this path (default stdout).").action(
36
+ async (opts) => {
37
+ const decision = opts.decision === "reject" ? "reject" : "approve";
38
+ const privateKeyPem = await fs.readFile(path.resolve(opts.key), "utf8");
39
+ const kid = approvalChain.publicKeyKid(
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ __require("crypto").createPublicKey(privateKeyPem).export({ format: "pem", type: "spki" }).toString()
42
+ );
43
+ const record = approvalChain.signApprovalRecord(
44
+ {
45
+ deployId: opts.deployId,
46
+ env: opts.env,
47
+ approver: opts.approver,
48
+ decision,
49
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
50
+ digest: opts.digest
51
+ },
52
+ privateKeyPem,
53
+ { kid }
54
+ );
55
+ const payload = JSON.stringify(record, null, 2) + "\n";
56
+ if (opts.out) {
57
+ await fs.mkdir(path.dirname(path.resolve(opts.out)), { recursive: true });
58
+ await fs.writeFile(path.resolve(opts.out), payload);
59
+ process.stderr.write(`Wrote ${opts.out}
60
+ `);
61
+ } else {
62
+ process.stdout.write(payload);
63
+ }
64
+ }
65
+ );
66
+ return c;
67
+ }
68
+ function verifyCommand() {
69
+ const c = new Command("verify");
70
+ c.description("Evaluate the signed chain against the env policy.").requiredOption("--deploy-id <id>", "Stable deploy id.").requiredOption("--env <env>", "Target environment.").requiredOption("--config <path>", "Path to approval.json.").requiredOption("--records <path>", "Path to JSON file containing a SignedApprovalRecord[].").option("--digest <hex>", "Optional live compare+safety digest.").option("--format <fmt>", "text | json. Default text.", "text").action(
71
+ async (opts) => {
72
+ const configPath = path.resolve(opts.config);
73
+ const config = await approvalChain.loadApprovalChainConfig(configPath);
74
+ if (!config) {
75
+ process.stderr.write(`Config file not found: ${configPath}
76
+ `);
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ const records = JSON.parse(
81
+ await fs.readFile(path.resolve(opts.records), "utf8")
82
+ );
83
+ const registry = approvalChain.resolveRegistry(config, configPath);
84
+ const outcome = await approvalChain.evaluateApprovalChain({
85
+ deployId: opts.deployId,
86
+ env: opts.env,
87
+ currentDigest: opts.digest,
88
+ records,
89
+ config,
90
+ registry
91
+ });
92
+ if ((opts.format ?? "text").toLowerCase() === "json") {
93
+ process.stdout.write(JSON.stringify(outcome, null, 2) + "\n");
94
+ } else {
95
+ process.stdout.write(
96
+ `${outcome.satisfied ? "OK" : "BLOCKED"}: ${outcome.satisfiedBy.length} valid / ${outcome.missingCount} missing
97
+ `
98
+ );
99
+ if (outcome.satisfiedBy.length > 0)
100
+ process.stdout.write(` approvers: ${outcome.satisfiedBy.join(", ")}
101
+ `);
102
+ if (outcome.ignored.length > 0)
103
+ process.stdout.write(
104
+ ` ignored: ${outcome.ignored.map((i) => `${i.approver}=${i.reason}`).join(", ")}
105
+ `
106
+ );
107
+ if (outcome.blockReason) process.stdout.write(` ${outcome.blockReason}
108
+ `);
109
+ }
110
+ if (!outcome.satisfied) process.exitCode = 1;
111
+ }
112
+ );
113
+ return c;
114
+ }
115
+ export {
116
+ approvalChainCommand
117
+ };
118
+ //# sourceMappingURL=approval-chain-GWJKZHVU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/approval-chain.ts"],"sourcesContent":["/**\n * `ddt approval-chain` — M-of-N signed approval workflow (DSR.8).\n *\n * Subcommands: keygen / sign / verify. Mirrors `sdt approval-chain`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type * as nodeCrypto from 'node:crypto';\nimport { Command } from 'commander';\nimport { approvalChain } from '@ddt-tools/core';\n\nexport function approvalChainCommand(): Command {\n const cmd = new Command('approval-chain');\n cmd.description('Signed M-of-N approval workflow (DSR.8).');\n cmd.addCommand(keygenCommand());\n cmd.addCommand(signCommand());\n cmd.addCommand(verifyCommand());\n return cmd;\n}\n\nfunction keygenCommand(): Command {\n const c = new Command('keygen');\n c.description('Generate an Ed25519 keypair for an approver.')\n .requiredOption('--id <approver>', 'Approver id (used as the filename stem).')\n .requiredOption('--out <dir>', 'Output directory for the keypair.')\n .action(async (opts: { id: string; out: string }) => {\n const dir = path.resolve(opts.out);\n await fs.mkdir(dir, { recursive: true });\n const pair = approvalChain.generateApproverKeyPair();\n const priv = path.join(dir, `${opts.id}.pem`);\n const pub = path.join(dir, `${opts.id}.pub.pem`);\n await fs.writeFile(priv, pair.privateKeyPem, { mode: 0o600 });\n await fs.writeFile(pub, pair.publicKeyPem);\n process.stdout.write(`Wrote ${priv} and ${pub} (kid=${pair.kid}).\\n`);\n });\n return c;\n}\n\nfunction signCommand(): Command {\n const c = new Command('sign');\n c.description('Produce a signed approval record.')\n .requiredOption('--deploy-id <id>', 'Stable deploy id.')\n .requiredOption('--env <env>', 'Target environment (matches a key in approval.json).')\n .requiredOption('--approver <id>', \"Approver id (must appear in the env's approvers list).\")\n .requiredOption('--decision <decision>', 'approve | reject', 'approve')\n .requiredOption('--key <path>', \"Path to the approver's private key (PEM).\")\n .option('--digest <hex>', 'Optional compare+safety digest the approver reviewed.')\n .option('--out <path>', 'Write the signed record JSON to this path (default stdout).')\n .action(\n async (opts: {\n deployId: string;\n env: string;\n approver: string;\n decision: string;\n key: string;\n digest?: string;\n out?: string;\n }) => {\n const decision = (opts.decision === 'reject' ? 'reject' : 'approve') as\n | 'approve'\n | 'reject';\n const privateKeyPem = await fs.readFile(path.resolve(opts.key), 'utf8');\n const kid = approvalChain.publicKeyKid(\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n (require('node:crypto') as typeof nodeCrypto)\n .createPublicKey(privateKeyPem)\n .export({ format: 'pem', type: 'spki' })\n .toString(),\n );\n const record = approvalChain.signApprovalRecord(\n {\n deployId: opts.deployId,\n env: opts.env,\n approver: opts.approver,\n decision,\n ts: new Date().toISOString(),\n digest: opts.digest,\n },\n privateKeyPem,\n { kid },\n );\n const payload = JSON.stringify(record, null, 2) + '\\n';\n if (opts.out) {\n await fs.mkdir(path.dirname(path.resolve(opts.out)), { recursive: true });\n await fs.writeFile(path.resolve(opts.out), payload);\n process.stderr.write(`Wrote ${opts.out}\\n`);\n } else {\n process.stdout.write(payload);\n }\n },\n );\n return c;\n}\n\nfunction verifyCommand(): Command {\n const c = new Command('verify');\n c.description('Evaluate the signed chain against the env policy.')\n .requiredOption('--deploy-id <id>', 'Stable deploy id.')\n .requiredOption('--env <env>', 'Target environment.')\n .requiredOption('--config <path>', 'Path to approval.json.')\n .requiredOption('--records <path>', 'Path to JSON file containing a SignedApprovalRecord[].')\n .option('--digest <hex>', 'Optional live compare+safety digest.')\n .option('--format <fmt>', 'text | json. Default text.', 'text')\n .action(\n async (opts: {\n deployId: string;\n env: string;\n config: string;\n records: string;\n digest?: string;\n format?: string;\n }) => {\n const configPath = path.resolve(opts.config);\n const config = await approvalChain.loadApprovalChainConfig(configPath);\n if (!config) {\n process.stderr.write(`Config file not found: ${configPath}\\n`);\n process.exitCode = 1;\n return;\n }\n const records = JSON.parse(\n await fs.readFile(path.resolve(opts.records), 'utf8'),\n ) as approvalChain.SignedApprovalRecord[];\n const registry = approvalChain.resolveRegistry(config, configPath);\n const outcome = await approvalChain.evaluateApprovalChain({\n deployId: opts.deployId,\n env: opts.env,\n currentDigest: opts.digest,\n records,\n config,\n registry,\n });\n if ((opts.format ?? 'text').toLowerCase() === 'json') {\n process.stdout.write(JSON.stringify(outcome, null, 2) + '\\n');\n } else {\n process.stdout.write(\n `${outcome.satisfied ? 'OK' : 'BLOCKED'}: ${outcome.satisfiedBy.length} valid / ${outcome.missingCount} missing\\n`,\n );\n if (outcome.satisfiedBy.length > 0)\n process.stdout.write(` approvers: ${outcome.satisfiedBy.join(', ')}\\n`);\n if (outcome.ignored.length > 0)\n process.stdout.write(\n ` ignored: ${outcome.ignored.map((i) => `${i.approver}=${i.reason}`).join(', ')}\\n`,\n );\n if (outcome.blockReason) process.stdout.write(` ${outcome.blockReason}\\n`);\n }\n if (!outcome.satisfied) process.exitCode = 1;\n },\n );\n return c;\n}\n"],"mappings":"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","names":[]}