@sdt-tools/cli 0.2.0 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/dist/advise-tests-6DRSZMBL.js +87 -0
  2. package/dist/advise-tests-6DRSZMBL.js.map +1 -0
  3. package/dist/ai-G4MJWHTM.js +89 -0
  4. package/dist/ai-G4MJWHTM.js.map +1 -0
  5. package/dist/anonymize-QR6JGXA7.js +123 -0
  6. package/dist/anonymize-QR6JGXA7.js.map +1 -0
  7. package/dist/approval-YVHYTV53.js +73 -0
  8. package/dist/approval-YVHYTV53.js.map +1 -0
  9. package/dist/approval-chain-54KKJZS3.js +120 -0
  10. package/dist/approval-chain-54KKJZS3.js.map +1 -0
  11. package/dist/audit-log-QZFH7LUX.js +159 -0
  12. package/dist/audit-log-QZFH7LUX.js.map +1 -0
  13. package/dist/backlog-V2YUIQDL.js +76 -0
  14. package/dist/backlog-V2YUIQDL.js.map +1 -0
  15. package/dist/bisect-GEVYAVL5.js +111 -0
  16. package/dist/bisect-GEVYAVL5.js.map +1 -0
  17. package/dist/bookmarks-57LKS7P6.js +107 -0
  18. package/dist/bookmarks-57LKS7P6.js.map +1 -0
  19. package/dist/branch-W2MGMPSH.js +88 -0
  20. package/dist/branch-W2MGMPSH.js.map +1 -0
  21. package/dist/build-VNIQFKSP.js +23 -0
  22. package/dist/build-VNIQFKSP.js.map +1 -0
  23. package/dist/catalog-JLB5VCEV.js +137 -0
  24. package/dist/catalog-JLB5VCEV.js.map +1 -0
  25. package/dist/changelog-M7XGDYSY.js +220 -0
  26. package/dist/changelog-M7XGDYSY.js.map +1 -0
  27. package/dist/chunk-DGUM43GV.js +11 -0
  28. package/dist/chunk-DGUM43GV.js.map +1 -0
  29. package/dist/chunk-EWXM4KJN.js +25 -0
  30. package/dist/chunk-EWXM4KJN.js.map +1 -0
  31. package/dist/chunk-JP2EZLR5.js +50 -0
  32. package/dist/chunk-JP2EZLR5.js.map +1 -0
  33. package/dist/chunk-VM2H4LAO.js +15 -0
  34. package/dist/chunk-VM2H4LAO.js.map +1 -0
  35. package/dist/chunk-ZWY4ZRHL.js +44 -0
  36. package/dist/chunk-ZWY4ZRHL.js.map +1 -0
  37. package/dist/cli.js +506 -19014
  38. package/dist/cli.js.map +1 -1
  39. package/dist/compare-5O6UTWPJ.js +405 -0
  40. package/dist/compare-5O6UTWPJ.js.map +1 -0
  41. package/dist/compare-profiles-7ZSNIW7B.js +218 -0
  42. package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
  43. package/dist/completion-I5U5VVAX.js +82 -0
  44. package/dist/completion-I5U5VVAX.js.map +1 -0
  45. package/dist/connection-SYTH4V53.js +110 -0
  46. package/dist/connection-SYTH4V53.js.map +1 -0
  47. package/dist/cost-estimate-TJDDH6TO.js +328 -0
  48. package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
  49. package/dist/data-compare-UK2UXAS3.js +134 -0
  50. package/dist/data-compare-UK2UXAS3.js.map +1 -0
  51. package/dist/data-fit-Q45ENBRL.js +125 -0
  52. package/dist/data-fit-Q45ENBRL.js.map +1 -0
  53. package/dist/deploy-status-UUHKVDTI.js +58 -0
  54. package/dist/deploy-status-UUHKVDTI.js.map +1 -0
  55. package/dist/design-PO6UPBL7.js +138 -0
  56. package/dist/design-PO6UPBL7.js.map +1 -0
  57. package/dist/diagnose-6IFMELFR.js +145 -0
  58. package/dist/diagnose-6IFMELFR.js.map +1 -0
  59. package/dist/discover-A7OSZAHK.js +78 -0
  60. package/dist/discover-A7OSZAHK.js.map +1 -0
  61. package/dist/docs-CVRKGUSW.js +177 -0
  62. package/dist/docs-CVRKGUSW.js.map +1 -0
  63. package/dist/drift-XDA3BDYN.js +226 -0
  64. package/dist/drift-XDA3BDYN.js.map +1 -0
  65. package/dist/drift-gate-V7QSIOGZ.js +94 -0
  66. package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
  67. package/dist/error-lookup-7ZWCZJ44.js +56 -0
  68. package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
  69. package/dist/errorReporting-ZRNJ3VW7.js +109 -0
  70. package/dist/errorReporting-ZRNJ3VW7.js.map +1 -0
  71. package/dist/exec-PKBHLI7T.js +121 -0
  72. package/dist/exec-PKBHLI7T.js.map +1 -0
  73. package/dist/explain-LWKJOTL7.js +192 -0
  74. package/dist/explain-LWKJOTL7.js.map +1 -0
  75. package/dist/explorer-QOVM6VBD.js +61 -0
  76. package/dist/explorer-QOVM6VBD.js.map +1 -0
  77. package/dist/export-IYYBZ5HE.js +42 -0
  78. package/dist/export-IYYBZ5HE.js.map +1 -0
  79. package/dist/extract-VMMVRQVT.js +102 -0
  80. package/dist/extract-VMMVRQVT.js.map +1 -0
  81. package/dist/features-LE6BDZ2S.js +59 -0
  82. package/dist/features-LE6BDZ2S.js.map +1 -0
  83. package/dist/feedback-M7DM2EQC.js +161 -0
  84. package/dist/feedback-M7DM2EQC.js.map +1 -0
  85. package/dist/find-EME2JG2I.js +176 -0
  86. package/dist/find-EME2JG2I.js.map +1 -0
  87. package/dist/format-TRLWLMGS.js +141 -0
  88. package/dist/format-TRLWLMGS.js.map +1 -0
  89. package/dist/generate-6NAZGZDV.js +152 -0
  90. package/dist/generate-6NAZGZDV.js.map +1 -0
  91. package/dist/graph-QNQDAUO7.js +161 -0
  92. package/dist/graph-QNQDAUO7.js.map +1 -0
  93. package/dist/history-RONA7ZTI.js +199 -0
  94. package/dist/history-RONA7ZTI.js.map +1 -0
  95. package/dist/hosts-YBXY2ZG5.js +49 -0
  96. package/dist/hosts-YBXY2ZG5.js.map +1 -0
  97. package/dist/impact-T2JSANHS.js +59 -0
  98. package/dist/impact-T2JSANHS.js.map +1 -0
  99. package/dist/import-AELYLY6A.js +32 -0
  100. package/dist/import-AELYLY6A.js.map +1 -0
  101. package/dist/index.cjs +36 -6
  102. package/dist/index.cjs.map +1 -1
  103. package/dist/index.js +60 -25
  104. package/dist/index.js.map +1 -1
  105. package/dist/init-SWRRJMGI.js +57 -0
  106. package/dist/init-SWRRJMGI.js.map +1 -0
  107. package/dist/install-hooks-6SIAGTAF.js +109 -0
  108. package/dist/install-hooks-6SIAGTAF.js.map +1 -0
  109. package/dist/license-OAF22PLZ.js +46 -0
  110. package/dist/license-OAF22PLZ.js.map +1 -0
  111. package/dist/lineage-EW66XJ6O.js +552 -0
  112. package/dist/lineage-EW66XJ6O.js.map +1 -0
  113. package/dist/lint-FQ2OTYTQ.js +143 -0
  114. package/dist/lint-FQ2OTYTQ.js.map +1 -0
  115. package/dist/mcp-3QI4TH4N.js +344 -0
  116. package/dist/mcp-3QI4TH4N.js.map +1 -0
  117. package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
  118. package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
  119. package/dist/migrate-platform-NTRTOGNR.js +91 -0
  120. package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
  121. package/dist/optimize-CJYWMAWA.js +105 -0
  122. package/dist/optimize-CJYWMAWA.js.map +1 -0
  123. package/dist/perf-LL2CPCJF.js +205 -0
  124. package/dist/perf-LL2CPCJF.js.map +1 -0
  125. package/dist/pii-FBDRDQ2E.js +136 -0
  126. package/dist/pii-FBDRDQ2E.js.map +1 -0
  127. package/dist/pilot-CCQERKPH.js +29 -0
  128. package/dist/pilot-CCQERKPH.js.map +1 -0
  129. package/dist/pr-comment-S5FF4QRX.js +79 -0
  130. package/dist/pr-comment-S5FF4QRX.js.map +1 -0
  131. package/dist/preview-5U4YVCRM.js +47 -0
  132. package/dist/preview-5U4YVCRM.js.map +1 -0
  133. package/dist/profile-7VC57KD2.js +101 -0
  134. package/dist/profile-7VC57KD2.js.map +1 -0
  135. package/dist/promote-AASEFTIA.js +408 -0
  136. package/dist/promote-AASEFTIA.js.map +1 -0
  137. package/dist/publish-Y2J56K4Y.js +715 -0
  138. package/dist/publish-Y2J56K4Y.js.map +1 -0
  139. package/dist/purge-QMXZKCMD.js +57 -0
  140. package/dist/purge-QMXZKCMD.js.map +1 -0
  141. package/dist/query-log-6OM4GI7W.js +112 -0
  142. package/dist/query-log-6OM4GI7W.js.map +1 -0
  143. package/dist/refactor-LTZQLJ35.js +5799 -0
  144. package/dist/refactor-LTZQLJ35.js.map +1 -0
  145. package/dist/refresh-4TY2AGOU.js +38 -0
  146. package/dist/refresh-4TY2AGOU.js.map +1 -0
  147. package/dist/replay-OOC25FZN.js +117 -0
  148. package/dist/replay-OOC25FZN.js.map +1 -0
  149. package/dist/revert-ODMUVJW6.js +110 -0
  150. package/dist/revert-ODMUVJW6.js.map +1 -0
  151. package/dist/review-XXPWOBFP.js +158 -0
  152. package/dist/review-XXPWOBFP.js.map +1 -0
  153. package/dist/rollback-suggest-6G2HEKFR.js +79 -0
  154. package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
  155. package/dist/safer-alternative-QFVNLG3L.js +89 -0
  156. package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
  157. package/dist/safety-7QWRSUEZ.js +168 -0
  158. package/dist/safety-7QWRSUEZ.js.map +1 -0
  159. package/dist/savings-RHIXP6IT.js +95 -0
  160. package/dist/savings-RHIXP6IT.js.map +1 -0
  161. package/dist/scan-secrets-5YCQ4UCU.js +54 -0
  162. package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
  163. package/dist/schema-CIZXCQD2.js +429 -0
  164. package/dist/schema-CIZXCQD2.js.map +1 -0
  165. package/dist/script-K7CIN2P6.js +153 -0
  166. package/dist/script-K7CIN2P6.js.map +1 -0
  167. package/dist/search-BUZ5NXZZ.js +151 -0
  168. package/dist/search-BUZ5NXZZ.js.map +1 -0
  169. package/dist/seed-76QAK276.js +96 -0
  170. package/dist/seed-76QAK276.js.map +1 -0
  171. package/dist/sketch-PTLKDIK3.js +88 -0
  172. package/dist/sketch-PTLKDIK3.js.map +1 -0
  173. package/dist/snapshot-XLPR2OZ5.js +177 -0
  174. package/dist/snapshot-XLPR2OZ5.js.map +1 -0
  175. package/dist/snippets-EK4DK5CN.js +74 -0
  176. package/dist/snippets-EK4DK5CN.js.map +1 -0
  177. package/dist/standards-7T2UY6DD.js +241 -0
  178. package/dist/standards-7T2UY6DD.js.map +1 -0
  179. package/dist/suggest-VGRYSAR6.js +39 -0
  180. package/dist/suggest-VGRYSAR6.js.map +1 -0
  181. package/dist/suggest-constraints-MY5WKUHA.js +160 -0
  182. package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
  183. package/dist/suite-TRNGZWQM.js +88 -0
  184. package/dist/suite-TRNGZWQM.js.map +1 -0
  185. package/dist/telemetry-3U2QLA2S.js +75 -0
  186. package/dist/telemetry-3U2QLA2S.js.map +1 -0
  187. package/dist/template-ZERIXVXF.js +403 -0
  188. package/dist/template-ZERIXVXF.js.map +1 -0
  189. package/dist/test-5M2ED3WT.js +169 -0
  190. package/dist/test-5M2ED3WT.js.map +1 -0
  191. package/dist/trial-U732FONV.js +31 -0
  192. package/dist/trial-U732FONV.js.map +1 -0
  193. package/dist/validate-T6D2WCOK.js +106 -0
  194. package/dist/validate-T6D2WCOK.js.map +1 -0
  195. package/dist/verify-KXVASEEG.js +76 -0
  196. package/dist/verify-KXVASEEG.js.map +1 -0
  197. package/dist/watch-I6K4BNMA.js +80 -0
  198. package/dist/watch-I6K4BNMA.js.map +1 -0
  199. package/dist/xcompare-TPFLQO6W.js +87 -0
  200. package/dist/xcompare-TPFLQO6W.js.map +1 -0
  201. package/package.json +2 -2
  202. package/dist/cli.cjs +0 -19040
  203. package/dist/cli.cjs.map +0 -1
  204. package/dist/cli.d.cts +0 -1
  205. package/dist/cli.d.ts +0 -1
