@ddt-tools/cli 0.2.0 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +499 -19402
- package/dist/cli.js.map +1 -1
- package/dist/compare-P7JOV76O.js +379 -0
- package/dist/compare-P7JOV76O.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-3LPE2IJY.js +109 -0
- package/dist/errorReporting-3LPE2IJY.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-2RNYDL4E.js +79 -0
- package/dist/import-2RNYDL4E.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-F7FND5X7.js +343 -0
- package/dist/mcp-F7FND5X7.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-AYCRMCE2.js +739 -0
- package/dist/publish-AYCRMCE2.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,171 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/snapshot.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { createConnection, getProfile, queryExecution, safety } from "@ddt-tools/core";
|
|
8
|
+
var DEFAULT_ROOT = safety.DEFAULT_SNAPSHOT_REGISTRY_DIR;
|
|
9
|
+
function snapshotCommand() {
|
|
10
|
+
const cmd = new Command("snapshot");
|
|
11
|
+
cmd.description(
|
|
12
|
+
"Inspect / prune the pre-deploy snapshot registry written by `ddt publish --apply`."
|
|
13
|
+
);
|
|
14
|
+
cmd.command("list").description("List recorded snapshot batches (newest first).").option("--root <path>", "Snapshot registry directory.", DEFAULT_ROOT).option("--expired-only", "Only show batches whose TTL has elapsed.", false).option("--json", "Emit JSON.", false).action(async (opts) => {
|
|
15
|
+
const root = String(opts.root);
|
|
16
|
+
const batches = await safety.loadSnapshotRegistry(root);
|
|
17
|
+
const filtered = opts.expiredOnly ? safety.listExpiredBatches(batches) : batches;
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (filtered.length === 0) {
|
|
23
|
+
console.log(
|
|
24
|
+
opts.expiredOnly ? `No expired snapshot batches in ${root}.` : `No snapshot batches recorded in ${root}.`
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
console.log(
|
|
30
|
+
`${filtered.length} snapshot batch(es) in ${root}` + (opts.expiredOnly ? " (expired only)" : "")
|
|
31
|
+
);
|
|
32
|
+
for (const b of filtered) {
|
|
33
|
+
const expired = Date.parse(b.expiresAt) <= now ? " EXPIRED" : "";
|
|
34
|
+
console.log(
|
|
35
|
+
` ${b.batchId} (${b.entries.length} obj${b.entries.length === 1 ? "" : "s"}) created=${b.createdAt} expires=${b.expiresAt}${expired}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
cmd.command("show").description("Show one batch in full (entries + context).").argument("<batch-id>").option("--root <path>", "Snapshot registry directory.", DEFAULT_ROOT).option("--json", "Emit JSON.", false).action(async (batchId, opts) => {
|
|
40
|
+
const root = String(opts.root);
|
|
41
|
+
let batch;
|
|
42
|
+
try {
|
|
43
|
+
batch = await safety.readSnapshotBatch(root, batchId);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46
|
+
console.error(`Snapshot batch "${batchId}" not found in ${root}: ${msg}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (opts.json) {
|
|
51
|
+
console.log(JSON.stringify(batch, null, 2));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log(`Batch: ${batch.batchId}`);
|
|
55
|
+
console.log(`createdAt: ${batch.createdAt}`);
|
|
56
|
+
console.log(`expiresAt: ${batch.expiresAt}`);
|
|
57
|
+
console.log(`ttlHours: ${batch.ttlHours}`);
|
|
58
|
+
if (batch.context) {
|
|
59
|
+
const ctx = batch.context;
|
|
60
|
+
const bits = [
|
|
61
|
+
ctx.projectName ? `project=${ctx.projectName}` : "",
|
|
62
|
+
ctx.projectVersion ? `version=${ctx.projectVersion}` : "",
|
|
63
|
+
ctx.profile ? `profile=${ctx.profile}` : "",
|
|
64
|
+
ctx.pacPath ? `pac=${ctx.pacPath}` : ""
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
if (bits.length > 0) console.log(`context: ${bits.join("; ")}`);
|
|
67
|
+
}
|
|
68
|
+
console.log(`Entries (${batch.entries.length}):`);
|
|
69
|
+
for (const e of batch.entries) {
|
|
70
|
+
console.log(
|
|
71
|
+
` ${e.objectType.padEnd(18)} ${e.sourceFqn} \u2192 ${e.snapshotFqn}` + (e.triggerCodes.length > 0 ? ` [${e.triggerCodes.join(", ")}]` : "")
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
cmd.command("prune").description(
|
|
76
|
+
"Drop snapshots whose TTL has elapsed. Dry-run by default; pass --apply --yes to execute."
|
|
77
|
+
).option("--root <path>", "Snapshot registry directory.", DEFAULT_ROOT).option(
|
|
78
|
+
"--connection <profile>",
|
|
79
|
+
"Connection profile. Required with --apply (the executor needs a live target)."
|
|
80
|
+
).option("--apply", "Execute DROP statements against --connection. Requires --yes.", false).option("--yes", "Confirm destructive prune. Required with --apply.", false).option("--out <path>", "Write the prune SQL (all expired batches concatenated) to this file.").option(
|
|
81
|
+
"--registry-only",
|
|
82
|
+
"Only remove registry JSON entries (do not emit/execute DROP SQL). Use when the snapshot tables have already been cleaned up out-of-band.",
|
|
83
|
+
false
|
|
84
|
+
).action(async (opts) => {
|
|
85
|
+
const root = String(opts.root);
|
|
86
|
+
const batches = await safety.loadSnapshotRegistry(root);
|
|
87
|
+
const expired = safety.listExpiredBatches(batches);
|
|
88
|
+
if (expired.length === 0) {
|
|
89
|
+
console.log(`No expired snapshot batches in ${root}.`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const allSql = [];
|
|
93
|
+
for (const b of expired) {
|
|
94
|
+
allSql.push(...safety.emitPruneSnapshotSql(b));
|
|
95
|
+
}
|
|
96
|
+
const fullSql = allSql.join("\n");
|
|
97
|
+
if (opts.out) {
|
|
98
|
+
const out = path.resolve(String(opts.out));
|
|
99
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
100
|
+
await fs.writeFile(out, fullSql + "\n", "utf8");
|
|
101
|
+
console.log(`Wrote prune SQL \u2192 ${out}`);
|
|
102
|
+
}
|
|
103
|
+
if (!opts.apply) {
|
|
104
|
+
console.log(
|
|
105
|
+
`${expired.length} expired snapshot batch(es) would be pruned (${countEntries(expired)} object(s) total). ` + (opts.registryOnly ? "Pass --apply --yes to remove registry entries." : "Pass --apply --yes --connection <profile> to execute and remove registry entries.")
|
|
106
|
+
);
|
|
107
|
+
for (const b of expired) {
|
|
108
|
+
console.log(` ${b.batchId} (expired ${b.expiresAt}, ${b.entries.length} obj)`);
|
|
109
|
+
}
|
|
110
|
+
if (!opts.out) {
|
|
111
|
+
console.log("");
|
|
112
|
+
console.log("--- PRUNE SQL ---");
|
|
113
|
+
console.log(fullSql);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!opts.yes) {
|
|
118
|
+
console.error("--apply requires --yes (destructive prune).");
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let executed = 0;
|
|
123
|
+
let failed = 0;
|
|
124
|
+
if (!opts.registryOnly) {
|
|
125
|
+
if (!opts.connection) {
|
|
126
|
+
console.error("--apply requires --connection <profile> (or pass --registry-only).");
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const profile = await getProfile(String(opts.connection));
|
|
131
|
+
const conn = createConnection(profile);
|
|
132
|
+
try {
|
|
133
|
+
await conn.connect();
|
|
134
|
+
for (const b of expired) {
|
|
135
|
+
for (const stmt of queryExecution.splitStatements(safety.emitPruneSnapshotSql(b).join("\n")).map((s) => s.sql)) {
|
|
136
|
+
try {
|
|
137
|
+
await conn.executeRows(stmt);
|
|
138
|
+
executed += 1;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
failed += 1;
|
|
141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
142
|
+
console.error(` [prune ${b.batchId}] ${msg}
|
|
143
|
+
${stmt}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
await conn.disconnect();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let removed = 0;
|
|
152
|
+
for (const b of expired) {
|
|
153
|
+
await safety.deleteSnapshotBatch(root, b.batchId);
|
|
154
|
+
removed += 1;
|
|
155
|
+
}
|
|
156
|
+
console.log(
|
|
157
|
+
`Pruned ${removed} registry entry/entries` + (opts.registryOnly ? " (registry-only, no SQL executed)." : ` \xB7 ${executed} DROP statement(s) executed` + (failed > 0 ? `, ${failed} failed.` : "."))
|
|
158
|
+
);
|
|
159
|
+
if (failed > 0) process.exitCode = 1;
|
|
160
|
+
});
|
|
161
|
+
return cmd;
|
|
162
|
+
}
|
|
163
|
+
function countEntries(batches) {
|
|
164
|
+
let n = 0;
|
|
165
|
+
for (const b of batches) n += b.entries.length;
|
|
166
|
+
return n;
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
snapshotCommand
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=snapshot-YMVS322L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/snapshot.ts"],"sourcesContent":["/**\n * `ddt snapshot` — surface the TRUST.1 pre-deploy snapshot registry from\n * the CLI.\n *\n * ddt snapshot list [--root <path>] [--json] [--expired-only]\n * ddt snapshot show <batch-id> [--root <path>] [--json]\n * ddt snapshot prune [--root <path>] [--connection <profile>]\n * [--apply] [--yes] [--out <path>]\n *\n * `publish --apply` writes a registry entry under `.ddt/snapshots/<batchId>.json`\n * every time it takes a Delta SHALLOW CLONE pre-deploy. The TTL is per-batch\n * (default 168h). This command lets an operator inspect batches, audit them,\n * and drop both the snapshot tables and the registry rows when the retention\n * horizon has passed.\n *\n * Mirrors `Snowflake/packages/cli/src/commands/snapshot.ts`.\n */\nimport path from 'node:path';\nimport { promises as fs } from 'node:fs';\nimport { Command } from 'commander';\nimport { createConnection, getProfile, queryExecution, safety } from '@ddt-tools/core';\n\nconst DEFAULT_ROOT = safety.DEFAULT_SNAPSHOT_REGISTRY_DIR;\n\nexport function snapshotCommand(): Command {\n const cmd = new Command('snapshot');\n cmd.description(\n 'Inspect / prune the pre-deploy snapshot registry written by `ddt publish --apply`.',\n );\n\n cmd\n .command('list')\n .description('List recorded snapshot batches (newest first).')\n .option('--root <path>', 'Snapshot registry directory.', DEFAULT_ROOT)\n .option('--expired-only', 'Only show batches whose TTL has elapsed.', false)\n .option('--json', 'Emit JSON.', false)\n .action(async (opts: Record<string, unknown>) => {\n const root = String(opts.root);\n const batches = await safety.loadSnapshotRegistry(root);\n const filtered = opts.expiredOnly ? safety.listExpiredBatches(batches) : batches;\n\n if (opts.json) {\n console.log(JSON.stringify(filtered, null, 2));\n return;\n }\n if (filtered.length === 0) {\n console.log(\n opts.expiredOnly\n ? `No expired snapshot batches in ${root}.`\n : `No snapshot batches recorded in ${root}.`,\n );\n return;\n }\n const now = Date.now();\n console.log(\n `${filtered.length} snapshot batch(es) in ${root}` +\n (opts.expiredOnly ? ' (expired only)' : ''),\n );\n for (const b of filtered) {\n const expired = Date.parse(b.expiresAt) <= now ? ' EXPIRED' : '';\n console.log(\n ` ${b.batchId} (${b.entries.length} obj${b.entries.length === 1 ? '' : 's'}) ` +\n `created=${b.createdAt} expires=${b.expiresAt}${expired}`,\n );\n }\n });\n\n cmd\n .command('show')\n .description('Show one batch in full (entries + context).')\n .argument('<batch-id>')\n .option('--root <path>', 'Snapshot registry directory.', DEFAULT_ROOT)\n .option('--json', 'Emit JSON.', false)\n .action(async (batchId: string, opts: Record<string, unknown>) => {\n const root = String(opts.root);\n let batch;\n try {\n batch = await safety.readSnapshotBatch(root, batchId);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Snapshot batch \"${batchId}\" not found in ${root}: ${msg}`);\n process.exitCode = 1;\n return;\n }\n if (opts.json) {\n console.log(JSON.stringify(batch, null, 2));\n return;\n }\n console.log(`Batch: ${batch.batchId}`);\n console.log(`createdAt: ${batch.createdAt}`);\n console.log(`expiresAt: ${batch.expiresAt}`);\n console.log(`ttlHours: ${batch.ttlHours}`);\n if (batch.context) {\n const ctx = batch.context;\n const bits = [\n ctx.projectName ? `project=${ctx.projectName}` : '',\n ctx.projectVersion ? `version=${ctx.projectVersion}` : '',\n ctx.profile ? `profile=${ctx.profile}` : '',\n ctx.pacPath ? `pac=${ctx.pacPath}` : '',\n ].filter(Boolean);\n if (bits.length > 0) console.log(`context: ${bits.join('; ')}`);\n }\n console.log(`Entries (${batch.entries.length}):`);\n for (const e of batch.entries) {\n console.log(\n ` ${e.objectType.padEnd(18)} ${e.sourceFqn} → ${e.snapshotFqn}` +\n (e.triggerCodes.length > 0 ? ` [${e.triggerCodes.join(', ')}]` : ''),\n );\n }\n });\n\n cmd\n .command('prune')\n .description(\n 'Drop snapshots whose TTL has elapsed. Dry-run by default; pass --apply --yes to execute.',\n )\n .option('--root <path>', 'Snapshot registry directory.', DEFAULT_ROOT)\n .option(\n '--connection <profile>',\n 'Connection profile. Required with --apply (the executor needs a live target).',\n )\n .option('--apply', 'Execute DROP statements against --connection. Requires --yes.', false)\n .option('--yes', 'Confirm destructive prune. Required with --apply.', false)\n .option('--out <path>', 'Write the prune SQL (all expired batches concatenated) to this file.')\n .option(\n '--registry-only',\n 'Only remove registry JSON entries (do not emit/execute DROP SQL). ' +\n 'Use when the snapshot tables have already been cleaned up out-of-band.',\n false,\n )\n .action(async (opts: Record<string, unknown>) => {\n const root = String(opts.root);\n const batches = await safety.loadSnapshotRegistry(root);\n const expired = safety.listExpiredBatches(batches);\n\n if (expired.length === 0) {\n console.log(`No expired snapshot batches in ${root}.`);\n return;\n }\n\n const allSql: string[] = [];\n for (const b of expired) {\n allSql.push(...safety.emitPruneSnapshotSql(b));\n }\n const fullSql = allSql.join('\\n');\n\n if (opts.out) {\n const out = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(out), { recursive: true });\n await fs.writeFile(out, fullSql + '\\n', 'utf8');\n console.log(`Wrote prune SQL → ${out}`);\n }\n\n if (!opts.apply) {\n console.log(\n `${expired.length} expired snapshot batch(es) would be pruned ` +\n `(${countEntries(expired)} object(s) total). ` +\n (opts.registryOnly\n ? 'Pass --apply --yes to remove registry entries.'\n : 'Pass --apply --yes --connection <profile> to execute and remove registry entries.'),\n );\n for (const b of expired) {\n console.log(` ${b.batchId} (expired ${b.expiresAt}, ${b.entries.length} obj)`);\n }\n if (!opts.out) {\n console.log('');\n console.log('--- PRUNE SQL ---');\n console.log(fullSql);\n }\n return;\n }\n\n if (!opts.yes) {\n console.error('--apply requires --yes (destructive prune).');\n process.exitCode = 1;\n return;\n }\n\n let executed = 0;\n let failed = 0;\n if (!opts.registryOnly) {\n if (!opts.connection) {\n console.error('--apply requires --connection <profile> (or pass --registry-only).');\n process.exitCode = 1;\n return;\n }\n const profile = await getProfile(String(opts.connection));\n const conn = createConnection(profile);\n try {\n await conn.connect();\n for (const b of expired) {\n for (const stmt of queryExecution\n .splitStatements(safety.emitPruneSnapshotSql(b).join('\\n'))\n .map((s) => s.sql)) {\n try {\n await conn.executeRows(stmt);\n executed += 1;\n } catch (err) {\n failed += 1;\n const msg = err instanceof Error ? err.message : String(err);\n console.error(` [prune ${b.batchId}] ${msg}\\n ${stmt}`);\n }\n }\n }\n } finally {\n await conn.disconnect();\n }\n }\n\n // Always remove the registry entries we processed (even if some DROPs\n // failed — a partially-dropped batch is now in an unknown state, but\n // keeping its registry row pointing at vanished tables only adds noise\n // to the next prune. Failures were logged above for the operator).\n let removed = 0;\n for (const b of expired) {\n await safety.deleteSnapshotBatch(root, b.batchId);\n removed += 1;\n }\n\n console.log(\n `Pruned ${removed} registry entry/entries` +\n (opts.registryOnly\n ? ' (registry-only, no SQL executed).'\n : ` · ${executed} DROP statement(s) executed` +\n (failed > 0 ? `, ${failed} failed.` : '.')),\n );\n if (failed > 0) process.exitCode = 1;\n });\n\n return cmd;\n}\n\nfunction countEntries(batches: readonly safety.SnapshotBatch[]): number {\n let n = 0;\n for (const b of batches) n += b.entries.length;\n return n;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/snippets.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { snippets } from "@ddt-tools/core";
|
|
6
|
+
function snippetsCommand() {
|
|
7
|
+
const cmd = new Command("snippets");
|
|
8
|
+
cmd.description("Browse the built-in SQL snippet catalog (SCD, star schema, data quality).");
|
|
9
|
+
cmd.command("list").description("List every snippet (filterable by category).").option(
|
|
10
|
+
"--category <cat>",
|
|
11
|
+
"Filter by category: scd | star-schema | snowflake-schema | standards | data-quality."
|
|
12
|
+
).option("--format <fmt>", "text | json (default text).", "text").action((opts) => {
|
|
13
|
+
const list = opts.category ? snippets.listSnippets(opts.category) : snippets.listSnippets();
|
|
14
|
+
const fmt = String(opts.format ?? "text").toLowerCase();
|
|
15
|
+
if (fmt === "json") {
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
JSON.stringify(
|
|
18
|
+
list.map((s) => ({
|
|
19
|
+
id: s.id,
|
|
20
|
+
name: s.name,
|
|
21
|
+
category: s.category,
|
|
22
|
+
description: s.description
|
|
23
|
+
})),
|
|
24
|
+
null,
|
|
25
|
+
2
|
|
26
|
+
) + "\n"
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (fmt !== "text") {
|
|
31
|
+
throw new Error(`Unknown --format: ${opts.format}. Use text | json.`);
|
|
32
|
+
}
|
|
33
|
+
if (list.length === 0) {
|
|
34
|
+
process.stdout.write("No snippets matched. Try `ddt snippets list` without --category.\n");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const s of list) {
|
|
38
|
+
process.stdout.write(`${s.id} [${s.category}] \u2014 ${s.name}
|
|
39
|
+
${s.description}
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
cmd.command("show <id>").description("Print a single snippet by id.").option(
|
|
44
|
+
"--format <fmt>",
|
|
45
|
+
"text (rendered, placeholders inline) | raw (VS Code syntax) | json (default text).",
|
|
46
|
+
"text"
|
|
47
|
+
).action((id, opts) => {
|
|
48
|
+
const found = snippets.findSnippet(id);
|
|
49
|
+
if (found === void 0) {
|
|
50
|
+
throw new Error(`No snippet named '${id}'. Run \`ddt snippets list\` to see options.`);
|
|
51
|
+
}
|
|
52
|
+
const fmt = String(opts.format ?? "text").toLowerCase();
|
|
53
|
+
if (fmt === "json") {
|
|
54
|
+
process.stdout.write(JSON.stringify(found, null, 2) + "\n");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (fmt === "raw") {
|
|
58
|
+
process.stdout.write(found.body + "\n");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (fmt !== "text") {
|
|
62
|
+
throw new Error(`Unknown --format: ${opts.format}. Use text | json | raw.`);
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write(`-- ${found.name}
|
|
65
|
+
-- ${found.description}
|
|
66
|
+
`);
|
|
67
|
+
process.stdout.write(snippets.renderSnippetBody(found) + "\n");
|
|
68
|
+
});
|
|
69
|
+
return cmd;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
snippetsCommand
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=snippets-EVTN63OU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/snippets.ts"],"sourcesContent":["/**\n * `ddt snippets` — list + show the shipped snippets catalog (AUTH.2).\n *\n * Composes the pure `@ddt-tools/core/snippets` catalog into operator-facing\n * surfaces. List mode pages through every snippet (filterable by\n * category); show mode prints a single snippet's rendered body suitable\n * for copy-paste into a `.sql` file.\n *\n * Subcommands:\n * ddt snippets list [--category <c>] [--format text|json]\n * ddt snippets show <id> [--format text|json|raw]\n *\n * Mirrors `Snowflake/packages/cli/src/commands/snippets.ts`.\n */\nimport { Command } from 'commander';\nimport { snippets } from '@ddt-tools/core';\n\nexport function snippetsCommand(): Command {\n const cmd = new Command('snippets');\n cmd.description('Browse the built-in SQL snippet catalog (SCD, star schema, data quality).');\n\n cmd\n .command('list')\n .description('List every snippet (filterable by category).')\n .option(\n '--category <cat>',\n 'Filter by category: scd | star-schema | snowflake-schema | standards | data-quality.',\n )\n .option('--format <fmt>', 'text | json (default text).', 'text')\n .action((opts) => {\n const list = opts.category\n ? snippets.listSnippets(opts.category as snippets.SnippetCategory)\n : snippets.listSnippets();\n const fmt = String(opts.format ?? 'text').toLowerCase();\n if (fmt === 'json') {\n process.stdout.write(\n JSON.stringify(\n list.map((s) => ({\n id: s.id,\n name: s.name,\n category: s.category,\n description: s.description,\n })),\n null,\n 2,\n ) + '\\n',\n );\n return;\n }\n if (fmt !== 'text') {\n throw new Error(`Unknown --format: ${opts.format}. Use text | json.`);\n }\n if (list.length === 0) {\n process.stdout.write('No snippets matched. Try `ddt snippets list` without --category.\\n');\n return;\n }\n for (const s of list) {\n process.stdout.write(`${s.id} [${s.category}] — ${s.name}\\n ${s.description}\\n`);\n }\n });\n\n cmd\n .command('show <id>')\n .description('Print a single snippet by id.')\n .option(\n '--format <fmt>',\n 'text (rendered, placeholders inline) | raw (VS Code syntax) | json (default text).',\n 'text',\n )\n .action((id: string, opts) => {\n const found = snippets.findSnippet(id);\n if (found === undefined) {\n throw new Error(`No snippet named '${id}'. Run \\`ddt snippets list\\` to see options.`);\n }\n const fmt = String(opts.format ?? 'text').toLowerCase();\n if (fmt === 'json') {\n process.stdout.write(JSON.stringify(found, null, 2) + '\\n');\n return;\n }\n if (fmt === 'raw') {\n process.stdout.write(found.body + '\\n');\n return;\n }\n if (fmt !== 'text') {\n throw new Error(`Unknown --format: ${opts.format}. Use text | json | raw.`);\n }\n process.stdout.write(`-- ${found.name}\\n-- ${found.description}\\n`);\n process.stdout.write(snippets.renderSnippetBody(found) + '\\n');\n });\n\n return cmd;\n}\n"],"mappings":";;;AAcA,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAElB,SAAS,kBAA2B;AACzC,QAAM,MAAM,IAAI,QAAQ,UAAU;AAClC,MAAI,YAAY,2EAA2E;AAE3F,MACG,QAAQ,MAAM,EACd,YAAY,8CAA8C,EAC1D;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,kBAAkB,+BAA+B,MAAM,EAC9D,OAAO,CAAC,SAAS;AAChB,UAAM,OAAO,KAAK,WACd,SAAS,aAAa,KAAK,QAAoC,IAC/D,SAAS,aAAa;AAC1B,UAAM,MAAM,OAAO,KAAK,UAAU,MAAM,EAAE,YAAY;AACtD,QAAI,QAAQ,QAAQ;AAClB,cAAQ,OAAO;AAAA,QACb,KAAK;AAAA,UACH,KAAK,IAAI,CAAC,OAAO;AAAA,YACf,IAAI,EAAE;AAAA,YACN,MAAM,EAAE;AAAA,YACR,UAAU,EAAE;AAAA,YACZ,aAAa,EAAE;AAAA,UACjB,EAAE;AAAA,UACF;AAAA,UACA;AAAA,QACF,IAAI;AAAA,MACN;AACA;AAAA,IACF;AACA,QAAI,QAAQ,QAAQ;AAClB,YAAM,IAAI,MAAM,qBAAqB,KAAK,MAAM,oBAAoB;AAAA,IACtE;AACA,QAAI,KAAK,WAAW,GAAG;AACrB,cAAQ,OAAO,MAAM,oEAAoE;AACzF;AAAA,IACF;AACA,eAAW,KAAK,MAAM;AACpB,cAAQ,OAAO,MAAM,GAAG,EAAE,EAAE,KAAK,EAAE,QAAQ,YAAO,EAAE,IAAI;AAAA,IAAO,EAAE,WAAW;AAAA,CAAI;AAAA,IAClF;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,WAAW,EACnB,YAAY,+BAA+B,EAC3C;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,OAAO,CAAC,IAAY,SAAS;AAC5B,UAAM,QAAQ,SAAS,YAAY,EAAE;AACrC,QAAI,UAAU,QAAW;AACvB,YAAM,IAAI,MAAM,qBAAqB,EAAE,8CAA8C;AAAA,IACvF;AACA,UAAM,MAAM,OAAO,KAAK,UAAU,MAAM,EAAE,YAAY;AACtD,QAAI,QAAQ,QAAQ;AAClB,cAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,IAAI;AAC1D;AAAA,IACF;AACA,QAAI,QAAQ,OAAO;AACjB,cAAQ,OAAO,MAAM,MAAM,OAAO,IAAI;AACtC;AAAA,IACF;AACA,QAAI,QAAQ,QAAQ;AAClB,YAAM,IAAI,MAAM,qBAAqB,KAAK,MAAM,0BAA0B;AAAA,IAC5E;AACA,YAAQ,OAAO,MAAM,MAAM,MAAM,IAAI;AAAA,KAAQ,MAAM,WAAW;AAAA,CAAI;AAClE,YAAQ,OAAO,MAAM,SAAS,kBAAkB,KAAK,IAAI,IAAI;AAAA,EAC/D,CAAC;AAEH,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/standards.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { catalog, standards } from "@ddt-tools/core";
|
|
8
|
+
function standardsCommand() {
|
|
9
|
+
const cmd = new Command("standards");
|
|
10
|
+
cmd.description("Team SQL standards \u2014 init, check, fix, and explain.");
|
|
11
|
+
cmd.addCommand(initSubcommand());
|
|
12
|
+
cmd.addCommand(checkSubcommand());
|
|
13
|
+
cmd.addCommand(listRulesSubcommand());
|
|
14
|
+
return cmd;
|
|
15
|
+
}
|
|
16
|
+
function initSubcommand() {
|
|
17
|
+
const init = new Command("init");
|
|
18
|
+
init.description(
|
|
19
|
+
"Scaffold a starter .ddt/standards.json config with sensible defaults. Edit the file to match your team conventions, then run `ddt standards check`."
|
|
20
|
+
).option(
|
|
21
|
+
"--dir <path>",
|
|
22
|
+
"Project root directory containing or to receive .ddt/. Defaults to current directory."
|
|
23
|
+
).option("--force", "Overwrite an existing standards.json without prompting.").option(
|
|
24
|
+
"--preset <name>",
|
|
25
|
+
'Starter preset: "default" (Databricks-flavored team conventions) | "dbt" (dbt-community conventions \u2014 stg_/int_/fct_/dim_ prefixes + lower keyword case + dbtMode: "on").',
|
|
26
|
+
"default"
|
|
27
|
+
).action(async (opts) => {
|
|
28
|
+
const projectRoot = path.resolve(String(opts.dir ?? "."));
|
|
29
|
+
const ddtDir = path.join(projectRoot, ".ddt");
|
|
30
|
+
const configPath = path.join(ddtDir, "standards.json");
|
|
31
|
+
const presetName = String(opts.preset ?? "default").toLowerCase();
|
|
32
|
+
if (presetName !== "default" && presetName !== "dbt") {
|
|
33
|
+
console.error(`Unknown --preset: ${opts.preset}. Use "default" or "dbt".`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const preset = presetName;
|
|
38
|
+
await fs.mkdir(ddtDir, { recursive: true });
|
|
39
|
+
if (!opts.force) {
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(configPath);
|
|
42
|
+
console.error(
|
|
43
|
+
`standards.json already exists at ${configPath}.
|
|
44
|
+
Pass --force to overwrite.`
|
|
45
|
+
);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const content = standards.scaffoldStandardsConfig(preset);
|
|
51
|
+
await fs.writeFile(configPath, content, "utf8");
|
|
52
|
+
console.log(`\u2705 Created ${configPath}${preset !== "default" ? ` (${preset} preset)` : ""}`);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log("Edit the file to match your team conventions:");
|
|
55
|
+
console.log(" \u2022 naming.tablePrefixes \u2014 required table name prefixes (e.g. dim_, fct_)");
|
|
56
|
+
console.log(" \u2022 naming.caseStyle \u2014 snake_case | UPPER_CASE | camelCase | none");
|
|
57
|
+
console.log(" \u2022 structure.mandatoryColumns \u2014 columns every table must include");
|
|
58
|
+
console.log(" \u2022 content.noSelectStar \u2014 ban SELECT * in views");
|
|
59
|
+
console.log("");
|
|
60
|
+
console.log("Mode values: warn | error | enforce | autofix | autofix-silent | off");
|
|
61
|
+
console.log("Then run `ddt standards check` to lint your SQL surface.");
|
|
62
|
+
});
|
|
63
|
+
return init;
|
|
64
|
+
}
|
|
65
|
+
function checkSubcommand() {
|
|
66
|
+
const check = new Command("check");
|
|
67
|
+
check.description(
|
|
68
|
+
"Check SQL files against the team standards config. Without --fix this is a read-only lint pass; with --fix it rewrites files for deterministic violations (sql-format.keyword-case). Use --ai-fix to route ambiguous violations through the configured AI provider."
|
|
69
|
+
).argument("[paths...]", "SQL files or directories to check (defaults to project root)").option(
|
|
70
|
+
"--dir <path>",
|
|
71
|
+
"Project root containing .ddt/standards.json. Defaults to current directory."
|
|
72
|
+
).option(
|
|
73
|
+
"--fix",
|
|
74
|
+
"Apply autofixes (sql-format.keyword-case today; opt-in per-rule via standards.json mode = autofix or autofix-silent)."
|
|
75
|
+
).option(
|
|
76
|
+
"--ai-fix",
|
|
77
|
+
"Suggest fixes via the configured AI provider for non-autofixable findings."
|
|
78
|
+
).option(
|
|
79
|
+
"--explain <rule>",
|
|
80
|
+
"Print the deep explanation for a single rule (e.g. naming.case-style)."
|
|
81
|
+
).option("--json", "Emit findings as JSON instead of human-readable text.").option(
|
|
82
|
+
"--dbt",
|
|
83
|
+
"Force dbt-mode on: pre-mask {{ ... }} / {% ... %} Jinja blocks before running the four family checkers. Default: auto-detect."
|
|
84
|
+
).option(
|
|
85
|
+
"--no-dbt",
|
|
86
|
+
"Force dbt-mode off: feed Jinja text directly to the checkers (almost always wrong on dbt models)."
|
|
87
|
+
).action(
|
|
88
|
+
async (paths, opts) => {
|
|
89
|
+
if (opts.explain) {
|
|
90
|
+
const body = standards.formatRuleExplanation(opts.explain);
|
|
91
|
+
if (!body) {
|
|
92
|
+
console.error(
|
|
93
|
+
`Unknown rule: ${opts.explain}. Run \`ddt standards list-rules\` for the full list.`
|
|
94
|
+
);
|
|
95
|
+
process.exit(2);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.log(body);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const projectRoot = path.resolve(String(opts.dir ?? "."));
|
|
102
|
+
const loadedConfig = await standards.loadStandards(projectRoot);
|
|
103
|
+
if (!loadedConfig) {
|
|
104
|
+
console.error(
|
|
105
|
+
`No standards config found at ${path.join(projectRoot, standards.STANDARDS_CONFIG_FILENAME)}. Run \`ddt standards init\` first.`
|
|
106
|
+
);
|
|
107
|
+
process.exit(2);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const config = opts.dbt === true ? { ...loadedConfig, dbtMode: "on" } : opts.dbt === false ? { ...loadedConfig, dbtMode: "off" } : loadedConfig;
|
|
111
|
+
const targets = paths.length > 0 ? paths : [projectRoot];
|
|
112
|
+
const files = await collectSqlFiles(targets);
|
|
113
|
+
if (files.length === 0) {
|
|
114
|
+
console.error("No .sql files matched the given paths.");
|
|
115
|
+
process.exit(2);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { results } = await catalog.mapPool(
|
|
119
|
+
files,
|
|
120
|
+
async (filePath) => ({
|
|
121
|
+
filePath: path.relative(projectRoot, filePath),
|
|
122
|
+
sql: await fs.readFile(filePath, "utf8")
|
|
123
|
+
}),
|
|
124
|
+
{ concurrency: 16, stopOnError: true }
|
|
125
|
+
);
|
|
126
|
+
const readFiles = results.filter(
|
|
127
|
+
(r) => r !== void 0
|
|
128
|
+
);
|
|
129
|
+
if (opts.fix) {
|
|
130
|
+
const fixResults = standards.runMultiFileFix(readFiles, config);
|
|
131
|
+
await Promise.all(
|
|
132
|
+
fixResults.map(async (r) => {
|
|
133
|
+
if (!r.changed) return;
|
|
134
|
+
const absolute = path.resolve(projectRoot, r.filePath);
|
|
135
|
+
await fs.writeFile(absolute, r.result.fixedSql, "utf8");
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
if (opts.json) {
|
|
139
|
+
console.log(JSON.stringify(fixResults, null, 2));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(standards.formatFixReport(fixResults));
|
|
142
|
+
}
|
|
143
|
+
const stillBroken = fixResults.some((r) => r.result.remainingFindings.length > 0);
|
|
144
|
+
process.exit(stillBroken ? 1 : 0);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const result = standards.runMultiFileCheck(readFiles, config);
|
|
148
|
+
if (opts.aiFix) {
|
|
149
|
+
await runAiFix(result, config, projectRoot);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (opts.json) {
|
|
153
|
+
console.log(JSON.stringify(result, null, 2));
|
|
154
|
+
} else {
|
|
155
|
+
console.log(standards.formatCheckReport(result));
|
|
156
|
+
}
|
|
157
|
+
process.exit(result.blockingFiles > 0 ? 1 : 0);
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
return check;
|
|
161
|
+
}
|
|
162
|
+
async function runAiFix(result, config, projectRoot) {
|
|
163
|
+
if (!config) return;
|
|
164
|
+
const standardsCtx = standards.serializeStandardsContext({
|
|
165
|
+
naming: config.naming,
|
|
166
|
+
structure: config.structure,
|
|
167
|
+
sqlFormat: config.sqlFormat,
|
|
168
|
+
content: config.content
|
|
169
|
+
});
|
|
170
|
+
let suggested = 0;
|
|
171
|
+
for (const file of result.files) {
|
|
172
|
+
for (const finding of file.findings) {
|
|
173
|
+
if (finding.severity !== "error" && finding.severity !== "warning") continue;
|
|
174
|
+
const absolute = path.resolve(projectRoot, file.filePath);
|
|
175
|
+
const sql = await fs.readFile(absolute, "utf8");
|
|
176
|
+
try {
|
|
177
|
+
const fix = await standards.aiFixStandard({
|
|
178
|
+
snippet: sql,
|
|
179
|
+
finding,
|
|
180
|
+
standardsContext: standardsCtx
|
|
181
|
+
});
|
|
182
|
+
console.log(`
|
|
183
|
+
${file.filePath} \u2014 ${finding.rule}:`);
|
|
184
|
+
console.log(
|
|
185
|
+
` Suggested rewrite (tokens used: ${fix.usage.promptTokens}p + ${fix.usage.completionTokens}c):`
|
|
186
|
+
);
|
|
187
|
+
console.log(fix.rewrittenSnippet.replace(/^/gm, " "));
|
|
188
|
+
if (fix.explanation) {
|
|
189
|
+
console.log(` Explanation: ${fix.explanation}`);
|
|
190
|
+
}
|
|
191
|
+
suggested++;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(` AI-fix failed: ${err.message ?? String(err)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
console.log(`
|
|
198
|
+
${suggested} AI suggestion(s) generated. Apply manually after review.`);
|
|
199
|
+
}
|
|
200
|
+
function listRulesSubcommand() {
|
|
201
|
+
const list = new Command("list-rules");
|
|
202
|
+
list.description("List every standards rule the checker knows about, with auto-fix capability.").action(() => {
|
|
203
|
+
console.log(standards.formatRuleList());
|
|
204
|
+
});
|
|
205
|
+
return list;
|
|
206
|
+
}
|
|
207
|
+
async function collectSqlFiles(paths) {
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const p of paths) {
|
|
210
|
+
const abs = path.resolve(p);
|
|
211
|
+
try {
|
|
212
|
+
const stat = await fs.stat(abs);
|
|
213
|
+
if (stat.isFile() && abs.endsWith(".sql")) {
|
|
214
|
+
out.push(abs);
|
|
215
|
+
} else if (stat.isDirectory()) {
|
|
216
|
+
await walkDirectory(abs, out);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
async function walkDirectory(dir, accumulator) {
|
|
224
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
225
|
+
for (const e of entries) {
|
|
226
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
227
|
+
const child = path.join(dir, e.name);
|
|
228
|
+
if (e.isDirectory()) {
|
|
229
|
+
await walkDirectory(child, accumulator);
|
|
230
|
+
} else if (e.isFile() && e.name.endsWith(".sql")) {
|
|
231
|
+
accumulator.push(child);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
standardsCommand
|
|
237
|
+
};
|
|
238
|
+
//# sourceMappingURL=standards-FGJW3CQL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/standards.ts"],"sourcesContent":["/**\n * `ddt standards` — team SQL standards commands.\n *\n * Subcommands:\n * init — scaffold a starter `.ddt/standards.json` config\n * check — check SQL files against team standards (STD.6)\n * list-rules — list every rule the checker knows (STD.6)\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { catalog, standards } from '@ddt-tools/core';\n\nexport function standardsCommand(): Command {\n const cmd = new Command('standards');\n cmd.description('Team SQL standards — init, check, fix, and explain.');\n\n cmd.addCommand(initSubcommand());\n cmd.addCommand(checkSubcommand());\n cmd.addCommand(listRulesSubcommand());\n\n return cmd;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// `ddt standards init`\n// ─────────────────────────────────────────────────────────────────────\n\nfunction initSubcommand(): Command {\n const init = new Command('init');\n init\n .description(\n 'Scaffold a starter .ddt/standards.json config with sensible defaults. ' +\n 'Edit the file to match your team conventions, then run `ddt standards check`.',\n )\n .option(\n '--dir <path>',\n 'Project root directory containing or to receive .ddt/. Defaults to current directory.',\n )\n .option('--force', 'Overwrite an existing standards.json without prompting.')\n .option(\n '--preset <name>',\n 'Starter preset: \"default\" (Databricks-flavored team conventions) | \"dbt\" (dbt-community conventions — stg_/int_/fct_/dim_ prefixes + lower keyword case + dbtMode: \"on\").',\n 'default',\n )\n .action(async (opts: { dir?: string; force?: boolean; preset?: string }) => {\n const projectRoot = path.resolve(String(opts.dir ?? '.'));\n const ddtDir = path.join(projectRoot, '.ddt');\n const configPath = path.join(ddtDir, 'standards.json');\n\n // DBTC.5 — preset selector validates here so an unknown name fails\n // fast (and not later as a runtime schema error).\n const presetName = String(opts.preset ?? 'default').toLowerCase();\n if (presetName !== 'default' && presetName !== 'dbt') {\n console.error(`Unknown --preset: ${opts.preset}. Use \"default\" or \"dbt\".`);\n process.exit(1);\n return;\n }\n const preset = presetName as standards.StandardsPreset;\n\n await fs.mkdir(ddtDir, { recursive: true });\n\n if (!opts.force) {\n try {\n await fs.access(configPath);\n console.error(\n `standards.json already exists at ${configPath}.\\n` + `Pass --force to overwrite.`,\n );\n process.exit(1);\n } catch {\n // file doesn't exist — proceed\n }\n }\n\n const content = standards.scaffoldStandardsConfig(preset);\n await fs.writeFile(configPath, content, 'utf8');\n console.log(`✅ Created ${configPath}${preset !== 'default' ? ` (${preset} preset)` : ''}`);\n console.log('');\n console.log('Edit the file to match your team conventions:');\n console.log(' • naming.tablePrefixes — required table name prefixes (e.g. dim_, fct_)');\n console.log(' • naming.caseStyle — snake_case | UPPER_CASE | camelCase | none');\n console.log(' • structure.mandatoryColumns — columns every table must include');\n console.log(' • content.noSelectStar — ban SELECT * in views');\n console.log('');\n console.log('Mode values: warn | error | enforce | autofix | autofix-silent | off');\n console.log('Then run `ddt standards check` to lint your SQL surface.');\n });\n return init;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// `ddt standards check`\n// ─────────────────────────────────────────────────────────────────────\n\nfunction checkSubcommand(): Command {\n const check = new Command('check');\n check\n .description(\n 'Check SQL files against the team standards config. Without --fix this is a read-only lint pass; with --fix it rewrites files for deterministic violations (sql-format.keyword-case). Use --ai-fix to route ambiguous violations through the configured AI provider.',\n )\n .argument('[paths...]', 'SQL files or directories to check (defaults to project root)')\n .option(\n '--dir <path>',\n 'Project root containing .ddt/standards.json. Defaults to current directory.',\n )\n .option(\n '--fix',\n 'Apply autofixes (sql-format.keyword-case today; opt-in per-rule via standards.json mode = autofix or autofix-silent).',\n )\n .option(\n '--ai-fix',\n 'Suggest fixes via the configured AI provider for non-autofixable findings.',\n )\n .option(\n '--explain <rule>',\n 'Print the deep explanation for a single rule (e.g. naming.case-style).',\n )\n .option('--json', 'Emit findings as JSON instead of human-readable text.')\n .option(\n '--dbt',\n 'Force dbt-mode on: pre-mask {{ ... }} / {% ... %} Jinja blocks before running the four family checkers. Default: auto-detect.',\n )\n .option(\n '--no-dbt',\n 'Force dbt-mode off: feed Jinja text directly to the checkers (almost always wrong on dbt models).',\n )\n .action(\n async (\n paths: string[],\n opts: {\n dir?: string;\n fix?: boolean;\n aiFix?: boolean;\n explain?: string;\n json?: boolean;\n dbt?: boolean;\n },\n ) => {\n // --explain is a stand-alone diagnostic mode — short-circuit before any I/O.\n if (opts.explain) {\n const body = standards.formatRuleExplanation(opts.explain);\n if (!body) {\n console.error(\n `Unknown rule: ${opts.explain}. Run \\`ddt standards list-rules\\` for the full list.`,\n );\n process.exit(2);\n return;\n }\n console.log(body);\n return;\n }\n\n const projectRoot = path.resolve(String(opts.dir ?? '.'));\n const loadedConfig = await standards.loadStandards(projectRoot);\n if (!loadedConfig) {\n console.error(\n `No standards config found at ${path.join(projectRoot, standards.STANDARDS_CONFIG_FILENAME)}. Run \\`ddt standards init\\` first.`,\n );\n process.exit(2);\n return;\n }\n // DBTC.4 — --dbt / --no-dbt overrides config.dbtMode when present.\n const config =\n opts.dbt === true\n ? { ...loadedConfig, dbtMode: 'on' as const }\n : opts.dbt === false\n ? { ...loadedConfig, dbtMode: 'off' as const }\n : loadedConfig;\n\n const targets = paths.length > 0 ? paths : [projectRoot];\n const files = await collectSqlFiles(targets);\n if (files.length === 0) {\n console.error('No .sql files matched the given paths.');\n process.exit(2);\n return;\n }\n\n // RH4.3 — bounded-concurrency reads (cap FDs on large projects).\n // stopOnError mirrors the prior Promise.all semantics: the first\n // failed read rejects the whole pass. Under stopOnError every slot\n // is filled in input order before mapPool resolves, so the\n // undefined-narrowing below never drops a real entry.\n const { results } = await catalog.mapPool(\n files,\n async (filePath) => ({\n filePath: path.relative(projectRoot, filePath),\n sql: await fs.readFile(filePath, 'utf8'),\n }),\n { concurrency: 16, stopOnError: true },\n );\n const readFiles = results.filter(\n (r): r is { filePath: string; sql: string } => r !== undefined,\n );\n\n if (opts.fix) {\n const fixResults = standards.runMultiFileFix(readFiles, config);\n // Write rewritten files back, mapping the relative path back to absolute.\n await Promise.all(\n fixResults.map(async (r) => {\n if (!r.changed) return;\n const absolute = path.resolve(projectRoot, r.filePath);\n await fs.writeFile(absolute, r.result.fixedSql, 'utf8');\n }),\n );\n if (opts.json) {\n console.log(JSON.stringify(fixResults, null, 2));\n } else {\n console.log(standards.formatFixReport(fixResults));\n }\n // Exit non-zero if any non-autofixable findings remain — CI catches them.\n const stillBroken = fixResults.some((r) => r.result.remainingFindings.length > 0);\n process.exit(stillBroken ? 1 : 0);\n return;\n }\n\n const result = standards.runMultiFileCheck(readFiles, config);\n if (opts.aiFix) {\n await runAiFix(result, config, projectRoot);\n return;\n }\n\n if (opts.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(standards.formatCheckReport(result));\n }\n process.exit(result.blockingFiles > 0 ? 1 : 0);\n },\n );\n return check;\n}\n\nasync function runAiFix(\n result: {\n files: Array<{\n filePath: string;\n findings: Array<{\n rule: string;\n severity: string;\n message: string;\n line?: number;\n identifier?: string;\n suggestion?: string;\n family: string;\n autofix?: boolean;\n }>;\n }>;\n },\n config: Awaited<ReturnType<typeof standards.loadStandards>>,\n projectRoot: string,\n): Promise<void> {\n if (!config) return;\n const standardsCtx = standards.serializeStandardsContext({\n naming: config.naming,\n structure: config.structure,\n sqlFormat: config.sqlFormat,\n content: config.content,\n });\n let suggested = 0;\n for (const file of result.files) {\n for (const finding of file.findings) {\n if (finding.severity !== 'error' && finding.severity !== 'warning') continue;\n const absolute = path.resolve(projectRoot, file.filePath);\n const sql = await fs.readFile(absolute, 'utf8');\n try {\n const fix = await standards.aiFixStandard({\n snippet: sql,\n finding: finding as unknown as Parameters<typeof standards.aiFixStandard>[0]['finding'],\n standardsContext: standardsCtx,\n });\n console.log(`\\n${file.filePath} — ${finding.rule}:`);\n console.log(\n ` Suggested rewrite (tokens used: ${fix.usage.promptTokens}p + ${fix.usage.completionTokens}c):`,\n );\n console.log(fix.rewrittenSnippet.replace(/^/gm, ' '));\n if (fix.explanation) {\n console.log(` Explanation: ${fix.explanation}`);\n }\n suggested++;\n } catch (err: unknown) {\n console.error(` AI-fix failed: ${(err as Error).message ?? String(err)}`);\n }\n }\n }\n console.log(`\\n${suggested} AI suggestion(s) generated. Apply manually after review.`);\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// `ddt standards list-rules`\n// ─────────────────────────────────────────────────────────────────────\n\nfunction listRulesSubcommand(): Command {\n const list = new Command('list-rules');\n list\n .description('List every standards rule the checker knows about, with auto-fix capability.')\n .action(() => {\n console.log(standards.formatRuleList());\n });\n return list;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────────\n\nasync function collectSqlFiles(paths: ReadonlyArray<string>): Promise<string[]> {\n const out: string[] = [];\n for (const p of paths) {\n const abs = path.resolve(p);\n try {\n const stat = await fs.stat(abs);\n if (stat.isFile() && abs.endsWith('.sql')) {\n out.push(abs);\n } else if (stat.isDirectory()) {\n await walkDirectory(abs, out);\n }\n } catch {\n // Skip non-existent paths silently — the CLI's \"no files matched\"\n // message covers the empty case.\n }\n }\n return out;\n}\n\nasync function walkDirectory(dir: string, accumulator: string[]): Promise<void> {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n for (const e of entries) {\n if (e.name.startsWith('.') || e.name === 'node_modules') continue;\n const child = path.join(dir, e.name);\n if (e.isDirectory()) {\n await walkDirectory(child, accumulator);\n } else if (e.isFile() && e.name.endsWith('.sql')) {\n accumulator.push(child);\n }\n }\n}\n"],"mappings":";;;AAQA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,SAAS,iBAAiB;AAE5B,SAAS,mBAA4B;AAC1C,QAAM,MAAM,IAAI,QAAQ,WAAW;AACnC,MAAI,YAAY,0DAAqD;AAErE,MAAI,WAAW,eAAe,CAAC;AAC/B,MAAI,WAAW,gBAAgB,CAAC;AAChC,MAAI,WAAW,oBAAoB,CAAC;AAEpC,SAAO;AACT;AAMA,SAAS,iBAA0B;AACjC,QAAM,OAAO,IAAI,QAAQ,MAAM;AAC/B,OACG;AAAA,IACC;AAAA,EAEF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,WAAW,yDAAyD,EAC3E;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,OAAO,OAAO,SAA6D;AAC1E,UAAM,cAAc,KAAK,QAAQ,OAAO,KAAK,OAAO,GAAG,CAAC;AACxD,UAAM,SAAS,KAAK,KAAK,aAAa,MAAM;AAC5C,UAAM,aAAa,KAAK,KAAK,QAAQ,gBAAgB;AAIrD,UAAM,aAAa,OAAO,KAAK,UAAU,SAAS,EAAE,YAAY;AAChE,QAAI,eAAe,aAAa,eAAe,OAAO;AACpD,cAAQ,MAAM,qBAAqB,KAAK,MAAM,2BAA2B;AACzE,cAAQ,KAAK,CAAC;AACd;AAAA,IACF;AACA,UAAM,SAAS;AAEf,UAAM,GAAG,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AAE1C,QAAI,CAAC,KAAK,OAAO;AACf,UAAI;AACF,cAAM,GAAG,OAAO,UAAU;AAC1B,gBAAQ;AAAA,UACN,oCAAoC,UAAU;AAAA;AAAA,QAChD;AACA,gBAAQ,KAAK,CAAC;AAAA,MAChB,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,UAAM,UAAU,UAAU,wBAAwB,MAAM;AACxD,UAAM,GAAG,UAAU,YAAY,SAAS,MAAM;AAC9C,YAAQ,IAAI,kBAAa,UAAU,GAAG,WAAW,YAAY,KAAK,MAAM,aAAa,EAAE,EAAE;AACzF,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,+CAA+C;AAC3D,YAAQ,IAAI,sFAA4E;AACxF,YAAQ,IAAI,kFAAwE;AACpF,YAAQ,IAAI,6EAAmE;AAC/E,YAAQ,IAAI,6DAAmD;AAC/D,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,sEAAsE;AAClF,YAAQ,IAAI,0DAA0D;AAAA,EACxE,CAAC;AACH,SAAO;AACT;AAMA,SAAS,kBAA2B;AAClC,QAAM,QAAQ,IAAI,QAAQ,OAAO;AACjC,QACG;AAAA,IACC;AAAA,EACF,EACC,SAAS,cAAc,8DAA8D,EACrF;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,UAAU,uDAAuD,EACxE;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC,OACE,OACA,SAQG;AAEH,UAAI,KAAK,SAAS;AAChB,cAAM,OAAO,UAAU,sBAAsB,KAAK,OAAO;AACzD,YAAI,CAAC,MAAM;AACT,kBAAQ;AAAA,YACN,iBAAiB,KAAK,OAAO;AAAA,UAC/B;AACA,kBAAQ,KAAK,CAAC;AACd;AAAA,QACF;AACA,gBAAQ,IAAI,IAAI;AAChB;AAAA,MACF;AAEA,YAAM,cAAc,KAAK,QAAQ,OAAO,KAAK,OAAO,GAAG,CAAC;AACxD,YAAM,eAAe,MAAM,UAAU,cAAc,WAAW;AAC9D,UAAI,CAAC,cAAc;AACjB,gBAAQ;AAAA,UACN,gCAAgC,KAAK,KAAK,aAAa,UAAU,yBAAyB,CAAC;AAAA,QAC7F;AACA,gBAAQ,KAAK,CAAC;AACd;AAAA,MACF;AAEA,YAAM,SACJ,KAAK,QAAQ,OACT,EAAE,GAAG,cAAc,SAAS,KAAc,IAC1C,KAAK,QAAQ,QACX,EAAE,GAAG,cAAc,SAAS,MAAe,IAC3C;AAER,YAAM,UAAU,MAAM,SAAS,IAAI,QAAQ,CAAC,WAAW;AACvD,YAAM,QAAQ,MAAM,gBAAgB,OAAO;AAC3C,UAAI,MAAM,WAAW,GAAG;AACtB,gBAAQ,MAAM,wCAAwC;AACtD,gBAAQ,KAAK,CAAC;AACd;AAAA,MACF;AAOA,YAAM,EAAE,QAAQ,IAAI,MAAM,QAAQ;AAAA,QAChC;AAAA,QACA,OAAO,cAAc;AAAA,UACnB,UAAU,KAAK,SAAS,aAAa,QAAQ;AAAA,UAC7C,KAAK,MAAM,GAAG,SAAS,UAAU,MAAM;AAAA,QACzC;AAAA,QACA,EAAE,aAAa,IAAI,aAAa,KAAK;AAAA,MACvC;AACA,YAAM,YAAY,QAAQ;AAAA,QACxB,CAAC,MAA8C,MAAM;AAAA,MACvD;AAEA,UAAI,KAAK,KAAK;AACZ,cAAM,aAAa,UAAU,gBAAgB,WAAW,MAAM;AAE9D,cAAM,QAAQ;AAAA,UACZ,WAAW,IAAI,OAAO,MAAM;AAC1B,gBAAI,CAAC,EAAE,QAAS;AAChB,kBAAM,WAAW,KAAK,QAAQ,aAAa,EAAE,QAAQ;AACrD,kBAAM,GAAG,UAAU,UAAU,EAAE,OAAO,UAAU,MAAM;AAAA,UACxD,CAAC;AAAA,QACH;AACA,YAAI,KAAK,MAAM;AACb,kBAAQ,IAAI,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAAA,QACjD,OAAO;AACL,kBAAQ,IAAI,UAAU,gBAAgB,UAAU,CAAC;AAAA,QACnD;AAEA,cAAM,cAAc,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,kBAAkB,SAAS,CAAC;AAChF,gBAAQ,KAAK,cAAc,IAAI,CAAC;AAChC;AAAA,MACF;AAEA,YAAM,SAAS,UAAU,kBAAkB,WAAW,MAAM;AAC5D,UAAI,KAAK,OAAO;AACd,cAAM,SAAS,QAAQ,QAAQ,WAAW;AAC1C;AAAA,MACF;AAEA,UAAI,KAAK,MAAM;AACb,gBAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,MAC7C,OAAO;AACL,gBAAQ,IAAI,UAAU,kBAAkB,MAAM,CAAC;AAAA,MACjD;AACA,cAAQ,KAAK,OAAO,gBAAgB,IAAI,IAAI,CAAC;AAAA,IAC/C;AAAA,EACF;AACF,SAAO;AACT;AAEA,eAAe,SACb,QAeA,QACA,aACe;AACf,MAAI,CAAC,OAAQ;AACb,QAAM,eAAe,UAAU,0BAA0B;AAAA,IACvD,QAAQ,OAAO;AAAA,IACf,WAAW,OAAO;AAAA,IAClB,WAAW,OAAO;AAAA,IAClB,SAAS,OAAO;AAAA,EAClB,CAAC;AACD,MAAI,YAAY;AAChB,aAAW,QAAQ,OAAO,OAAO;AAC/B,eAAW,WAAW,KAAK,UAAU;AACnC,UAAI,QAAQ,aAAa,WAAW,QAAQ,aAAa,UAAW;AACpE,YAAM,WAAW,KAAK,QAAQ,aAAa,KAAK,QAAQ;AACxD,YAAM,MAAM,MAAM,GAAG,SAAS,UAAU,MAAM;AAC9C,UAAI;AACF,cAAM,MAAM,MAAM,UAAU,cAAc;AAAA,UACxC,SAAS;AAAA,UACT;AAAA,UACA,kBAAkB;AAAA,QACpB,CAAC;AACD,gBAAQ,IAAI;AAAA,EAAK,KAAK,QAAQ,WAAM,QAAQ,IAAI,GAAG;AACnD,gBAAQ;AAAA,UACN,qCAAqC,IAAI,MAAM,YAAY,OAAO,IAAI,MAAM,gBAAgB;AAAA,QAC9F;AACA,gBAAQ,IAAI,IAAI,iBAAiB,QAAQ,OAAO,MAAM,CAAC;AACvD,YAAI,IAAI,aAAa;AACnB,kBAAQ,IAAI,kBAAkB,IAAI,WAAW,EAAE;AAAA,QACjD;AACA;AAAA,MACF,SAAS,KAAc;AACrB,gBAAQ,MAAM,oBAAqB,IAAc,WAAW,OAAO,GAAG,CAAC,EAAE;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AACA,UAAQ,IAAI;AAAA,EAAK,SAAS,2DAA2D;AACvF;AAMA,SAAS,sBAA+B;AACtC,QAAM,OAAO,IAAI,QAAQ,YAAY;AACrC,OACG,YAAY,8EAA8E,EAC1F,OAAO,MAAM;AACZ,YAAQ,IAAI,UAAU,eAAe,CAAC;AAAA,EACxC,CAAC;AACH,SAAO;AACT;AAMA,eAAe,gBAAgB,OAAiD;AAC9E,QAAM,MAAgB,CAAC;AACvB,aAAW,KAAK,OAAO;AACrB,UAAM,MAAM,KAAK,QAAQ,CAAC;AAC1B,QAAI;AACF,YAAM,OAAO,MAAM,GAAG,KAAK,GAAG;AAC9B,UAAI,KAAK,OAAO,KAAK,IAAI,SAAS,MAAM,GAAG;AACzC,YAAI,KAAK,GAAG;AAAA,MACd,WAAW,KAAK,YAAY,GAAG;AAC7B,cAAM,cAAc,KAAK,GAAG;AAAA,MAC9B;AAAA,IACF,QAAQ;AAAA,IAGR;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,cAAc,KAAa,aAAsC;AAC9E,QAAM,UAAU,MAAM,GAAG,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAC7D,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,KAAK,WAAW,GAAG,KAAK,EAAE,SAAS,eAAgB;AACzD,UAAM,QAAQ,KAAK,KAAK,KAAK,EAAE,IAAI;AACnC,QAAI,EAAE,YAAY,GAAG;AACnB,YAAM,cAAc,OAAO,WAAW;AAAA,IACxC,WAAW,EAAE,OAAO,KAAK,EAAE,KAAK,SAAS,MAAM,GAAG;AAChD,kBAAY,KAAK,KAAK;AAAA,IACxB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/suggest.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import {
|
|
8
|
+
loadProject,
|
|
9
|
+
pac,
|
|
10
|
+
parseProjectModel,
|
|
11
|
+
schema
|
|
12
|
+
} from "@ddt-tools/core";
|
|
13
|
+
function suggestCommand() {
|
|
14
|
+
const cmd = new Command("suggest");
|
|
15
|
+
cmd.description(
|
|
16
|
+
"Suggest FK / PK / UK / composite-PK candidates with reasoning. Output is Markdown by default."
|
|
17
|
+
).requiredOption("--source <path>", ".ddtproj or .ddtpac to analyze.").option("--format <fmt>", "markdown | json. Default markdown.", "markdown").option("-o, --out <path>", "Output file. Defaults to stdout.").action(async (opts) => {
|
|
18
|
+
const model = await loadModel(String(opts.source));
|
|
19
|
+
const report = schema.suggestForModel(model);
|
|
20
|
+
const fmt = String(opts.format ?? "markdown").toLowerCase();
|
|
21
|
+
const text = fmt === "json" ? JSON.stringify(report, null, 2) : schema.formatSuggestionReport(report);
|
|
22
|
+
if (opts.out) {
|
|
23
|
+
const p = path.resolve(String(opts.out));
|
|
24
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
25
|
+
await fs.writeFile(p, text + (text.endsWith("\n") ? "" : "\n"), "utf8");
|
|
26
|
+
console.error(`Wrote ${p} (${report.totalSuggestions} suggestion(s)).`);
|
|
27
|
+
} else {
|
|
28
|
+
process.stdout.write(text + (text.endsWith("\n") ? "" : "\n"));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return cmd;
|
|
32
|
+
}
|
|
33
|
+
async function loadModel(sourcePath) {
|
|
34
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
35
|
+
const c = await pac.readPac(sourcePath);
|
|
36
|
+
return c.model;
|
|
37
|
+
}
|
|
38
|
+
const loaded = await loadProject(sourcePath);
|
|
39
|
+
return await parseProjectModel(loaded);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
suggestCommand
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=suggest-V3LVIFZ5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/suggest.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n loadProject,\n pac,\n parseProjectModel,\n schema,\n type DatabricksObject,\n} from '@ddt-tools/core';\n\n/**\n * `ddt suggest` — propose FK / PK / UK / composite-PK candidates\n * with reasoning. Mirrors `sdt suggest`.\n */\nexport function suggestCommand(): Command {\n const cmd = new Command('suggest');\n cmd\n .description(\n 'Suggest FK / PK / UK / composite-PK candidates with reasoning. Output is Markdown by default.',\n )\n .requiredOption('--source <path>', '.ddtproj or .ddtpac to analyze.')\n .option('--format <fmt>', 'markdown | json. Default markdown.', 'markdown')\n .option('-o, --out <path>', 'Output file. Defaults to stdout.')\n .action(async (opts) => {\n const model = await loadModel(String(opts.source));\n const report = schema.suggestForModel(model);\n const fmt = String(opts.format ?? 'markdown').toLowerCase();\n const text =\n fmt === 'json' ? JSON.stringify(report, null, 2) : schema.formatSuggestionReport(report);\n if (opts.out) {\n const p = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, text + (text.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${p} (${report.totalSuggestions} suggestion(s)).`);\n } else {\n process.stdout.write(text + (text.endsWith('\\n') ? '' : '\\n'));\n }\n });\n return cmd;\n}\n\nasync function loadModel(sourcePath: string): Promise<DatabricksObject[]> {\n if (sourcePath.endsWith('.ddtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await loadProject(sourcePath);\n return await parseProjectModel(loaded);\n}\n"],"mappings":";;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAMA,SAAS,iBAA0B;AACxC,QAAM,MAAM,IAAI,QAAQ,SAAS;AACjC,MACG;AAAA,IACC;AAAA,EACF,EACC,eAAe,mBAAmB,iCAAiC,EACnE,OAAO,kBAAkB,sCAAsC,UAAU,EACzE,OAAO,oBAAoB,kCAAkC,EAC7D,OAAO,OAAO,SAAS;AACtB,UAAM,QAAQ,MAAM,UAAU,OAAO,KAAK,MAAM,CAAC;AACjD,UAAM,SAAS,OAAO,gBAAgB,KAAK;AAC3C,UAAM,MAAM,OAAO,KAAK,UAAU,UAAU,EAAE,YAAY;AAC1D,UAAM,OACJ,QAAQ,SAAS,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,OAAO,uBAAuB,MAAM;AACzF,QAAI,KAAK,KAAK;AACZ,YAAM,IAAI,KAAK,QAAQ,OAAO,KAAK,GAAG,CAAC;AACvC,YAAM,GAAG,MAAM,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACnD,YAAM,GAAG,UAAU,GAAG,QAAQ,KAAK,SAAS,IAAI,IAAI,KAAK,OAAO,MAAM;AACtE,cAAQ,MAAM,SAAS,CAAC,KAAK,OAAO,gBAAgB,kBAAkB;AAAA,IACxE,OAAO;AACL,cAAQ,OAAO,MAAM,QAAQ,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK;AAAA,IAC/D;AAAA,EACF,CAAC;AACH,SAAO;AACT;AAEA,eAAe,UAAU,YAAiD;AACxE,MAAI,WAAW,SAAS,SAAS,GAAG;AAClC,UAAM,IAAI,MAAM,IAAI,QAAQ,UAAU;AACtC,WAAO,EAAE;AAAA,EACX;AACA,QAAM,SAAS,MAAM,YAAY,UAAU;AAC3C,SAAO,MAAM,kBAAkB,MAAM;AACvC;","names":[]}
|