@sdt-tools/cli 0.2.0 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advise-tests-6DRSZMBL.js +87 -0
- package/dist/advise-tests-6DRSZMBL.js.map +1 -0
- package/dist/ai-G4MJWHTM.js +89 -0
- package/dist/ai-G4MJWHTM.js.map +1 -0
- package/dist/anonymize-QR6JGXA7.js +123 -0
- package/dist/anonymize-QR6JGXA7.js.map +1 -0
- package/dist/approval-YVHYTV53.js +73 -0
- package/dist/approval-YVHYTV53.js.map +1 -0
- package/dist/approval-chain-54KKJZS3.js +120 -0
- package/dist/approval-chain-54KKJZS3.js.map +1 -0
- package/dist/audit-log-QZFH7LUX.js +159 -0
- package/dist/audit-log-QZFH7LUX.js.map +1 -0
- package/dist/backlog-V2YUIQDL.js +76 -0
- package/dist/backlog-V2YUIQDL.js.map +1 -0
- package/dist/bisect-GEVYAVL5.js +111 -0
- package/dist/bisect-GEVYAVL5.js.map +1 -0
- package/dist/bookmarks-57LKS7P6.js +107 -0
- package/dist/bookmarks-57LKS7P6.js.map +1 -0
- package/dist/branch-W2MGMPSH.js +88 -0
- package/dist/branch-W2MGMPSH.js.map +1 -0
- package/dist/build-VNIQFKSP.js +23 -0
- package/dist/build-VNIQFKSP.js.map +1 -0
- package/dist/catalog-JLB5VCEV.js +137 -0
- package/dist/catalog-JLB5VCEV.js.map +1 -0
- package/dist/changelog-M7XGDYSY.js +220 -0
- package/dist/changelog-M7XGDYSY.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-EWXM4KJN.js +25 -0
- package/dist/chunk-EWXM4KJN.js.map +1 -0
- package/dist/chunk-JP2EZLR5.js +50 -0
- package/dist/chunk-JP2EZLR5.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-ZWY4ZRHL.js +44 -0
- package/dist/chunk-ZWY4ZRHL.js.map +1 -0
- package/dist/cli.js +511 -19014
- package/dist/cli.js.map +1 -1
- package/dist/compare-5O6UTWPJ.js +405 -0
- package/dist/compare-5O6UTWPJ.js.map +1 -0
- package/dist/compare-profiles-7ZSNIW7B.js +218 -0
- package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
- package/dist/completion-I5U5VVAX.js +82 -0
- package/dist/completion-I5U5VVAX.js.map +1 -0
- package/dist/connection-GNTZDHXF.js +133 -0
- package/dist/connection-GNTZDHXF.js.map +1 -0
- package/dist/cost-estimate-TJDDH6TO.js +328 -0
- package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
- package/dist/data-compare-UK2UXAS3.js +134 -0
- package/dist/data-compare-UK2UXAS3.js.map +1 -0
- package/dist/data-fit-Q45ENBRL.js +125 -0
- package/dist/data-fit-Q45ENBRL.js.map +1 -0
- package/dist/deploy-status-UUHKVDTI.js +58 -0
- package/dist/deploy-status-UUHKVDTI.js.map +1 -0
- package/dist/design-PO6UPBL7.js +138 -0
- package/dist/design-PO6UPBL7.js.map +1 -0
- package/dist/diagnose-6IFMELFR.js +145 -0
- package/dist/diagnose-6IFMELFR.js.map +1 -0
- package/dist/discover-A7OSZAHK.js +78 -0
- package/dist/discover-A7OSZAHK.js.map +1 -0
- package/dist/docs-CVRKGUSW.js +177 -0
- package/dist/docs-CVRKGUSW.js.map +1 -0
- package/dist/drift-XDA3BDYN.js +226 -0
- package/dist/drift-XDA3BDYN.js.map +1 -0
- package/dist/drift-gate-V7QSIOGZ.js +94 -0
- package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
- package/dist/error-lookup-7ZWCZJ44.js +56 -0
- package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
- package/dist/errorReporting-AQXKKGZH.js +109 -0
- package/dist/errorReporting-AQXKKGZH.js.map +1 -0
- package/dist/exec-PKBHLI7T.js +121 -0
- package/dist/exec-PKBHLI7T.js.map +1 -0
- package/dist/explain-LWKJOTL7.js +192 -0
- package/dist/explain-LWKJOTL7.js.map +1 -0
- package/dist/explorer-QOVM6VBD.js +61 -0
- package/dist/explorer-QOVM6VBD.js.map +1 -0
- package/dist/export-IYYBZ5HE.js +42 -0
- package/dist/export-IYYBZ5HE.js.map +1 -0
- package/dist/extract-VMMVRQVT.js +102 -0
- package/dist/extract-VMMVRQVT.js.map +1 -0
- package/dist/features-LE6BDZ2S.js +59 -0
- package/dist/features-LE6BDZ2S.js.map +1 -0
- package/dist/feedback-M7DM2EQC.js +161 -0
- package/dist/feedback-M7DM2EQC.js.map +1 -0
- package/dist/find-EME2JG2I.js +176 -0
- package/dist/find-EME2JG2I.js.map +1 -0
- package/dist/format-TRLWLMGS.js +141 -0
- package/dist/format-TRLWLMGS.js.map +1 -0
- package/dist/generate-6NAZGZDV.js +152 -0
- package/dist/generate-6NAZGZDV.js.map +1 -0
- package/dist/graph-QNQDAUO7.js +161 -0
- package/dist/graph-QNQDAUO7.js.map +1 -0
- package/dist/history-RONA7ZTI.js +199 -0
- package/dist/history-RONA7ZTI.js.map +1 -0
- package/dist/hosts-YBXY2ZG5.js +49 -0
- package/dist/hosts-YBXY2ZG5.js.map +1 -0
- package/dist/impact-T2JSANHS.js +59 -0
- package/dist/impact-T2JSANHS.js.map +1 -0
- package/dist/import-AELYLY6A.js +32 -0
- package/dist/import-AELYLY6A.js.map +1 -0
- package/dist/import-script-2OF5BI6A.js +83 -0
- package/dist/import-script-2OF5BI6A.js.map +1 -0
- package/dist/index.cjs +71 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +95 -31
- package/dist/index.js.map +1 -1
- package/dist/init-SWRRJMGI.js +57 -0
- package/dist/init-SWRRJMGI.js.map +1 -0
- package/dist/install-hooks-6SIAGTAF.js +109 -0
- package/dist/install-hooks-6SIAGTAF.js.map +1 -0
- package/dist/license-OAF22PLZ.js +46 -0
- package/dist/license-OAF22PLZ.js.map +1 -0
- package/dist/lineage-EW66XJ6O.js +552 -0
- package/dist/lineage-EW66XJ6O.js.map +1 -0
- package/dist/lint-FQ2OTYTQ.js +143 -0
- package/dist/lint-FQ2OTYTQ.js.map +1 -0
- package/dist/mcp-SARDMCDV.js +344 -0
- package/dist/mcp-SARDMCDV.js.map +1 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
- package/dist/migrate-platform-NTRTOGNR.js +91 -0
- package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
- package/dist/optimize-CJYWMAWA.js +105 -0
- package/dist/optimize-CJYWMAWA.js.map +1 -0
- package/dist/perf-LL2CPCJF.js +205 -0
- package/dist/perf-LL2CPCJF.js.map +1 -0
- package/dist/pii-FBDRDQ2E.js +136 -0
- package/dist/pii-FBDRDQ2E.js.map +1 -0
- package/dist/pilot-CCQERKPH.js +29 -0
- package/dist/pilot-CCQERKPH.js.map +1 -0
- package/dist/pr-comment-S5FF4QRX.js +79 -0
- package/dist/pr-comment-S5FF4QRX.js.map +1 -0
- package/dist/preview-5U4YVCRM.js +47 -0
- package/dist/preview-5U4YVCRM.js.map +1 -0
- package/dist/profile-7VC57KD2.js +101 -0
- package/dist/profile-7VC57KD2.js.map +1 -0
- package/dist/promote-AASEFTIA.js +408 -0
- package/dist/promote-AASEFTIA.js.map +1 -0
- package/dist/publish-UMVIWH6H.js +721 -0
- package/dist/publish-UMVIWH6H.js.map +1 -0
- package/dist/purge-QMXZKCMD.js +57 -0
- package/dist/purge-QMXZKCMD.js.map +1 -0
- package/dist/query-log-6OM4GI7W.js +112 -0
- package/dist/query-log-6OM4GI7W.js.map +1 -0
- package/dist/refactor-LTZQLJ35.js +5799 -0
- package/dist/refactor-LTZQLJ35.js.map +1 -0
- package/dist/refresh-4TY2AGOU.js +38 -0
- package/dist/refresh-4TY2AGOU.js.map +1 -0
- package/dist/replay-OOC25FZN.js +117 -0
- package/dist/replay-OOC25FZN.js.map +1 -0
- package/dist/revert-ODMUVJW6.js +110 -0
- package/dist/revert-ODMUVJW6.js.map +1 -0
- package/dist/review-XXPWOBFP.js +158 -0
- package/dist/review-XXPWOBFP.js.map +1 -0
- package/dist/rollback-suggest-6G2HEKFR.js +79 -0
- package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
- package/dist/safer-alternative-QFVNLG3L.js +89 -0
- package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
- package/dist/safety-7QWRSUEZ.js +168 -0
- package/dist/safety-7QWRSUEZ.js.map +1 -0
- package/dist/savings-RHIXP6IT.js +95 -0
- package/dist/savings-RHIXP6IT.js.map +1 -0
- package/dist/scan-secrets-5YCQ4UCU.js +54 -0
- package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
- package/dist/schema-CIZXCQD2.js +429 -0
- package/dist/schema-CIZXCQD2.js.map +1 -0
- package/dist/script-K7CIN2P6.js +153 -0
- package/dist/script-K7CIN2P6.js.map +1 -0
- package/dist/search-BUZ5NXZZ.js +151 -0
- package/dist/search-BUZ5NXZZ.js.map +1 -0
- package/dist/seed-76QAK276.js +96 -0
- package/dist/seed-76QAK276.js.map +1 -0
- package/dist/sketch-PTLKDIK3.js +88 -0
- package/dist/sketch-PTLKDIK3.js.map +1 -0
- package/dist/snapshot-XLPR2OZ5.js +177 -0
- package/dist/snapshot-XLPR2OZ5.js.map +1 -0
- package/dist/snippets-EK4DK5CN.js +74 -0
- package/dist/snippets-EK4DK5CN.js.map +1 -0
- package/dist/standards-7T2UY6DD.js +241 -0
- package/dist/standards-7T2UY6DD.js.map +1 -0
- package/dist/suggest-VGRYSAR6.js +39 -0
- package/dist/suggest-VGRYSAR6.js.map +1 -0
- package/dist/suggest-constraints-MY5WKUHA.js +160 -0
- package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
- package/dist/suite-TRNGZWQM.js +88 -0
- package/dist/suite-TRNGZWQM.js.map +1 -0
- package/dist/telemetry-3U2QLA2S.js +75 -0
- package/dist/telemetry-3U2QLA2S.js.map +1 -0
- package/dist/template-ZERIXVXF.js +403 -0
- package/dist/template-ZERIXVXF.js.map +1 -0
- package/dist/test-5M2ED3WT.js +169 -0
- package/dist/test-5M2ED3WT.js.map +1 -0
- package/dist/trial-U732FONV.js +31 -0
- package/dist/trial-U732FONV.js.map +1 -0
- package/dist/validate-T6D2WCOK.js +106 -0
- package/dist/validate-T6D2WCOK.js.map +1 -0
- package/dist/verify-KXVASEEG.js +76 -0
- package/dist/verify-KXVASEEG.js.map +1 -0
- package/dist/watch-I6K4BNMA.js +80 -0
- package/dist/watch-I6K4BNMA.js.map +1 -0
- package/dist/xcompare-TPFLQO6W.js +87 -0
- package/dist/xcompare-TPFLQO6W.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19040
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- 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 "@sdt-tools/core";
|
|
8
|
+
function adviseTestsCommand() {
|
|
9
|
+
const cmd = new Command("advise-tests");
|
|
10
|
+
cmd.description(
|
|
11
|
+
"PR-time AI regression-test advisor. Walks the SDT 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, "sdt");
|
|
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-6DRSZMBL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/advise-tests.ts"],"sourcesContent":["/**\n * `sdt 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 * `@sdt-tools/core/features.SDT_FEATURE_CATALOG`, and asks the configured AI\n * provider to recommend a minimal regression-test set.\n *\n * Mirrors `ddt advise-tests`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { ai, regressionAdvisor } from '@sdt-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 SDT 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, 'sdt');\n });\n return cmd;\n}\n\nexport async function runAdviseTests(\n opts: Record<string, unknown>,\n toolName: 'sdt' | '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,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-VM2H4LAO.js";
|
|
4
|
+
import "./chunk-DGUM43GV.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/ai.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { ai } from "@sdt-tools/core";
|
|
9
|
+
function aiCommand() {
|
|
10
|
+
const cmd = new Command("ai");
|
|
11
|
+
cmd.description("Configure and test the AI provider adapter (BYO key).");
|
|
12
|
+
cmd.command("status").description("Show the resolved AI provider config (provider, model, key source).").action(async () => {
|
|
13
|
+
const config = await ai.resolveAiConfig();
|
|
14
|
+
const apiKey = ai.resolveApiKey(config);
|
|
15
|
+
logger.success("AI provider config (configured):");
|
|
16
|
+
logger.dim(` provider: ${config.provider}`);
|
|
17
|
+
logger.dim(` model: ${config.model}`);
|
|
18
|
+
logger.dim(` endpoint: ${config.endpoint ?? "(provider default)"}`);
|
|
19
|
+
logger.dim(` apiKey: ${apiKey ? "\u2713 resolved" : "\u2717 not set"}`);
|
|
20
|
+
logger.dim(` maxTokens: ${config.maxTokens ?? "(provider default)"}`);
|
|
21
|
+
logger.dim(` temperature: ${config.temperature ?? "(provider default)"}`);
|
|
22
|
+
logger.dim(
|
|
23
|
+
` preferLocal: ${config.preferLocal !== false ? "true (local-first)" : "false (cloud only)"}`
|
|
24
|
+
);
|
|
25
|
+
logger.dim(` configPath: ${ai.defaultConfigPath()}`);
|
|
26
|
+
if (config.preferLocal !== false) {
|
|
27
|
+
const probe = await ai.detectOllamaAvailability({ fetch: globalThis.fetch });
|
|
28
|
+
logger.success("Local Ollama probe:");
|
|
29
|
+
if (probe.available) {
|
|
30
|
+
const best = ai.pickBestLocalModel(probe.models);
|
|
31
|
+
logger.dim(` status: \u2713 available at ${probe.endpoint}`);
|
|
32
|
+
logger.dim(` models: ${probe.models.join(", ") || "(none pulled)"}`);
|
|
33
|
+
logger.dim(` selected: ${best ?? "(none)"}`);
|
|
34
|
+
logger.success("Effective routing: Ollama (local, zero cloud cost)");
|
|
35
|
+
} else {
|
|
36
|
+
logger.dim(` status: \u2717 unavailable (${probe.reason})`);
|
|
37
|
+
logger.dim(` fallback: ${config.provider} / ${config.model}`);
|
|
38
|
+
logger.success(`Effective routing: ${config.provider} (cloud)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
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(
|
|
43
|
+
"--ai-max-spend <usd>",
|
|
44
|
+
"Refuse the call if today's estimated spend \u2265 this (USD). 0 = no cap.",
|
|
45
|
+
"0"
|
|
46
|
+
).action(async (opts) => {
|
|
47
|
+
const result = await ai.complete(
|
|
48
|
+
[
|
|
49
|
+
{ role: "system", content: "You are a terse assistant. Follow instructions exactly." },
|
|
50
|
+
{ role: "user", content: String(opts.prompt) }
|
|
51
|
+
],
|
|
52
|
+
{
|
|
53
|
+
feature: "ai.test",
|
|
54
|
+
maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
logger.success(`Response from ${result.provider} (${result.model}):`);
|
|
58
|
+
console.log(result.text);
|
|
59
|
+
logger.dim(
|
|
60
|
+
` tokens: ${result.usage.promptTokens} in / ${result.usage.completionTokens} out`
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
cmd.command("usage").description("Summarize the local AI usage ledger.").action(async () => {
|
|
64
|
+
const records = await ai.readUsage();
|
|
65
|
+
const today = ai.todayRecords(records);
|
|
66
|
+
const todayUsd = ai.totalSpend(today);
|
|
67
|
+
const allUsd = ai.totalSpend(records);
|
|
68
|
+
logger.success("AI usage (local ledger):");
|
|
69
|
+
logger.dim(` today: ${today.length} call(s), ~$${todayUsd.toFixed(4)}`);
|
|
70
|
+
logger.dim(` total: ${records.length} call(s), ~$${allUsd.toFixed(4)}`);
|
|
71
|
+
logger.dim(` ledger: ${ai.defaultUsagePath()}`);
|
|
72
|
+
if (records.length > 0) {
|
|
73
|
+
const byFeature = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const r of records) {
|
|
75
|
+
const key = r.feature ?? "(unknown)";
|
|
76
|
+
byFeature.set(key, (byFeature.get(key) ?? 0) + 1);
|
|
77
|
+
}
|
|
78
|
+
logger.dim(" features:");
|
|
79
|
+
for (const [feature, count] of [...byFeature.entries()].sort((a, b) => b[1] - a[1])) {
|
|
80
|
+
logger.dim(` ${feature}: ${count}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return cmd;
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
aiCommand
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=ai-G4MJWHTM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/ai.ts"],"sourcesContent":["/**\n * `sdt ai` — configure + test the AI provider adapter.\n *\n * Subcommands:\n * sdt ai status — show current config (provider, model, key source)\n * sdt ai test — round-trip a hello-world prompt to confirm the\n * provider works end-to-end\n * sdt ai usage — summarize ai-usage.json (today + this month)\n *\n * Phase-1 of the AI rollout (see docs/AI_FEATURES.md). Higher-level\n * features (`compare --explain`, safer-alternative suggester, etc.)\n * land in Phase 2+ and call `ai.complete()` from `@sdt-tools/core/ai`.\n */\nimport { Command } from 'commander';\nimport { ai } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\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 logger.success('AI provider config (configured):');\n logger.dim(` provider: ${config.provider}`);\n logger.dim(` model: ${config.model}`);\n logger.dim(` endpoint: ${config.endpoint ?? '(provider default)'}`);\n logger.dim(` apiKey: ${apiKey ? '✓ resolved' : '✗ not set'}`);\n logger.dim(` maxTokens: ${config.maxTokens ?? '(provider default)'}`);\n logger.dim(` temperature: ${config.temperature ?? '(provider default)'}`);\n logger.dim(\n ` preferLocal: ${config.preferLocal !== false ? 'true (local-first)' : 'false (cloud only)'}`,\n );\n logger.dim(` configPath: ${ai.defaultConfigPath()}`);\n\n if (config.preferLocal !== false) {\n const probe = await ai.detectOllamaAvailability({ fetch: globalThis.fetch });\n logger.success('Local Ollama probe:');\n if (probe.available) {\n const best = ai.pickBestLocalModel(probe.models);\n logger.dim(` status: ✓ available at ${probe.endpoint}`);\n logger.dim(` models: ${probe.models.join(', ') || '(none pulled)'}`);\n logger.dim(` selected: ${best ?? '(none)'}`);\n logger.success('Effective routing: Ollama (local, zero cloud cost)');\n } else {\n logger.dim(` status: ✗ unavailable (${probe.reason})`);\n logger.dim(` fallback: ${config.provider} / ${config.model}`);\n logger.success(`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 logger.success(`Response from ${result.provider} (${result.model}):`);\n console.log(result.text);\n logger.dim(\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 logger.success('AI usage (local ledger):');\n logger.dim(` today: ${today.length} call(s), ~$${todayUsd.toFixed(4)}`);\n logger.dim(` total: ${records.length} call(s), ~$${allUsd.toFixed(4)}`);\n logger.dim(` 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 logger.dim(' features:');\n for (const [feature, count] of [...byFeature.entries()].sort((a, b) => b[1] - a[1])) {\n logger.dim(` ${feature}: ${count}`);\n }\n }\n });\n\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 { pac, project } from "@sdt-tools/core";
|
|
8
|
+
function anonymizeCommand() {
|
|
9
|
+
const cmd = new Command("anonymize");
|
|
10
|
+
cmd.description("Generate MASKING POLICY DDL for PII columns detected in the project model.").requiredOption("--source <path>", ".sdtproj or .sdtpac to analyze.").option(
|
|
11
|
+
"--unmasked-role <name>",
|
|
12
|
+
"Role the unmasked value is visible to. Default 'PROD_ETL'.",
|
|
13
|
+
"PROD_ETL"
|
|
14
|
+
).option("-o, --out <path>", "Output file path. Default: ./anonymize.sql in CWD.").action(async (opts) => {
|
|
15
|
+
const sourcePath = String(opts.source);
|
|
16
|
+
const model = await loadModel(sourcePath);
|
|
17
|
+
const unmaskedRole = String(opts.unmaskedRole);
|
|
18
|
+
const candidates = collectPiiColumns(model);
|
|
19
|
+
if (candidates.length === 0) {
|
|
20
|
+
console.error(
|
|
21
|
+
"No PII columns detected (heuristic on column names: email, phone, ssn, dob, \u2026)."
|
|
22
|
+
);
|
|
23
|
+
console.error(
|
|
24
|
+
"If the schema has PII you know about, add a MASKING POLICY by hand and re-run lint to confirm."
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const sql = renderAnonymizeSql(candidates, unmaskedRole);
|
|
29
|
+
const outPath = opts.out ? path.resolve(String(opts.out)) : path.resolve("anonymize.sql");
|
|
30
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
31
|
+
await fs.writeFile(outPath, sql, "utf8");
|
|
32
|
+
console.error(`Wrote ${outPath} (${candidates.length} column(s), ${sql.length} bytes).`);
|
|
33
|
+
console.error(
|
|
34
|
+
"Review the role allow-list (`unmaskedRole`) before applying. Default fully hides values from every role except the one passed in."
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
return cmd;
|
|
38
|
+
}
|
|
39
|
+
function collectPiiColumns(model) {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const o of model) {
|
|
42
|
+
if (o.objectType !== "TABLE") continue;
|
|
43
|
+
const rec = o;
|
|
44
|
+
const cols = rec.columns;
|
|
45
|
+
if (!Array.isArray(cols)) continue;
|
|
46
|
+
const fqn = [o.fqn.database, o.fqn.schema, o.fqn.name].filter(Boolean).join(".");
|
|
47
|
+
for (const c of cols) {
|
|
48
|
+
const category = classify(c.name);
|
|
49
|
+
if (category) out.push({ fqn, 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, unmaskedRole) {
|
|
66
|
+
const lines = [];
|
|
67
|
+
lines.push(`-- Generated by \`sdt anonymize\`.`);
|
|
68
|
+
lines.push(
|
|
69
|
+
`-- Edit the role allow-list before applying. Default: only ${unmaskedRole} sees unmasked values.`
|
|
70
|
+
);
|
|
71
|
+
lines.push("");
|
|
72
|
+
const categoriesNeeded = new Set(cols.map((c) => c.category));
|
|
73
|
+
for (const cat of categoriesNeeded) {
|
|
74
|
+
lines.push(`-- Masking policy for ${cat} columns`);
|
|
75
|
+
lines.push(
|
|
76
|
+
`CREATE OR ALTER MASKING POLICY MASK_${cat.toUpperCase().replace(/-/g, "_")} AS (val STRING) RETURNS STRING ->`
|
|
77
|
+
);
|
|
78
|
+
lines.push(` CASE`);
|
|
79
|
+
lines.push(` WHEN CURRENT_ROLE() IN ('${unmaskedRole}') THEN val`);
|
|
80
|
+
lines.push(` ELSE ${maskExpression(cat)}`);
|
|
81
|
+
lines.push(` END;`);
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
lines.push("-- Apply policies to columns");
|
|
85
|
+
for (const c of cols) {
|
|
86
|
+
lines.push(
|
|
87
|
+
`ALTER TABLE ${c.fqn} MODIFY COLUMN ${c.column} SET MASKING POLICY MASK_${c.category.toUpperCase().replace(/-/g, "_")};`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
lines.push("");
|
|
91
|
+
return lines.join("\n") + "\n";
|
|
92
|
+
}
|
|
93
|
+
function maskExpression(cat) {
|
|
94
|
+
switch (cat) {
|
|
95
|
+
case "email":
|
|
96
|
+
return `REGEXP_REPLACE(val, '^(.).*(@.*)$', '\\\\1***\\\\2')`;
|
|
97
|
+
case "phone":
|
|
98
|
+
return `'***-***-' || RIGHT(val, 4)`;
|
|
99
|
+
case "ssn":
|
|
100
|
+
return `'***-**-' || RIGHT(val, 4)`;
|
|
101
|
+
case "credit-card":
|
|
102
|
+
return `'****-****-****-' || RIGHT(val, 4)`;
|
|
103
|
+
case "dob":
|
|
104
|
+
return `LEFT(val, 4) || '-XX-XX'`;
|
|
105
|
+
case "name":
|
|
106
|
+
case "address":
|
|
107
|
+
case "generic":
|
|
108
|
+
default:
|
|
109
|
+
return `SHA2(val, 256)`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function loadModel(sourcePath) {
|
|
113
|
+
if (sourcePath.endsWith(".sdtpac")) {
|
|
114
|
+
const c = await pac.readPac(sourcePath);
|
|
115
|
+
return c.model;
|
|
116
|
+
}
|
|
117
|
+
const loaded = await project.loadProject(sourcePath);
|
|
118
|
+
return await project.parseProjectModel(loaded);
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
anonymizeCommand
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=anonymize-QR6JGXA7.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 { pac, project } from '@sdt-tools/core';\nimport type { SnowflakeObject } from '@sdt-tools/core/model';\n\n/**\n * `sdt anonymize` — generate masking-policy DDL for PII columns\n * discovered in a project model. Pairs with the L012 `pii-without-mask`\n * lint rule: the lint surfaces *which* columns need masking; this\n * command writes the policy DDL ready to copy into the project.\n *\n * Use case: refresh prod → dev pipeline. Most teams need to\n * pseudonymize PII before any non-prod role sees the data. This\n * command auto-generates a starter set of MASKING POLICY statements\n * + the ALTER TABLE ... ALTER COLUMN ... SET MASKING POLICY calls\n * to apply them.\n *\n * Output is intentionally **conservative**: every emitted policy has\n * `RETURN VAL` (no-op) when the current role is in `('PROD_ETL')` so\n * production keeps working as-is; every other role sees a hashed /\n * redacted value. Adjust the role allow-list before applying.\n */\nexport function anonymizeCommand(): Command {\n const cmd = new Command('anonymize');\n cmd\n .description('Generate MASKING POLICY DDL for PII columns detected in the project model.')\n .requiredOption('--source <path>', '.sdtproj or .sdtpac to analyze.')\n .option(\n '--unmasked-role <name>',\n \"Role the unmasked value is visible to. Default 'PROD_ETL'.\",\n 'PROD_ETL',\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 unmaskedRole = String(opts.unmaskedRole);\n const candidates = collectPiiColumns(model);\n if (candidates.length === 0) {\n console.error(\n 'No PII columns detected (heuristic on column names: email, phone, ssn, dob, …).',\n );\n console.error(\n 'If the schema has PII you know about, add a MASKING POLICY by hand and re-run lint to confirm.',\n );\n return;\n }\n const sql = renderAnonymizeSql(candidates, unmaskedRole);\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 role allow-list (`unmaskedRole`) before applying. Default fully hides values from every role except the one passed in.',\n );\n });\n return cmd;\n}\n\ninterface PiiColumn {\n fqn: string;\n column: string;\n category: 'email' | 'phone' | 'name' | 'dob' | 'ssn' | 'address' | 'credit-card' | 'generic';\n}\n\n/**\n * Heuristic PII detector — matches column names against a curated\n * list of likely PII patterns. Same patterns the L012 lint rule\n * uses. We pick the masking strategy from the category.\n */\nfunction collectPiiColumns(model: readonly SnowflakeObject[]): PiiColumn[] {\n const out: PiiColumn[] = [];\n for (const o of model) {\n if (o.objectType !== '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 fqn = [o.fqn.database, o.fqn.schema, o.fqn.name].filter(Boolean).join('.');\n for (const c of cols) {\n const category = classify(c.name);\n if (category) out.push({ fqn, 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(cols: PiiColumn[], unmaskedRole: string): string {\n const lines: string[] = [];\n lines.push(`-- Generated by \\`sdt anonymize\\`.`);\n lines.push(\n `-- Edit the role allow-list before applying. Default: only ${unmaskedRole} sees unmasked values.`,\n );\n lines.push('');\n // One reusable policy per category; the policies belong in a shared schema.\n const categoriesNeeded = new Set(cols.map((c) => c.category));\n for (const cat of categoriesNeeded) {\n lines.push(`-- Masking policy for ${cat} columns`);\n lines.push(\n `CREATE OR ALTER MASKING POLICY MASK_${cat.toUpperCase().replace(/-/g, '_')} AS (val STRING) RETURNS STRING ->`,\n );\n lines.push(` CASE`);\n lines.push(` WHEN CURRENT_ROLE() IN ('${unmaskedRole}') THEN val`);\n lines.push(` ELSE ${maskExpression(cat)}`);\n lines.push(` END;`);\n lines.push('');\n }\n lines.push('-- Apply policies to columns');\n for (const c of cols) {\n lines.push(\n `ALTER TABLE ${c.fqn} MODIFY COLUMN ${c.column} SET MASKING POLICY MASK_${c.category.toUpperCase().replace(/-/g, '_')};`,\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 `'***-***-' || RIGHT(val, 4)`;\n case 'ssn':\n return `'***-**-' || RIGHT(val, 4)`;\n case 'credit-card':\n return `'****-****-****-' || RIGHT(val, 4)`;\n case 'dob':\n return `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) {\n if (sourcePath.endsWith('.sdtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await project.loadProject(sourcePath);\n return await project.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 "@sdt-tools/core";
|
|
7
|
+
var DEFAULT_ROOT = path.join(".sdt", "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-YVHYTV53.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/approval.ts"],"sourcesContent":["/**\n * `sdt approval` — manage multi-approver gate tokens (Team-tier).\n *\n * sdt approval add <deploy-id> --as <approver> [--message <text>] [--digest <hex>]\n * sdt approval list <deploy-id>\n * sdt approval clear <deploy-id>\n * sdt approval check <deploy-id> --required N --allowed <ids>\n *\n * Tokens default to `.sdt/approvals/` under the current working\n * directory; override with `--root <path>` on every subcommand.\n *\n * Mirrors `ddt approval`.\n */\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { approval } from '@sdt-tools/core';\n\nconst DEFAULT_ROOT = path.join('.sdt', '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":";;;AAaA,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,IAAM,eAAe,KAAK,KAAK,QAAQ,WAAW;AAE3C,SAAS,kBAA2B;AACzC,QAAM,MAAM,IAAI,QAAQ,UAAU;AAClC,MAAI,YAAY,qEAAqE;AAErF,MACG,QAAQ,KAAK,EACb,YAAY,qCAAqC,EACjD,SAAS,eAAe,0DAA0D,EAClF,eAAe,mBAAmB,sDAAsD,EACxF,OAAO,oBAAoB,4BAA4B,EACvD,OAAO,kBAAkB,uDAAuD,EAChF,OAAO,iBAAiB,wBAAwB,YAAY,EAC5D,OAAO,OAAO,UAAkB,SAAkC;AACjE,UAAM,SAAkC;AAAA,MACtC,UAAU,OAAO,KAAK,EAAE;AAAA,MACxB,WAAU,oBAAI,KAAK,GAAE,YAAY;AAAA,MACjC,GAAI,KAAK,UAAU,EAAE,SAAS,OAAO,KAAK,OAAO,EAAE,IAAI,CAAC;AAAA,MACxD,GAAI,KAAK,SAAS,EAAE,QAAQ,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;AAAA,IACvD;AACA,UAAM,OAAO,MAAM,SAAS,eAAe,OAAO,KAAK,IAAI,GAAG,UAAU,MAAM;AAC9E,YAAQ;AAAA,MACN,0BAA0B,OAAO,QAAQ,QAAQ,QAAQ,KACnD,KAAK,UAAU,MAAM;AAAA,IAC7B;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,MAAM,EACd,YAAY,0CAA0C,EACtD,SAAS,aAAa,EACtB,OAAO,iBAAiB,wBAAwB,YAAY,EAC5D,OAAO,UAAU,cAAc,KAAK,EACpC,OAAO,OAAO,UAAkB,SAAkC;AACjE,UAAM,OAAO,MAAM,SAAS,iBAAiB,OAAO,KAAK,IAAI,GAAG,QAAQ;AACxE,QAAI,KAAK,MAAM;AACb,cAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AACzC;AAAA,IACF;AACA,QAAI,KAAK,UAAU,WAAW,GAAG;AAC/B,cAAQ,IAAI,8BAA8B,QAAQ,IAAI;AACtD;AAAA,IACF;AACA,YAAQ,IAAI,iBAAiB,QAAQ,GAAG;AACxC,eAAW,KAAK,KAAK,WAAW;AAC9B,YAAM,OAAO,CAAC,EAAE,QAAQ;AACxB,UAAI,EAAE,OAAQ,MAAK,KAAK,UAAU,EAAE,MAAM,EAAE;AAC5C,UAAI,EAAE,QAAS,MAAK,KAAK,WAAW,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE;AAC/D,cAAQ,IAAI,OAAO,EAAE,QAAQ,MAAM,KAAK,KAAK,IAAI,CAAC,GAAG;AAAA,IACvD;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,OAAO,EACf,YAAY,uCAAuC,EACnD,SAAS,aAAa,EACtB,OAAO,iBAAiB,wBAAwB,YAAY,EAC5D,OAAO,OAAO,UAAkB,SAAkC;AACjE,UAAM,SAAS,eAAe,OAAO,KAAK,IAAI,GAAG,QAAQ;AACzD,YAAQ,IAAI,yBAAyB,QAAQ,GAAG;AAAA,EAClD,CAAC;AAEH,MACG,QAAQ,OAAO,EACf,YAAY,4DAA4D,EACxE,SAAS,aAAa,EACtB,eAAe,kBAAkB,0CAA0C,GAAG,EAC9E,OAAO,mBAAmB,wDAAwD,EAClF,OAAO,kBAAkB,yDAAyD,EAClF,OAAO,iBAAiB,wBAAwB,YAAY,EAC5D,OAAO,UAAU,cAAc,KAAK,EACpC,OAAO,OAAO,UAAkB,SAAkC;AACjE,UAAM,mBAAmB,KAAK,UAC1B,OAAO,KAAK,OAAO,EAChB,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO,IACjB,CAAC;AACL,UAAM,OAAO,MAAM,SAAS,iBAAiB,OAAO,KAAK,IAAI,GAAG,QAAQ;AACxE,UAAM,UAAU,SAAS;AAAA,MACvB;AAAA,QACE;AAAA,QACA,UAAU,OAAO,KAAK,YAAY,GAAG,KAAK;AAAA,QAC1C;AAAA,QACA,GAAI,KAAK,SAAS,EAAE,eAAe,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;AAAA,MAC9D;AAAA,MACA,KAAK;AAAA,IACP;AACA,QAAI,KAAK,MAAM;AACb,cAAQ,IAAI,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,IAC9C,WAAW,QAAQ,WAAW;AAC5B,cAAQ;AAAA,QACN,OAAO,QAAQ,YAAY,MAAM,uBAAkB,QAAQ,YAAY,KAAK,IAAI,CAAC;AAAA,MACnF;AAAA,IACF,OAAO;AACL,cAAQ,MAAM,YAAY,QAAQ,eAAe,6BAA6B,GAAG;AAAA,IACnF;AACA,QAAI,CAAC,QAAQ,UAAW,SAAQ,WAAW;AAAA,EAC7C,CAAC;AAEH,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,120 @@
|
|
|
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 "@sdt-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
|
+
// derive kid from the public counterpart (Node lets us re-export from private)
|
|
41
|
+
// Use the standard re-export path by importing crypto here.
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
__require("crypto").createPublicKey(privateKeyPem).export({ format: "pem", type: "spki" }).toString()
|
|
44
|
+
);
|
|
45
|
+
const record = approvalChain.signApprovalRecord(
|
|
46
|
+
{
|
|
47
|
+
deployId: opts.deployId,
|
|
48
|
+
env: opts.env,
|
|
49
|
+
approver: opts.approver,
|
|
50
|
+
decision,
|
|
51
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
52
|
+
digest: opts.digest
|
|
53
|
+
},
|
|
54
|
+
privateKeyPem,
|
|
55
|
+
{ kid }
|
|
56
|
+
);
|
|
57
|
+
const payload = JSON.stringify(record, null, 2) + "\n";
|
|
58
|
+
if (opts.out) {
|
|
59
|
+
await fs.mkdir(path.dirname(path.resolve(opts.out)), { recursive: true });
|
|
60
|
+
await fs.writeFile(path.resolve(opts.out), payload);
|
|
61
|
+
process.stderr.write(`Wrote ${opts.out}
|
|
62
|
+
`);
|
|
63
|
+
} else {
|
|
64
|
+
process.stdout.write(payload);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
return c;
|
|
69
|
+
}
|
|
70
|
+
function verifyCommand() {
|
|
71
|
+
const c = new Command("verify");
|
|
72
|
+
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(
|
|
73
|
+
async (opts) => {
|
|
74
|
+
const configPath = path.resolve(opts.config);
|
|
75
|
+
const config = await approvalChain.loadApprovalChainConfig(configPath);
|
|
76
|
+
if (!config) {
|
|
77
|
+
process.stderr.write(`Config file not found: ${configPath}
|
|
78
|
+
`);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const records = JSON.parse(
|
|
83
|
+
await fs.readFile(path.resolve(opts.records), "utf8")
|
|
84
|
+
);
|
|
85
|
+
const registry = approvalChain.resolveRegistry(config, configPath);
|
|
86
|
+
const outcome = await approvalChain.evaluateApprovalChain({
|
|
87
|
+
deployId: opts.deployId,
|
|
88
|
+
env: opts.env,
|
|
89
|
+
currentDigest: opts.digest,
|
|
90
|
+
records,
|
|
91
|
+
config,
|
|
92
|
+
registry
|
|
93
|
+
});
|
|
94
|
+
if ((opts.format ?? "text").toLowerCase() === "json") {
|
|
95
|
+
process.stdout.write(JSON.stringify(outcome, null, 2) + "\n");
|
|
96
|
+
} else {
|
|
97
|
+
process.stdout.write(
|
|
98
|
+
`${outcome.satisfied ? "OK" : "BLOCKED"}: ${outcome.satisfiedBy.length} valid / ${outcome.missingCount} missing
|
|
99
|
+
`
|
|
100
|
+
);
|
|
101
|
+
if (outcome.satisfiedBy.length > 0)
|
|
102
|
+
process.stdout.write(` approvers: ${outcome.satisfiedBy.join(", ")}
|
|
103
|
+
`);
|
|
104
|
+
if (outcome.ignored.length > 0)
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
` ignored: ${outcome.ignored.map((i) => `${i.approver}=${i.reason}`).join(", ")}
|
|
107
|
+
`
|
|
108
|
+
);
|
|
109
|
+
if (outcome.blockReason) process.stdout.write(` ${outcome.blockReason}
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
112
|
+
if (!outcome.satisfied) process.exitCode = 1;
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
return c;
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
approvalChainCommand
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=approval-chain-54KKJZS3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/approval-chain.ts"],"sourcesContent":["/**\n * `sdt approval-chain` — M-of-N signed approval workflow (DSR.8).\n *\n * Subcommands:\n * sdt approval-chain keygen --out <dir> --id <approver>\n * Generate an Ed25519 keypair for an approver.\n * Writes <dir>/<approver>.pem and <dir>/<approver>.pub.pem.\n *\n * sdt approval-chain sign --deploy-id <id> --env <env>\n * --approver <id> --decision approve|reject\n * --key <private.pem> [--digest <hex>]\n * [--out <signed.json>]\n * Produce a signed approval record.\n *\n * sdt approval-chain verify --deploy-id <id> --env <env>\n * --config <approval.json>\n * --records <signed-records.json>\n * [--digest <hex>]\n * Evaluate the chain against the env policy.\n *\n * Mirrors `ddt 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 '@sdt-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 // derive kid from the public counterpart (Node lets us re-export from private)\n // Use the standard re-export path by importing crypto here.\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":[]}
|