@ddt-tools/cli 0.2.0 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advise-tests-YNMKVJCD.js +87 -0
- package/dist/advise-tests-YNMKVJCD.js.map +1 -0
- package/dist/ai-NTNPYEKZ.js +86 -0
- package/dist/ai-NTNPYEKZ.js.map +1 -0
- package/dist/anonymize-LERTWUQO.js +139 -0
- package/dist/anonymize-LERTWUQO.js.map +1 -0
- package/dist/approval-GGZGKIU4.js +73 -0
- package/dist/approval-GGZGKIU4.js.map +1 -0
- package/dist/approval-chain-GWJKZHVU.js +118 -0
- package/dist/approval-chain-GWJKZHVU.js.map +1 -0
- package/dist/audit-log-2PH55BU4.js +159 -0
- package/dist/audit-log-2PH55BU4.js.map +1 -0
- package/dist/backlog-QNXGOUF4.js +76 -0
- package/dist/backlog-QNXGOUF4.js.map +1 -0
- package/dist/bisect-W3XKKRWG.js +111 -0
- package/dist/bisect-W3XKKRWG.js.map +1 -0
- package/dist/bookmarks-XVOGXGMC.js +107 -0
- package/dist/bookmarks-XVOGXGMC.js.map +1 -0
- package/dist/branch-S3I2IJGQ.js +103 -0
- package/dist/branch-S3I2IJGQ.js.map +1 -0
- package/dist/build-MP3JQEFO.js +20 -0
- package/dist/build-MP3JQEFO.js.map +1 -0
- package/dist/catalog-3J3NFNXP.js +137 -0
- package/dist/catalog-3J3NFNXP.js.map +1 -0
- package/dist/changelog-ZQAH3ULB.js +216 -0
- package/dist/changelog-ZQAH3ULB.js.map +1 -0
- package/dist/chunk-2FT6HXKS.js +55 -0
- package/dist/chunk-2FT6HXKS.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-DL3V7UJ2.js +25 -0
- package/dist/chunk-DL3V7UJ2.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-XFXG347C.js +40 -0
- package/dist/chunk-XFXG347C.js.map +1 -0
- package/dist/cli.js +504 -19402
- package/dist/cli.js.map +1 -1
- package/dist/compare-IOEATL6G.js +435 -0
- package/dist/compare-IOEATL6G.js.map +1 -0
- package/dist/compare-profiles-H33CXZPD.js +219 -0
- package/dist/compare-profiles-H33CXZPD.js.map +1 -0
- package/dist/completion-ZSNCQKJ2.js +89 -0
- package/dist/completion-ZSNCQKJ2.js.map +1 -0
- package/dist/connection-CDGVEFUC.js +148 -0
- package/dist/connection-CDGVEFUC.js.map +1 -0
- package/dist/cost-estimate-S2MKHT2H.js +321 -0
- package/dist/cost-estimate-S2MKHT2H.js.map +1 -0
- package/dist/data-compare-46ZI7KHL.js +128 -0
- package/dist/data-compare-46ZI7KHL.js.map +1 -0
- package/dist/data-fit-WGEPLD5S.js +127 -0
- package/dist/data-fit-WGEPLD5S.js.map +1 -0
- package/dist/deploy-status-4H5KJFRC.js +58 -0
- package/dist/deploy-status-4H5KJFRC.js.map +1 -0
- package/dist/design-ILX3ZSWW.js +135 -0
- package/dist/design-ILX3ZSWW.js.map +1 -0
- package/dist/diagnose-WPUL67E4.js +150 -0
- package/dist/diagnose-WPUL67E4.js.map +1 -0
- package/dist/discover-DEO2R5T6.js +78 -0
- package/dist/discover-DEO2R5T6.js.map +1 -0
- package/dist/docs-QNY3MUVO.js +183 -0
- package/dist/docs-QNY3MUVO.js.map +1 -0
- package/dist/drift-FDRNPWQA.js +233 -0
- package/dist/drift-FDRNPWQA.js.map +1 -0
- package/dist/drift-gate-6BWWWMHW.js +103 -0
- package/dist/drift-gate-6BWWWMHW.js.map +1 -0
- package/dist/error-lookup-4R3Y4RBC.js +56 -0
- package/dist/error-lookup-4R3Y4RBC.js.map +1 -0
- package/dist/errorReporting-LX6WT4JH.js +109 -0
- package/dist/errorReporting-LX6WT4JH.js.map +1 -0
- package/dist/exec-JOLH5LPT.js +122 -0
- package/dist/exec-JOLH5LPT.js.map +1 -0
- package/dist/explain-NS26WE2Y.js +189 -0
- package/dist/explain-NS26WE2Y.js.map +1 -0
- package/dist/explorer-GSYYYOAL.js +58 -0
- package/dist/explorer-GSYYYOAL.js.map +1 -0
- package/dist/extract-4LWEZG4O.js +152 -0
- package/dist/extract-4LWEZG4O.js.map +1 -0
- package/dist/features-KQV4OFIZ.js +54 -0
- package/dist/features-KQV4OFIZ.js.map +1 -0
- package/dist/feedback-CBLGXUEG.js +158 -0
- package/dist/feedback-CBLGXUEG.js.map +1 -0
- package/dist/find-SMXRCZ76.js +176 -0
- package/dist/find-SMXRCZ76.js.map +1 -0
- package/dist/format-HMGG6MY3.js +277 -0
- package/dist/format-HMGG6MY3.js.map +1 -0
- package/dist/generate-W7VLBDLI.js +160 -0
- package/dist/generate-W7VLBDLI.js.map +1 -0
- package/dist/graph-YYL5UYCJ.js +168 -0
- package/dist/graph-YYL5UYCJ.js.map +1 -0
- package/dist/history-GDRFP4PG.js +184 -0
- package/dist/history-GDRFP4PG.js.map +1 -0
- package/dist/hosts-DRFZTMIJ.js +45 -0
- package/dist/hosts-DRFZTMIJ.js.map +1 -0
- package/dist/impact-A4NU6CB2.js +63 -0
- package/dist/impact-A4NU6CB2.js.map +1 -0
- package/dist/import-EGOVKTLX.js +29 -0
- package/dist/import-EGOVKTLX.js.map +1 -0
- package/dist/import-script-R5RXPDH6.js +79 -0
- package/dist/import-script-R5RXPDH6.js.map +1 -0
- package/dist/index.cjs +11 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/init-EAOGNGXI.js +54 -0
- package/dist/init-EAOGNGXI.js.map +1 -0
- package/dist/install-hooks-G3Y5LVXK.js +109 -0
- package/dist/install-hooks-G3Y5LVXK.js.map +1 -0
- package/dist/license-Z5YSC7XQ.js +43 -0
- package/dist/license-Z5YSC7XQ.js.map +1 -0
- package/dist/lineage-C5CGVP36.js +555 -0
- package/dist/lineage-C5CGVP36.js.map +1 -0
- package/dist/lint-AQFPZ3WG.js +144 -0
- package/dist/lint-AQFPZ3WG.js.map +1 -0
- package/dist/mcp-6ZXOAF7S.js +343 -0
- package/dist/mcp-6ZXOAF7S.js.map +1 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js +156 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js.map +1 -0
- package/dist/migrate-platform-E7VZFPO5.js +91 -0
- package/dist/migrate-platform-E7VZFPO5.js.map +1 -0
- package/dist/optimize-WUJ5ZN5Y.js +109 -0
- package/dist/optimize-WUJ5ZN5Y.js.map +1 -0
- package/dist/perf-UULZSREY.js +200 -0
- package/dist/perf-UULZSREY.js.map +1 -0
- package/dist/pii-QHU32VML.js +146 -0
- package/dist/pii-QHU32VML.js.map +1 -0
- package/dist/pilot-BR6GVK32.js +29 -0
- package/dist/pilot-BR6GVK32.js.map +1 -0
- package/dist/pr-comment-2FOA3EXG.js +81 -0
- package/dist/pr-comment-2FOA3EXG.js.map +1 -0
- package/dist/preview-XNY422OU.js +46 -0
- package/dist/preview-XNY422OU.js.map +1 -0
- package/dist/profile-SQTBNKYS.js +98 -0
- package/dist/profile-SQTBNKYS.js.map +1 -0
- package/dist/promote-FSGUPIPD.js +417 -0
- package/dist/promote-FSGUPIPD.js.map +1 -0
- package/dist/publish-HLP3XHM5.js +766 -0
- package/dist/publish-HLP3XHM5.js.map +1 -0
- package/dist/purge-Y5IOTXKA.js +56 -0
- package/dist/purge-Y5IOTXKA.js.map +1 -0
- package/dist/query-log-SDDGMJLJ.js +112 -0
- package/dist/query-log-SDDGMJLJ.js.map +1 -0
- package/dist/refactor-TC7S43F2.js +5809 -0
- package/dist/refactor-TC7S43F2.js.map +1 -0
- package/dist/refresh-MDJYOYV5.js +39 -0
- package/dist/refresh-MDJYOYV5.js.map +1 -0
- package/dist/replay-E4664A5K.js +118 -0
- package/dist/replay-E4664A5K.js.map +1 -0
- package/dist/revert-QWQWCJJB.js +111 -0
- package/dist/revert-QWQWCJJB.js.map +1 -0
- package/dist/review-7CAVLD67.js +164 -0
- package/dist/review-7CAVLD67.js.map +1 -0
- package/dist/rollback-suggest-C6D5YFCA.js +79 -0
- package/dist/rollback-suggest-C6D5YFCA.js.map +1 -0
- package/dist/safer-alternative-QR4QEFUV.js +84 -0
- package/dist/safer-alternative-QR4QEFUV.js.map +1 -0
- package/dist/safety-OFWUFLK4.js +165 -0
- package/dist/safety-OFWUFLK4.js.map +1 -0
- package/dist/savings-MEBE4TXI.js +95 -0
- package/dist/savings-MEBE4TXI.js.map +1 -0
- package/dist/scan-secrets-XCUBMLHL.js +54 -0
- package/dist/scan-secrets-XCUBMLHL.js.map +1 -0
- package/dist/schema-7JZIG6QR.js +447 -0
- package/dist/schema-7JZIG6QR.js.map +1 -0
- package/dist/script-BMYVBHFR.js +167 -0
- package/dist/script-BMYVBHFR.js.map +1 -0
- package/dist/search-TA3C3AZT.js +151 -0
- package/dist/search-TA3C3AZT.js.map +1 -0
- package/dist/seed-W4Q3L2IU.js +101 -0
- package/dist/seed-W4Q3L2IU.js.map +1 -0
- package/dist/sketch-6B2V6FJV.js +83 -0
- package/dist/sketch-6B2V6FJV.js.map +1 -0
- package/dist/snapshot-YMVS322L.js +171 -0
- package/dist/snapshot-YMVS322L.js.map +1 -0
- package/dist/snippets-EVTN63OU.js +74 -0
- package/dist/snippets-EVTN63OU.js.map +1 -0
- package/dist/standards-FGJW3CQL.js +238 -0
- package/dist/standards-FGJW3CQL.js.map +1 -0
- package/dist/suggest-V3LVIFZ5.js +44 -0
- package/dist/suggest-V3LVIFZ5.js.map +1 -0
- package/dist/suggest-constraints-EX2FCWOQ.js +154 -0
- package/dist/suggest-constraints-EX2FCWOQ.js.map +1 -0
- package/dist/suite-YTQ3CNX5.js +85 -0
- package/dist/suite-YTQ3CNX5.js.map +1 -0
- package/dist/telemetry-KOIY3NEQ.js +90 -0
- package/dist/telemetry-KOIY3NEQ.js.map +1 -0
- package/dist/template-MUJ6X6LN.js +396 -0
- package/dist/template-MUJ6X6LN.js.map +1 -0
- package/dist/test-XFSQHR2S.js +169 -0
- package/dist/test-XFSQHR2S.js.map +1 -0
- package/dist/trial-GFTGYCR3.js +31 -0
- package/dist/trial-GFTGYCR3.js.map +1 -0
- package/dist/validate-LFDEZFFH.js +107 -0
- package/dist/validate-LFDEZFFH.js.map +1 -0
- package/dist/verify-KRDYOJCR.js +76 -0
- package/dist/verify-KRDYOJCR.js.map +1 -0
- package/dist/watch-FSG23RR3.js +80 -0
- package/dist/watch-FSG23RR3.js.map +1 -0
- package/dist/xcompare-U4TXTTIR.js +87 -0
- package/dist/xcompare-U4TXTTIR.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19298
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
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 {
|
|
9
|
+
loadProject,
|
|
10
|
+
parseProjectModel,
|
|
11
|
+
renderDocsReport,
|
|
12
|
+
renderErdMarkdown,
|
|
13
|
+
renderErd3dHtml,
|
|
14
|
+
pac,
|
|
15
|
+
ai,
|
|
16
|
+
comments
|
|
17
|
+
} from "@ddt-tools/core";
|
|
18
|
+
import { buildDocsPublishPlan, renderDocsWorkflowYaml, searchDocs } from "@ddt-tools/core/docs";
|
|
19
|
+
function docsCommand() {
|
|
20
|
+
const cmd = new Command("docs");
|
|
21
|
+
cmd.description("Generate HTML schema docs from a .ddtproj or .ddtpac.").requiredOption("--source <path>", ".ddtproj or .ddtpac to document.").option("-o, --out <path>", "Output HTML file path (omit when using --augment-comments).").option("--title <text>", "Page title.", "Schema docs").option(
|
|
22
|
+
"--augment-comments",
|
|
23
|
+
"Skip HTML rendering. Instead, call the configured AI provider to suggest comments for every empty COMMENT slot and emit COMMENT ON / ALTER COLUMN SQL.",
|
|
24
|
+
false
|
|
25
|
+
).option(
|
|
26
|
+
"--augment-out <path>",
|
|
27
|
+
"When --augment-comments is set, write the script here. Default ./augment-comments.sql."
|
|
28
|
+
).option(
|
|
29
|
+
"--augment-max-calls <n>",
|
|
30
|
+
"When --augment-comments is set, cap AI calls. Default unbounded.",
|
|
31
|
+
(v) => parseInt(v, 10)
|
|
32
|
+
).option(
|
|
33
|
+
"--augment-max-length <n>",
|
|
34
|
+
"When --augment-comments is set, cap suggestion length in characters. Default 200.",
|
|
35
|
+
(v) => parseInt(v, 10)
|
|
36
|
+
).action(
|
|
37
|
+
async (opts) => {
|
|
38
|
+
const model = await loadModel(String(opts.source));
|
|
39
|
+
if (opts.augmentComments) {
|
|
40
|
+
const targets = comments.findMissingComments(model);
|
|
41
|
+
if (targets.length === 0) {
|
|
42
|
+
console.log("No missing COMMENT slots found. Nothing to augment.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(`Found ${targets.length} missing comment slot(s). Calling AI provider\u2026`);
|
|
46
|
+
const suggestions = await comments.augmentComments(targets, {
|
|
47
|
+
completeFn: async (prompt) => {
|
|
48
|
+
const r = await ai.complete([{ role: "user", content: prompt }], {
|
|
49
|
+
feature: "docs.augment-comments"
|
|
50
|
+
});
|
|
51
|
+
return r.text;
|
|
52
|
+
},
|
|
53
|
+
...opts.augmentMaxLength ? { maxCommentLength: opts.augmentMaxLength } : {},
|
|
54
|
+
...opts.augmentMaxCalls ? { maxCalls: opts.augmentMaxCalls } : {}
|
|
55
|
+
});
|
|
56
|
+
const sql = comments.renderAlterCommentScript(suggestions);
|
|
57
|
+
const accepted = suggestions.filter((s) => s.suggestion.trim().length > 0).length;
|
|
58
|
+
const uncertain = suggestions.length - accepted;
|
|
59
|
+
const outPath2 = opts.augmentOut ? path.resolve(String(opts.augmentOut)) : path.resolve("augment-comments.sql");
|
|
60
|
+
await fs.mkdir(path.dirname(outPath2), { recursive: true });
|
|
61
|
+
await fs.writeFile(outPath2, sql, "utf8");
|
|
62
|
+
console.log(
|
|
63
|
+
`Wrote ${outPath2} (${accepted} suggestion(s), ${uncertain} flagged for review).`
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!opts.out) {
|
|
68
|
+
throw new Error("Missing --out. Required unless --augment-comments is set.");
|
|
69
|
+
}
|
|
70
|
+
const html = renderDocsReport(model, { title: String(opts.title ?? "Schema docs") });
|
|
71
|
+
const outPath = path.resolve(String(opts.out));
|
|
72
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
73
|
+
await fs.writeFile(outPath, html, "utf8");
|
|
74
|
+
console.log(`Wrote ${outPath} (${html.length} bytes, ${model.length} objects).`);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
cmd.command("build").description(
|
|
78
|
+
"Build the HTML schema docs site (alias for the parent command with --out default)."
|
|
79
|
+
).requiredOption("--source <path>", ".ddtproj or .ddtpac to document.").option("-o, --out <path>", "Output HTML file path.", "dist/docs/index.html").option("--title <text>", "Page title.", "Schema docs").action(async (opts) => {
|
|
80
|
+
const model = await loadModel(String(opts.source));
|
|
81
|
+
const html = renderDocsReport(model, { title: String(opts.title ?? "Schema docs") });
|
|
82
|
+
const outPath = path.resolve(String(opts.out));
|
|
83
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
84
|
+
await fs.writeFile(outPath, html, "utf8");
|
|
85
|
+
console.log(`Wrote ${outPath} (${html.length} bytes, ${model.length} objects).`);
|
|
86
|
+
});
|
|
87
|
+
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) => {
|
|
88
|
+
if (opts.emitWorkflow) {
|
|
89
|
+
const yaml = renderDocsWorkflowYaml({ branch: opts.branch, trigger: "main" });
|
|
90
|
+
const wPath = path.resolve(String(opts.emitWorkflow));
|
|
91
|
+
await fs.mkdir(path.dirname(wPath), { recursive: true });
|
|
92
|
+
await fs.writeFile(wPath, yaml, "utf8");
|
|
93
|
+
console.log(`Wrote workflow template \u2192 ${wPath}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const plan = buildDocsPublishPlan({
|
|
97
|
+
siteDir: path.resolve(String(opts.site)),
|
|
98
|
+
branch: opts.branch,
|
|
99
|
+
remote: opts.remote,
|
|
100
|
+
message: opts.message
|
|
101
|
+
});
|
|
102
|
+
for (const step of plan.steps) {
|
|
103
|
+
console.log(` ${step.description}`);
|
|
104
|
+
if (opts.dryRun) {
|
|
105
|
+
console.log(` [dry-run] ${step.command}`);
|
|
106
|
+
} else {
|
|
107
|
+
execSync(step.command, { stdio: "inherit" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!opts.dryRun) console.log(`Done \u2014 site pushed to ${plan.remote}/${plan.branch}.`);
|
|
111
|
+
});
|
|
112
|
+
cmd.command("search").description("Search object names, comments, and column names in a .ddtproj or .ddtpac.").argument("<query>", "Search query string.").requiredOption("--source <path>", ".ddtproj or .ddtpac to search.").option("-n, --max-results <n>", "Maximum number of results to return.", (v) => parseInt(v, 10)).action(async (query, opts) => {
|
|
113
|
+
const model = await loadModel(String(opts.source));
|
|
114
|
+
const hits = searchDocs(model, query, {
|
|
115
|
+
maxResults: opts.maxResults
|
|
116
|
+
});
|
|
117
|
+
if (hits.length === 0) {
|
|
118
|
+
console.log(`No results for "${query}".`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const hit of hits) {
|
|
122
|
+
console.log(`[${hit.score}] ${hit.objectFqn} (${hit.objectType}) \u2014 ${hit.field}`);
|
|
123
|
+
console.log(` ${hit.snippet}`);
|
|
124
|
+
}
|
|
125
|
+
console.log(`
|
|
126
|
+
${hits.length} result(s) for "${query}".`);
|
|
127
|
+
});
|
|
128
|
+
return cmd;
|
|
129
|
+
}
|
|
130
|
+
function erdCommand() {
|
|
131
|
+
const cmd = new Command("erd");
|
|
132
|
+
cmd.description(
|
|
133
|
+
"Generate an ER diagram (Mermaid Markdown or interactive 3D HTML) from a .ddtproj or .ddtpac."
|
|
134
|
+
).requiredOption("--source <path>", ".ddtproj or .ddtpac to diagram.").requiredOption(
|
|
135
|
+
"-o, --out <path>",
|
|
136
|
+
"Output file path. Format inferred from extension (.md \u2192 mermaid, .html \u2192 html3d) unless --format is given."
|
|
137
|
+
).option("--title <text>", "Diagram title.", "Schema ER diagram").option(
|
|
138
|
+
"--format <fmt>",
|
|
139
|
+
"Output format: mermaid | html3d. Default: inferred from --out extension, falling back to mermaid."
|
|
140
|
+
).action(async (opts) => {
|
|
141
|
+
const model = await loadModel(String(opts.source));
|
|
142
|
+
const title = String(opts.title ?? "Schema ER diagram");
|
|
143
|
+
const fmt = resolveErdFormat(String(opts.out), opts.format);
|
|
144
|
+
let content;
|
|
145
|
+
if (fmt === "html3d") {
|
|
146
|
+
content = renderErd3dHtml(model, { title });
|
|
147
|
+
} else {
|
|
148
|
+
content = renderErdMarkdown(model, title);
|
|
149
|
+
}
|
|
150
|
+
const outPath = path.resolve(String(opts.out));
|
|
151
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
152
|
+
await fs.writeFile(outPath, content, "utf8");
|
|
153
|
+
console.log(
|
|
154
|
+
`Wrote ${outPath} (${content.length} bytes, ${model.length} objects, format=${fmt}).`
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
return cmd;
|
|
158
|
+
}
|
|
159
|
+
function resolveErdFormat(outPath, explicit) {
|
|
160
|
+
if (explicit) {
|
|
161
|
+
const e = explicit.toLowerCase();
|
|
162
|
+
if (e === "mermaid" || e === "md") return "mermaid";
|
|
163
|
+
if (e === "html3d" || e === "3d" || e === "html") return "html3d";
|
|
164
|
+
throw new Error(`Unknown --format "${explicit}". Expected: mermaid | html3d.`);
|
|
165
|
+
}
|
|
166
|
+
const ext = outPath.toLowerCase();
|
|
167
|
+
if (ext.endsWith(".html") || ext.endsWith(".htm")) return "html3d";
|
|
168
|
+
return "mermaid";
|
|
169
|
+
}
|
|
170
|
+
async function loadModel(sourcePath) {
|
|
171
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
172
|
+
const pacContents = await pac.readPac(sourcePath);
|
|
173
|
+
return pacContents.model;
|
|
174
|
+
}
|
|
175
|
+
const loaded = await loadProject(sourcePath);
|
|
176
|
+
const parsed = await parseProjectModel(loaded);
|
|
177
|
+
return parsed;
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
docsCommand,
|
|
181
|
+
erdCommand
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=docs-QNY3MUVO.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 {\n loadProject,\n parseProjectModel,\n renderDocsReport,\n renderErdMarkdown,\n renderErd3dHtml,\n pac,\n ai,\n comments,\n type DatabricksObject,\n} from '@ddt-tools/core';\nimport { buildDocsPublishPlan, renderDocsWorkflowYaml, searchDocs } from '@ddt-tools/core/docs';\n\n/**\n * `ddt docs` — auto-generate HTML schema docs from a `.ddtproj` or\n * `.ddtpac`. dbt-docs-style: catalog → 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 .ddtproj or .ddtpac.')\n .requiredOption('--source <path>', '.ddtproj or .ddtpac 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 and emit COMMENT ON / ALTER COLUMN SQL.',\n false,\n )\n .option(\n '--augment-out <path>',\n 'When --augment-comments is set, write the 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>', '.ddtproj or .ddtpac 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 .ddtproj or .ddtpac.')\n .argument('<query>', 'Search query string.')\n .requiredOption('--source <path>', '.ddtproj or .ddtpac 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 * `ddt erd` — auto-generate an ER diagram from a `.ddtproj` or `.ddtpac`.\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 .ddtproj or .ddtpac.',\n )\n .requiredOption('--source <path>', '.ddtproj or .ddtpac 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<DatabricksObject[]> {\n if (sourcePath.endsWith('.ddtpac')) {\n const pacContents = await pac.readPac(sourcePath);\n return pacContents.model;\n }\n const loaded = await loadProject(sourcePath);\n const parsed = await parseProjectModel(loaded);\n return parsed as DatabricksObject[];\n}\n"],"mappings":"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","names":["outPath"]}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachRelatedOptions
|
|
3
|
+
} from "./chunk-DL3V7UJ2.js";
|
|
4
|
+
import {
|
|
5
|
+
addMappingFlags,
|
|
6
|
+
buildMappingFromOptions
|
|
7
|
+
} from "./chunk-2FT6HXKS.js";
|
|
8
|
+
import "./chunk-DGUM43GV.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/drift.ts
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
import {
|
|
13
|
+
CompareEngine,
|
|
14
|
+
DbtManifestSource,
|
|
15
|
+
InMemorySource,
|
|
16
|
+
ProjectSource,
|
|
17
|
+
AccountExtractor,
|
|
18
|
+
createConnection,
|
|
19
|
+
defaultExtractors,
|
|
20
|
+
driftAnomaly,
|
|
21
|
+
getProfile
|
|
22
|
+
} from "@ddt-tools/core";
|
|
23
|
+
function driftWatchSubcommand() {
|
|
24
|
+
const sub = new Command("watch");
|
|
25
|
+
sub.description(
|
|
26
|
+
"Poll a project against the live Unity Catalog on a fixed interval. Prints DRIFT_DETECTED events to stdout (or POSTs to --webhook) when the catalog diverges from the project."
|
|
27
|
+
).requiredOption("--source <path>", "Path to .ddtproj or .ddtpac").requiredOption("--connection <name>", "Connection profile name").option("--catalog <catalog>", "Limit drift check to a single catalog.").option("--schema <schema>", "Limit drift check to a single schema (requires --catalog).").option("--interval <seconds>", "Poll interval in seconds (min 5).", "60").option(
|
|
28
|
+
"--webhook <url>",
|
|
29
|
+
"POST drift events as JSON to this URL (Slack/Teams/generic receiver)."
|
|
30
|
+
).option("--format <fmt>", "Output format: text | json.", "text").option(
|
|
31
|
+
"--quiet",
|
|
32
|
+
"Suppress CLEAN status lines; emit only DRIFT_DETECTED and error events.",
|
|
33
|
+
false
|
|
34
|
+
).action(async (opts) => {
|
|
35
|
+
const intervalSecs = Math.max(5, parseInt(String(opts.interval), 10) || 60);
|
|
36
|
+
const format = String(opts.format) === "json" ? "json" : "text";
|
|
37
|
+
const webhookUrl = opts.webhook ? String(opts.webhook) : void 0;
|
|
38
|
+
const quiet = !!opts.quiet;
|
|
39
|
+
const sourcePath = String(opts.source);
|
|
40
|
+
const scope = {
|
|
41
|
+
...opts.catalog ? { catalog: String(opts.catalog) } : {},
|
|
42
|
+
...opts.schema ? { schema: String(opts.schema) } : {}
|
|
43
|
+
};
|
|
44
|
+
const profile = await getProfile(String(opts.connection));
|
|
45
|
+
const conn = createConnection(profile);
|
|
46
|
+
await conn.connect();
|
|
47
|
+
const src = sourcePath.endsWith(".ddtpac") ? await (async () => {
|
|
48
|
+
const { PacSource } = await import("@ddt-tools/core");
|
|
49
|
+
return new PacSource(sourcePath, "source");
|
|
50
|
+
})() : new ProjectSource(sourcePath, "source");
|
|
51
|
+
if (!quiet && format === "text") {
|
|
52
|
+
console.log(
|
|
53
|
+
`drift watch: polling every ${intervalSecs}s \u2014 ${sourcePath} \u2192 ${profile.auth.host}`
|
|
54
|
+
);
|
|
55
|
+
console.log("Press Ctrl+C to stop.");
|
|
56
|
+
}
|
|
57
|
+
const postToWebhook = async (event) => {
|
|
58
|
+
if (!webhookUrl) return;
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(webhookUrl, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(event)
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok && format === "text") console.warn(`Webhook POST failed: HTTP ${res.status}`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (format === "text")
|
|
68
|
+
console.warn(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const pollOnce = async () => {
|
|
72
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
73
|
+
try {
|
|
74
|
+
const accountExtractor = new AccountExtractor(defaultExtractors());
|
|
75
|
+
const liveObjs = await accountExtractor.extract(conn, scope);
|
|
76
|
+
const live = new InMemorySource(liveObjs, { kind: "live", label: profile.auth.host });
|
|
77
|
+
const engine = new CompareEngine();
|
|
78
|
+
const result = await engine.compare(src, live);
|
|
79
|
+
const s = result.summary;
|
|
80
|
+
const drifted = s.added + s.removed + s.modified;
|
|
81
|
+
if (drifted > 0) {
|
|
82
|
+
const event = {
|
|
83
|
+
type: "DRIFT_DETECTED",
|
|
84
|
+
timestamp: ts,
|
|
85
|
+
source: sourcePath,
|
|
86
|
+
added: s.added,
|
|
87
|
+
removed: s.removed,
|
|
88
|
+
modified: s.modified
|
|
89
|
+
};
|
|
90
|
+
if (format === "json") process.stdout.write(JSON.stringify(event) + "\n");
|
|
91
|
+
else console.warn(`[${ts}] DRIFT_DETECTED +${s.added} -${s.removed} ~${s.modified}`);
|
|
92
|
+
await postToWebhook(event);
|
|
93
|
+
} else if (!quiet) {
|
|
94
|
+
if (format === "json")
|
|
95
|
+
process.stdout.write(
|
|
96
|
+
JSON.stringify({ type: "CLEAN", timestamp: ts, source: sourcePath }) + "\n"
|
|
97
|
+
);
|
|
98
|
+
else console.log(`[${ts}] clean`);
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
if (format === "json")
|
|
103
|
+
process.stdout.write(
|
|
104
|
+
JSON.stringify({ type: "POLL_ERROR", timestamp: ts, error: message }) + "\n"
|
|
105
|
+
);
|
|
106
|
+
else console.error(`[${ts}] poll error: ${message}`);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
await pollOnce();
|
|
110
|
+
const timer = setInterval(() => {
|
|
111
|
+
void pollOnce();
|
|
112
|
+
}, intervalSecs * 1e3);
|
|
113
|
+
process.once("SIGINT", () => {
|
|
114
|
+
clearInterval(timer);
|
|
115
|
+
void conn.disconnect().then(() => process.exit(0));
|
|
116
|
+
});
|
|
117
|
+
await new Promise(() => {
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return sub;
|
|
121
|
+
}
|
|
122
|
+
function driftCommand() {
|
|
123
|
+
const cmd = new Command("drift");
|
|
124
|
+
cmd.description(
|
|
125
|
+
"Refuse with non-zero exit when the live target has drifted from --source or a dbt manifest."
|
|
126
|
+
).option("--source <path>", "Source: .ddtproj or .ddtpac (required unless --vs-dbt-project).").option(
|
|
127
|
+
"--vs-dbt-project <path>",
|
|
128
|
+
"Compare a compiled dbt project (or target/manifest.json) against the live target. Run `dbt compile` first. When set, --source is not required."
|
|
129
|
+
).requiredOption("--connection <name>", "Connection profile to extract live state from.").option("--catalog <catalog>", "Limit drift check to a single catalog.").option("--schema <schema>", "Limit drift check to a single schema (requires --catalog).").option("--ignore-case", "Compare object FQNs case-insensitively.", false).option(
|
|
130
|
+
"--allow-extras",
|
|
131
|
+
"Treat objects that exist on the target but not in source as OK (no drift).",
|
|
132
|
+
false
|
|
133
|
+
).option(
|
|
134
|
+
"--anomalies",
|
|
135
|
+
"Also classify drift via the anomaly detector (new grants to `account users`, bypass-principal grants, owner changes, audit-column drops, mask removals, new bypass-named groups).",
|
|
136
|
+
false
|
|
137
|
+
).option(
|
|
138
|
+
"--anomalies-only",
|
|
139
|
+
"Skip the simple drift summary and emit only the anomaly report. Implies --anomalies.",
|
|
140
|
+
false
|
|
141
|
+
).option(
|
|
142
|
+
"--fail-on-anomaly <severity>",
|
|
143
|
+
"Exit non-zero when any anomaly at or above this severity fires. Values: critical | high | medium | low. Default: drift presence alone determines exit code."
|
|
144
|
+
);
|
|
145
|
+
addMappingFlags(cmd);
|
|
146
|
+
cmd.action(async (opts) => {
|
|
147
|
+
if (!opts.source && !opts.vsDBtProject) {
|
|
148
|
+
console.error("Provide either --source <path> or --vs-dbt-project <path>.");
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const nameMapping = await buildMappingFromOptions(opts);
|
|
153
|
+
const profile = await getProfile(String(opts.connection));
|
|
154
|
+
const conn = createConnection(profile);
|
|
155
|
+
try {
|
|
156
|
+
await conn.connect();
|
|
157
|
+
const accountExtractor = new AccountExtractor(defaultExtractors());
|
|
158
|
+
const scope = {
|
|
159
|
+
...opts.catalog ? { catalog: String(opts.catalog) } : {},
|
|
160
|
+
...opts.schema ? { schema: String(opts.schema) } : {}
|
|
161
|
+
};
|
|
162
|
+
const liveObjs = await accountExtractor.extract(conn, scope);
|
|
163
|
+
const live = new InMemorySource(liveObjs, { kind: "live", label: profile.auth.host });
|
|
164
|
+
const src = opts.vsDBtProject ? new DbtManifestSource(String(opts.vsDBtProject), `dbt:${opts.vsDBtProject}`) : String(opts.source).endsWith(".ddtpac") ? (
|
|
165
|
+
// PacSource via inline import to keep this command self-contained.
|
|
166
|
+
await (async () => {
|
|
167
|
+
const { PacSource } = await import("@ddt-tools/core");
|
|
168
|
+
return new PacSource(String(opts.source), "source");
|
|
169
|
+
})()
|
|
170
|
+
) : new ProjectSource(String(opts.source), "source");
|
|
171
|
+
const engine = new CompareEngine();
|
|
172
|
+
const result = await engine.compare(src, live, {
|
|
173
|
+
ignoreCase: !!opts.ignoreCase,
|
|
174
|
+
...nameMapping ? { nameMapping } : {}
|
|
175
|
+
});
|
|
176
|
+
const s = result.summary;
|
|
177
|
+
const drifted = s.added + s.modified + (opts.allowExtras ? 0 : s.removed);
|
|
178
|
+
if (!opts.anomaliesOnly) {
|
|
179
|
+
console.log(`Source: ${result.source.kind}:${result.source.label}`);
|
|
180
|
+
console.log(`Target: ${result.target.kind}:${result.target.label}`);
|
|
181
|
+
console.log(`Summary: +${s.added} -${s.removed} ~${s.modified} =${s.unchanged}`);
|
|
182
|
+
console.log(`Drifted: ${drifted}` + (opts.allowExtras ? " (extras allowed)" : ""));
|
|
183
|
+
if (drifted > 0) {
|
|
184
|
+
console.log("");
|
|
185
|
+
console.log("Drift detected:");
|
|
186
|
+
for (const o of result.objects) {
|
|
187
|
+
if (o.kind === "unchanged") continue;
|
|
188
|
+
if (o.kind === "removed" && opts.allowExtras) continue;
|
|
189
|
+
const glyph = o.kind === "added" ? "+ source-only" : o.kind === "removed" ? "- target-only" : "~ modified";
|
|
190
|
+
console.log(` ${glyph} ${o.identity.objectType} ${o.identity.fqn}`);
|
|
191
|
+
}
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (opts.anomalies || opts.anomaliesOnly) {
|
|
196
|
+
const report = driftAnomaly.detectAnomalies(result);
|
|
197
|
+
if (report.totalAnomalies === 0) {
|
|
198
|
+
console.log("Anomaly scan: 0 findings.");
|
|
199
|
+
} else {
|
|
200
|
+
console.log("");
|
|
201
|
+
console.log(`Anomaly scan: ${report.totalAnomalies} finding(s):`);
|
|
202
|
+
for (const a of report.anomalies) {
|
|
203
|
+
console.log(` [${a.severity}] ${a.category}: ${a.fqn} \u2014 ${a.reason}`);
|
|
204
|
+
}
|
|
205
|
+
const failOn = opts.failOnAnomaly?.toLowerCase();
|
|
206
|
+
if (failOn && ["critical", "high", "medium", "low"].includes(failOn)) {
|
|
207
|
+
const failRank = driftAnomaly.anomalySeverityRank(
|
|
208
|
+
failOn
|
|
209
|
+
);
|
|
210
|
+
const triggered = report.anomalies.some(
|
|
211
|
+
(a) => driftAnomaly.anomalySeverityRank(a.severity) <= failRank
|
|
212
|
+
);
|
|
213
|
+
if (triggered) process.exitCode = 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
await conn.disconnect();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
attachRelatedOptions(cmd, [
|
|
222
|
+
"compare.ignoreCase",
|
|
223
|
+
"compare.ignoreComments",
|
|
224
|
+
"compare.ignoreFormattingDifferences",
|
|
225
|
+
"compare.excludeObjectTypes"
|
|
226
|
+
]);
|
|
227
|
+
cmd.addCommand(driftWatchSubcommand());
|
|
228
|
+
return cmd;
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
driftCommand
|
|
232
|
+
};
|
|
233
|
+
//# sourceMappingURL=drift-FDRNPWQA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/drift.ts"],"sourcesContent":["import { Command } from 'commander';\nimport {\n CompareEngine,\n DbtManifestSource,\n InMemorySource,\n ProjectSource,\n AccountExtractor,\n createConnection,\n defaultExtractors,\n driftAnomaly,\n getProfile,\n type CompareSource,\n} from '@ddt-tools/core';\nimport { addMappingFlags, buildMappingFromOptions } from '../util/mapping.js';\nimport { attachRelatedOptions } from '../util/help-catalog.js';\n\nfunction driftWatchSubcommand(): Command {\n const sub = new Command('watch');\n sub\n .description(\n 'Poll a project against the live Unity Catalog on a fixed interval. Prints DRIFT_DETECTED events to stdout (or POSTs to --webhook) when the catalog diverges from the project.',\n )\n .requiredOption('--source <path>', 'Path to .ddtproj or .ddtpac')\n .requiredOption('--connection <name>', 'Connection profile name')\n .option('--catalog <catalog>', 'Limit drift check to a single catalog.')\n .option('--schema <schema>', 'Limit drift check to a single schema (requires --catalog).')\n .option('--interval <seconds>', 'Poll interval in seconds (min 5).', '60')\n .option(\n '--webhook <url>',\n 'POST drift events as JSON to this URL (Slack/Teams/generic receiver).',\n )\n .option('--format <fmt>', 'Output format: text | json.', 'text')\n .option(\n '--quiet',\n 'Suppress CLEAN status lines; emit only DRIFT_DETECTED and error events.',\n false,\n )\n .action(async (opts) => {\n const intervalSecs = Math.max(5, parseInt(String(opts.interval), 10) || 60);\n const format = String(opts.format) === 'json' ? 'json' : 'text';\n const webhookUrl = opts.webhook ? String(opts.webhook) : undefined;\n const quiet = !!opts.quiet;\n const sourcePath = String(opts.source);\n const scope = {\n ...(opts.catalog ? { catalog: String(opts.catalog) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n\n const profile = await getProfile(String(opts.connection));\n const conn = createConnection(profile);\n await conn.connect();\n\n const src: CompareSource = sourcePath.endsWith('.ddtpac')\n ? await (async () => {\n const { PacSource } = await import('@ddt-tools/core');\n return new PacSource(sourcePath, 'source');\n })()\n : new ProjectSource(sourcePath, 'source');\n\n if (!quiet && format === 'text') {\n console.log(\n `drift watch: polling every ${intervalSecs}s — ${sourcePath} → ${profile.auth.host}`,\n );\n console.log('Press Ctrl+C to stop.');\n }\n\n const postToWebhook = async (event: Record<string, unknown>): Promise<void> => {\n if (!webhookUrl) return;\n try {\n const res = await fetch(webhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(event),\n });\n if (!res.ok && format === 'text') console.warn(`Webhook POST failed: HTTP ${res.status}`);\n } catch (err) {\n if (format === 'text')\n console.warn(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);\n }\n };\n\n const pollOnce = async (): Promise<void> => {\n const ts = new Date().toISOString();\n try {\n const accountExtractor = new AccountExtractor(defaultExtractors());\n const liveObjs = await accountExtractor.extract(conn, scope);\n const live = new InMemorySource(liveObjs, { kind: 'live', label: profile.auth.host });\n const engine = new CompareEngine();\n const result = await engine.compare(src, live);\n const s = result.summary;\n const drifted = s.added + s.removed + s.modified;\n if (drifted > 0) {\n const event = {\n type: 'DRIFT_DETECTED',\n timestamp: ts,\n source: sourcePath,\n added: s.added,\n removed: s.removed,\n modified: s.modified,\n };\n if (format === 'json') process.stdout.write(JSON.stringify(event) + '\\n');\n else console.warn(`[${ts}] DRIFT_DETECTED +${s.added} -${s.removed} ~${s.modified}`);\n await postToWebhook(event);\n } else if (!quiet) {\n if (format === 'json')\n process.stdout.write(\n JSON.stringify({ type: 'CLEAN', timestamp: ts, source: sourcePath }) + '\\n',\n );\n else console.log(`[${ts}] clean`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (format === 'json')\n process.stdout.write(\n JSON.stringify({ type: 'POLL_ERROR', timestamp: ts, error: message }) + '\\n',\n );\n else console.error(`[${ts}] poll error: ${message}`);\n }\n };\n\n await pollOnce();\n const timer = setInterval(() => {\n void pollOnce();\n }, intervalSecs * 1000);\n process.once('SIGINT', () => {\n clearInterval(timer);\n void conn.disconnect().then(() => process.exit(0));\n });\n await new Promise<void>(() => {});\n });\n return sub;\n}\n\n/**\n * `ddt drift` — refuse-on-drift CI gate. Compares a `.ddtproj` (or\n * `.ddtpac`) against a live target via the configured connection profile;\n * exits non-zero when the live state differs.\n *\n * Use it as a nightly cron / scheduled GitHub Action to catch unsanctioned\n * changes to prod.\n */\nexport function driftCommand(): Command {\n const cmd = new Command('drift');\n cmd\n .description(\n 'Refuse with non-zero exit when the live target has drifted from --source or a dbt manifest.',\n )\n .option('--source <path>', 'Source: .ddtproj or .ddtpac (required unless --vs-dbt-project).')\n .option(\n '--vs-dbt-project <path>',\n 'Compare a compiled dbt project (or target/manifest.json) against the live target. ' +\n 'Run `dbt compile` first. When set, --source is not required.',\n )\n .requiredOption('--connection <name>', 'Connection profile to extract live state from.')\n .option('--catalog <catalog>', 'Limit drift check to a single catalog.')\n .option('--schema <schema>', 'Limit drift check to a single schema (requires --catalog).')\n .option('--ignore-case', 'Compare object FQNs case-insensitively.', false)\n .option(\n '--allow-extras',\n 'Treat objects that exist on the target but not in source as OK (no drift).',\n false,\n )\n .option(\n '--anomalies',\n 'Also classify drift via the anomaly detector (new grants to `account users`, bypass-principal grants, owner changes, audit-column drops, mask removals, new bypass-named groups).',\n false,\n )\n .option(\n '--anomalies-only',\n 'Skip the simple drift summary and emit only the anomaly report. Implies --anomalies.',\n false,\n )\n .option(\n '--fail-on-anomaly <severity>',\n 'Exit non-zero when any anomaly at or above this severity fires. Values: critical | high | medium | low. Default: drift presence alone determines exit code.',\n );\n addMappingFlags(cmd);\n cmd.action(async (opts) => {\n if (!opts.source && !opts.vsDBtProject) {\n console.error('Provide either --source <path> or --vs-dbt-project <path>.');\n process.exitCode = 1;\n return;\n }\n const nameMapping = await buildMappingFromOptions(opts);\n const profile = await getProfile(String(opts.connection));\n const conn = createConnection(profile);\n try {\n await conn.connect();\n const accountExtractor = new AccountExtractor(defaultExtractors());\n const scope = {\n ...(opts.catalog ? { catalog: String(opts.catalog) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n const liveObjs = await accountExtractor.extract(conn, scope);\n const live = new InMemorySource(liveObjs, { kind: 'live', label: profile.auth.host });\n const src: CompareSource = opts.vsDBtProject\n ? new DbtManifestSource(String(opts.vsDBtProject), `dbt:${opts.vsDBtProject}`)\n : String(opts.source).endsWith('.ddtpac')\n ? // PacSource via inline import to keep this command self-contained.\n await (async () => {\n const { PacSource } = await import('@ddt-tools/core');\n return new PacSource(String(opts.source), 'source');\n })()\n : new ProjectSource(String(opts.source), 'source');\n\n const engine = new CompareEngine();\n const result = await engine.compare(src, live, {\n ignoreCase: !!opts.ignoreCase,\n ...(nameMapping ? { nameMapping } : {}),\n });\n\n const s = result.summary;\n const drifted = s.added + s.modified + (opts.allowExtras ? 0 : s.removed);\n if (!opts.anomaliesOnly) {\n console.log(`Source: ${result.source.kind}:${result.source.label}`);\n console.log(`Target: ${result.target.kind}:${result.target.label}`);\n console.log(`Summary: +${s.added} -${s.removed} ~${s.modified} =${s.unchanged}`);\n console.log(`Drifted: ${drifted}` + (opts.allowExtras ? ' (extras allowed)' : ''));\n if (drifted > 0) {\n console.log('');\n console.log('Drift detected:');\n for (const o of result.objects) {\n if (o.kind === 'unchanged') continue;\n if (o.kind === 'removed' && opts.allowExtras) continue;\n const glyph =\n o.kind === 'added'\n ? '+ source-only'\n : o.kind === 'removed'\n ? '- target-only'\n : '~ modified';\n console.log(` ${glyph} ${o.identity.objectType} ${o.identity.fqn}`);\n }\n process.exitCode = 1;\n }\n }\n\n if (opts.anomalies || opts.anomaliesOnly) {\n const report = driftAnomaly.detectAnomalies(result);\n if (report.totalAnomalies === 0) {\n console.log('Anomaly scan: 0 findings.');\n } else {\n console.log('');\n console.log(`Anomaly scan: ${report.totalAnomalies} finding(s):`);\n for (const a of report.anomalies) {\n console.log(` [${a.severity}] ${a.category}: ${a.fqn} — ${a.reason}`);\n }\n const failOn = (opts.failOnAnomaly as string | undefined)?.toLowerCase();\n if (failOn && ['critical', 'high', 'medium', 'low'].includes(failOn)) {\n const failRank = driftAnomaly.anomalySeverityRank(\n failOn as driftAnomaly.AnomalySeverity,\n );\n const triggered = report.anomalies.some(\n (a) => driftAnomaly.anomalySeverityRank(a.severity) <= failRank,\n );\n if (triggered) process.exitCode = 1;\n }\n }\n }\n } finally {\n await conn.disconnect();\n }\n });\n attachRelatedOptions(cmd, [\n 'compare.ignoreCase',\n 'compare.ignoreComments',\n 'compare.ignoreFormattingDifferences',\n 'compare.excludeObjectTypes',\n ]);\n cmd.addCommand(driftWatchSubcommand());\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/drift-gate.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import {
|
|
6
|
+
AccountExtractor,
|
|
7
|
+
CompareEngine,
|
|
8
|
+
InMemorySource,
|
|
9
|
+
createConnection,
|
|
10
|
+
defaultExtractors,
|
|
11
|
+
getProfile
|
|
12
|
+
} from "@ddt-tools/core";
|
|
13
|
+
function driftGateCommand() {
|
|
14
|
+
const cmd = new Command("drift-gate");
|
|
15
|
+
cmd.description(
|
|
16
|
+
"Refuse-on-drift CI gate across multiple replica workspaces. Compares replicas against a primary."
|
|
17
|
+
).requiredOption("--primary <profile>", "Primary workspace connection profile.").requiredOption("--replicas <list>", "Comma-separated replica connection profile names.").option("--catalog <catalog>", "Limit drift comparison to a single catalog.").option("--schema <schema>", "Limit drift comparison to a single schema (requires --catalog).").option(
|
|
18
|
+
"--threshold <n>",
|
|
19
|
+
"Max allowed differences per replica before the gate fails. Default 0.",
|
|
20
|
+
"0"
|
|
21
|
+
).option("--format <fmt>", "Output: table | json.", "table").action(async (opts) => {
|
|
22
|
+
const primary = await getProfile(String(opts.primary));
|
|
23
|
+
const replicaNames = String(opts.replicas).split(",").map((s) => s.trim()).filter(Boolean);
|
|
24
|
+
if (replicaNames.length === 0) {
|
|
25
|
+
throw new Error("At least one replica profile is required via --replicas.");
|
|
26
|
+
}
|
|
27
|
+
const threshold = Number.parseInt(String(opts.threshold), 10);
|
|
28
|
+
const scope = {
|
|
29
|
+
...opts.catalog ? { catalog: String(opts.catalog) } : {},
|
|
30
|
+
...opts.schema ? { schema: String(opts.schema) } : {}
|
|
31
|
+
};
|
|
32
|
+
const primaryConn = createConnection(primary);
|
|
33
|
+
let primaryObjs;
|
|
34
|
+
try {
|
|
35
|
+
await primaryConn.connect();
|
|
36
|
+
primaryObjs = await new AccountExtractor(defaultExtractors()).extract(primaryConn, scope);
|
|
37
|
+
} finally {
|
|
38
|
+
await primaryConn.disconnect();
|
|
39
|
+
}
|
|
40
|
+
const primarySource = new InMemorySource(primaryObjs, {
|
|
41
|
+
kind: "live",
|
|
42
|
+
label: `primary:${primary.auth.host}`
|
|
43
|
+
});
|
|
44
|
+
const results = [];
|
|
45
|
+
const engine = new CompareEngine();
|
|
46
|
+
for (const replicaName of replicaNames) {
|
|
47
|
+
const replicaProfile = await getProfile(replicaName);
|
|
48
|
+
const replicaConn = createConnection(replicaProfile);
|
|
49
|
+
let replicaObjs;
|
|
50
|
+
try {
|
|
51
|
+
await replicaConn.connect();
|
|
52
|
+
replicaObjs = await new AccountExtractor(defaultExtractors()).extract(replicaConn, scope);
|
|
53
|
+
} finally {
|
|
54
|
+
await replicaConn.disconnect();
|
|
55
|
+
}
|
|
56
|
+
const replicaSource = new InMemorySource(replicaObjs, {
|
|
57
|
+
kind: "live",
|
|
58
|
+
label: `replica:${replicaProfile.auth.host}`
|
|
59
|
+
});
|
|
60
|
+
const result = await engine.compare(primarySource, replicaSource);
|
|
61
|
+
const { added, removed, modified } = result.summary;
|
|
62
|
+
results.push({
|
|
63
|
+
profile: replicaName,
|
|
64
|
+
workspaceHost: replicaProfile.auth.host,
|
|
65
|
+
added,
|
|
66
|
+
removed,
|
|
67
|
+
modified,
|
|
68
|
+
total: added + removed + modified
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (opts.format === "json") {
|
|
72
|
+
console.log(
|
|
73
|
+
JSON.stringify({ primary: primary.auth.host, threshold, replicas: results }, null, 2)
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
console.log(`Drift gate \u2014 primary ${primary.auth.host}`);
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log(
|
|
79
|
+
" REPLICA-PROFILE WORKSPACE-HOST ADD RM MOD TOTAL STATUS"
|
|
80
|
+
);
|
|
81
|
+
console.log(
|
|
82
|
+
" ---------------------- ------------------------------ --- -- --- ----- ------"
|
|
83
|
+
);
|
|
84
|
+
for (const r of results) {
|
|
85
|
+
const status = r.total > threshold ? "DRIFT" : "OK";
|
|
86
|
+
console.log(
|
|
87
|
+
` ${r.profile.padEnd(22).slice(0, 22)} ${r.workspaceHost.padEnd(30).slice(0, 30)} ${String(r.added).padStart(3)} ${String(r.removed).padStart(2)} ${String(r.modified).padStart(3)} ${String(r.total).padStart(5)} ${status}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const drifting = results.filter((r) => r.total > threshold);
|
|
92
|
+
if (drifting.length > 0) {
|
|
93
|
+
console.error("");
|
|
94
|
+
console.error(`Drift detected in ${drifting.length} replica(s) (threshold=${threshold}).`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return cmd;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
driftGateCommand
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=drift-gate-6BWWWMHW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/drift-gate.ts"],"sourcesContent":["/**\n * `ddt drift-gate` — multi-region/replica drift gate.\n *\n * Compares the live schema in N replica workspaces against a primary\n * workspace's live schema. Useful for orgs that replicate Unity Catalog\n * state across regions (Delta Sharing, multi-workspace deployments) and\n * want a CI gate that fires the moment any replica drifts from the\n * primary.\n *\n * Algorithm:\n * 1. Extract primary's live model.\n * 2. For each replica, extract live model + compare against primary.\n * 3. Aggregate per-replica diff counts.\n * 4. Exit non-zero if any replica has drift > threshold.\n *\n * Combine with the existing `ddt drift` command (project ↔ region) to\n * cover both vectors: (a) project drift (drift) and (b) inter-region\n * drift (drift-gate).\n */\nimport { Command } from 'commander';\nimport {\n AccountExtractor,\n CompareEngine,\n InMemorySource,\n createConnection,\n defaultExtractors,\n getProfile,\n} from '@ddt-tools/core';\n\ninterface RegionResult {\n profile: string;\n workspaceHost: string;\n added: number;\n removed: number;\n modified: number;\n total: number;\n}\n\nexport function driftGateCommand(): Command {\n const cmd = new Command('drift-gate');\n cmd\n .description(\n 'Refuse-on-drift CI gate across multiple replica workspaces. Compares replicas against a primary.',\n )\n .requiredOption('--primary <profile>', 'Primary workspace connection profile.')\n .requiredOption('--replicas <list>', 'Comma-separated replica connection profile names.')\n .option('--catalog <catalog>', 'Limit drift comparison to a single catalog.')\n .option('--schema <schema>', 'Limit drift comparison to a single schema (requires --catalog).')\n .option(\n '--threshold <n>',\n 'Max allowed differences per replica before the gate fails. Default 0.',\n '0',\n )\n .option('--format <fmt>', 'Output: table | json.', 'table')\n .action(async (opts) => {\n const primary = await getProfile(String(opts.primary));\n const replicaNames = String(opts.replicas)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (replicaNames.length === 0) {\n throw new Error('At least one replica profile is required via --replicas.');\n }\n const threshold = Number.parseInt(String(opts.threshold), 10);\n const scope = {\n ...(opts.catalog ? { catalog: String(opts.catalog) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n\n // 1. Extract primary.\n const primaryConn = createConnection(primary);\n let primaryObjs;\n try {\n await primaryConn.connect();\n primaryObjs = await new AccountExtractor(defaultExtractors()).extract(primaryConn, scope);\n } finally {\n await primaryConn.disconnect();\n }\n const primarySource = new InMemorySource(primaryObjs, {\n kind: 'live',\n label: `primary:${primary.auth.host}`,\n });\n\n const results: RegionResult[] = [];\n const engine = new CompareEngine();\n\n // 2. Per-replica compare.\n for (const replicaName of replicaNames) {\n const replicaProfile = await getProfile(replicaName);\n const replicaConn = createConnection(replicaProfile);\n let replicaObjs;\n try {\n await replicaConn.connect();\n replicaObjs = await new AccountExtractor(defaultExtractors()).extract(replicaConn, scope);\n } finally {\n await replicaConn.disconnect();\n }\n const replicaSource = new InMemorySource(replicaObjs, {\n kind: 'live',\n label: `replica:${replicaProfile.auth.host}`,\n });\n const result = await engine.compare(primarySource, replicaSource);\n const { added, removed, modified } = result.summary;\n results.push({\n profile: replicaName,\n workspaceHost: replicaProfile.auth.host,\n added,\n removed,\n modified,\n total: added + removed + modified,\n });\n }\n\n if (opts.format === 'json') {\n console.log(\n JSON.stringify({ primary: primary.auth.host, threshold, replicas: results }, null, 2),\n );\n } else {\n console.log(`Drift gate — primary ${primary.auth.host}`);\n console.log('');\n console.log(\n ' REPLICA-PROFILE WORKSPACE-HOST ADD RM MOD TOTAL STATUS',\n );\n console.log(\n ' ---------------------- ------------------------------ --- -- --- ----- ------',\n );\n for (const r of results) {\n const status = r.total > threshold ? 'DRIFT' : 'OK';\n console.log(\n ` ${r.profile.padEnd(22).slice(0, 22)} ${r.workspaceHost.padEnd(30).slice(0, 30)} ${String(r.added).padStart(3)} ${String(r.removed).padStart(2)} ${String(r.modified).padStart(3)} ${String(r.total).padStart(5)} ${status}`,\n );\n }\n }\n\n const drifting = results.filter((r) => r.total > threshold);\n if (drifting.length > 0) {\n console.error('');\n console.error(`Drift detected in ${drifting.length} replica(s) (threshold=${threshold}).`);\n process.exitCode = 1;\n }\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}
|