@@ -0,0 +1,715 @@
1
+ import {
2
+ addMappingFlags,
3
+ buildMappingFromOptions
4
+ } from "./chunk-JP2EZLR5.js";
5
+ import {
6
+ attachRelatedOptions
7
+ } from "./chunk-EWXM4KJN.js";
8
+ import {
9
+ logger
10
+ } from "./chunk-VM2H4LAO.js";
11
+ import "./chunk-DGUM43GV.js";
12
+
13
+ // src/commands/publish.ts
14
+ import path from "path";
15
+ import { promises as fs } from "fs";
16
+ import { Command } from "commander";
17
+ import { readPac } from "@sdt-tools/core/pac";
18
+ import {
19
+ CompareEngine,
20
+ LiveSource,
21
+ buildExecutionPlan,
22
+ defaultSafePlan,
23
+ validatePlan
24
+ } from "@sdt-tools/core/compare";
25
+ import {
26
+ ScriptGenerator,
27
+ SnowflakeExecutor,
28
+ buildDeployManifest,
29
+ executePlan,
30
+ resolveDeployScripts,
31
+ formatDeployScript,
32
+ colorizeMigrationScript,
33
+ splitStatements as coreSplitStatements
34
+ } from "@sdt-tools/core/deploy";
35
+ import { compileSlice, mergeDeployOptions } from "@sdt-tools/core/project";
36
+ import {
37
+ ai,
38
+ aiPreflight,
39
+ approval,
40
+ pacFreshness,
41
+ protectedObjects,
42
+ safety,
43
+ license,
44
+ testFramework
45
+ } from "@sdt-tools/core";
46
+ import { executeDcmProjectDeploy, mapDcmResultToDeploymentReport } from "@sdt-tools/core/exporters";
47
+ import { assessDcmCompatibility, formatDcmCompatibilityReport } from "@sdt-tools/core/safety";
48
+ import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
49
+ var ModelSource = class {
50
+ kind = "pac";
51
+ platform = "Snowflake";
52
+ label;
53
+ model;
54
+ constructor(label, model) {
55
+ this.label = label;
56
+ this.model = model;
57
+ }
58
+ async load() {
59
+ return this.model;
60
+ }
61
+ };
62
+ function splitStatements(sql) {
63
+ return coreSplitStatements(sql).map((s) => s.sql);
64
+ }
65
+ function publishCommand() {
66
+ const cmd = new Command("publish");
67
+ cmd.description(
68
+ "Compare a .sdtpac to a live Snowflake target and apply (or dry-run) the migration."
69
+ ).option(
70
+ "--pac <path>",
71
+ "Path to a built .sdtpac. Required for a normal publish; omit only with --restore-from-snapshot."
72
+ ).requiredOption("-c, --connection <profile>", "Connection profile name").option(
73
+ "--restore-from-snapshot <batchId>",
74
+ "Recovery mode: skip the compare step and emit ALTER TABLE \u2026 SWAP WITH against the snapshot batch <batchId> from the registry. Dry-run by default; pass --apply --yes to execute. This is the command printed by TRUST.4's post-deploy-smoke + TRUST.8's restore-hint when a deploy fails \u2014 they tell the operator to run `sdt publish --restore-from-snapshot <id>`."
75
+ ).option(
76
+ "--snapshot-dir <path>",
77
+ "Snapshot registry directory used with --restore-from-snapshot.",
78
+ ".sdt/snapshots"
79
+ ).option("--db <database>", "Target database (defaults to pac scope.database)").option("--schema <schema>", "Target schema (defaults to pac scope.schema)").option("--dry-run", "Print the migration script without executing it", false).option("--apply", "Execute the migration against the live target (gated by safety)", false).option("--out <path>", "Write the generated migration script to this path").option(
80
+ "--color <mode>",
81
+ "Color stdout: auto | always | never. Honors NO_COLOR / SDT_NO_COLOR env vars in auto mode.",
82
+ "auto"
83
+ ).option("--no-pre-scripts", "Skip pre-deploy scripts even if the pac configures them").option("--no-post-scripts", "Skip post-deploy scripts even if the pac configures them").option(
84
+ "--no-slice",
85
+ "Disable the pac's Project Slice (if it has one). Default: the slice baked into the pac is applied automatically and target objects outside the slice are left untouched."
86
+ ).option("--yes", "Confirm REQUIRE_YES_FLAG safety gate (required for --apply)", false).option(
87
+ "--confirm-production",
88
+ "Confirm REQUIRE_CONFIRM_PRODUCTION gate (required for prod-named profiles with destructive changes)",
89
+ false
90
+ ).option(
91
+ "--query-tag <tag>",
92
+ "Set Snowflake QUERY_TAG for this deploy (audit / observability). The CLI runs ALTER SESSION SET QUERY_TAG='<tag>' before any migration statement so QUERY_HISTORY can attribute the deploy. Defaults to 'sdt:<projectName>@<projectVersion>:<unix-ms>'."
93
+ ).option(
94
+ "--manifest <path>",
95
+ "After --apply, write a JSON deploy manifest (every step + status + duration + reverse SQL) to <path>. Replayable via `sdt revert --manifest <path>`."
96
+ ).option(
97
+ "--ai-preflight",
98
+ "Before --apply, ask AI to grade the operator's plain-language deploy description against the actual diff + safety findings. Blocks on 'mismatch' unless --confirm-after-preflight-mismatch is also passed.",
99
+ false
100
+ ).option(
101
+ "--ai-preflight-text <narrative>",
102
+ "Non-interactive form of --ai-preflight. Supplies the narrative directly instead of reading from stdin."
103
+ ).option(
104
+ "--strict-ai-preflight",
105
+ "Also block on 'partial' (not just 'mismatch'). Useful for production deploys.",
106
+ false
107
+ ).option(
108
+ "--confirm-after-preflight-mismatch",
109
+ "Proceed even when --ai-preflight returns a mismatch verdict. Acknowledges that the AI grader flagged a discrepancy.",
110
+ false
111
+ ).option(
112
+ "--require-approvals <n>",
113
+ "Multi-approver gate: refuse --apply until <n> distinct approvers have signed off via `sdt approval add <deploy-id> --as <user>`. Team-tier."
114
+ ).option(
115
+ "--approver <ids>",
116
+ "Comma-separated allow-list of approver IDs honoured by --require-approvals. Empty = any approver counts."
117
+ ).option(
118
+ "--deploy-id <id>",
119
+ "Stable deploy identifier paired with --require-approvals (default: <connection>:<db>.<schema>)."
120
+ ).option(
121
+ "--approvals-root <path>",
122
+ "Approvals directory (default: .sdt/approvals).",
123
+ path.join(".sdt", "approvals")
124
+ ).option(
125
+ "--allow-protected <fqns>",
126
+ "Comma-separated FQNs to allow despite .sdt-protection.json protection. Repeatable.",
127
+ (val, prev) => [...prev, val],
128
+ []
129
+ ).option(
130
+ "--allow-all-protected",
131
+ "Override all protection config entries (caution: disables DSR.4 guardrails).",
132
+ false
133
+ ).option(
134
+ "--freshness <mode>",
135
+ "Pac age check: warn (log if stale, continue), strict (block if stale), skip (no check). Default warn.",
136
+ "warn"
137
+ ).option(
138
+ "--post-deploy-tests <project>",
139
+ "After a successful --apply, discover YAML tests under <project>/tests/ and run them against the same connection. Exits 2 on any blocking (error-severity) DQ failure. Emits POST_DEPLOY_DQ_FAILED hint with --restore-from-snapshot recipe when tests fail."
140
+ ).option(
141
+ "--via-dcm <stage>",
142
+ "Enterprise: delegate execution to Snowflake DCM Projects. <stage> is the DCM project FQN (e.g. @MY_DB.PUBLIC.MY_STAGE/my_project). SDT runs its safety pre-flight and DCM compatibility check, then executes EXECUTE DCM PROJECT DEPLOY. DCM must have been populated via `sdt export --dcm` first."
143
+ ).option(
144
+ "--profile <name>",
145
+ "VARSYNTAX.2 \u2014 look up `deploymentProfiles[<name>].variables` from the pac manifest and substitute `$(VAR)` references in the generated migration script. Errors if the named profile is absent from the pac (e.g. pac built before VARSYNTAX.2, or profile not declared in .sdtproj). Distinct from `--connection`, which selects the Snowflake credential profile."
146
+ ).option(
147
+ "--variables <kv>",
148
+ "VARSYNTAX.2 \u2014 comma-separated KEY=VALUE pairs for `$(VAR)` substitution. Merged on top of `--profile <name>` variables (CLI values win on key collision)."
149
+ ).action(async (opts) => {
150
+ if (opts.apply && opts.dryRun) {
151
+ logger.error("--apply and --dry-run are mutually exclusive.");
152
+ process.exitCode = 1;
153
+ return;
154
+ }
155
+ if (opts.restoreFromSnapshot) {
156
+ await runRestoreFromSnapshot(opts);
157
+ return;
158
+ }
159
+ if (!opts.pac) {
160
+ logger.error("--pac <path> is required (unless --restore-from-snapshot is given).");
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+ const pacPath = path.resolve(String(opts.pac));
165
+ const pac = await readPac(pacPath);
166
+ const freshnessMode = opts.freshness ?? "warn";
167
+ if (freshnessMode !== "skip") {
168
+ const fr = pacFreshness.checkPacFreshness(pac.manifest.builtAt);
169
+ if (fr.status !== "fresh") {
170
+ const msg = pacFreshness.formatFreshnessWarning(fr, pacPath);
171
+ if (freshnessMode === "strict") {
172
+ logger.error(msg);
173
+ process.exitCode = 1;
174
+ return;
175
+ } else {
176
+ logger.warn(msg);
177
+ }
178
+ }
179
+ }
180
+ const profileName = String(opts.connection);
181
+ const profile = await getProfile(profileName);
182
+ const conn = new SnowflakeConnection(profile);
183
+ const db = opts.db ? String(opts.db) : pac.manifest.scope.database;
184
+ const schema = opts.schema ? String(opts.schema) : pac.manifest.scope.schema;
185
+ const live = new LiveSource(conn, { database: db, schema });
186
+ const source = new ModelSource(pacPath, pac.model);
187
+ const baseline = pac.manifest.deployOptions;
188
+ const merged = mergeDeployOptions(baseline);
189
+ const isProd = safety.looksLikeProduction(profileName);
190
+ const resolved = {
191
+ ...merged,
192
+ deployment: safety.applyProductionSafetyFloor(merged.deployment, isProd)
193
+ };
194
+ if (isProd && merged.deployment.preDeployClone !== true) {
195
+ logger.info(
196
+ ' Profile "' + profileName + '" looks like production \u2014 forcing deployOptions.deployment.preDeployClone=true (safety floor).'
197
+ );
198
+ }
199
+ const preScripts = opts.preScripts === false ? [] : resolveDeployScripts(pac.source, resolved.deployment.preDeployScriptPatterns);
200
+ const postScripts = opts.postScripts === false ? [] : resolveDeployScripts(pac.source, resolved.deployment.postDeployScriptPatterns);
201
+ const nameMapping = await buildMappingFromOptions(opts);
202
+ const engine = new CompareEngine();
203
+ logger.step("Comparing pac to live...");
204
+ const sliceConfig = opts.slice === false ? void 0 : pac.manifest.slice;
205
+ const slice = sliceConfig ? compileSlice(sliceConfig) : void 0;
206
+ const result = await engine.compare(source, live, {
207
+ ...nameMapping ? { nameMapping } : {},
208
+ ...slice ? { sliceFilter: slice } : {}
209
+ });
210
+ if (slice) {
211
+ const outside = result.outsideScope?.length ?? 0;
212
+ const refs = result.referenced?.length ?? 0;
213
+ logger.info(
214
+ " Slice active: " + result.objects.length + " owned \xB7 " + outside + " outside scope (untouched) \xB7 " + refs + " referenced"
215
+ );
216
+ }
217
+ logger.info(
218
+ " +" + result.summary.added + " -" + result.summary.removed + " ~" + result.summary.modified + " =" + result.summary.unchanged
219
+ );
220
+ const protConfig = await protectedObjects.loadProtectionConfig(path.dirname(pacPath));
221
+ if (protConfig) {
222
+ const ops = result.objects.filter((o) => o.kind !== "unchanged" && o.kind !== "added").map((o) => ({
223
+ fqn: o.identity.fqn,
224
+ objectType: String(o.identity.objectType),
225
+ kind: o.kind
226
+ }));
227
+ const allowedFqns = (opts.allowProtected ?? []).flatMap(
228
+ (s) => s.split(",").map((f) => f.trim())
229
+ );
230
+ const violations = protectedObjects.checkProtectedObjects(ops, protConfig, {
231
+ allowedFqns,
232
+ allowAll: Boolean(opts.allowAllProtected)
233
+ });
234
+ if (violations.length > 0) {
235
+ logger.error(protectedObjects.formatProtectionViolations(violations));
236
+ process.exitCode = 1;
237
+ await conn.disconnect();
238
+ return;
239
+ }
240
+ }
241
+ const deploymentProfileName = opts.profile ? String(opts.profile) : void 0;
242
+ let publishVariables;
243
+ if (deploymentProfileName) {
244
+ const profileBlock = pac.manifest.deploymentProfiles?.[deploymentProfileName];
245
+ if (!profileBlock) {
246
+ const available = Object.keys(pac.manifest.deploymentProfiles ?? {});
247
+ logger.error(
248
+ `--profile ${deploymentProfileName}: no deploymentProfile by that name in the pac manifest. ` + (available.length === 0 ? "The pac carries no deploymentProfiles (was it built before VARSYNTAX.2, or does the .sdtproj declare any?)." : `Available: ${available.join(", ")}.`)
249
+ );
250
+ process.exitCode = 1;
251
+ await conn.disconnect();
252
+ return;
253
+ }
254
+ if (profileBlock.variables) publishVariables = { ...profileBlock.variables };
255
+ }
256
+ const cliVariables = parsePublishVariables(opts.variables);
257
+ if (cliVariables) {
258
+ publishVariables = { ...publishVariables ?? {}, ...cliVariables };
259
+ }
260
+ const gen = new ScriptGenerator();
261
+ const generateContext = {
262
+ projectName: pac.manifest.projectName,
263
+ projectVersion: pac.manifest.projectVersion,
264
+ ...deploymentProfileName ? { profile: deploymentProfileName } : {},
265
+ ...profile.auth?.username ? { user: profile.auth.username } : {}
266
+ };
267
+ const body = gen.generate(result, {
268
+ banner: "SDT publish: " + pac.manifest.projectName + " v" + pac.manifest.projectVersion,
269
+ deployment: resolved.deployment,
270
+ ...publishVariables ? { variables: publishVariables } : {},
271
+ context: generateContext
272
+ });
273
+ const sections = [];
274
+ if (preScripts.length > 0) {
275
+ sections.push("-- " + "=".repeat(68));
276
+ sections.push("-- PRE-DEPLOY (" + preScripts.length + " script(s))");
277
+ sections.push("-- " + "=".repeat(68));
278
+ for (const s of preScripts) sections.push(formatDeployScript(s, "pre"));
279
+ }
280
+ sections.push(body.sql);
281
+ if (postScripts.length > 0) {
282
+ sections.push("");
283
+ sections.push("-- " + "=".repeat(68));
284
+ sections.push("-- POST-DEPLOY (" + postScripts.length + " script(s))");
285
+ sections.push("-- " + "=".repeat(68));
286
+ for (const s of postScripts) sections.push(formatDeployScript(s, "post"));
287
+ }
288
+ const fullScript = sections.join("\n");
289
+ logger.info(
290
+ " pre-deploy: " + preScripts.length + " script(s); post-deploy: " + postScripts.length + " script(s)"
291
+ );
292
+ if (opts.out) {
293
+ const out = path.resolve(String(opts.out));
294
+ await fs.mkdir(path.dirname(out), { recursive: true });
295
+ await fs.writeFile(out, fullScript);
296
+ logger.success("Wrote migration script to " + out);
297
+ }
298
+ if (opts.dryRun || !opts.apply) {
299
+ const colorMode = opts.color ?? "auto";
300
+ process.stdout.write(colorizeMigrationScript(fullScript, { mode: colorMode }) + "\n");
301
+ await conn.disconnect();
302
+ return;
303
+ }
304
+ const assessment = safety.assess(result, { deployment: resolved.deployment });
305
+ logger.info(
306
+ " safety: " + assessment.unrecoverable.length + " unrecoverable, " + assessment.destructive.length + " destructive, " + assessment.expensive.length + " expensive, " + assessment.warnings.length + " warnings"
307
+ );
308
+ if (assessment.blocked) {
309
+ logger.error("Deploy refused: " + assessment.blockReason);
310
+ await conn.disconnect();
311
+ process.exitCode = 1;
312
+ return;
313
+ }
314
+ const satisfied = safety.satisfiedGates({
315
+ yes: Boolean(opts.yes),
316
+ confirmProduction: Boolean(opts.confirmProduction) || !safety.looksLikeProduction(profileName),
317
+ allowDropTable: resolved.deployment.allowDropTable,
318
+ allowDropColumn: resolved.deployment.allowDropColumn,
319
+ allowNarrowing: resolved.deployment.allowNarrowingTypes,
320
+ allowTableRebuild: resolved.deployment.allowTableRebuild,
321
+ allowUnrecoverableDrop: resolved.deployment.allowUnrecoverableDrop
322
+ });
323
+ const outstanding = safety.outstandingGates(assessment, satisfied);
324
+ if (outstanding.length > 0) {
325
+ logger.error(
326
+ "Outstanding safety gate(s): " + outstanding.join(", ") + ". Pass --yes / --confirm-production or set the matching deployOptions.deployment.* flag in the profile."
327
+ );
328
+ await conn.disconnect();
329
+ process.exitCode = 1;
330
+ return;
331
+ }
332
+ if (opts.requireApprovals !== void 0 && opts.requireApprovals !== false) {
333
+ const required = Number(opts.requireApprovals) || 0;
334
+ const allowed = opts.approver ? String(opts.approver).split(",").map((s) => s.trim()).filter(Boolean) : [];
335
+ const deployId = String(opts.deployId ?? `${profileName}:${db}.${schema}`);
336
+ const approvalsRoot = String(opts.approvalsRoot);
337
+ const file = await approval.readApprovalFile(approvalsRoot, deployId);
338
+ const outcome = approval.evaluateApprovalGate(
339
+ { deployId, required, allowedApprovers: allowed },
340
+ file.approvals
341
+ );
342
+ if (!outcome.satisfied) {
343
+ logger.error(
344
+ "Approval gate blocked deploy: " + (outcome.blockReason ?? "gate not satisfied") + `. Record approvals with \`sdt approval add ${deployId} --as <user>\`.`
345
+ );
346
+ await conn.disconnect();
347
+ process.exitCode = 1;
348
+ return;
349
+ }
350
+ logger.info(
351
+ ` approvals: ${outcome.satisfiedBy.length}/${required} (${outcome.satisfiedBy.join(", ")})`
352
+ );
353
+ }
354
+ if (opts.aiPreflight) {
355
+ const narrative = await readPreflightNarrative(opts.aiPreflightText);
356
+ if (!narrative) {
357
+ logger.error(
358
+ "--ai-preflight needs a narrative. Provide it inline via --ai-preflight-text or via stdin."
359
+ );
360
+ await conn.disconnect();
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+ logger.step("Grading deploy narrative with AI...");
365
+ try {
366
+ const findings = [
367
+ ...assessment.unrecoverable.map((f) => ({
368
+ code: String(f.code),
369
+ fqn: f.fqn,
370
+ reason: f.reason
371
+ })),
372
+ ...assessment.destructive.map((f) => ({
373
+ code: String(f.code),
374
+ fqn: f.fqn,
375
+ reason: f.reason
376
+ })),
377
+ ...assessment.expensive.map((f) => ({
378
+ code: String(f.code),
379
+ fqn: f.fqn,
380
+ reason: f.reason
381
+ }))
382
+ ];
383
+ const compareSummary = `added ${result.summary.added}, modified ${result.summary.modified}, removed ${result.summary.removed}, unchanged ${result.summary.unchanged}`;
384
+ const verdict = await aiPreflight.gradePreflight(
385
+ {
386
+ narrative,
387
+ compareSummary,
388
+ safetyFindings: findings,
389
+ diffSampleDdl: fullScript,
390
+ target: `${db}.${schema}@${profileName}`
391
+ },
392
+ {
393
+ completeFn: async (prompt) => {
394
+ const r = await ai.complete([{ role: "user", content: prompt }], {
395
+ feature: "ai-preflight"
396
+ });
397
+ return r.text;
398
+ }
399
+ }
400
+ );
401
+ logger.info(" preflight verdict: " + verdict.verdict);
402
+ if (verdict.reasoning) logger.info(" preflight reasoning: " + verdict.reasoning);
403
+ for (const d of verdict.discrepancies) logger.info(" \u2022 " + d);
404
+ const isBlocking = verdict.verdict === "mismatch" || opts.strictAiPreflight && verdict.verdict === "partial";
405
+ if (isBlocking && !opts.confirmAfterPreflightMismatch) {
406
+ logger.error(
407
+ `Deploy refused: AI preflight returned "${verdict.verdict}". Re-run with --confirm-after-preflight-mismatch to override, or revise the narrative.`
408
+ );
409
+ await conn.disconnect();
410
+ process.exitCode = 1;
411
+ return;
412
+ }
413
+ if (verdict.parseFailed) {
414
+ logger.warn(
415
+ " preflight verdict could not be parsed \u2014 continuing without a clean alignment signal."
416
+ );
417
+ }
418
+ } catch (err) {
419
+ const msg = err instanceof Error ? err.message : String(err);
420
+ logger.error(
421
+ `--ai-preflight failed: ${msg}. Configure an AI provider via \`sdt ai status\` or omit --ai-preflight.`
422
+ );
423
+ await conn.disconnect();
424
+ process.exitCode = 1;
425
+ return;
426
+ }
427
+ }
428
+ if (opts.viaDcm) {
429
+ const dcmStage = String(opts.viaDcm);
430
+ const lic = await license.loadLicense();
431
+ const licCheck = license.checkProFeature(lic, "dcm-deploy");
432
+ if (licCheck.warning) logger.warn("[Enterprise] " + licCheck.warning);
433
+ const dcmCompatReport = assessDcmCompatibility(result);
434
+ if (dcmCompatReport.blocked) {
435
+ logger.error("--via-dcm blocked:\n" + formatDcmCompatibilityReport(dcmCompatReport));
436
+ await conn.disconnect();
437
+ process.exitCode = 1;
438
+ return;
439
+ }
440
+ if (dcmCompatReport.findings.length > 0) {
441
+ logger.warn(
442
+ "DCM compatibility warnings:\n" + formatDcmCompatibilityReport(dcmCompatReport)
443
+ );
444
+ }
445
+ if (opts.dryRun || !opts.apply) {
446
+ logger.info(
447
+ "DCM compatibility check passed (" + dcmCompatReport.findings.length + " warning(s)). Use --apply to execute via DCM."
448
+ );
449
+ await conn.disconnect();
450
+ return;
451
+ }
452
+ logger.step("Executing via DCM: EXECUTE DCM PROJECT " + dcmStage + " DEPLOY");
453
+ const dcmResult = await executeDcmProjectDeploy(conn, dcmStage);
454
+ const bodyReport2 = mapDcmResultToDeploymentReport(dcmResult);
455
+ if (dcmResult.overallStatus === "SUCCESS") {
456
+ logger.success(
457
+ "DCM deploy complete. " + dcmResult.objects.length + " object(s) processed."
458
+ );
459
+ } else {
460
+ logger.error(
461
+ "DCM deploy finished with failures: " + dcmResult.failures.length + " failed, " + dcmResult.objects.length + " total."
462
+ );
463
+ process.exitCode = 1;
464
+ }
465
+ if (opts.manifest) {
466
+ const m = buildDeployManifest(bodyReport2, profile.account);
467
+ const mPath = path.resolve(String(opts.manifest));
468
+ await fs.mkdir(path.dirname(mPath), { recursive: true });
469
+ await fs.writeFile(mPath, JSON.stringify(m, null, 2) + "\n", "utf8");
470
+ logger.success("Wrote deploy manifest \u2192 " + mPath);
471
+ }
472
+ await conn.disconnect();
473
+ return;
474
+ }
475
+ const queryTag = typeof opts.queryTag === "string" && opts.queryTag.length > 0 ? String(opts.queryTag) : `sdt:${pac.manifest.projectName}@${pac.manifest.projectVersion}:${Date.now()}`;
476
+ try {
477
+ await conn.query(`ALTER SESSION SET QUERY_TAG = '${queryTag.replace(/'/g, "''")}'`);
478
+ logger.info(" query_tag: " + queryTag);
479
+ } catch (err) {
480
+ const msg = err instanceof Error ? err.message : String(err);
481
+ logger.error(
482
+ "Failed to set QUERY_TAG: " + msg + ". Continuing without tag \u2014 audit attribution will be missing."
483
+ );
484
+ }
485
+ logger.step("Applying migration...");
486
+ let executed = 0;
487
+ let failed = 0;
488
+ const runOne = async (sql, label) => {
489
+ const stmts = splitStatements(sql);
490
+ for (const stmt of stmts) {
491
+ try {
492
+ await conn.query(stmt);
493
+ executed += 1;
494
+ } catch (err) {
495
+ failed += 1;
496
+ const msg = err instanceof Error ? err.message : String(err);
497
+ logger.error(" [" + label + "] failed: " + msg + "\n " + stmt);
498
+ return false;
499
+ }
500
+ }
501
+ return true;
502
+ };
503
+ let ok = true;
504
+ for (const s of preScripts) {
505
+ logger.info(" pre: " + s.path);
506
+ ok = await runOne(s.sql, "pre/" + s.path);
507
+ if (!ok) break;
508
+ }
509
+ let bodyReport;
510
+ if (ok) {
511
+ const plan = defaultSafePlan(result);
512
+ const validated = validatePlan(result, plan);
513
+ const steps = buildExecutionPlan(result, validated, {
514
+ deployment: resolved.deployment
515
+ });
516
+ if (steps.length > 0) {
517
+ const executor = new SnowflakeExecutor(conn, {
518
+ onStatement: (sql) => {
519
+ executed += 1;
520
+ const head = sql.split("\n")[0]?.slice(0, 100) ?? "";
521
+ logger.info(" \u2192 " + head + (sql.length > 100 ? " \u2026" : ""));
522
+ }
523
+ });
524
+ bodyReport = await executePlan(steps, executor, {
525
+ rollbackOnFailure: false,
526
+ onStepEnd: (r) => {
527
+ if (r.status === "FAILED") {
528
+ failed += 1;
529
+ logger.error(
530
+ " [" + r.step.fqn + "] failed: " + (r.error ?? "unknown") + "\n " + r.step.forwardSql
531
+ );
532
+ }
533
+ }
534
+ });
535
+ if (bodyReport.finalState !== "CLEAN_SUCCESS") ok = false;
536
+ }
537
+ }
538
+ if (ok) {
539
+ for (const s of postScripts) {
540
+ logger.info(" post: " + s.path);
541
+ ok = await runOne(s.sql, "post/" + s.path);
542
+ if (!ok) break;
543
+ }
544
+ }
545
+ if (opts.manifest && bodyReport) {
546
+ const m = buildDeployManifest(bodyReport, profile.account);
547
+ const mPath = path.resolve(String(opts.manifest));
548
+ await fs.mkdir(path.dirname(mPath), { recursive: true });
549
+ await fs.writeFile(mPath, JSON.stringify(m, null, 2) + "\n", "utf8");
550
+ logger.success("Wrote deploy manifest \u2192 " + mPath);
551
+ }
552
+ if (ok) {
553
+ logger.success("Deploy complete. " + executed + " statement(s) executed.");
554
+ if (opts.postDeployTests) {
555
+ const projectRoot = path.resolve(String(opts.postDeployTests));
556
+ logger.step("Running post-deploy DQ tests from " + projectRoot + "...");
557
+ const testFiles = await testFramework.discoverTests(projectRoot);
558
+ if (testFiles.length === 0) {
559
+ logger.warn(
560
+ " POST_DEPLOY_DQ: no tests found under " + projectRoot + "/tests/ \u2014 skipping."
561
+ );
562
+ } else {
563
+ const report = await testFramework.runTestFiles(testFiles, conn);
564
+ process.stdout.write(testFramework.renderTextReport(report));
565
+ if (report.summary.blocking) {
566
+ logger.error(
567
+ "POST_DEPLOY_DQ_FAILED: " + report.summary.fail + " fail / " + report.summary.error + " error." + (opts.manifest ? " Recover: `sdt revert --manifest " + String(opts.manifest) + "`" : " Consider re-deploying prior version or using `sdt revert --manifest` if --manifest was used.")
568
+ );
569
+ process.exitCode = 2;
570
+ } else {
571
+ logger.success(
572
+ " POST_DEPLOY_DQ: " + report.summary.pass + "/" + report.summary.total + " pass."
573
+ );
574
+ }
575
+ }
576
+ }
577
+ } else {
578
+ logger.error(
579
+ "Deploy aborted after " + executed + " statement(s); " + failed + " failure(s). Recover via UNDROP / clone-swap if preDeployClone was set."
580
+ );
581
+ process.exitCode = 1;
582
+ }
583
+ await conn.disconnect();
584
+ });
585
+ addMappingFlags(cmd);
586
+ attachRelatedOptions(cmd, [
587
+ "deployment.allowDropTable",
588
+ "deployment.allowDropColumn",
589
+ "deployment.allowUnrecoverableDrop",
590
+ "deployment.allowNarrowingTypes",
591
+ "deployment.preDeployClone",
592
+ "deployment.blockOnPossibleDataLoss",
593
+ "deployment.preserveTargetOnlyObjects"
594
+ ]);
595
+ return cmd;
596
+ }
597
+ async function runRestoreFromSnapshot(opts) {
598
+ const batchId = String(opts.restoreFromSnapshot);
599
+ const registryDir = path.resolve(String(opts.snapshotDir ?? ".sdt/snapshots"));
600
+ const profileName = String(opts.connection);
601
+ let batch;
602
+ try {
603
+ batch = await safety.readSnapshotBatch(registryDir, batchId);
604
+ } catch (err) {
605
+ const msg = err instanceof Error ? err.message : String(err);
606
+ logger.error(`Snapshot batch "${batchId}" not found in ${registryDir}: ${msg}`);
607
+ process.exitCode = 1;
608
+ return;
609
+ }
610
+ const sqlLines = safety.emitRestoreFromSnapshotSql(batch);
611
+ const sql = sqlLines.join("\n");
612
+ logger.info(`Restoring from batch ${batch.batchId} (createdAt=${batch.createdAt})`);
613
+ logger.info(
614
+ ` ${batch.entries.length} object(s) recorded; ${sqlLines.filter((l) => !l.startsWith("--")).length} statement(s) will run.`
615
+ );
616
+ if (opts.out) {
617
+ const out = path.resolve(String(opts.out));
618
+ await fs.mkdir(path.dirname(out), { recursive: true });
619
+ await fs.writeFile(out, sql + "\n", "utf8");
620
+ logger.success(`Wrote restore SQL \u2192 ${out}`);
621
+ }
622
+ if (opts.dryRun || !opts.apply) {
623
+ const colorMode = opts.color ?? "auto";
624
+ process.stdout.write(colorizeMigrationScript(sql, { mode: colorMode }) + "\n");
625
+ return;
626
+ }
627
+ if (!opts.yes) {
628
+ logger.error("--apply requires --yes (restore replaces production tables via SWAP WITH).");
629
+ process.exitCode = 1;
630
+ return;
631
+ }
632
+ if (safety.looksLikeProduction(profileName) && !opts.confirmProduction) {
633
+ logger.error(
634
+ `Profile "${profileName}" looks like production \u2014 also pass --confirm-production to restore.`
635
+ );
636
+ process.exitCode = 1;
637
+ return;
638
+ }
639
+ const profile = await getProfile(profileName);
640
+ const conn = new SnowflakeConnection(profile);
641
+ let executed = 0;
642
+ let failed = 0;
643
+ try {
644
+ for (const stmt of coreSplitStatements(sql).map((s) => s.sql)) {
645
+ try {
646
+ await conn.query(stmt);
647
+ executed += 1;
648
+ const head = stmt.split("\n")[0]?.slice(0, 100) ?? "";
649
+ logger.info(" \u2192 " + head + (stmt.length > 100 ? " \u2026" : ""));
650
+ } catch (err) {
651
+ failed += 1;
652
+ const msg = err instanceof Error ? err.message : String(err);
653
+ logger.error(` [restore ${batch.batchId}] ${msg}
654
+ ${stmt}`);
655
+ }
656
+ }
657
+ } finally {
658
+ await conn.disconnect();
659
+ }
660
+ if (failed === 0) {
661
+ logger.success(`Restore complete. ${executed} statement(s) executed.`);
662
+ } else {
663
+ logger.error(
664
+ `Restore finished with failures: ${executed} ok, ${failed} failed. Inspect the snapshot tables under ${registryDir} and recover by hand.`
665
+ );
666
+ process.exitCode = 1;
667
+ }
668
+ }
669
+ async function readPreflightNarrative(inlineText) {
670
+ if (typeof inlineText === "string" && inlineText.trim().length > 0) {
671
+ return inlineText.trim();
672
+ }
673
+ if (process.stdin.isTTY) {
674
+ process.stdout.write(
675
+ "\n--ai-preflight is active. In one paragraph, describe what you expect this deploy to do.\nEnd with an empty line (or Ctrl-D):\n> "
676
+ );
677
+ }
678
+ process.stdin.setEncoding("utf8");
679
+ let buffer = "";
680
+ await new Promise((resolve) => {
681
+ const onData = (chunk) => {
682
+ buffer += chunk;
683
+ if (/\n\s*\n/.test(buffer)) {
684
+ process.stdin.removeListener("data", onData);
685
+ process.stdin.removeListener("end", onEnd);
686
+ resolve();
687
+ }
688
+ };
689
+ const onEnd = () => {
690
+ process.stdin.removeListener("data", onData);
691
+ resolve();
692
+ };
693
+ process.stdin.on("data", onData);
694
+ process.stdin.on("end", onEnd);
695
+ });
696
+ const trimmed = buffer.trim();
697
+ return trimmed.length > 0 ? trimmed : void 0;
698
+ }
699
+ function parsePublishVariables(raw) {
700
+ if (!raw) return void 0;
701
+ const out = {};
702
+ for (const pair of String(raw).split(",")) {
703
+ const eq = pair.indexOf("=");
704
+ if (eq <= 0) continue;
705
+ const k = pair.slice(0, eq).trim();
706
+ const v = pair.slice(eq + 1).trim();
707
+ if (k.length > 0) out[k] = v;
708
+ }
709
+ return Object.keys(out).length > 0 ? out : void 0;
710
+ }
711
+ export {
712
+ parsePublishVariables,
713
+ publishCommand
714
+ };
715
+ //# sourceMappingURL=publish-Y2J56K4Y.js.map