@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,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addMappingFlags,
|
|
3
|
+
buildMappingFromOptions
|
|
4
|
+
} from "./chunk-2FT6HXKS.js";
|
|
5
|
+
import "./chunk-DGUM43GV.js";
|
|
6
|
+
|
|
7
|
+
// src/commands/script.ts
|
|
8
|
+
import { promises as fs } from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import {
|
|
12
|
+
CompareEngine,
|
|
13
|
+
PacSource,
|
|
14
|
+
ProjectSource,
|
|
15
|
+
ScriptGenerator,
|
|
16
|
+
colorizeMigrationScript,
|
|
17
|
+
mergeDeployOptions,
|
|
18
|
+
renderHtmlReport,
|
|
19
|
+
safety,
|
|
20
|
+
loadProject,
|
|
21
|
+
pac as pacNs
|
|
22
|
+
} from "@ddt-tools/core";
|
|
23
|
+
function scriptCommand() {
|
|
24
|
+
const cmd = new Command("script");
|
|
25
|
+
cmd.description(
|
|
26
|
+
"Generate a deploy SQL script for source \u2192 target. Always offline \u2014 does not touch the workspace."
|
|
27
|
+
).requiredOption("--source <path>", "Source: .ddtproj or .ddtpac (the desired state).").requiredOption("--target <path>", "Target: .ddtproj or .ddtpac (the current state).").option("-o, --out <path>", "Write the script to <path>. Defaults to stdout.").option("--format <fmt>", "sql | json", "sql").option("--variables <kv>", "Comma-separated KEY=VALUE pairs for $(VAR) substitution.").option(
|
|
28
|
+
"--profile <name>",
|
|
29
|
+
"VARSYNTAX.5 \u2014 look up `deploymentProfiles[<name>].variables` from the --source artifact (`.ddtproj` reads `project.deploymentProfiles`; `.ddtpac` reads `manifest.deploymentProfiles`) and substitute `$(VAR)` references at script-emission time. `--variables` overrides on key collision."
|
|
30
|
+
).option("--ignore-case", "Compare object FQNs case-insensitively.", false).option("--banner <text>", "Banner line prepended to the script.").option("--report-html <path>", "Also write a self-contained HTML compare-report to <path>.").option(
|
|
31
|
+
"--color <mode>",
|
|
32
|
+
"Color stdout: auto | always | never. Honors NO_COLOR / DDT_NO_COLOR env vars in auto mode. Files written via -o are never colorized.",
|
|
33
|
+
"auto"
|
|
34
|
+
);
|
|
35
|
+
addMappingFlags(cmd);
|
|
36
|
+
cmd.action(async (opts) => {
|
|
37
|
+
const format = String(opts.format ?? "sql").toLowerCase();
|
|
38
|
+
if (format !== "sql" && format !== "json") {
|
|
39
|
+
throw new Error(`Unknown --format: ${opts.format}. Use sql or json.`);
|
|
40
|
+
}
|
|
41
|
+
const nameMapping = await buildMappingFromOptions(opts);
|
|
42
|
+
const source = await sourceFor(String(opts.source), "source");
|
|
43
|
+
const target = await sourceFor(String(opts.target), "target");
|
|
44
|
+
const engine = new CompareEngine();
|
|
45
|
+
const result = await engine.compare(source, target, {
|
|
46
|
+
ignoreCase: !!opts.ignoreCase,
|
|
47
|
+
...nameMapping ? { nameMapping } : {}
|
|
48
|
+
});
|
|
49
|
+
const deployment = mergeDeployOptions().deployment;
|
|
50
|
+
const cliVariables = parseVariables(opts.variables);
|
|
51
|
+
let profileVariables;
|
|
52
|
+
let ctxProjectName;
|
|
53
|
+
let ctxProjectVersion;
|
|
54
|
+
const sourcePath = String(opts.source);
|
|
55
|
+
if (opts.profile || sourcePath.endsWith(".ddtproj") || sourcePath.endsWith(".ddtpac")) {
|
|
56
|
+
try {
|
|
57
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
58
|
+
const srcPac = await pacNs.readPac(sourcePath);
|
|
59
|
+
ctxProjectName = srcPac.manifest.projectName;
|
|
60
|
+
ctxProjectVersion = srcPac.manifest.projectVersion;
|
|
61
|
+
if (opts.profile) {
|
|
62
|
+
const block = srcPac.manifest.deploymentProfiles?.[String(opts.profile)];
|
|
63
|
+
if (!block) {
|
|
64
|
+
const available = Object.keys(srcPac.manifest.deploymentProfiles ?? {});
|
|
65
|
+
throw new Error(
|
|
66
|
+
`--profile ${String(opts.profile)}: no deploymentProfile by that name in the --source pac manifest. ` + (available.length === 0 ? "The pac carries no deploymentProfiles (was it built before VARSYNTAX.2, or does the .ddtproj declare any?)." : `Available: ${available.join(", ")}.`)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (block.variables) profileVariables = { ...block.variables };
|
|
70
|
+
}
|
|
71
|
+
} else if (sourcePath.endsWith(".ddtproj")) {
|
|
72
|
+
const loaded = await loadProject(sourcePath);
|
|
73
|
+
ctxProjectName = loaded.project.name;
|
|
74
|
+
ctxProjectVersion = loaded.project.version;
|
|
75
|
+
if (opts.profile) {
|
|
76
|
+
const block = loaded.project.deploymentProfiles?.[String(opts.profile)];
|
|
77
|
+
if (!block) {
|
|
78
|
+
const available = Object.keys(loaded.project.deploymentProfiles ?? {});
|
|
79
|
+
throw new Error(
|
|
80
|
+
`--profile ${String(opts.profile)}: no deploymentProfile by that name in the --source .ddtproj. ` + (available.length === 0 ? "The project declares no deploymentProfiles." : `Available: ${available.join(", ")}.`)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (block.variables) profileVariables = { ...block.variables };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (opts.profile) throw e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const variables = profileVariables || cliVariables ? { ...profileVariables ?? {}, ...cliVariables ?? {} } : void 0;
|
|
91
|
+
const generator = new ScriptGenerator();
|
|
92
|
+
const script = generator.generate(result, {
|
|
93
|
+
deployment,
|
|
94
|
+
variables,
|
|
95
|
+
...opts.banner ? { banner: String(opts.banner) } : {},
|
|
96
|
+
context: {
|
|
97
|
+
...opts.profile ? { profile: String(opts.profile) } : {},
|
|
98
|
+
...ctxProjectName ? { projectName: ctxProjectName } : {},
|
|
99
|
+
...ctxProjectVersion ? { projectVersion: ctxProjectVersion } : {}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const assessment = safety.assess(result);
|
|
103
|
+
let payload;
|
|
104
|
+
if (format === "json") {
|
|
105
|
+
payload = JSON.stringify(
|
|
106
|
+
{
|
|
107
|
+
source: result.source,
|
|
108
|
+
target: result.target,
|
|
109
|
+
summary: { ...result.summary, ...script.summary },
|
|
110
|
+
safety: assessment,
|
|
111
|
+
statements: script.statements
|
|
112
|
+
},
|
|
113
|
+
null,
|
|
114
|
+
2
|
|
115
|
+
);
|
|
116
|
+
} else {
|
|
117
|
+
const header = [
|
|
118
|
+
`-- ${assessment.blocked ? "BLOCKED: " + assessment.blockReason : "Generated by ddt script"}`,
|
|
119
|
+
`-- unrecoverable=${assessment.unrecoverable.length} destructive=${assessment.destructive.length} expensive=${assessment.expensive.length} warnings=${assessment.warnings.length}`,
|
|
120
|
+
""
|
|
121
|
+
].join("\n");
|
|
122
|
+
payload = header + script.sql;
|
|
123
|
+
}
|
|
124
|
+
if (opts.reportHtml) {
|
|
125
|
+
const html = renderHtmlReport(result, {
|
|
126
|
+
title: `Compare ${result.source.label} \u2192 ${result.target.label}`,
|
|
127
|
+
safety: assessment,
|
|
128
|
+
generatedSql: script.sql
|
|
129
|
+
});
|
|
130
|
+
const htmlPath = path.resolve(String(opts.reportHtml));
|
|
131
|
+
await fs.mkdir(path.dirname(htmlPath), { recursive: true });
|
|
132
|
+
await fs.writeFile(htmlPath, html, "utf8");
|
|
133
|
+
console.error(`Wrote ${htmlPath} (${html.length} bytes).`);
|
|
134
|
+
}
|
|
135
|
+
if (opts.out) {
|
|
136
|
+
const outPath = path.resolve(String(opts.out));
|
|
137
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
138
|
+
await fs.writeFile(outPath, payload + (payload.endsWith("\n") ? "" : "\n"), "utf8");
|
|
139
|
+
console.error(`Wrote ${outPath} (${payload.length} bytes).`);
|
|
140
|
+
} else {
|
|
141
|
+
const colorMode = opts.color ?? "auto";
|
|
142
|
+
const rendered = format === "sql" ? colorizeMigrationScript(payload, { mode: colorMode }) : payload;
|
|
143
|
+
process.stdout.write(rendered);
|
|
144
|
+
if (!rendered.endsWith("\n")) process.stdout.write("\n");
|
|
145
|
+
}
|
|
146
|
+
if (assessment.blocked) {
|
|
147
|
+
process.exitCode = 2;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return cmd;
|
|
151
|
+
}
|
|
152
|
+
async function sourceFor(filePath, label) {
|
|
153
|
+
return filePath.endsWith(".ddtpac") ? new PacSource(filePath, label) : new ProjectSource(filePath, label);
|
|
154
|
+
}
|
|
155
|
+
function parseVariables(raw) {
|
|
156
|
+
if (!raw) return void 0;
|
|
157
|
+
const out = {};
|
|
158
|
+
for (const pair of String(raw).split(",")) {
|
|
159
|
+
const [k, v] = pair.split("=");
|
|
160
|
+
if (k && v !== void 0) out[k.trim()] = v.trim();
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
scriptCommand
|
|
166
|
+
};
|
|
167
|
+
//# sourceMappingURL=script-BMYVBHFR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/script.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n CompareEngine,\n PacSource,\n ProjectSource,\n ScriptGenerator,\n colorizeMigrationScript,\n mergeDeployOptions,\n renderHtmlReport,\n safety,\n loadProject,\n pac as pacNs,\n type CompareSource,\n type DeploymentProfile,\n} from '@ddt-tools/core';\nimport { addMappingFlags, buildMappingFromOptions } from '../util/mapping.js';\n\n/**\n * `ddt script` — generate a deploy SQL script the user can review or edit\n * before applying. Mirrors SqlPackage `/Action:Script` / SSDT's \"Generate\n * Script\" affordance.\n *\n * Output formats:\n * - sql (default): human-readable migration script with banner comments\n * - json: the raw CompareResult + safety assessment + per-statement\n * metadata, suitable for piping into a CI gate or a UI\n *\n * The script writer never touches the workspace. To apply, the user runs the\n * script through their own SQL client or calls `ddt publish --apply` (v0.4).\n */\nexport function scriptCommand(): Command {\n const cmd = new Command('script');\n cmd\n .description(\n 'Generate a deploy SQL script for source → target. Always offline — does not touch the workspace.',\n )\n .requiredOption('--source <path>', 'Source: .ddtproj or .ddtpac (the desired state).')\n .requiredOption('--target <path>', 'Target: .ddtproj or .ddtpac (the current state).')\n .option('-o, --out <path>', 'Write the script to <path>. Defaults to stdout.')\n .option('--format <fmt>', 'sql | json', 'sql')\n .option('--variables <kv>', 'Comma-separated KEY=VALUE pairs for $(VAR) substitution.')\n .option(\n '--profile <name>',\n 'VARSYNTAX.5 — look up `deploymentProfiles[<name>].variables` from the --source artifact (`.ddtproj` reads `project.deploymentProfiles`; `.ddtpac` reads `manifest.deploymentProfiles`) and substitute `$(VAR)` references at script-emission time. `--variables` overrides on key collision.',\n )\n .option('--ignore-case', 'Compare object FQNs case-insensitively.', false)\n .option('--banner <text>', 'Banner line prepended to the script.')\n .option('--report-html <path>', 'Also write a self-contained HTML compare-report to <path>.')\n .option(\n '--color <mode>',\n 'Color stdout: auto | always | never. Honors NO_COLOR / DDT_NO_COLOR env vars in auto mode. Files written via -o are never colorized.',\n 'auto',\n );\n addMappingFlags(cmd);\n cmd.action(async (opts) => {\n const format = String(opts.format ?? 'sql').toLowerCase();\n if (format !== 'sql' && format !== 'json') {\n throw new Error(`Unknown --format: ${opts.format}. Use sql or json.`);\n }\n const nameMapping = await buildMappingFromOptions(opts);\n const source = await sourceFor(String(opts.source), 'source');\n const target = await sourceFor(String(opts.target), 'target');\n const engine = new CompareEngine();\n const result = await engine.compare(source, target, {\n ignoreCase: !!opts.ignoreCase,\n ...(nameMapping ? { nameMapping } : {}),\n });\n\n const deployment = mergeDeployOptions().deployment;\n const cliVariables = parseVariables(opts.variables);\n\n // VARSYNTAX.5 — resolve --profile <name> from the --source artifact's\n // deploymentProfiles bag (if present), merge with --variables CLI\n // override (CLI wins on key collision). Also gather the project context\n // (name / version) so VARSYNTAX.3 built-ins (`$(DDT_PROFILE)`,\n // `$(DDT_PROJECT_NAME)`, `$(DDT_PROJECT_VERSION)`, …) auto-populate.\n let profileVariables: Record<string, string> | undefined;\n let ctxProjectName: string | undefined;\n let ctxProjectVersion: string | undefined;\n const sourcePath = String(opts.source);\n if (opts.profile || sourcePath.endsWith('.ddtproj') || sourcePath.endsWith('.ddtpac')) {\n try {\n if (sourcePath.endsWith('.ddtpac')) {\n const srcPac = await pacNs.readPac(sourcePath);\n ctxProjectName = srcPac.manifest.projectName;\n ctxProjectVersion = srcPac.manifest.projectVersion;\n if (opts.profile) {\n const block: DeploymentProfile | undefined =\n srcPac.manifest.deploymentProfiles?.[String(opts.profile)];\n if (!block) {\n const available = Object.keys(srcPac.manifest.deploymentProfiles ?? {});\n throw new Error(\n `--profile ${String(opts.profile)}: no deploymentProfile by that name in the --source pac manifest. ` +\n (available.length === 0\n ? 'The pac carries no deploymentProfiles (was it built before VARSYNTAX.2, or does the .ddtproj declare any?).'\n : `Available: ${available.join(', ')}.`),\n );\n }\n if (block.variables) profileVariables = { ...block.variables };\n }\n } else if (sourcePath.endsWith('.ddtproj')) {\n const loaded = await loadProject(sourcePath);\n ctxProjectName = loaded.project.name;\n ctxProjectVersion = loaded.project.version;\n if (opts.profile) {\n const block: DeploymentProfile | undefined =\n loaded.project.deploymentProfiles?.[String(opts.profile)];\n if (!block) {\n const available = Object.keys(loaded.project.deploymentProfiles ?? {});\n throw new Error(\n `--profile ${String(opts.profile)}: no deploymentProfile by that name in the --source .ddtproj. ` +\n (available.length === 0\n ? 'The project declares no deploymentProfiles.'\n : `Available: ${available.join(', ')}.`),\n );\n }\n if (block.variables) profileVariables = { ...block.variables };\n }\n }\n } catch (e) {\n if (opts.profile) throw e;\n }\n }\n const variables =\n profileVariables || cliVariables\n ? { ...(profileVariables ?? {}), ...(cliVariables ?? {}) }\n : undefined;\n\n const generator = new ScriptGenerator();\n const script = generator.generate(result, {\n deployment,\n variables,\n ...(opts.banner ? { banner: String(opts.banner) } : {}),\n context: {\n ...(opts.profile ? { profile: String(opts.profile) } : {}),\n ...(ctxProjectName ? { projectName: ctxProjectName } : {}),\n ...(ctxProjectVersion ? { projectVersion: ctxProjectVersion } : {}),\n },\n });\n const assessment = safety.assess(result);\n\n let payload: string;\n if (format === 'json') {\n payload = JSON.stringify(\n {\n source: result.source,\n target: result.target,\n summary: { ...result.summary, ...script.summary },\n safety: assessment,\n statements: script.statements,\n },\n null,\n 2,\n );\n } else {\n const header = [\n `-- ${assessment.blocked ? 'BLOCKED: ' + assessment.blockReason : 'Generated by ddt script'}`,\n `-- unrecoverable=${assessment.unrecoverable.length} destructive=${assessment.destructive.length} expensive=${assessment.expensive.length} warnings=${assessment.warnings.length}`,\n '',\n ].join('\\n');\n payload = header + script.sql;\n }\n\n if (opts.reportHtml) {\n const html = renderHtmlReport(result, {\n title: `Compare ${result.source.label} → ${result.target.label}`,\n safety: assessment,\n generatedSql: script.sql,\n });\n const htmlPath = path.resolve(String(opts.reportHtml));\n await fs.mkdir(path.dirname(htmlPath), { recursive: true });\n await fs.writeFile(htmlPath, html, 'utf8');\n console.error(`Wrote ${htmlPath} (${html.length} bytes).`);\n }\n\n if (opts.out) {\n const outPath = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, payload + (payload.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${outPath} (${payload.length} bytes).`);\n } else {\n // Colorize stdout for SQL only — JSON stays raw so downstream tools can\n // parse it. ANSI escapes are stripped when stdout is not a TTY (auto mode).\n const colorMode = (opts.color as 'auto' | 'always' | 'never' | undefined) ?? 'auto';\n const rendered =\n format === 'sql' ? colorizeMigrationScript(payload, { mode: colorMode }) : payload;\n process.stdout.write(rendered);\n if (!rendered.endsWith('\\n')) process.stdout.write('\\n');\n }\n if (assessment.blocked) {\n process.exitCode = 2;\n }\n });\n return cmd;\n}\n\nasync function sourceFor(filePath: string, label: string): Promise<CompareSource> {\n return filePath.endsWith('.ddtpac')\n ? new PacSource(filePath, label)\n : new ProjectSource(filePath, label);\n}\n\nfunction parseVariables(raw: unknown): Record<string, string> | undefined {\n if (!raw) return undefined;\n const out: Record<string, string> = {};\n for (const pair of String(raw).split(',')) {\n const [k, v] = pair.split('=');\n if (k && v !== undefined) out[k.trim()] = v.trim();\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/search.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { catalog } from "@ddt-tools/core";
|
|
9
|
+
function buildDriftReport(results) {
|
|
10
|
+
const allFqns = /* @__PURE__ */ new Set();
|
|
11
|
+
const byFqn = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const r of results) {
|
|
13
|
+
if (r.error) continue;
|
|
14
|
+
for (const m of r.matches) {
|
|
15
|
+
allFqns.add(m.fqn);
|
|
16
|
+
if (!byFqn.has(m.fqn)) byFqn.set(m.fqn, /* @__PURE__ */ new Set());
|
|
17
|
+
byFqn.get(m.fqn).add(r.profile);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const successProfiles = results.filter((r) => !r.error).map((r) => r.profile);
|
|
21
|
+
const driftLines = [];
|
|
22
|
+
for (const fqn of allFqns) {
|
|
23
|
+
const present = byFqn.get(fqn);
|
|
24
|
+
if (present.size < successProfiles.length) {
|
|
25
|
+
const absent = successProfiles.filter((p) => !present.has(p));
|
|
26
|
+
driftLines.push(
|
|
27
|
+
` DRIFT ${fqn} [present: ${[...present].join(", ")} absent: ${absent.join(", ")}]`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return driftLines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
function renderText(results, showDrift) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
for (const r of results) {
|
|
36
|
+
if (r.error) {
|
|
37
|
+
lines.push(`[${r.profile}] ERROR: ${r.error}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (r.matches.length === 0) {
|
|
41
|
+
lines.push(`[${r.profile}] no matches`);
|
|
42
|
+
} else {
|
|
43
|
+
for (const m of r.matches) {
|
|
44
|
+
lines.push(`[${r.profile}] ${m.fqn} (${m.objectType})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (showDrift) {
|
|
49
|
+
const drift = buildDriftReport(results);
|
|
50
|
+
if (drift) {
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push("--- Drift detected ---");
|
|
53
|
+
lines.push(drift);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
async function loadProfileNames(profilesOpt, profilesDir) {
|
|
59
|
+
if (profilesOpt.toLowerCase() !== "all") {
|
|
60
|
+
return profilesOpt.split(",").map((p) => p.trim()).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const entries = await fs.readdir(profilesDir);
|
|
64
|
+
return entries.filter((e) => e.endsWith(".json") || e.endsWith(".toml")).map((e) => path.basename(e, path.extname(e)));
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function defaultSearchFn(profile, pattern, opts) {
|
|
70
|
+
const root = opts.root ?? process.cwd();
|
|
71
|
+
const cache = new catalog.CatalogCache({ root, connection: profile });
|
|
72
|
+
const snapshot = await cache.get();
|
|
73
|
+
if (snapshot.catalogs.length === 0) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`No catalog cache for profile "${profile}" at ${cache.path}. Run \`ddt catalog refresh --connection ${profile} --root ${root}\` or \`ddt extract --connection ${profile} --output ${root} --write-catalog-cache\` first.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const needle = pattern.toLowerCase();
|
|
79
|
+
const typeFilter = opts.objectType ? opts.objectType.toUpperCase() : void 0;
|
|
80
|
+
const matches = [];
|
|
81
|
+
for (const cat of snapshot.catalogs) {
|
|
82
|
+
for (const schema of cat.schemas) {
|
|
83
|
+
for (const obj of schema.objects) {
|
|
84
|
+
if (typeFilter && obj.objectType !== typeFilter) continue;
|
|
85
|
+
const name = obj.name.toLowerCase();
|
|
86
|
+
const hit = opts.exact ? name === needle : name.includes(needle);
|
|
87
|
+
if (hit) {
|
|
88
|
+
matches.push({
|
|
89
|
+
profile,
|
|
90
|
+
fqn: `${obj.catalog}.${obj.schema}.${obj.name}`,
|
|
91
|
+
objectType: obj.objectType
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return matches;
|
|
98
|
+
}
|
|
99
|
+
function searchCommand(searchFn = defaultSearchFn) {
|
|
100
|
+
const cmd = new Command("search");
|
|
101
|
+
cmd.description("Find objects matching a name pattern across one or more connection profiles.").argument("<pattern>", "Name pattern to search for (case-insensitive substring).").requiredOption(
|
|
102
|
+
"--profiles <list|all>",
|
|
103
|
+
'Comma-separated profile names, or "all" to scan every configured profile.'
|
|
104
|
+
).option("--exact", "Exact name match instead of substring.", false).option("--type <objectType>", "Filter results to a specific object type (e.g. TABLE, VIEW).").option("--format <fmt>", "text | json (default text).", "text").option(
|
|
105
|
+
"--profiles-dir <dir>",
|
|
106
|
+
"Directory to scan when --profiles all is used.",
|
|
107
|
+
path.join(os.homedir(), ".ddt", "connections")
|
|
108
|
+
).option(
|
|
109
|
+
"--root <dir>",
|
|
110
|
+
"Project / cache root containing the .ddt/cache directory. Default: cwd.",
|
|
111
|
+
process.cwd()
|
|
112
|
+
).option("--no-drift", "Suppress the drift section in text output.").action(async (pattern, opts) => {
|
|
113
|
+
const profiles = await loadProfileNames(opts.profiles, opts.profilesDir);
|
|
114
|
+
if (profiles.length === 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
opts.profiles.toLowerCase() === "all" ? `No profiles found in ${opts.profilesDir}. Run \`ddt connection add\` first.` : "--profiles must list at least one profile."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const results = await Promise.all(
|
|
120
|
+
profiles.map(async (p) => {
|
|
121
|
+
try {
|
|
122
|
+
const matches = await searchFn(p, pattern, {
|
|
123
|
+
exact: opts.exact,
|
|
124
|
+
objectType: opts.type,
|
|
125
|
+
root: opts.root
|
|
126
|
+
});
|
|
127
|
+
return { profile: p, matches };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return {
|
|
130
|
+
profile: p,
|
|
131
|
+
matches: [],
|
|
132
|
+
error: err instanceof Error ? err.message : String(err)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
const fmt = String(opts.format ?? "text").toLowerCase();
|
|
138
|
+
if (fmt === "json") {
|
|
139
|
+
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (fmt !== "text") throw new Error(`Unknown --format: ${opts.format}. Use text | json.`);
|
|
143
|
+
const text = renderText(results, opts.drift !== false);
|
|
144
|
+
process.stdout.write(text ? text + "\n" : "No results.\n");
|
|
145
|
+
});
|
|
146
|
+
return cmd;
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
searchCommand
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=search-TA3C3AZT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/search.ts"],"sourcesContent":["/**\n * `ddt search <pattern> --profiles <list|all>` — AUTH.4.\n *\n * Finds objects whose name matches a pattern across one or more connection\n * profiles. Surfaces presence/absence drift: if an object exists in some\n * profiles but not others, those profiles are flagged.\n *\n * The search is case-insensitive substring match by default; --exact\n * switches to exact name match; --type filters by objectType.\n *\n * Mirrors `Snowflake/packages/cli/src/commands/search.ts`.\n */\nimport { Command } from 'commander';\nimport { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { catalog } from '@ddt-tools/core';\n\nexport interface SearchMatch {\n profile: string;\n fqn: string;\n objectType: string;\n}\n\nexport interface SearchResult {\n profile: string;\n matches: SearchMatch[];\n error?: string;\n}\n\nexport interface SearchFnOptions {\n exact: boolean;\n objectType?: string;\n /** Project / cache root. Defaults to `process.cwd()`. */\n root?: string;\n}\n\nexport type SearchFn = (\n profile: string,\n pattern: string,\n opts: SearchFnOptions,\n) => Promise<SearchMatch[]>;\n\nfunction buildDriftReport(results: SearchResult[]): string {\n const allFqns = new Set<string>();\n const byFqn = new Map<string, Set<string>>();\n for (const r of results) {\n if (r.error) continue;\n for (const m of r.matches) {\n allFqns.add(m.fqn);\n if (!byFqn.has(m.fqn)) byFqn.set(m.fqn, new Set());\n byFqn.get(m.fqn)!.add(r.profile);\n }\n }\n const successProfiles = results.filter((r) => !r.error).map((r) => r.profile);\n const driftLines: string[] = [];\n for (const fqn of allFqns) {\n const present = byFqn.get(fqn)!;\n if (present.size < successProfiles.length) {\n const absent = successProfiles.filter((p) => !present.has(p));\n driftLines.push(\n ` DRIFT ${fqn} [present: ${[...present].join(', ')} absent: ${absent.join(', ')}]`,\n );\n }\n }\n return driftLines.join('\\n');\n}\n\nfunction renderText(results: SearchResult[], showDrift: boolean): string {\n const lines: string[] = [];\n for (const r of results) {\n if (r.error) {\n lines.push(`[${r.profile}] ERROR: ${r.error}`);\n continue;\n }\n if (r.matches.length === 0) {\n lines.push(`[${r.profile}] no matches`);\n } else {\n for (const m of r.matches) {\n lines.push(`[${r.profile}] ${m.fqn} (${m.objectType})`);\n }\n }\n }\n if (showDrift) {\n const drift = buildDriftReport(results);\n if (drift) {\n lines.push('');\n lines.push('--- Drift detected ---');\n lines.push(drift);\n }\n }\n return lines.join('\\n');\n}\n\nasync function loadProfileNames(profilesOpt: string, profilesDir: string): Promise<string[]> {\n if (profilesOpt.toLowerCase() !== 'all') {\n return profilesOpt\n .split(',')\n .map((p) => p.trim())\n .filter(Boolean);\n }\n try {\n const entries = await fs.readdir(profilesDir);\n return entries\n .filter((e) => e.endsWith('.json') || e.endsWith('.toml'))\n .map((e) => path.basename(e, path.extname(e)));\n } catch {\n return [];\n }\n}\n\n/**\n * Wire to the on-disk catalog cache (`<root>/.ddt/cache/<profile>/catalog.msgpack`\n * written by `ddt catalog refresh` / `ddt extract --write-catalog-cache`).\n *\n * 1. Open `catalog.CatalogCache({root, connection: profile})`.\n * 2. Read the snapshot — `get()` returns an empty snapshot when the\n * file is absent / corrupt / version-mismatched.\n * 3. If the snapshot is empty, surface an actionable error naming\n * the expected file path and the two commands that populate it.\n * 4. Walk every (catalog, schema, object), filter by `--type`, then\n * match the name (case-insensitive substring by default, exact\n * when `--exact` is set), and return `SearchMatch[]` with the\n * fully-qualified `catalog.schema.name`.\n *\n * Injectable for tests via the `searchFn` parameter on `searchCommand`.\n */\nasync function defaultSearchFn(\n profile: string,\n pattern: string,\n opts: SearchFnOptions,\n): Promise<SearchMatch[]> {\n const root = opts.root ?? process.cwd();\n const cache = new catalog.CatalogCache({ root, connection: profile });\n const snapshot = await cache.get();\n if (snapshot.catalogs.length === 0) {\n throw new Error(\n `No catalog cache for profile \"${profile}\" at ${cache.path}. ` +\n `Run \\`ddt catalog refresh --connection ${profile} --root ${root}\\` ` +\n `or \\`ddt extract --connection ${profile} --output ${root} --write-catalog-cache\\` first.`,\n );\n }\n const needle = pattern.toLowerCase();\n const typeFilter = opts.objectType ? opts.objectType.toUpperCase() : undefined;\n const matches: SearchMatch[] = [];\n for (const cat of snapshot.catalogs) {\n for (const schema of cat.schemas) {\n for (const obj of schema.objects) {\n if (typeFilter && obj.objectType !== typeFilter) continue;\n const name = obj.name.toLowerCase();\n const hit = opts.exact ? name === needle : name.includes(needle);\n if (hit) {\n matches.push({\n profile,\n fqn: `${obj.catalog}.${obj.schema}.${obj.name}`,\n objectType: obj.objectType,\n });\n }\n }\n }\n }\n return matches;\n}\n\nexport function searchCommand(searchFn: SearchFn = defaultSearchFn): Command {\n const cmd = new Command('search');\n cmd\n .description('Find objects matching a name pattern across one or more connection profiles.')\n .argument('<pattern>', 'Name pattern to search for (case-insensitive substring).')\n .requiredOption(\n '--profiles <list|all>',\n 'Comma-separated profile names, or \"all\" to scan every configured profile.',\n )\n .option('--exact', 'Exact name match instead of substring.', false)\n .option('--type <objectType>', 'Filter results to a specific object type (e.g. TABLE, VIEW).')\n .option('--format <fmt>', 'text | json (default text).', 'text')\n .option(\n '--profiles-dir <dir>',\n 'Directory to scan when --profiles all is used.',\n path.join(os.homedir(), '.ddt', 'connections'),\n )\n .option(\n '--root <dir>',\n 'Project / cache root containing the .ddt/cache directory. Default: cwd.',\n process.cwd(),\n )\n .option('--no-drift', 'Suppress the drift section in text output.')\n .action(async (pattern: string, opts) => {\n const profiles = await loadProfileNames(opts.profiles as string, opts.profilesDir as string);\n if (profiles.length === 0) {\n throw new Error(\n opts.profiles.toLowerCase() === 'all'\n ? `No profiles found in ${opts.profilesDir}. Run \\`ddt connection add\\` first.`\n : '--profiles must list at least one profile.',\n );\n }\n\n const results: SearchResult[] = await Promise.all(\n profiles.map(async (p) => {\n try {\n const matches = await searchFn(p, pattern, {\n exact: opts.exact as boolean,\n objectType: opts.type as string | undefined,\n root: opts.root as string,\n });\n return { profile: p, matches };\n } catch (err) {\n return {\n profile: p,\n matches: [],\n error: err instanceof Error ? err.message : String(err),\n };\n }\n }),\n );\n\n const fmt = String(opts.format ?? 'text').toLowerCase();\n if (fmt === 'json') {\n process.stdout.write(JSON.stringify(results, null, 2) + '\\n');\n return;\n }\n if (fmt !== 'text') throw new Error(`Unknown --format: ${opts.format}. Use text | json.`);\n const text = renderText(results, opts.drift !== false);\n process.stdout.write(text ? text + '\\n' : 'No results.\\n');\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/seed.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import {
|
|
8
|
+
DatabricksExecutor,
|
|
9
|
+
createConnection,
|
|
10
|
+
discoverSeeds,
|
|
11
|
+
getProfile,
|
|
12
|
+
loadProject,
|
|
13
|
+
renderSeedMerge
|
|
14
|
+
} from "@ddt-tools/core";
|
|
15
|
+
function seedCommand() {
|
|
16
|
+
const cmd = new Command("seed").description(
|
|
17
|
+
"Reference / dimension data seeds. Declare static rows next to DDL; engine generates MERGEs."
|
|
18
|
+
);
|
|
19
|
+
cmd.command("list").description("Enumerate seed files under <project>/seeds/.").requiredOption("-p, --project <path>", "Path to the .ddtproj file.").action(async (opts) => {
|
|
20
|
+
const loaded = await loadProject(String(opts.project));
|
|
21
|
+
const seeds = await discoverSeeds(loaded.rootDir);
|
|
22
|
+
if (seeds.length === 0) {
|
|
23
|
+
console.log(
|
|
24
|
+
"No seeds found. Add files under <project>/seeds/<catalog>/<schema>/<table>.{json,csv}."
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const s of seeds) {
|
|
29
|
+
console.log(
|
|
30
|
+
` ${s.catalog}.${s.schema}.${s.table.padEnd(28)} ${String(s.rows.length).padStart(5)} rows keys=[${s.keys.join(", ")}]`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log(`${seeds.length} seed(s).`);
|
|
35
|
+
});
|
|
36
|
+
cmd.command("render").description("Emit MERGE statements for every seed file. Offline; no workspace contact.").requiredOption("-p, --project <path>", "Path to the .ddtproj file.").option("-o, --out <path>", "Write to file. Defaults to stdout.").option(
|
|
37
|
+
"--delete-not-in-source",
|
|
38
|
+
"Add `WHEN NOT MATCHED BY SOURCE THEN DELETE` so target-only rows are removed. Default off (safer).",
|
|
39
|
+
false
|
|
40
|
+
).action(async (opts) => {
|
|
41
|
+
const loaded = await loadProject(String(opts.project));
|
|
42
|
+
const seeds = await discoverSeeds(loaded.rootDir);
|
|
43
|
+
const renderOpts = { deleteNotInSource: !!opts.deleteNotInSource };
|
|
44
|
+
const sql = seeds.map((s) => renderSeedMerge(s, renderOpts)).join("\n\n") + "\n";
|
|
45
|
+
if (opts.out) {
|
|
46
|
+
const outPath = path.resolve(String(opts.out));
|
|
47
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
48
|
+
await fs.writeFile(outPath, sql, "utf8");
|
|
49
|
+
console.error(`Wrote ${outPath} (${sql.length} bytes, ${seeds.length} seed(s)).`);
|
|
50
|
+
} else {
|
|
51
|
+
process.stdout.write(sql);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
cmd.command("apply").description("Execute every seed MERGE against a connection profile.").requiredOption("-p, --project <path>", "Path to the .ddtproj file.").requiredOption("--connection <name>", "Connection profile to apply against.").requiredOption("--yes", "Explicit confirmation. Required because apply writes data.").option("--dry-run", "Print the MERGEs without executing.", false).option(
|
|
55
|
+
"--delete-not-in-source",
|
|
56
|
+
"Add `WHEN NOT MATCHED BY SOURCE THEN DELETE` so target-only rows are removed. Default off (safer).",
|
|
57
|
+
false
|
|
58
|
+
).action(async (opts) => {
|
|
59
|
+
const loaded = await loadProject(String(opts.project));
|
|
60
|
+
const seeds = await discoverSeeds(loaded.rootDir);
|
|
61
|
+
if (seeds.length === 0) {
|
|
62
|
+
console.log("No seeds found. Nothing to apply.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const renderOpts = { deleteNotInSource: !!opts.deleteNotInSource };
|
|
66
|
+
if (opts.dryRun) {
|
|
67
|
+
for (const s of seeds) console.log(renderSeedMerge(s, renderOpts) + "\n");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const profile = await getProfile(String(opts.connection));
|
|
71
|
+
const conn = createConnection(profile);
|
|
72
|
+
let succeeded = 0;
|
|
73
|
+
let failed = 0;
|
|
74
|
+
try {
|
|
75
|
+
await conn.connect();
|
|
76
|
+
const exec = new DatabricksExecutor(conn);
|
|
77
|
+
for (const s of seeds) {
|
|
78
|
+
process.stdout.write(`\u25B6 ${s.catalog}.${s.schema}.${s.table} (${s.rows.length} rows) \u2026`);
|
|
79
|
+
const t0 = Date.now();
|
|
80
|
+
try {
|
|
81
|
+
await exec.execute(renderSeedMerge(s, renderOpts));
|
|
82
|
+
console.log(` \u2713 (${Date.now() - t0}ms)`);
|
|
83
|
+
succeeded++;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.log(` \u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
failed++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
await conn.disconnect();
|
|
91
|
+
}
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(`Summary: ${succeeded} succeeded, ${failed} failed.`);
|
|
94
|
+
if (failed > 0) process.exitCode = 1;
|
|
95
|
+
});
|
|
96
|
+
return cmd;
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
seedCommand
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=seed-W4Q3L2IU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/seed.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n DatabricksExecutor,\n createConnection,\n discoverSeeds,\n getProfile,\n loadProject,\n renderSeedMerge,\n} from '@ddt-tools/core';\n\n/**\n * `ddt seed` — declare static reference rows next to DDL; engine\n * generates MERGE statements to keep them in sync.\n *\n * Subcommands:\n * list enumerate seed files + row counts\n * render -p <project> emit MERGEs to stdout (offline)\n * apply -p <project> -c <conn> execute the MERGEs against a workspace\n */\nexport function seedCommand(): Command {\n const cmd = new Command('seed').description(\n 'Reference / dimension data seeds. Declare static rows next to DDL; engine generates MERGEs.',\n );\n\n cmd\n .command('list')\n .description('Enumerate seed files under <project>/seeds/.')\n .requiredOption('-p, --project <path>', 'Path to the .ddtproj file.')\n .action(async (opts) => {\n const loaded = await loadProject(String(opts.project));\n const seeds = await discoverSeeds(loaded.rootDir);\n if (seeds.length === 0) {\n console.log(\n 'No seeds found. Add files under <project>/seeds/<catalog>/<schema>/<table>.{json,csv}.',\n );\n return;\n }\n for (const s of seeds) {\n console.log(\n ` ${s.catalog}.${s.schema}.${s.table.padEnd(28)} ${String(s.rows.length).padStart(5)} rows keys=[${s.keys.join(', ')}]`,\n );\n }\n console.log('');\n console.log(`${seeds.length} seed(s).`);\n });\n\n cmd\n .command('render')\n .description('Emit MERGE statements for every seed file. Offline; no workspace contact.')\n .requiredOption('-p, --project <path>', 'Path to the .ddtproj file.')\n .option('-o, --out <path>', 'Write to file. Defaults to stdout.')\n .option(\n '--delete-not-in-source',\n 'Add `WHEN NOT MATCHED BY SOURCE THEN DELETE` so target-only rows are removed. Default off (safer).',\n false,\n )\n .action(async (opts) => {\n const loaded = await loadProject(String(opts.project));\n const seeds = await discoverSeeds(loaded.rootDir);\n const renderOpts = { deleteNotInSource: !!opts.deleteNotInSource };\n const sql = seeds.map((s) => renderSeedMerge(s, renderOpts)).join('\\n\\n') + '\\n';\n if (opts.out) {\n const outPath = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, sql, 'utf8');\n console.error(`Wrote ${outPath} (${sql.length} bytes, ${seeds.length} seed(s)).`);\n } else {\n process.stdout.write(sql);\n }\n });\n\n cmd\n .command('apply')\n .description('Execute every seed MERGE against a connection profile.')\n .requiredOption('-p, --project <path>', 'Path to the .ddtproj file.')\n .requiredOption('--connection <name>', 'Connection profile to apply against.')\n .requiredOption('--yes', 'Explicit confirmation. Required because apply writes data.')\n .option('--dry-run', 'Print the MERGEs without executing.', false)\n .option(\n '--delete-not-in-source',\n 'Add `WHEN NOT MATCHED BY SOURCE THEN DELETE` so target-only rows are removed. Default off (safer).',\n false,\n )\n .action(async (opts) => {\n const loaded = await loadProject(String(opts.project));\n const seeds = await discoverSeeds(loaded.rootDir);\n if (seeds.length === 0) {\n console.log('No seeds found. Nothing to apply.');\n return;\n }\n const renderOpts = { deleteNotInSource: !!opts.deleteNotInSource };\n if (opts.dryRun) {\n for (const s of seeds) console.log(renderSeedMerge(s, renderOpts) + '\\n');\n return;\n }\n const profile = await getProfile(String(opts.connection));\n const conn = createConnection(profile);\n let succeeded = 0;\n let failed = 0;\n try {\n await conn.connect();\n const exec = new DatabricksExecutor(conn);\n for (const s of seeds) {\n process.stdout.write(`▶ ${s.catalog}.${s.schema}.${s.table} (${s.rows.length} rows) …`);\n const t0 = Date.now();\n try {\n await exec.execute(renderSeedMerge(s, renderOpts));\n console.log(` ✓ (${Date.now() - t0}ms)`);\n succeeded++;\n } catch (err) {\n console.log(` ✗ ${err instanceof Error ? err.message : String(err)}`);\n failed++;\n }\n }\n } finally {\n await conn.disconnect();\n }\n console.log('');\n console.log(`Summary: ${succeeded} succeeded, ${failed} failed.`);\n if (failed > 0) process.exitCode = 1;\n });\n\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/sketch.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { ai, objectSketch } from "@ddt-tools/core";
|
|
7
|
+
var KINDS = [
|
|
8
|
+
"managed-table",
|
|
9
|
+
"external-table",
|
|
10
|
+
"streaming-table",
|
|
11
|
+
"materialized-view",
|
|
12
|
+
"view",
|
|
13
|
+
"function",
|
|
14
|
+
"volume"
|
|
15
|
+
];
|
|
16
|
+
function sketchCommand() {
|
|
17
|
+
const cmd = new Command("sketch");
|
|
18
|
+
cmd.description(
|
|
19
|
+
"AI-assist: scaffold idiomatic Databricks UC DDL from a prose description. Output always carries a REVIEW BEFORE DEPLOY header."
|
|
20
|
+
).argument("<kind>", `Object kind: ${KINDS.join(" | ")}`).requiredOption("--description <text>", 'Free-form description. Use "-" to read from stdin.').option("--target <fqn>", "Target FQN (e.g. analytics.public.orders).").option("--context <text>", 'Optional additional context (e.g. "use SQL warehouse small").').option("--out <path>", "Output file. Default stdout.").option("--format <fmt>", "Output format: text | json. Default text.", "text").option(
|
|
21
|
+
"--ai-max-spend <usd>",
|
|
22
|
+
"Refuse the call if today's estimated spend \u2265 this (USD). 0 = no cap.",
|
|
23
|
+
"0"
|
|
24
|
+
).action(async (kindArg, opts) => {
|
|
25
|
+
const kind = String(kindArg).toLowerCase();
|
|
26
|
+
if (!KINDS.includes(kind)) {
|
|
27
|
+
throw new Error(`Unknown kind "${kindArg}". Use one of: ${KINDS.join(" | ")}`);
|
|
28
|
+
}
|
|
29
|
+
const description = String(opts.description) === "-" ? await readStdin() : String(opts.description);
|
|
30
|
+
const targetFqn = opts.target ? splitFqn(String(opts.target)) : void 0;
|
|
31
|
+
const result = await objectSketch.sketchToDdl(
|
|
32
|
+
{
|
|
33
|
+
description,
|
|
34
|
+
objectKind: kind,
|
|
35
|
+
targetFqn,
|
|
36
|
+
additionalContext: opts.context ? String(opts.context) : void 0
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
completeFn: async (prompt) => {
|
|
40
|
+
const r = await ai.complete([{ role: "user", content: prompt }], {
|
|
41
|
+
feature: "object-sketch",
|
|
42
|
+
maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
|
|
43
|
+
});
|
|
44
|
+
return r.text;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify({ ...result, rawModelText: void 0 }, null, 2) : result.generatedSql;
|
|
49
|
+
if (opts.out) {
|
|
50
|
+
await fs.writeFile(String(opts.out), output, "utf8");
|
|
51
|
+
console.log(`Wrote ${String(opts.out)} (${output.length} bytes)`);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(output);
|
|
54
|
+
}
|
|
55
|
+
if (result.assumptions.length > 0) {
|
|
56
|
+
console.error("");
|
|
57
|
+
console.error("Model assumptions:");
|
|
58
|
+
for (const a of result.assumptions) console.error(` - ${a}`);
|
|
59
|
+
}
|
|
60
|
+
if (result.parseFailed) {
|
|
61
|
+
console.warn("Model output could not be parsed \u2014 see the raw text via --format json.");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return cmd;
|
|
65
|
+
}
|
|
66
|
+
function splitFqn(fqn) {
|
|
67
|
+
const parts = fqn.split(".");
|
|
68
|
+
if (parts.length === 1) return { name: parts[0] };
|
|
69
|
+
if (parts.length === 2) return { schema: parts[0], name: parts[1] };
|
|
70
|
+
if (parts.length === 3) return { catalog: parts[0], schema: parts[1], name: parts[2] };
|
|
71
|
+
throw new Error(`Invalid --target "${fqn}": expected 1, 2, or 3 dot-separated parts.`);
|
|
72
|
+
}
|
|
73
|
+
async function readStdin() {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
for await (const chunk of process.stdin) {
|
|
76
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
77
|
+
}
|
|
78
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
sketchCommand
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=sketch-6B2V6FJV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/sketch.ts"],"sourcesContent":["/**\n * `ddt sketch <kind>` — generate idiomatic Databricks UC DDL from a free-form\n * prose description via the configured AI provider.\n *\n * Composes `@ddt-tools/core/objectSketch.sketchToDdl` with `@ddt-tools/core/ai.complete`.\n * Output is always wrapped in a \"REVIEW BEFORE DEPLOY\" header so the user\n * never confuses model output with hand-authored DDL.\n *\n * Inputs:\n * <kind> managed-table | external-table | streaming-table\n * | materialized-view | view | function | volume\n * --description <text> Free-form description (or \"-\" to read from stdin)\n * --target <fqn> Optional target FQN (e.g. analytics.public.orders)\n * --context <text> Optional additional context\n * --out <path> Optional output file. Default: stdout.\n * --format <fmt> text | json. Default text (just the DDL).\n */\nimport { promises as fs } from 'node:fs';\nimport { Command } from 'commander';\nimport { ai, objectSketch } from '@ddt-tools/core';\n\nconst KINDS = [\n 'managed-table',\n 'external-table',\n 'streaming-table',\n 'materialized-view',\n 'view',\n 'function',\n 'volume',\n] as const;\n\ntype Kind = (typeof KINDS)[number];\n\nexport function sketchCommand(): Command {\n const cmd = new Command('sketch');\n cmd\n .description(\n 'AI-assist: scaffold idiomatic Databricks UC DDL from a prose description. Output always carries a REVIEW BEFORE DEPLOY header.',\n )\n .argument('<kind>', `Object kind: ${KINDS.join(' | ')}`)\n .requiredOption('--description <text>', 'Free-form description. Use \"-\" to read from stdin.')\n .option('--target <fqn>', 'Target FQN (e.g. analytics.public.orders).')\n .option('--context <text>', 'Optional additional context (e.g. \"use SQL warehouse small\").')\n .option('--out <path>', 'Output file. Default stdout.')\n .option('--format <fmt>', 'Output format: text | json. Default text.', 'text')\n .option(\n '--ai-max-spend <usd>',\n \"Refuse the call if today's estimated spend ≥ this (USD). 0 = no cap.\",\n '0',\n )\n .action(async (kindArg, opts) => {\n const kind = String(kindArg).toLowerCase() as Kind;\n if (!KINDS.includes(kind)) {\n throw new Error(`Unknown kind \"${kindArg}\". Use one of: ${KINDS.join(' | ')}`);\n }\n\n const description =\n String(opts.description) === '-' ? await readStdin() : String(opts.description);\n const targetFqn = opts.target ? splitFqn(String(opts.target)) : undefined;\n\n const result = await objectSketch.sketchToDdl(\n {\n description,\n objectKind: kind,\n targetFqn,\n additionalContext: opts.context ? String(opts.context) : undefined,\n },\n {\n completeFn: async (prompt) => {\n const r = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'object-sketch',\n maxSpendUsd: Number(opts.aiMaxSpend ?? '0') || 0,\n });\n return r.text;\n },\n },\n );\n\n const output =\n String(opts.format).toLowerCase() === 'json'\n ? JSON.stringify({ ...result, rawModelText: undefined }, null, 2)\n : result.generatedSql;\n\n if (opts.out) {\n await fs.writeFile(String(opts.out), output, 'utf8');\n console.log(`Wrote ${String(opts.out)} (${output.length} bytes)`);\n } else {\n console.log(output);\n }\n\n if (result.assumptions.length > 0) {\n console.error('');\n console.error('Model assumptions:');\n for (const a of result.assumptions) console.error(` - ${a}`);\n }\n if (result.parseFailed) {\n console.warn('Model output could not be parsed — see the raw text via --format json.');\n }\n });\n return cmd;\n}\n\nfunction splitFqn(fqn: string): { catalog?: string; schema?: string; name: string } {\n const parts = fqn.split('.');\n if (parts.length === 1) return { name: parts[0]! };\n if (parts.length === 2) return { schema: parts[0], name: parts[1]! };\n if (parts.length === 3) return { catalog: parts[0], schema: parts[1], name: parts[2]! };\n throw new Error(`Invalid --target \"${fqn}\": expected 1, 2, or 3 dot-separated parts.`);\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer));\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"],"mappings":"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","names":[]}
|