@sdt-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.
- 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 +506 -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-SYTH4V53.js +110 -0
- package/dist/connection-SYTH4V53.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-ZRNJ3VW7.js +109 -0
- package/dist/errorReporting-ZRNJ3VW7.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/index.cjs +36 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +60 -25
- 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-3QI4TH4N.js +344 -0
- package/dist/mcp-3QI4TH4N.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-Y2J56K4Y.js +715 -0
- package/dist/publish-Y2J56K4Y.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,177 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/docs.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { renderDocsReport, renderErdMarkdown, renderErd3dHtml } from "@sdt-tools/core/compare";
|
|
9
|
+
import { loadProject, parseProjectModel } from "@sdt-tools/core/project";
|
|
10
|
+
import { readPac } from "@sdt-tools/core/pac";
|
|
11
|
+
import { ai, comments } from "@sdt-tools/core";
|
|
12
|
+
import { buildDocsPublishPlan, renderDocsWorkflowYaml, searchDocs } from "@sdt-tools/core/docs";
|
|
13
|
+
function docsCommand() {
|
|
14
|
+
const cmd = new Command("docs");
|
|
15
|
+
cmd.description("Generate HTML schema docs from a .sdtproj or .sdtpac.").requiredOption("--source <path>", ".sdtproj or .sdtpac to document.").option("-o, --out <path>", "Output HTML file path (omit when using --augment-comments).").option("--title <text>", "Page title.", "Schema docs").option(
|
|
16
|
+
"--augment-comments",
|
|
17
|
+
"Skip HTML rendering. Instead, call the configured AI provider to suggest comments for every empty COMMENT slot (table-level + column-level) and emit ALTER \u2026 COMMENT SQL.",
|
|
18
|
+
false
|
|
19
|
+
).option(
|
|
20
|
+
"--augment-out <path>",
|
|
21
|
+
"When --augment-comments is set, write the ALTER script here. Default ./augment-comments.sql."
|
|
22
|
+
).option(
|
|
23
|
+
"--augment-max-calls <n>",
|
|
24
|
+
"When --augment-comments is set, cap AI calls. Default unbounded.",
|
|
25
|
+
(v) => parseInt(v, 10)
|
|
26
|
+
).option(
|
|
27
|
+
"--augment-max-length <n>",
|
|
28
|
+
"When --augment-comments is set, cap suggestion length in characters. Default 200.",
|
|
29
|
+
(v) => parseInt(v, 10)
|
|
30
|
+
).action(
|
|
31
|
+
async (opts) => {
|
|
32
|
+
const model = await loadModel(String(opts.source));
|
|
33
|
+
if (opts.augmentComments) {
|
|
34
|
+
const targets = comments.findMissingComments(model);
|
|
35
|
+
if (targets.length === 0) {
|
|
36
|
+
console.log("No missing COMMENT slots found. Nothing to augment.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log(`Found ${targets.length} missing comment slot(s). Calling AI provider\u2026`);
|
|
40
|
+
const suggestions = await comments.augmentComments(targets, {
|
|
41
|
+
completeFn: async (prompt) => {
|
|
42
|
+
const r = await ai.complete([{ role: "user", content: prompt }], {
|
|
43
|
+
feature: "docs.augment-comments"
|
|
44
|
+
});
|
|
45
|
+
return r.text;
|
|
46
|
+
},
|
|
47
|
+
...opts.augmentMaxLength ? { maxCommentLength: opts.augmentMaxLength } : {},
|
|
48
|
+
...opts.augmentMaxCalls ? { maxCalls: opts.augmentMaxCalls } : {}
|
|
49
|
+
});
|
|
50
|
+
const sql = comments.renderAlterCommentScript(suggestions);
|
|
51
|
+
const accepted = suggestions.filter((s) => s.suggestion.trim().length > 0).length;
|
|
52
|
+
const uncertain = suggestions.length - accepted;
|
|
53
|
+
const outPath2 = opts.augmentOut ? path.resolve(String(opts.augmentOut)) : path.resolve("augment-comments.sql");
|
|
54
|
+
await fs.mkdir(path.dirname(outPath2), { recursive: true });
|
|
55
|
+
await fs.writeFile(outPath2, sql, "utf8");
|
|
56
|
+
console.log(
|
|
57
|
+
`Wrote ${outPath2} (${accepted} suggestion(s), ${uncertain} flagged for review).`
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!opts.out) {
|
|
62
|
+
throw new Error("Missing --out. Required unless --augment-comments is set.");
|
|
63
|
+
}
|
|
64
|
+
const html = renderDocsReport(model, { title: String(opts.title ?? "Schema docs") });
|
|
65
|
+
const outPath = path.resolve(String(opts.out));
|
|
66
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
67
|
+
await fs.writeFile(outPath, html, "utf8");
|
|
68
|
+
console.log(`Wrote ${outPath} (${html.length} bytes, ${model.length} objects).`);
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
cmd.command("build").description(
|
|
72
|
+
"Build the HTML schema docs site (alias for the parent command with --out default)."
|
|
73
|
+
).requiredOption("--source <path>", ".sdtproj or .sdtpac to document.").option("-o, --out <path>", "Output HTML file path.", "dist/docs/index.html").option("--title <text>", "Page title.", "Schema docs").action(async (opts) => {
|
|
74
|
+
const model = await loadModel(String(opts.source));
|
|
75
|
+
const html = renderDocsReport(model, { title: String(opts.title ?? "Schema docs") });
|
|
76
|
+
const outPath = path.resolve(String(opts.out));
|
|
77
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
78
|
+
await fs.writeFile(outPath, html, "utf8");
|
|
79
|
+
console.log(`Wrote ${outPath} (${html.length} bytes, ${model.length} objects).`);
|
|
80
|
+
});
|
|
81
|
+
cmd.command("publish").description("Publish the generated docs site to a gh-pages branch.").requiredOption("--site <path>", "Path to the built docs site directory (e.g. dist/docs).").option("--branch <name>", "Target branch name.", "gh-pages").option("--remote <name>", "Git remote name.", "origin").option("--message <text>", "Commit message.", "chore(docs): deploy schema docs [skip ci]").option("--dry-run", "Print the commands that would be executed without running them.", false).option("--emit-workflow <path>", "Write a GitHub Actions workflow YAML template to this path.").action(async (opts) => {
|
|
82
|
+
if (opts.emitWorkflow) {
|
|
83
|
+
const yaml = renderDocsWorkflowYaml({ branch: opts.branch, trigger: "main" });
|
|
84
|
+
const wPath = path.resolve(String(opts.emitWorkflow));
|
|
85
|
+
await fs.mkdir(path.dirname(wPath), { recursive: true });
|
|
86
|
+
await fs.writeFile(wPath, yaml, "utf8");
|
|
87
|
+
console.log(`Wrote workflow template \u2192 ${wPath}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const plan = buildDocsPublishPlan({
|
|
91
|
+
siteDir: path.resolve(String(opts.site)),
|
|
92
|
+
branch: opts.branch,
|
|
93
|
+
remote: opts.remote,
|
|
94
|
+
message: opts.message
|
|
95
|
+
});
|
|
96
|
+
for (const step of plan.steps) {
|
|
97
|
+
console.log(` ${step.description}`);
|
|
98
|
+
if (opts.dryRun) {
|
|
99
|
+
console.log(` [dry-run] ${step.command}`);
|
|
100
|
+
} else {
|
|
101
|
+
execSync(step.command, { stdio: "inherit" });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!opts.dryRun) console.log(`Done \u2014 site pushed to ${plan.remote}/${plan.branch}.`);
|
|
105
|
+
});
|
|
106
|
+
cmd.command("search").description("Search object names, comments, and column names in a .sdtproj or .sdtpac.").argument("<query>", "Search query string.").requiredOption("--source <path>", ".sdtproj or .sdtpac to search.").option("-n, --max-results <n>", "Maximum number of results to return.", (v) => parseInt(v, 10)).action(async (query, opts) => {
|
|
107
|
+
const model = await loadModel(String(opts.source));
|
|
108
|
+
const hits = searchDocs(model, query, {
|
|
109
|
+
maxResults: opts.maxResults
|
|
110
|
+
});
|
|
111
|
+
if (hits.length === 0) {
|
|
112
|
+
console.log(`No results for "${query}".`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const hit of hits) {
|
|
116
|
+
console.log(`[${hit.score}] ${hit.objectFqn} (${hit.objectType}) \u2014 ${hit.field}`);
|
|
117
|
+
console.log(` ${hit.snippet}`);
|
|
118
|
+
}
|
|
119
|
+
console.log(`
|
|
120
|
+
${hits.length} result(s) for "${query}".`);
|
|
121
|
+
});
|
|
122
|
+
return cmd;
|
|
123
|
+
}
|
|
124
|
+
function erdCommand() {
|
|
125
|
+
const cmd = new Command("erd");
|
|
126
|
+
cmd.description(
|
|
127
|
+
"Generate an ER diagram (Mermaid Markdown or interactive 3D HTML) from a .sdtproj or .sdtpac."
|
|
128
|
+
).requiredOption("--source <path>", ".sdtproj or .sdtpac to diagram.").requiredOption(
|
|
129
|
+
"-o, --out <path>",
|
|
130
|
+
"Output file path. Format inferred from extension (.md \u2192 mermaid, .html \u2192 html3d) unless --format is given."
|
|
131
|
+
).option("--title <text>", "Diagram title.", "Schema ER diagram").option(
|
|
132
|
+
"--format <fmt>",
|
|
133
|
+
"Output format: mermaid | html3d. Default: inferred from --out extension, falling back to mermaid."
|
|
134
|
+
).action(async (opts) => {
|
|
135
|
+
const model = await loadModel(String(opts.source));
|
|
136
|
+
const title = String(opts.title ?? "Schema ER diagram");
|
|
137
|
+
const fmt = resolveErdFormat(String(opts.out), opts.format);
|
|
138
|
+
let content;
|
|
139
|
+
if (fmt === "html3d") {
|
|
140
|
+
content = renderErd3dHtml(model, { title });
|
|
141
|
+
} else {
|
|
142
|
+
content = renderErdMarkdown(model, title);
|
|
143
|
+
}
|
|
144
|
+
const outPath = path.resolve(String(opts.out));
|
|
145
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
146
|
+
await fs.writeFile(outPath, content, "utf8");
|
|
147
|
+
console.log(
|
|
148
|
+
`Wrote ${outPath} (${content.length} bytes, ${model.length} objects, format=${fmt}).`
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
return cmd;
|
|
152
|
+
}
|
|
153
|
+
function resolveErdFormat(outPath, explicit) {
|
|
154
|
+
if (explicit) {
|
|
155
|
+
const e = explicit.toLowerCase();
|
|
156
|
+
if (e === "mermaid" || e === "md") return "mermaid";
|
|
157
|
+
if (e === "html3d" || e === "3d" || e === "html") return "html3d";
|
|
158
|
+
throw new Error(`Unknown --format "${explicit}". Expected: mermaid | html3d.`);
|
|
159
|
+
}
|
|
160
|
+
const ext = outPath.toLowerCase();
|
|
161
|
+
if (ext.endsWith(".html") || ext.endsWith(".htm")) return "html3d";
|
|
162
|
+
return "mermaid";
|
|
163
|
+
}
|
|
164
|
+
async function loadModel(sourcePath) {
|
|
165
|
+
if (sourcePath.endsWith(".sdtpac")) {
|
|
166
|
+
const pacContents = await readPac(sourcePath);
|
|
167
|
+
return pacContents.model;
|
|
168
|
+
}
|
|
169
|
+
const loaded = await loadProject(sourcePath);
|
|
170
|
+
const parsed = await parseProjectModel(loaded);
|
|
171
|
+
return parsed;
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
docsCommand,
|
|
175
|
+
erdCommand
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=docs-CVRKGUSW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/docs.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport { execSync } from 'node:child_process';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { renderDocsReport, renderErdMarkdown, renderErd3dHtml } from '@sdt-tools/core/compare';\nimport { loadProject, parseProjectModel } from '@sdt-tools/core/project';\nimport { readPac } from '@sdt-tools/core/pac';\nimport { ai, comments } from '@sdt-tools/core';\nimport { buildDocsPublishPlan, renderDocsWorkflowYaml, searchDocs } from '@sdt-tools/core/docs';\nimport type { AnySnowflakeObject } from '@sdt-tools/core/model';\n\n/**\n * `sdt docs` — auto-generate HTML schema docs from a `.sdtproj` or\n * `.sdtpac`. dbt-docs-style: database → schema → object navigation with\n * per-object detail panels.\n */\nexport function docsCommand(): Command {\n const cmd = new Command('docs');\n cmd\n .description('Generate HTML schema docs from a .sdtproj or .sdtpac.')\n .requiredOption('--source <path>', '.sdtproj or .sdtpac to document.')\n .option('-o, --out <path>', 'Output HTML file path (omit when using --augment-comments).')\n .option('--title <text>', 'Page title.', 'Schema docs')\n .option(\n '--augment-comments',\n 'Skip HTML rendering. Instead, call the configured AI provider to suggest comments for every empty COMMENT slot (table-level + column-level) and emit ALTER … COMMENT SQL.',\n false,\n )\n .option(\n '--augment-out <path>',\n 'When --augment-comments is set, write the ALTER script here. Default ./augment-comments.sql.',\n )\n .option(\n '--augment-max-calls <n>',\n 'When --augment-comments is set, cap AI calls. Default unbounded.',\n (v) => parseInt(v, 10),\n )\n .option(\n '--augment-max-length <n>',\n 'When --augment-comments is set, cap suggestion length in characters. Default 200.',\n (v) => parseInt(v, 10),\n )\n .action(\n async (opts: {\n source: string;\n out?: string;\n title?: string;\n augmentComments?: boolean;\n augmentOut?: string;\n augmentMaxCalls?: number;\n augmentMaxLength?: number;\n }) => {\n const model = await loadModel(String(opts.source));\n\n if (opts.augmentComments) {\n const targets = comments.findMissingComments(model);\n if (targets.length === 0) {\n console.log('No missing COMMENT slots found. Nothing to augment.');\n return;\n }\n console.log(`Found ${targets.length} missing comment slot(s). Calling AI provider…`);\n const suggestions = await comments.augmentComments(targets, {\n completeFn: async (prompt: string) => {\n const r = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'docs.augment-comments',\n });\n return r.text;\n },\n ...(opts.augmentMaxLength ? { maxCommentLength: opts.augmentMaxLength } : {}),\n ...(opts.augmentMaxCalls ? { maxCalls: opts.augmentMaxCalls } : {}),\n });\n const sql = comments.renderAlterCommentScript(suggestions);\n const accepted = suggestions.filter((s) => s.suggestion.trim().length > 0).length;\n const uncertain = suggestions.length - accepted;\n const outPath = opts.augmentOut\n ? path.resolve(String(opts.augmentOut))\n : path.resolve('augment-comments.sql');\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, sql, 'utf8');\n console.log(\n `Wrote ${outPath} (${accepted} suggestion(s), ${uncertain} flagged for review).`,\n );\n return;\n }\n\n if (!opts.out) {\n throw new Error('Missing --out. Required unless --augment-comments is set.');\n }\n const html = renderDocsReport(model, { title: String(opts.title ?? 'Schema docs') });\n const outPath = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, html, 'utf8');\n console.log(`Wrote ${outPath} (${html.length} bytes, ${model.length} objects).`);\n },\n );\n\n cmd\n .command('build')\n .description(\n 'Build the HTML schema docs site (alias for the parent command with --out default).',\n )\n .requiredOption('--source <path>', '.sdtproj or .sdtpac to document.')\n .option('-o, --out <path>', 'Output HTML file path.', 'dist/docs/index.html')\n .option('--title <text>', 'Page title.', 'Schema docs')\n .action(async (opts: { source: string; out: string; title?: string }) => {\n const model = await loadModel(String(opts.source));\n const html = renderDocsReport(model, { title: String(opts.title ?? 'Schema docs') });\n const outPath = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, html, 'utf8');\n console.log(`Wrote ${outPath} (${html.length} bytes, ${model.length} objects).`);\n });\n\n cmd\n .command('publish')\n .description('Publish the generated docs site to a gh-pages branch.')\n .requiredOption('--site <path>', 'Path to the built docs site directory (e.g. dist/docs).')\n .option('--branch <name>', 'Target branch name.', 'gh-pages')\n .option('--remote <name>', 'Git remote name.', 'origin')\n .option('--message <text>', 'Commit message.', 'chore(docs): deploy schema docs [skip ci]')\n .option('--dry-run', 'Print the commands that would be executed without running them.', false)\n .option('--emit-workflow <path>', 'Write a GitHub Actions workflow YAML template to this path.')\n .action(async (opts) => {\n if (opts.emitWorkflow) {\n const yaml = renderDocsWorkflowYaml({ branch: opts.branch, trigger: 'main' });\n const wPath = path.resolve(String(opts.emitWorkflow));\n await fs.mkdir(path.dirname(wPath), { recursive: true });\n await fs.writeFile(wPath, yaml, 'utf8');\n console.log(`Wrote workflow template → ${wPath}`);\n return;\n }\n const plan = buildDocsPublishPlan({\n siteDir: path.resolve(String(opts.site)),\n branch: opts.branch,\n remote: opts.remote,\n message: opts.message,\n });\n for (const step of plan.steps) {\n console.log(` ${step.description}`);\n if (opts.dryRun) {\n console.log(` [dry-run] ${step.command}`);\n } else {\n execSync(step.command, { stdio: 'inherit' });\n }\n }\n if (!opts.dryRun) console.log(`Done — site pushed to ${plan.remote}/${plan.branch}.`);\n });\n\n cmd\n .command('search')\n .description('Search object names, comments, and column names in a .sdtproj or .sdtpac.')\n .argument('<query>', 'Search query string.')\n .requiredOption('--source <path>', '.sdtproj or .sdtpac to search.')\n .option('-n, --max-results <n>', 'Maximum number of results to return.', (v) => parseInt(v, 10))\n .action(async (query: string, opts: { source: string; maxResults?: number }) => {\n const model = await loadModel(String(opts.source));\n const hits = searchDocs(model as Parameters<typeof searchDocs>[0], query, {\n maxResults: opts.maxResults,\n });\n if (hits.length === 0) {\n console.log(`No results for \"${query}\".`);\n return;\n }\n for (const hit of hits) {\n console.log(`[${hit.score}] ${hit.objectFqn} (${hit.objectType}) — ${hit.field}`);\n console.log(` ${hit.snippet}`);\n }\n console.log(`\\n${hits.length} result(s) for \"${query}\".`);\n });\n\n return cmd;\n}\n\n/**\n * `sdt erd` — auto-generate an ER diagram from a `.sdtproj` or `.sdtpac`.\n *\n * Output format defaults to Mermaid Markdown for back-compat. Pass\n * `--format html3d` (or write to a `.html` file) to emit the interactive\n * 3D WebGL viewer — a single self-contained HTML file you can open in\n * any browser. The 3D viewer lays out schemas as horizontal planes with\n * tables as boxes and FKs as bezier curves; supports click-to-focus,\n * search, 2D toggle, and full orbit/pan/zoom. See DIAG-1 spec.\n */\nexport function erdCommand(): Command {\n const cmd = new Command('erd');\n cmd\n .description(\n 'Generate an ER diagram (Mermaid Markdown or interactive 3D HTML) from a .sdtproj or .sdtpac.',\n )\n .requiredOption('--source <path>', '.sdtproj or .sdtpac to diagram.')\n .requiredOption(\n '-o, --out <path>',\n 'Output file path. Format inferred from extension (.md → mermaid, .html → html3d) unless --format is given.',\n )\n .option('--title <text>', 'Diagram title.', 'Schema ER diagram')\n .option(\n '--format <fmt>',\n 'Output format: mermaid | html3d. Default: inferred from --out extension, falling back to mermaid.',\n )\n .action(async (opts: { source: string; out: string; title?: string; format?: string }) => {\n const model = await loadModel(String(opts.source));\n const title = String(opts.title ?? 'Schema ER diagram');\n const fmt = resolveErdFormat(String(opts.out), opts.format);\n let content: string;\n if (fmt === 'html3d') {\n content = renderErd3dHtml(model, { title });\n } else {\n content = renderErdMarkdown(model, title);\n }\n const outPath = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, content, 'utf8');\n console.log(\n `Wrote ${outPath} (${content.length} bytes, ${model.length} objects, format=${fmt}).`,\n );\n });\n return cmd;\n}\n\nfunction resolveErdFormat(outPath: string, explicit?: string): 'mermaid' | 'html3d' {\n if (explicit) {\n const e = explicit.toLowerCase();\n if (e === 'mermaid' || e === 'md') return 'mermaid';\n if (e === 'html3d' || e === '3d' || e === 'html') return 'html3d';\n throw new Error(`Unknown --format \"${explicit}\". Expected: mermaid | html3d.`);\n }\n const ext = outPath.toLowerCase();\n if (ext.endsWith('.html') || ext.endsWith('.htm')) return 'html3d';\n return 'mermaid';\n}\n\nasync function loadModel(sourcePath: string): Promise<AnySnowflakeObject[]> {\n if (sourcePath.endsWith('.sdtpac')) {\n const pacContents = await readPac(sourcePath);\n return pacContents.model as AnySnowflakeObject[];\n }\n const loaded = await loadProject(sourcePath);\n const parsed = await parseProjectModel(loaded);\n return parsed as AnySnowflakeObject[];\n}\n"],"mappings":"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","names":["outPath"]}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addMappingFlags,
|
|
3
|
+
buildMappingFromOptions
|
|
4
|
+
} from "./chunk-JP2EZLR5.js";
|
|
5
|
+
import {
|
|
6
|
+
attachRelatedOptions
|
|
7
|
+
} from "./chunk-EWXM4KJN.js";
|
|
8
|
+
import {
|
|
9
|
+
logger
|
|
10
|
+
} from "./chunk-VM2H4LAO.js";
|
|
11
|
+
import "./chunk-DGUM43GV.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/drift.ts
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import { readPac } from "@sdt-tools/core/pac";
|
|
17
|
+
import {
|
|
18
|
+
CompareEngine,
|
|
19
|
+
DbtManifestSource,
|
|
20
|
+
LiveSource
|
|
21
|
+
} from "@sdt-tools/core/compare";
|
|
22
|
+
import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
|
|
23
|
+
import { loadProject } from "@sdt-tools/core/project";
|
|
24
|
+
import { driftAnomaly } from "@sdt-tools/core";
|
|
25
|
+
var ModelSource = class {
|
|
26
|
+
kind = "pac";
|
|
27
|
+
platform = "Snowflake";
|
|
28
|
+
label;
|
|
29
|
+
model;
|
|
30
|
+
constructor(label, model) {
|
|
31
|
+
this.label = label;
|
|
32
|
+
this.model = model;
|
|
33
|
+
}
|
|
34
|
+
async load() {
|
|
35
|
+
return this.model;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function driftWatchSubcommand() {
|
|
39
|
+
const sub = new Command("watch");
|
|
40
|
+
sub.description(
|
|
41
|
+
"Poll a project against the live Snowflake account on a fixed interval. Prints DRIFT_DETECTED events to stdout (or POSTs to --webhook) when the catalog diverges from the project pac."
|
|
42
|
+
).requiredOption("-p, --project <path>", "Path to .sdtproj").requiredOption("-c, --connection <profile>", "Connection profile name").option("--interval <seconds>", "Poll interval in seconds (min 5).", "60").option(
|
|
43
|
+
"--webhook <url>",
|
|
44
|
+
"POST drift events as JSON to this URL (Slack/Teams/generic receiver)."
|
|
45
|
+
).option("--format <fmt>", "Output format: text | json.", "text").option(
|
|
46
|
+
"--quiet",
|
|
47
|
+
"Suppress CLEAN status lines; emit only DRIFT_DETECTED and error events.",
|
|
48
|
+
false
|
|
49
|
+
).action(async (opts) => {
|
|
50
|
+
const intervalSecs = Math.max(5, parseInt(String(opts.interval), 10) || 60);
|
|
51
|
+
const format = String(opts.format) === "json" ? "json" : "text";
|
|
52
|
+
const webhookUrl = opts.webhook ? String(opts.webhook) : void 0;
|
|
53
|
+
const quiet = !!opts.quiet;
|
|
54
|
+
const profile = await getProfile(String(opts.connection));
|
|
55
|
+
const conn = new SnowflakeConnection(profile);
|
|
56
|
+
const loaded = await loadProject(String(opts.project));
|
|
57
|
+
const pacPath = path.join(loaded.rootDir, "bin", `${loaded.project.name}.sdtpac`);
|
|
58
|
+
const scope = {
|
|
59
|
+
database: loaded.project.scope.database,
|
|
60
|
+
schema: loaded.project.scope.schema
|
|
61
|
+
};
|
|
62
|
+
if (!quiet && format === "text") {
|
|
63
|
+
logger.info(
|
|
64
|
+
`drift watch: polling every ${intervalSecs}s \u2014 ${loaded.project.name} \u2192 ${profile.account}`
|
|
65
|
+
);
|
|
66
|
+
logger.info("Press Ctrl+C to stop.");
|
|
67
|
+
}
|
|
68
|
+
const postToWebhook = async (event) => {
|
|
69
|
+
if (!webhookUrl) return;
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(webhookUrl, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify(event)
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok && format === "text") logger.warn(`Webhook POST failed: HTTP ${res.status}`);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (format === "text")
|
|
79
|
+
logger.warn(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const pollOnce = async () => {
|
|
83
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
84
|
+
try {
|
|
85
|
+
const pac = await readPac(pacPath);
|
|
86
|
+
const declared = new ModelSource(pacPath, pac.model);
|
|
87
|
+
const live = new LiveSource(conn, scope);
|
|
88
|
+
const engine = new CompareEngine();
|
|
89
|
+
const result = await engine.compare(declared, live);
|
|
90
|
+
const s = result.summary;
|
|
91
|
+
const drifted = s.added + s.removed + s.modified;
|
|
92
|
+
if (drifted > 0) {
|
|
93
|
+
const event = {
|
|
94
|
+
type: "DRIFT_DETECTED",
|
|
95
|
+
timestamp: ts,
|
|
96
|
+
project: loaded.project.name,
|
|
97
|
+
added: s.added,
|
|
98
|
+
removed: s.removed,
|
|
99
|
+
modified: s.modified
|
|
100
|
+
};
|
|
101
|
+
if (format === "json") process.stdout.write(JSON.stringify(event) + "\n");
|
|
102
|
+
else logger.warn(`[${ts}] DRIFT_DETECTED +${s.added} -${s.removed} ~${s.modified}`);
|
|
103
|
+
await postToWebhook(event);
|
|
104
|
+
} else if (!quiet) {
|
|
105
|
+
if (format === "json")
|
|
106
|
+
process.stdout.write(
|
|
107
|
+
JSON.stringify({ type: "CLEAN", timestamp: ts, project: loaded.project.name }) + "\n"
|
|
108
|
+
);
|
|
109
|
+
else logger.info(`[${ts}] clean`);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
if (format === "json")
|
|
114
|
+
process.stdout.write(
|
|
115
|
+
JSON.stringify({ type: "POLL_ERROR", timestamp: ts, error: message }) + "\n"
|
|
116
|
+
);
|
|
117
|
+
else logger.error(`[${ts}] poll error: ${message}`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
await pollOnce();
|
|
121
|
+
const timer = setInterval(() => {
|
|
122
|
+
void pollOnce();
|
|
123
|
+
}, intervalSecs * 1e3);
|
|
124
|
+
process.once("SIGINT", () => {
|
|
125
|
+
clearInterval(timer);
|
|
126
|
+
void conn.disconnect().then(() => process.exit(0));
|
|
127
|
+
});
|
|
128
|
+
await new Promise(() => {
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
return sub;
|
|
132
|
+
}
|
|
133
|
+
function driftCommand() {
|
|
134
|
+
const cmd = new Command("drift");
|
|
135
|
+
cmd.description(
|
|
136
|
+
"Check whether a live Snowflake target has drifted from the project pac or a dbt manifest."
|
|
137
|
+
).option("-p, --project <path>", "Path to .sdtproj (required unless --vs-dbt-project is used)").option(
|
|
138
|
+
"--vs-dbt-project <path>",
|
|
139
|
+
"Compare a compiled dbt project (or target/manifest.json) against the live target. Run `dbt compile` first. When set, --project is not required."
|
|
140
|
+
).requiredOption("-c, --connection <profile>", "Connection profile name").option(
|
|
141
|
+
"--anomalies",
|
|
142
|
+
"Also classify drift via the anomaly detector (new grants to PUBLIC, bypass-role grants, owner changes, audit-column drops, mask removals, new bypass-named roles).",
|
|
143
|
+
false
|
|
144
|
+
).option(
|
|
145
|
+
"--anomalies-only",
|
|
146
|
+
"Skip the simple drift summary and emit only the anomaly report. Implies --anomalies.",
|
|
147
|
+
false
|
|
148
|
+
).option(
|
|
149
|
+
"--fail-on-anomaly <severity>",
|
|
150
|
+
"Exit non-zero when any anomaly at or above this severity fires. Values: critical | high | medium | low. Default: drift presence alone determines exit code."
|
|
151
|
+
);
|
|
152
|
+
addMappingFlags(cmd);
|
|
153
|
+
cmd.action(async (opts) => {
|
|
154
|
+
if (!opts.project && !opts.vsDBtProject) {
|
|
155
|
+
logger.error("Provide either --project <path> or --vs-dbt-project <path>.");
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const nameMapping = await buildMappingFromOptions(opts);
|
|
160
|
+
const profile = await getProfile(String(opts.connection));
|
|
161
|
+
const conn = new SnowflakeConnection(profile);
|
|
162
|
+
let declared;
|
|
163
|
+
let scope;
|
|
164
|
+
if (opts.vsDBtProject) {
|
|
165
|
+
declared = new DbtManifestSource(String(opts.vsDBtProject), `dbt:${opts.vsDBtProject}`);
|
|
166
|
+
scope = {};
|
|
167
|
+
} else {
|
|
168
|
+
const loaded = await loadProject(String(opts.project));
|
|
169
|
+
const pacPath = path.join(loaded.rootDir, "bin", `${loaded.project.name}.sdtpac`);
|
|
170
|
+
const pac = await readPac(pacPath);
|
|
171
|
+
declared = new ModelSource(pacPath, pac.model);
|
|
172
|
+
scope = {
|
|
173
|
+
database: loaded.project.scope.database,
|
|
174
|
+
schema: loaded.project.scope.schema
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const live = new LiveSource(conn, scope);
|
|
178
|
+
const engine = new CompareEngine();
|
|
179
|
+
const result = await engine.compare(declared, live, {
|
|
180
|
+
...nameMapping ? { nameMapping } : {}
|
|
181
|
+
});
|
|
182
|
+
const drifted = result.summary.added + result.summary.removed + result.summary.modified;
|
|
183
|
+
if (!opts.anomaliesOnly) {
|
|
184
|
+
if (drifted === 0) {
|
|
185
|
+
logger.success("No drift detected.");
|
|
186
|
+
} else {
|
|
187
|
+
logger.warn(
|
|
188
|
+
`Drift: +${result.summary.added} -${result.summary.removed} ~${result.summary.modified}`
|
|
189
|
+
);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (opts.anomalies || opts.anomaliesOnly) {
|
|
194
|
+
const report = driftAnomaly.detectAnomalies(result);
|
|
195
|
+
if (report.totalAnomalies === 0) {
|
|
196
|
+
logger.info("Anomaly scan: 0 findings.");
|
|
197
|
+
} else {
|
|
198
|
+
logger.warn(`Anomaly scan: ${report.totalAnomalies} finding(s):`);
|
|
199
|
+
for (const a of report.anomalies) {
|
|
200
|
+
logger.info(` [${a.severity}] ${a.category}: ${a.fqn} \u2014 ${a.reason}`);
|
|
201
|
+
}
|
|
202
|
+
const failOn = opts.failOnAnomaly?.toLowerCase();
|
|
203
|
+
if (failOn && ["critical", "high", "medium", "low"].includes(failOn)) {
|
|
204
|
+
const failRank = driftAnomaly.anomalySeverityRank(failOn);
|
|
205
|
+
const triggered = report.anomalies.some(
|
|
206
|
+
(a) => driftAnomaly.anomalySeverityRank(a.severity) <= failRank
|
|
207
|
+
);
|
|
208
|
+
if (triggered) process.exitCode = 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await conn.disconnect();
|
|
213
|
+
});
|
|
214
|
+
attachRelatedOptions(cmd, [
|
|
215
|
+
"compare.ignoreCase",
|
|
216
|
+
"compare.ignoreComments",
|
|
217
|
+
"compare.ignoreFormattingDifferences",
|
|
218
|
+
"compare.excludeObjectTypes"
|
|
219
|
+
]);
|
|
220
|
+
cmd.addCommand(driftWatchSubcommand());
|
|
221
|
+
return cmd;
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
driftCommand
|
|
225
|
+
};
|
|
226
|
+
//# sourceMappingURL=drift-XDA3BDYN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/drift.ts"],"sourcesContent":["import { Command } from 'commander';\nimport path from 'node:path';\nimport { readPac } from '@sdt-tools/core/pac';\nimport {\n CompareEngine,\n DbtManifestSource,\n LiveSource,\n type CompareSource,\n} from '@sdt-tools/core/compare';\nimport { getProfile, SnowflakeConnection } from '@sdt-tools/core/connection';\nimport { loadProject } from '@sdt-tools/core/project';\nimport { driftAnomaly } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\nimport { addMappingFlags, buildMappingFromOptions } from '../util/mapping.js';\nimport { attachRelatedOptions } from '../util/help-catalog.js';\n\nclass ModelSource implements CompareSource {\n readonly kind = 'pac' as const;\n readonly platform = 'Snowflake' as const;\n readonly label: string;\n private readonly model: Awaited<ReturnType<typeof readPac>>['model'];\n constructor(label: string, model: Awaited<ReturnType<typeof readPac>>['model']) {\n this.label = label;\n this.model = model;\n }\n async load() {\n return this.model;\n }\n}\n\nfunction driftWatchSubcommand(): Command {\n const sub = new Command('watch');\n sub\n .description(\n 'Poll a project against the live Snowflake account on a fixed interval. Prints DRIFT_DETECTED events to stdout (or POSTs to --webhook) when the catalog diverges from the project pac.',\n )\n .requiredOption('-p, --project <path>', 'Path to .sdtproj')\n .requiredOption('-c, --connection <profile>', 'Connection profile name')\n .option('--interval <seconds>', 'Poll interval in seconds (min 5).', '60')\n .option(\n '--webhook <url>',\n 'POST drift events as JSON to this URL (Slack/Teams/generic receiver).',\n )\n .option('--format <fmt>', 'Output format: text | json.', 'text')\n .option(\n '--quiet',\n 'Suppress CLEAN status lines; emit only DRIFT_DETECTED and error events.',\n false,\n )\n .action(async (opts) => {\n const intervalSecs = Math.max(5, parseInt(String(opts.interval), 10) || 60);\n const format = String(opts.format) === 'json' ? 'json' : 'text';\n const webhookUrl = opts.webhook ? String(opts.webhook) : undefined;\n const quiet = !!opts.quiet;\n\n const profile = await getProfile(String(opts.connection));\n const conn = new SnowflakeConnection(profile);\n const loaded = await loadProject(String(opts.project));\n const pacPath = path.join(loaded.rootDir, 'bin', `${loaded.project.name}.sdtpac`);\n const scope = {\n database: loaded.project.scope.database,\n schema: loaded.project.scope.schema,\n };\n\n if (!quiet && format === 'text') {\n logger.info(\n `drift watch: polling every ${intervalSecs}s — ${loaded.project.name} → ${profile.account}`,\n );\n logger.info('Press Ctrl+C to stop.');\n }\n\n const postToWebhook = async (event: Record<string, unknown>): Promise<void> => {\n if (!webhookUrl) return;\n try {\n const res = await fetch(webhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(event),\n });\n if (!res.ok && format === 'text') logger.warn(`Webhook POST failed: HTTP ${res.status}`);\n } catch (err) {\n if (format === 'text')\n logger.warn(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);\n }\n };\n\n const pollOnce = async (): Promise<void> => {\n const ts = new Date().toISOString();\n try {\n const pac = await readPac(pacPath);\n const declared = new ModelSource(pacPath, pac.model);\n const live = new LiveSource(conn, scope);\n const engine = new CompareEngine();\n const result = await engine.compare(declared, live);\n const s = result.summary;\n const drifted = s.added + s.removed + s.modified;\n if (drifted > 0) {\n const event = {\n type: 'DRIFT_DETECTED',\n timestamp: ts,\n project: loaded.project.name,\n added: s.added,\n removed: s.removed,\n modified: s.modified,\n };\n if (format === 'json') process.stdout.write(JSON.stringify(event) + '\\n');\n else logger.warn(`[${ts}] DRIFT_DETECTED +${s.added} -${s.removed} ~${s.modified}`);\n await postToWebhook(event);\n } else if (!quiet) {\n if (format === 'json')\n process.stdout.write(\n JSON.stringify({ type: 'CLEAN', timestamp: ts, project: loaded.project.name }) +\n '\\n',\n );\n else logger.info(`[${ts}] clean`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (format === 'json')\n process.stdout.write(\n JSON.stringify({ type: 'POLL_ERROR', timestamp: ts, error: message }) + '\\n',\n );\n else logger.error(`[${ts}] poll error: ${message}`);\n }\n };\n\n await pollOnce();\n const timer = setInterval(() => {\n void pollOnce();\n }, intervalSecs * 1000);\n process.once('SIGINT', () => {\n clearInterval(timer);\n void conn.disconnect().then(() => process.exit(0));\n });\n await new Promise<void>(() => {});\n });\n return sub;\n}\n\nexport function driftCommand(): Command {\n const cmd = new Command('drift');\n cmd\n .description(\n 'Check whether a live Snowflake target has drifted from the project pac or a dbt manifest.',\n )\n .option('-p, --project <path>', 'Path to .sdtproj (required unless --vs-dbt-project is used)')\n .option(\n '--vs-dbt-project <path>',\n 'Compare a compiled dbt project (or target/manifest.json) against the live target. ' +\n 'Run `dbt compile` first. When set, --project is not required.',\n )\n .requiredOption('-c, --connection <profile>', 'Connection profile name')\n .option(\n '--anomalies',\n 'Also classify drift via the anomaly detector (new grants to PUBLIC, bypass-role grants, owner changes, audit-column drops, mask removals, new bypass-named roles).',\n false,\n )\n .option(\n '--anomalies-only',\n 'Skip the simple drift summary and emit only the anomaly report. Implies --anomalies.',\n false,\n )\n .option(\n '--fail-on-anomaly <severity>',\n 'Exit non-zero when any anomaly at or above this severity fires. Values: critical | high | medium | low. Default: drift presence alone determines exit code.',\n );\n // Logical-name mapping (`--map`, `--map-file`) — rewrites declared-side\n // FQNs so a project authored against DEV names can be drift-checked\n // against a PROD target. See docs/LOGICAL_NAME_MAPPING.md. Mirrors\n // `ddt drift` (RH3.2 parity).\n addMappingFlags(cmd);\n cmd.action(async (opts) => {\n if (!opts.project && !opts.vsDBtProject) {\n logger.error('Provide either --project <path> or --vs-dbt-project <path>.');\n process.exitCode = 1;\n return;\n }\n\n const nameMapping = await buildMappingFromOptions(opts);\n const profile = await getProfile(String(opts.connection));\n const conn = new SnowflakeConnection(profile);\n\n let declared: CompareSource;\n let scope: { database?: string; schema?: string };\n\n if (opts.vsDBtProject) {\n declared = new DbtManifestSource(String(opts.vsDBtProject), `dbt:${opts.vsDBtProject}`);\n scope = {};\n } else {\n const loaded = await loadProject(String(opts.project));\n const pacPath = path.join(loaded.rootDir, 'bin', `${loaded.project.name}.sdtpac`);\n const pac = await readPac(pacPath);\n declared = new ModelSource(pacPath, pac.model);\n scope = {\n database: loaded.project.scope.database,\n schema: loaded.project.scope.schema,\n };\n }\n\n const live = new LiveSource(conn, scope);\n const engine = new CompareEngine();\n const result = await engine.compare(declared, live, {\n ...(nameMapping ? { nameMapping } : {}),\n });\n const drifted = result.summary.added + result.summary.removed + result.summary.modified;\n if (!opts.anomaliesOnly) {\n if (drifted === 0) {\n logger.success('No drift detected.');\n } else {\n logger.warn(\n `Drift: +${result.summary.added} -${result.summary.removed} ~${result.summary.modified}`,\n );\n process.exitCode = 1;\n }\n }\n\n if (opts.anomalies || opts.anomaliesOnly) {\n const report = driftAnomaly.detectAnomalies(result);\n if (report.totalAnomalies === 0) {\n logger.info('Anomaly scan: 0 findings.');\n } else {\n logger.warn(`Anomaly scan: ${report.totalAnomalies} finding(s):`);\n for (const a of report.anomalies) {\n logger.info(` [${a.severity}] ${a.category}: ${a.fqn} — ${a.reason}`);\n }\n const failOn = (opts.failOnAnomaly as string | undefined)?.toLowerCase();\n if (failOn && ['critical', 'high', 'medium', 'low'].includes(failOn)) {\n const failRank = driftAnomaly.anomalySeverityRank(failOn as driftAnomaly.AnomalySeverity);\n const triggered = report.anomalies.some(\n (a) => driftAnomaly.anomalySeverityRank(a.severity) <= failRank,\n );\n if (triggered) process.exitCode = 1;\n }\n }\n }\n await conn.disconnect();\n });\n attachRelatedOptions(cmd, [\n 'compare.ignoreCase',\n 'compare.ignoreComments',\n 'compare.ignoreFormattingDifferences',\n 'compare.excludeObjectTypes',\n ]);\n cmd.addCommand(driftWatchSubcommand());\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/drift-gate.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { CompareEngine, LiveSource } from "@sdt-tools/core/compare";
|
|
6
|
+
import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
|
|
7
|
+
function driftGateCommand() {
|
|
8
|
+
const cmd = new Command("drift-gate");
|
|
9
|
+
cmd.description(
|
|
10
|
+
"Refuse-on-drift CI gate across multiple replica Snowflake accounts. Compares replicas against a primary."
|
|
11
|
+
).requiredOption("--primary <profile>", "Primary account connection profile.").requiredOption("--replicas <list>", "Comma-separated replica connection profile names.").option("--database <database>", "Limit drift comparison to a single database.").option("--schema <schema>", "Limit drift comparison to a single schema (requires --database).").option(
|
|
12
|
+
"--threshold <n>",
|
|
13
|
+
"Max allowed differences per replica before the gate fails. Default 0.",
|
|
14
|
+
"0"
|
|
15
|
+
).option("--format <fmt>", "Output: table | json.", "table").action(async (opts) => {
|
|
16
|
+
const primaryProfile = await getProfile(String(opts.primary));
|
|
17
|
+
const replicaNames = String(opts.replicas).split(",").map((s) => s.trim()).filter(Boolean);
|
|
18
|
+
if (replicaNames.length === 0) {
|
|
19
|
+
throw new Error("At least one replica profile is required via --replicas.");
|
|
20
|
+
}
|
|
21
|
+
const threshold = Number.parseInt(String(opts.threshold), 10);
|
|
22
|
+
const scope = {
|
|
23
|
+
...opts.database ? { database: String(opts.database) } : {},
|
|
24
|
+
...opts.schema ? { schema: String(opts.schema) } : {}
|
|
25
|
+
};
|
|
26
|
+
const primaryConn = new SnowflakeConnection(primaryProfile);
|
|
27
|
+
const primarySource = new LiveSource(primaryConn, scope, `primary:${primaryProfile.account}`);
|
|
28
|
+
const results = [];
|
|
29
|
+
const engine = new CompareEngine();
|
|
30
|
+
try {
|
|
31
|
+
await primarySource.load();
|
|
32
|
+
for (const replicaName of replicaNames) {
|
|
33
|
+
const replicaProfile = await getProfile(replicaName);
|
|
34
|
+
const replicaConn = new SnowflakeConnection(replicaProfile);
|
|
35
|
+
const replicaSource = new LiveSource(
|
|
36
|
+
replicaConn,
|
|
37
|
+
scope,
|
|
38
|
+
`replica:${replicaProfile.account}`
|
|
39
|
+
);
|
|
40
|
+
try {
|
|
41
|
+
const result = await engine.compare(primarySource, replicaSource);
|
|
42
|
+
const { added, removed, modified } = result.summary;
|
|
43
|
+
results.push({
|
|
44
|
+
profile: replicaName,
|
|
45
|
+
account: replicaProfile.account,
|
|
46
|
+
added,
|
|
47
|
+
removed,
|
|
48
|
+
modified,
|
|
49
|
+
total: added + removed + modified
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
await replicaConn.disconnect();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
await primaryConn.disconnect();
|
|
57
|
+
}
|
|
58
|
+
if (opts.format === "json") {
|
|
59
|
+
console.log(
|
|
60
|
+
JSON.stringify(
|
|
61
|
+
{ primary: primaryProfile.account, threshold, replicas: results },
|
|
62
|
+
null,
|
|
63
|
+
2
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`Drift gate \u2014 primary ${primaryProfile.account}`);
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(
|
|
70
|
+
" REPLICA-PROFILE ACCOUNT ADD RM MOD TOTAL STATUS"
|
|
71
|
+
);
|
|
72
|
+
console.log(
|
|
73
|
+
" ---------------------- ------------------------------ --- -- --- ----- ------"
|
|
74
|
+
);
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
const status = r.total > threshold ? "DRIFT" : "OK";
|
|
77
|
+
console.log(
|
|
78
|
+
` ${r.profile.padEnd(22).slice(0, 22)} ${r.account.padEnd(30).slice(0, 30)} ${String(r.added).padStart(3)} ${String(r.removed).padStart(2)} ${String(r.modified).padStart(3)} ${String(r.total).padStart(5)} ${status}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const drifting = results.filter((r) => r.total > threshold);
|
|
83
|
+
if (drifting.length > 0) {
|
|
84
|
+
console.error("");
|
|
85
|
+
console.error(`Drift detected in ${drifting.length} replica(s) (threshold=${threshold}).`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return cmd;
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
driftGateCommand
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=drift-gate-V7QSIOGZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/drift-gate.ts"],"sourcesContent":["/**\n * `sdt drift-gate` — multi-region/replica drift gate.\n *\n * Compares the live schema in N replica accounts against a primary\n * account's live schema. Useful for orgs that replicate Snowflake\n * databases across regions (Snowflake replication, secondary databases)\n * and want a CI gate that fires the moment any replica drifts from the\n * primary.\n *\n * Mirrors `Databricks/packages/cli/src/commands/drift-gate.ts`.\n */\nimport { Command } from 'commander';\nimport { CompareEngine, LiveSource } from '@sdt-tools/core/compare';\nimport { getProfile, SnowflakeConnection } from '@sdt-tools/core/connection';\n\ninterface RegionResult {\n profile: string;\n account: string;\n added: number;\n removed: number;\n modified: number;\n total: number;\n}\n\nexport function driftGateCommand(): Command {\n const cmd = new Command('drift-gate');\n cmd\n .description(\n 'Refuse-on-drift CI gate across multiple replica Snowflake accounts. Compares replicas against a primary.',\n )\n .requiredOption('--primary <profile>', 'Primary account connection profile.')\n .requiredOption('--replicas <list>', 'Comma-separated replica connection profile names.')\n .option('--database <database>', 'Limit drift comparison to a single database.')\n .option('--schema <schema>', 'Limit drift comparison to a single schema (requires --database).')\n .option(\n '--threshold <n>',\n 'Max allowed differences per replica before the gate fails. Default 0.',\n '0',\n )\n .option('--format <fmt>', 'Output: table | json.', 'table')\n .action(async (opts) => {\n const primaryProfile = await getProfile(String(opts.primary));\n const replicaNames = String(opts.replicas)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (replicaNames.length === 0) {\n throw new Error('At least one replica profile is required via --replicas.');\n }\n const threshold = Number.parseInt(String(opts.threshold), 10);\n const scope = {\n ...(opts.database ? { database: String(opts.database) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n\n const primaryConn = new SnowflakeConnection(primaryProfile);\n const primarySource = new LiveSource(primaryConn, scope, `primary:${primaryProfile.account}`);\n\n const results: RegionResult[] = [];\n const engine = new CompareEngine();\n\n try {\n // Pre-load primary once so the source's lazy load happens here.\n await primarySource.load();\n\n for (const replicaName of replicaNames) {\n const replicaProfile = await getProfile(replicaName);\n const replicaConn = new SnowflakeConnection(replicaProfile);\n const replicaSource = new LiveSource(\n replicaConn,\n scope,\n `replica:${replicaProfile.account}`,\n );\n try {\n const result = await engine.compare(primarySource, replicaSource);\n const { added, removed, modified } = result.summary;\n results.push({\n profile: replicaName,\n account: replicaProfile.account,\n added,\n removed,\n modified,\n total: added + removed + modified,\n });\n } finally {\n await replicaConn.disconnect();\n }\n }\n } finally {\n await primaryConn.disconnect();\n }\n\n if (opts.format === 'json') {\n console.log(\n JSON.stringify(\n { primary: primaryProfile.account, threshold, replicas: results },\n null,\n 2,\n ),\n );\n } else {\n console.log(`Drift gate — primary ${primaryProfile.account}`);\n console.log('');\n console.log(\n ' REPLICA-PROFILE ACCOUNT ADD RM MOD TOTAL STATUS',\n );\n console.log(\n ' ---------------------- ------------------------------ --- -- --- ----- ------',\n );\n for (const r of results) {\n const status = r.total > threshold ? 'DRIFT' : 'OK';\n console.log(\n ` ${r.profile.padEnd(22).slice(0, 22)} ${r.account.padEnd(30).slice(0, 30)} ${String(r.added).padStart(3)} ${String(r.removed).padStart(2)} ${String(r.modified).padStart(3)} ${String(r.total).padStart(5)} ${status}`,\n );\n }\n }\n\n const drifting = results.filter((r) => r.total > threshold);\n if (drifting.length > 0) {\n console.error('');\n console.error(`Drift detected in ${drifting.length} replica(s) (threshold=${threshold}).`);\n process.exitCode = 1;\n }\n });\n return cmd;\n}\n"],"mappings":";;;AAWA,SAAS,eAAe;AACxB,SAAS,eAAe,kBAAkB;AAC1C,SAAS,YAAY,2BAA2B;AAWzC,SAAS,mBAA4B;AAC1C,QAAM,MAAM,IAAI,QAAQ,YAAY;AACpC,MACG;AAAA,IACC;AAAA,EACF,EACC,eAAe,uBAAuB,qCAAqC,EAC3E,eAAe,qBAAqB,mDAAmD,EACvF,OAAO,yBAAyB,8CAA8C,EAC9E,OAAO,qBAAqB,kEAAkE,EAC9F;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,OAAO,kBAAkB,yBAAyB,OAAO,EACzD,OAAO,OAAO,SAAS;AACtB,UAAM,iBAAiB,MAAM,WAAW,OAAO,KAAK,OAAO,CAAC;AAC5D,UAAM,eAAe,OAAO,KAAK,QAAQ,EACtC,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AACjB,QAAI,aAAa,WAAW,GAAG;AAC7B,YAAM,IAAI,MAAM,0DAA0D;AAAA,IAC5E;AACA,UAAM,YAAY,OAAO,SAAS,OAAO,KAAK,SAAS,GAAG,EAAE;AAC5D,UAAM,QAAQ;AAAA,MACZ,GAAI,KAAK,WAAW,EAAE,UAAU,OAAO,KAAK,QAAQ,EAAE,IAAI,CAAC;AAAA,MAC3D,GAAI,KAAK,SAAS,EAAE,QAAQ,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;AAAA,IACvD;AAEA,UAAM,cAAc,IAAI,oBAAoB,cAAc;AAC1D,UAAM,gBAAgB,IAAI,WAAW,aAAa,OAAO,WAAW,eAAe,OAAO,EAAE;AAE5F,UAAM,UAA0B,CAAC;AACjC,UAAM,SAAS,IAAI,cAAc;AAEjC,QAAI;AAEF,YAAM,cAAc,KAAK;AAEzB,iBAAW,eAAe,cAAc;AACtC,cAAM,iBAAiB,MAAM,WAAW,WAAW;AACnD,cAAM,cAAc,IAAI,oBAAoB,cAAc;AAC1D,cAAM,gBAAgB,IAAI;AAAA,UACxB;AAAA,UACA;AAAA,UACA,WAAW,eAAe,OAAO;AAAA,QACnC;AACA,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,QAAQ,eAAe,aAAa;AAChE,gBAAM,EAAE,OAAO,SAAS,SAAS,IAAI,OAAO;AAC5C,kBAAQ,KAAK;AAAA,YACX,SAAS;AAAA,YACT,SAAS,eAAe;AAAA,YACxB;AAAA,YACA;AAAA,YACA;AAAA,YACA,OAAO,QAAQ,UAAU;AAAA,UAC3B,CAAC;AAAA,QACH,UAAE;AACA,gBAAM,YAAY,WAAW;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,UAAE;AACA,YAAM,YAAY,WAAW;AAAA,IAC/B;AAEA,QAAI,KAAK,WAAW,QAAQ;AAC1B,cAAQ;AAAA,QACN,KAAK;AAAA,UACH,EAAE,SAAS,eAAe,SAAS,WAAW,UAAU,QAAQ;AAAA,UAChE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF,OAAO;AACL,cAAQ,IAAI,6BAAwB,eAAe,OAAO,EAAE;AAC5D,cAAQ,IAAI,EAAE;AACd,cAAQ;AAAA,QACN;AAAA,MACF;AACA,cAAQ;AAAA,QACN;AAAA,MACF;AACA,iBAAW,KAAK,SAAS;AACvB,cAAM,SAAS,EAAE,QAAQ,YAAY,UAAU;AAC/C,gBAAQ;AAAA,UACN,KAAK,EAAE,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,KAAK,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,KAAK,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,KAAK,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,KAAK,MAAM;AAAA,QAC9N;AAAA,MACF;AAAA,IACF;AAEA,UAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,EAAE,QAAQ,SAAS;AAC1D,QAAI,SAAS,SAAS,GAAG;AACvB,cAAQ,MAAM,EAAE;AAChB,cAAQ,MAAM,qBAAqB,SAAS,MAAM,0BAA0B,SAAS,IAAI;AACzF,cAAQ,WAAW;AAAA,IACrB;AAAA,EACF,CAAC;AACH,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/error-lookup.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { errorCatalog } from "@sdt-tools/core";
|
|
6
|
+
function errorLookupCommand() {
|
|
7
|
+
const cmd = new Command("error-lookup");
|
|
8
|
+
cmd.description("Look up a failure in the known-error catalog by code, fingerprint, or message.").option("--code <code>", "Adapter error code (e.g. OLS_LICENSE_EXPIRED).").option("--fingerprint <hex>", "16-char fingerprint from a prior failure.").option("--message <text>", "Free-text error message \u2014 falls back to regex match.").option("--list", "List every entry in the catalog (with code/key/cause).", false).option("--format <fmt>", "text | json. Default text.", "text").action(
|
|
9
|
+
(opts) => {
|
|
10
|
+
const fmt = (opts.format ?? "text").toLowerCase();
|
|
11
|
+
if (opts.list) {
|
|
12
|
+
const all = errorCatalog.DEFAULT_KNOWN_ERRORS;
|
|
13
|
+
if (fmt === "json") {
|
|
14
|
+
process.stdout.write(JSON.stringify(all, null, 2) + "\n");
|
|
15
|
+
} else {
|
|
16
|
+
for (const entry of all) {
|
|
17
|
+
const codeList = entry.codes?.join(", ") ?? "\u2014";
|
|
18
|
+
process.stdout.write(
|
|
19
|
+
`${entry.key}
|
|
20
|
+
codes: ${codeList}
|
|
21
|
+
cause: ${entry.causeSummary}
|
|
22
|
+
|
|
23
|
+
`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!opts.code && !opts.fingerprint && !opts.message) {
|
|
30
|
+
process.stderr.write("error-lookup: pass --code, --fingerprint, --message, or --list\n");
|
|
31
|
+
process.exitCode = 2;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const match = errorCatalog.lookupKnownError({
|
|
35
|
+
code: opts.code,
|
|
36
|
+
fingerprint: opts.fingerprint,
|
|
37
|
+
message: opts.message
|
|
38
|
+
});
|
|
39
|
+
if (!match) {
|
|
40
|
+
process.stderr.write("No catalog entry matched.\n");
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (fmt === "json") {
|
|
45
|
+
process.stdout.write(JSON.stringify(match, null, 2) + "\n");
|
|
46
|
+
} else {
|
|
47
|
+
process.stdout.write(errorCatalog.formatCatalogMatch(match) + "\n");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
return cmd;
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
errorLookupCommand
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=error-lookup-7ZWCZJ44.js.map
|