@sdt-tools/cli 0.2.0 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advise-tests-6DRSZMBL.js +87 -0
- package/dist/advise-tests-6DRSZMBL.js.map +1 -0
- package/dist/ai-G4MJWHTM.js +89 -0
- package/dist/ai-G4MJWHTM.js.map +1 -0
- package/dist/anonymize-QR6JGXA7.js +123 -0
- package/dist/anonymize-QR6JGXA7.js.map +1 -0
- package/dist/approval-YVHYTV53.js +73 -0
- package/dist/approval-YVHYTV53.js.map +1 -0
- package/dist/approval-chain-54KKJZS3.js +120 -0
- package/dist/approval-chain-54KKJZS3.js.map +1 -0
- package/dist/audit-log-QZFH7LUX.js +159 -0
- package/dist/audit-log-QZFH7LUX.js.map +1 -0
- package/dist/backlog-V2YUIQDL.js +76 -0
- package/dist/backlog-V2YUIQDL.js.map +1 -0
- package/dist/bisect-GEVYAVL5.js +111 -0
- package/dist/bisect-GEVYAVL5.js.map +1 -0
- package/dist/bookmarks-57LKS7P6.js +107 -0
- package/dist/bookmarks-57LKS7P6.js.map +1 -0
- package/dist/branch-W2MGMPSH.js +88 -0
- package/dist/branch-W2MGMPSH.js.map +1 -0
- package/dist/build-VNIQFKSP.js +23 -0
- package/dist/build-VNIQFKSP.js.map +1 -0
- package/dist/catalog-JLB5VCEV.js +137 -0
- package/dist/catalog-JLB5VCEV.js.map +1 -0
- package/dist/changelog-M7XGDYSY.js +220 -0
- package/dist/changelog-M7XGDYSY.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-EWXM4KJN.js +25 -0
- package/dist/chunk-EWXM4KJN.js.map +1 -0
- package/dist/chunk-JP2EZLR5.js +50 -0
- package/dist/chunk-JP2EZLR5.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-ZWY4ZRHL.js +44 -0
- package/dist/chunk-ZWY4ZRHL.js.map +1 -0
- package/dist/cli.js +511 -19014
- package/dist/cli.js.map +1 -1
- package/dist/compare-5O6UTWPJ.js +405 -0
- package/dist/compare-5O6UTWPJ.js.map +1 -0
- package/dist/compare-profiles-7ZSNIW7B.js +218 -0
- package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
- package/dist/completion-I5U5VVAX.js +82 -0
- package/dist/completion-I5U5VVAX.js.map +1 -0
- package/dist/connection-GNTZDHXF.js +133 -0
- package/dist/connection-GNTZDHXF.js.map +1 -0
- package/dist/cost-estimate-TJDDH6TO.js +328 -0
- package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
- package/dist/data-compare-UK2UXAS3.js +134 -0
- package/dist/data-compare-UK2UXAS3.js.map +1 -0
- package/dist/data-fit-Q45ENBRL.js +125 -0
- package/dist/data-fit-Q45ENBRL.js.map +1 -0
- package/dist/deploy-status-UUHKVDTI.js +58 -0
- package/dist/deploy-status-UUHKVDTI.js.map +1 -0
- package/dist/design-PO6UPBL7.js +138 -0
- package/dist/design-PO6UPBL7.js.map +1 -0
- package/dist/diagnose-6IFMELFR.js +145 -0
- package/dist/diagnose-6IFMELFR.js.map +1 -0
- package/dist/discover-A7OSZAHK.js +78 -0
- package/dist/discover-A7OSZAHK.js.map +1 -0
- package/dist/docs-CVRKGUSW.js +177 -0
- package/dist/docs-CVRKGUSW.js.map +1 -0
- package/dist/drift-XDA3BDYN.js +226 -0
- package/dist/drift-XDA3BDYN.js.map +1 -0
- package/dist/drift-gate-V7QSIOGZ.js +94 -0
- package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
- package/dist/error-lookup-7ZWCZJ44.js +56 -0
- package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
- package/dist/errorReporting-AQXKKGZH.js +109 -0
- package/dist/errorReporting-AQXKKGZH.js.map +1 -0
- package/dist/exec-PKBHLI7T.js +121 -0
- package/dist/exec-PKBHLI7T.js.map +1 -0
- package/dist/explain-LWKJOTL7.js +192 -0
- package/dist/explain-LWKJOTL7.js.map +1 -0
- package/dist/explorer-QOVM6VBD.js +61 -0
- package/dist/explorer-QOVM6VBD.js.map +1 -0
- package/dist/export-IYYBZ5HE.js +42 -0
- package/dist/export-IYYBZ5HE.js.map +1 -0
- package/dist/extract-VMMVRQVT.js +102 -0
- package/dist/extract-VMMVRQVT.js.map +1 -0
- package/dist/features-LE6BDZ2S.js +59 -0
- package/dist/features-LE6BDZ2S.js.map +1 -0
- package/dist/feedback-M7DM2EQC.js +161 -0
- package/dist/feedback-M7DM2EQC.js.map +1 -0
- package/dist/find-EME2JG2I.js +176 -0
- package/dist/find-EME2JG2I.js.map +1 -0
- package/dist/format-TRLWLMGS.js +141 -0
- package/dist/format-TRLWLMGS.js.map +1 -0
- package/dist/generate-6NAZGZDV.js +152 -0
- package/dist/generate-6NAZGZDV.js.map +1 -0
- package/dist/graph-QNQDAUO7.js +161 -0
- package/dist/graph-QNQDAUO7.js.map +1 -0
- package/dist/history-RONA7ZTI.js +199 -0
- package/dist/history-RONA7ZTI.js.map +1 -0
- package/dist/hosts-YBXY2ZG5.js +49 -0
- package/dist/hosts-YBXY2ZG5.js.map +1 -0
- package/dist/impact-T2JSANHS.js +59 -0
- package/dist/impact-T2JSANHS.js.map +1 -0
- package/dist/import-AELYLY6A.js +32 -0
- package/dist/import-AELYLY6A.js.map +1 -0
- package/dist/import-script-2OF5BI6A.js +83 -0
- package/dist/import-script-2OF5BI6A.js.map +1 -0
- package/dist/index.cjs +71 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +95 -31
- package/dist/index.js.map +1 -1
- package/dist/init-SWRRJMGI.js +57 -0
- package/dist/init-SWRRJMGI.js.map +1 -0
- package/dist/install-hooks-6SIAGTAF.js +109 -0
- package/dist/install-hooks-6SIAGTAF.js.map +1 -0
- package/dist/license-OAF22PLZ.js +46 -0
- package/dist/license-OAF22PLZ.js.map +1 -0
- package/dist/lineage-EW66XJ6O.js +552 -0
- package/dist/lineage-EW66XJ6O.js.map +1 -0
- package/dist/lint-FQ2OTYTQ.js +143 -0
- package/dist/lint-FQ2OTYTQ.js.map +1 -0
- package/dist/mcp-SARDMCDV.js +344 -0
- package/dist/mcp-SARDMCDV.js.map +1 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
- package/dist/migrate-platform-NTRTOGNR.js +91 -0
- package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
- package/dist/optimize-CJYWMAWA.js +105 -0
- package/dist/optimize-CJYWMAWA.js.map +1 -0
- package/dist/perf-LL2CPCJF.js +205 -0
- package/dist/perf-LL2CPCJF.js.map +1 -0
- package/dist/pii-FBDRDQ2E.js +136 -0
- package/dist/pii-FBDRDQ2E.js.map +1 -0
- package/dist/pilot-CCQERKPH.js +29 -0
- package/dist/pilot-CCQERKPH.js.map +1 -0
- package/dist/pr-comment-S5FF4QRX.js +79 -0
- package/dist/pr-comment-S5FF4QRX.js.map +1 -0
- package/dist/preview-5U4YVCRM.js +47 -0
- package/dist/preview-5U4YVCRM.js.map +1 -0
- package/dist/profile-7VC57KD2.js +101 -0
- package/dist/profile-7VC57KD2.js.map +1 -0
- package/dist/promote-AASEFTIA.js +408 -0
- package/dist/promote-AASEFTIA.js.map +1 -0
- package/dist/publish-UMVIWH6H.js +721 -0
- package/dist/publish-UMVIWH6H.js.map +1 -0
- package/dist/purge-QMXZKCMD.js +57 -0
- package/dist/purge-QMXZKCMD.js.map +1 -0
- package/dist/query-log-6OM4GI7W.js +112 -0
- package/dist/query-log-6OM4GI7W.js.map +1 -0
- package/dist/refactor-LTZQLJ35.js +5799 -0
- package/dist/refactor-LTZQLJ35.js.map +1 -0
- package/dist/refresh-4TY2AGOU.js +38 -0
- package/dist/refresh-4TY2AGOU.js.map +1 -0
- package/dist/replay-OOC25FZN.js +117 -0
- package/dist/replay-OOC25FZN.js.map +1 -0
- package/dist/revert-ODMUVJW6.js +110 -0
- package/dist/revert-ODMUVJW6.js.map +1 -0
- package/dist/review-XXPWOBFP.js +158 -0
- package/dist/review-XXPWOBFP.js.map +1 -0
- package/dist/rollback-suggest-6G2HEKFR.js +79 -0
- package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
- package/dist/safer-alternative-QFVNLG3L.js +89 -0
- package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
- package/dist/safety-7QWRSUEZ.js +168 -0
- package/dist/safety-7QWRSUEZ.js.map +1 -0
- package/dist/savings-RHIXP6IT.js +95 -0
- package/dist/savings-RHIXP6IT.js.map +1 -0
- package/dist/scan-secrets-5YCQ4UCU.js +54 -0
- package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
- package/dist/schema-CIZXCQD2.js +429 -0
- package/dist/schema-CIZXCQD2.js.map +1 -0
- package/dist/script-K7CIN2P6.js +153 -0
- package/dist/script-K7CIN2P6.js.map +1 -0
- package/dist/search-BUZ5NXZZ.js +151 -0
- package/dist/search-BUZ5NXZZ.js.map +1 -0
- package/dist/seed-76QAK276.js +96 -0
- package/dist/seed-76QAK276.js.map +1 -0
- package/dist/sketch-PTLKDIK3.js +88 -0
- package/dist/sketch-PTLKDIK3.js.map +1 -0
- package/dist/snapshot-XLPR2OZ5.js +177 -0
- package/dist/snapshot-XLPR2OZ5.js.map +1 -0
- package/dist/snippets-EK4DK5CN.js +74 -0
- package/dist/snippets-EK4DK5CN.js.map +1 -0
- package/dist/standards-7T2UY6DD.js +241 -0
- package/dist/standards-7T2UY6DD.js.map +1 -0
- package/dist/suggest-VGRYSAR6.js +39 -0
- package/dist/suggest-VGRYSAR6.js.map +1 -0
- package/dist/suggest-constraints-MY5WKUHA.js +160 -0
- package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
- package/dist/suite-TRNGZWQM.js +88 -0
- package/dist/suite-TRNGZWQM.js.map +1 -0
- package/dist/telemetry-3U2QLA2S.js +75 -0
- package/dist/telemetry-3U2QLA2S.js.map +1 -0
- package/dist/template-ZERIXVXF.js +403 -0
- package/dist/template-ZERIXVXF.js.map +1 -0
- package/dist/test-5M2ED3WT.js +169 -0
- package/dist/test-5M2ED3WT.js.map +1 -0
- package/dist/trial-U732FONV.js +31 -0
- package/dist/trial-U732FONV.js.map +1 -0
- package/dist/validate-T6D2WCOK.js +106 -0
- package/dist/validate-T6D2WCOK.js.map +1 -0
- package/dist/verify-KXVASEEG.js +76 -0
- package/dist/verify-KXVASEEG.js.map +1 -0
- package/dist/watch-I6K4BNMA.js +80 -0
- package/dist/watch-I6K4BNMA.js.map +1 -0
- package/dist/xcompare-TPFLQO6W.js +87 -0
- package/dist/xcompare-TPFLQO6W.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19040
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/refresh.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { compare, refresh } from "@sdt-tools/core";
|
|
8
|
+
function refreshCommand() {
|
|
9
|
+
return new Command("refresh").description(
|
|
10
|
+
"Generate a REFRESH script for the dynamic tables + tasks in the project. Operator runs the output script explicitly."
|
|
11
|
+
).requiredOption("--source <path>", ".sdtproj or .sdtpac defining the objects.").option("--no-dynamic-tables", "Skip ALTER DYNAMIC TABLE ... REFRESH entries.", false).option("--no-tasks", "Skip ALTER TASK ... RESUME entries.", false).option("-o, --output <path>", "Write script to a file instead of stdout.").action(async (opts) => {
|
|
12
|
+
const sourcePath = path.resolve(String(opts.source));
|
|
13
|
+
const source = new compare.ProjectSource(sourcePath);
|
|
14
|
+
const model = await source.load();
|
|
15
|
+
const result = refresh.generateRefreshScript(
|
|
16
|
+
model,
|
|
17
|
+
{
|
|
18
|
+
dynamicTables: opts.dynamicTables !== false,
|
|
19
|
+
tasks: opts.tasks !== false
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
const header = `-- ${result.summary.dynamicTablesRefreshed} dynamic table(s) \xB7 ${result.summary.tasksResumed} task(s) resumed
|
|
23
|
+
|
|
24
|
+
`;
|
|
25
|
+
const out = header + result.sql;
|
|
26
|
+
if (opts.output) {
|
|
27
|
+
const outPath = path.resolve(String(opts.output));
|
|
28
|
+
await fs.writeFile(outPath, out, "utf8");
|
|
29
|
+
console.error(`refresh: wrote script to ${outPath}`);
|
|
30
|
+
} else {
|
|
31
|
+
process.stdout.write(out);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
refreshCommand
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=refresh-4TY2AGOU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/refresh.ts"],"sourcesContent":["/**\n * `sdt refresh` — DCM compatibility item 4.\n *\n * Standalone refresh — emits ALTER DYNAMIC TABLE ... REFRESH + ALTER\n * TASK ... RESUME for every dynamic table / task in the project.\n *\n * Mirrors DCM's `EXECUTE DCM PROJECT <n> REFRESH ALL`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { compare, refresh } from '@sdt-tools/core';\n\nexport function refreshCommand(): Command {\n return new Command('refresh')\n .description(\n 'Generate a REFRESH script for the dynamic tables + tasks in the project. ' +\n 'Operator runs the output script explicitly.',\n )\n .requiredOption('--source <path>', '.sdtproj or .sdtpac defining the objects.')\n .option('--no-dynamic-tables', 'Skip ALTER DYNAMIC TABLE ... REFRESH entries.', false)\n .option('--no-tasks', 'Skip ALTER TASK ... RESUME entries.', false)\n .option('-o, --output <path>', 'Write script to a file instead of stdout.')\n .action(async (opts: Record<string, unknown>) => {\n const sourcePath = path.resolve(String(opts.source));\n const source = new compare.ProjectSource(sourcePath);\n const model = await source.load();\n const result = refresh.generateRefreshScript(\n model as Parameters<typeof refresh.generateRefreshScript>[0],\n {\n dynamicTables: opts.dynamicTables !== false,\n tasks: opts.tasks !== false,\n },\n );\n const header = `-- ${result.summary.dynamicTablesRefreshed} dynamic table(s) · ${result.summary.tasksResumed} task(s) resumed\\n\\n`;\n const out = header + result.sql;\n if (opts.output) {\n const outPath = path.resolve(String(opts.output));\n await fs.writeFile(outPath, out, 'utf8');\n console.error(`refresh: wrote script to ${outPath}`);\n } else {\n process.stdout.write(out);\n }\n });\n}\n"],"mappings":";;;AAQA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,SAAS,eAAe;AAE1B,SAAS,iBAA0B;AACxC,SAAO,IAAI,QAAQ,SAAS,EACzB;AAAA,IACC;AAAA,EAEF,EACC,eAAe,mBAAmB,2CAA2C,EAC7E,OAAO,uBAAuB,iDAAiD,KAAK,EACpF,OAAO,cAAc,uCAAuC,KAAK,EACjE,OAAO,uBAAuB,2CAA2C,EACzE,OAAO,OAAO,SAAkC;AAC/C,UAAM,aAAa,KAAK,QAAQ,OAAO,KAAK,MAAM,CAAC;AACnD,UAAM,SAAS,IAAI,QAAQ,cAAc,UAAU;AACnD,UAAM,QAAQ,MAAM,OAAO,KAAK;AAChC,UAAM,SAAS,QAAQ;AAAA,MACrB;AAAA,MACA;AAAA,QACE,eAAe,KAAK,kBAAkB;AAAA,QACtC,OAAO,KAAK,UAAU;AAAA,MACxB;AAAA,IACF;AACA,UAAM,SAAS,MAAM,OAAO,QAAQ,sBAAsB,0BAAuB,OAAO,QAAQ,YAAY;AAAA;AAAA;AAC5G,UAAM,MAAM,SAAS,OAAO;AAC5B,QAAI,KAAK,QAAQ;AACf,YAAM,UAAU,KAAK,QAAQ,OAAO,KAAK,MAAM,CAAC;AAChD,YAAM,GAAG,UAAU,SAAS,KAAK,MAAM;AACvC,cAAQ,MAAM,4BAA4B,OAAO,EAAE;AAAA,IACrD,OAAO;AACL,cAAQ,OAAO,MAAM,GAAG;AAAA,IAC1B;AAAA,EACF,CAAC;AACL;","names":[]}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/replay.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
|
|
8
|
+
import {
|
|
9
|
+
SnowflakeExecutor
|
|
10
|
+
} from "@sdt-tools/core/deploy";
|
|
11
|
+
async function discoverManifests(input) {
|
|
12
|
+
const stat = await fs.stat(input).catch(() => null);
|
|
13
|
+
if (!stat) throw new Error(`Manifest path not found: ${input}`);
|
|
14
|
+
if (stat.isFile()) return [path.resolve(input)];
|
|
15
|
+
const entries = await fs.readdir(input, { withFileTypes: true });
|
|
16
|
+
const files = entries.filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".json")).map((e) => path.join(input, e.name)).sort();
|
|
17
|
+
if (files.length === 0) throw new Error(`No .json manifests under ${input}.`);
|
|
18
|
+
return files;
|
|
19
|
+
}
|
|
20
|
+
function replayCommand() {
|
|
21
|
+
const cmd = new Command("replay");
|
|
22
|
+
cmd.description(
|
|
23
|
+
"Re-execute forwardSql from one or more deploy manifests against a target Snowflake account."
|
|
24
|
+
).requiredOption("--manifest <path>", "Path to a manifest .json (or a directory of them).").requiredOption("--connection <name>", "Connection profile to replay against.").option("--yes", "Explicit confirmation. Required because replay rebuilds DDL.", false).option("--filter <pattern>", "Only replay steps whose fqn includes this substring.").option("--from-step <id>", "Start at this step id (inclusive).").option("--to-step <id>", "Stop after this step id (inclusive).").option(
|
|
25
|
+
"--continue-on-error",
|
|
26
|
+
"Continue past failed statements. Default: stop on first failure.",
|
|
27
|
+
false
|
|
28
|
+
).option("--dry-run", "Print what would be replayed without executing.", false).action(async (opts) => {
|
|
29
|
+
const manifestPaths = await discoverManifests(String(opts.manifest));
|
|
30
|
+
const manifests = [];
|
|
31
|
+
for (const p of manifestPaths) {
|
|
32
|
+
const raw = await fs.readFile(p, "utf8");
|
|
33
|
+
const m = JSON.parse(raw);
|
|
34
|
+
if (m.version !== 1) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${p}: unsupported manifest version ${m.version} (this CLI understands v1)`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
manifests.push({ path: p, manifest: m });
|
|
40
|
+
}
|
|
41
|
+
manifests.sort((a, b) => a.manifest.deployedAt < b.manifest.deployedAt ? -1 : 1);
|
|
42
|
+
const profile = await getProfile(String(opts.connection));
|
|
43
|
+
const filter = opts.filter ? String(opts.filter) : "";
|
|
44
|
+
const fromId = opts.fromStep ? String(opts.fromStep) : "";
|
|
45
|
+
const toId = opts.toStep ? String(opts.toStep) : "";
|
|
46
|
+
const flatSteps = [];
|
|
47
|
+
let inRange = !fromId;
|
|
48
|
+
for (const { path: p, manifest } of manifests) {
|
|
49
|
+
for (const s of manifest.steps) {
|
|
50
|
+
if (!inRange) {
|
|
51
|
+
if (s.id === fromId) inRange = true;
|
|
52
|
+
else continue;
|
|
53
|
+
}
|
|
54
|
+
if (filter && !s.fqn.includes(filter)) continue;
|
|
55
|
+
if (s.status !== "SUCCESS" && s.status !== "ROLLED_BACK") continue;
|
|
56
|
+
flatSteps.push({ step: s, manifestPath: p, deployedAt: manifest.deployedAt });
|
|
57
|
+
if (toId && s.id === toId) {
|
|
58
|
+
inRange = false;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.log(
|
|
64
|
+
`Replay plan: ${flatSteps.length} step(s) across ${manifests.length} manifest(s) \u2192 ${profile.account}`
|
|
65
|
+
);
|
|
66
|
+
console.log("");
|
|
67
|
+
if (opts.dryRun) {
|
|
68
|
+
for (const { step, deployedAt } of flatSteps) {
|
|
69
|
+
console.log(`\u25B6 ${deployedAt} ${step.objectType} ${step.fqn}`);
|
|
70
|
+
console.log(` ${step.forwardSql.split("\n")[0]?.slice(0, 100) ?? ""}`);
|
|
71
|
+
}
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(`Dry-run summary: ${flatSteps.length} step(s) would replay.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!opts.yes) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Refusing to replay without --yes. Replay re-executes historical DDL against the target."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const conn = new SnowflakeConnection(profile);
|
|
82
|
+
let succeeded = 0;
|
|
83
|
+
let failed = 0;
|
|
84
|
+
try {
|
|
85
|
+
await conn.connect();
|
|
86
|
+
const executor = new SnowflakeExecutor(conn);
|
|
87
|
+
for (const { step, deployedAt } of flatSteps) {
|
|
88
|
+
process.stdout.write(`\u25B6 ${deployedAt} ${step.objectType} ${step.fqn} \u2026`);
|
|
89
|
+
const t0 = Date.now();
|
|
90
|
+
try {
|
|
91
|
+
await executor.execute(step.forwardSql);
|
|
92
|
+
console.log(` \u2713 (${Date.now() - t0}ms)`);
|
|
93
|
+
succeeded++;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
console.log(` \u2717 ${msg}`);
|
|
97
|
+
failed++;
|
|
98
|
+
if (!opts.continueOnError) {
|
|
99
|
+
console.error("");
|
|
100
|
+
console.error("Stopping on first failure. Use --continue-on-error to push through.");
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
await conn.disconnect();
|
|
107
|
+
}
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log(`Summary: ${succeeded} replayed, ${failed} failed.`);
|
|
110
|
+
if (failed > 0) process.exitCode = 1;
|
|
111
|
+
});
|
|
112
|
+
return cmd;
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
replayCommand
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=replay-OOC25FZN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/replay.ts"],"sourcesContent":["/**\n * `sdt replay --manifest <path>` — re-execute a previous deploy's\n * forwardSql against a fresh target.\n *\n * Use cases:\n * - Rebuild a dev/stage account from production's manifest history.\n * - Reproduce a customer's failure by replaying their support-bundle\n * manifest against an internal sandbox.\n *\n * Mirrors `Databricks/packages/cli/src/commands/replay.ts`. Symmetric to\n * `sdt revert` — same manifest format, same executor, forward instead\n * of reverse.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { getProfile, SnowflakeConnection } from '@sdt-tools/core/connection';\nimport {\n SnowflakeExecutor,\n type DeployManifest,\n type DeployManifestStep,\n} from '@sdt-tools/core/deploy';\n\nasync function discoverManifests(input: string): Promise<string[]> {\n const stat = await fs.stat(input).catch(() => null);\n if (!stat) throw new Error(`Manifest path not found: ${input}`);\n if (stat.isFile()) return [path.resolve(input)];\n const entries = await fs.readdir(input, { withFileTypes: true });\n const files = entries\n .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.json'))\n .map((e) => path.join(input, e.name))\n .sort();\n if (files.length === 0) throw new Error(`No .json manifests under ${input}.`);\n return files;\n}\n\nexport function replayCommand(): Command {\n const cmd = new Command('replay');\n cmd\n .description(\n 'Re-execute forwardSql from one or more deploy manifests against a target Snowflake account.',\n )\n .requiredOption('--manifest <path>', 'Path to a manifest .json (or a directory of them).')\n .requiredOption('--connection <name>', 'Connection profile to replay against.')\n .option('--yes', 'Explicit confirmation. Required because replay rebuilds DDL.', false)\n .option('--filter <pattern>', 'Only replay steps whose fqn includes this substring.')\n .option('--from-step <id>', 'Start at this step id (inclusive).')\n .option('--to-step <id>', 'Stop after this step id (inclusive).')\n .option(\n '--continue-on-error',\n 'Continue past failed statements. Default: stop on first failure.',\n false,\n )\n .option('--dry-run', 'Print what would be replayed without executing.', false)\n .action(async (opts) => {\n const manifestPaths = await discoverManifests(String(opts.manifest));\n const manifests: { path: string; manifest: DeployManifest }[] = [];\n for (const p of manifestPaths) {\n const raw = await fs.readFile(p, 'utf8');\n const m = JSON.parse(raw) as DeployManifest;\n if (m.version !== 1) {\n throw new Error(\n `${p}: unsupported manifest version ${m.version} (this CLI understands v1)`,\n );\n }\n manifests.push({ path: p, manifest: m });\n }\n manifests.sort((a, b) => (a.manifest.deployedAt < b.manifest.deployedAt ? -1 : 1));\n\n const profile = await getProfile(String(opts.connection));\n const filter = opts.filter ? String(opts.filter) : '';\n const fromId = opts.fromStep ? String(opts.fromStep) : '';\n const toId = opts.toStep ? String(opts.toStep) : '';\n\n type FlatStep = { step: DeployManifestStep; manifestPath: string; deployedAt: string };\n const flatSteps: FlatStep[] = [];\n let inRange = !fromId;\n for (const { path: p, manifest } of manifests) {\n for (const s of manifest.steps) {\n if (!inRange) {\n if (s.id === fromId) inRange = true;\n else continue;\n }\n if (filter && !s.fqn.includes(filter)) continue;\n if (s.status !== 'SUCCESS' && s.status !== 'ROLLED_BACK') continue;\n flatSteps.push({ step: s, manifestPath: p, deployedAt: manifest.deployedAt });\n if (toId && s.id === toId) {\n inRange = false;\n break;\n }\n }\n }\n\n console.log(\n `Replay plan: ${flatSteps.length} step(s) across ${manifests.length} manifest(s) → ${profile.account}`,\n );\n console.log('');\n\n if (opts.dryRun) {\n for (const { step, deployedAt } of flatSteps) {\n console.log(`▶ ${deployedAt} ${step.objectType} ${step.fqn}`);\n console.log(` ${step.forwardSql.split('\\n')[0]?.slice(0, 100) ?? ''}`);\n }\n console.log('');\n console.log(`Dry-run summary: ${flatSteps.length} step(s) would replay.`);\n return;\n }\n\n if (!opts.yes) {\n throw new Error(\n 'Refusing to replay without --yes. Replay re-executes historical DDL against the target.',\n );\n }\n\n const conn = new SnowflakeConnection(profile);\n let succeeded = 0;\n let failed = 0;\n try {\n await conn.connect();\n const executor = new SnowflakeExecutor(conn);\n for (const { step, deployedAt } of flatSteps) {\n process.stdout.write(`▶ ${deployedAt} ${step.objectType} ${step.fqn} …`);\n const t0 = Date.now();\n try {\n await executor.execute(step.forwardSql);\n console.log(` ✓ (${Date.now() - t0}ms)`);\n succeeded++;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.log(` ✗ ${msg}`);\n failed++;\n if (!opts.continueOnError) {\n console.error('');\n console.error('Stopping on first failure. Use --continue-on-error to push through.');\n break;\n }\n }\n }\n } finally {\n await conn.disconnect();\n }\n\n console.log('');\n console.log(`Summary: ${succeeded} replayed, ${failed} failed.`);\n if (failed > 0) process.exitCode = 1;\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/revert.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
|
|
7
|
+
import {
|
|
8
|
+
SnowflakeExecutor,
|
|
9
|
+
revertStepsFromManifest
|
|
10
|
+
} from "@sdt-tools/core/deploy";
|
|
11
|
+
function revertCommand() {
|
|
12
|
+
const cmd = new Command("revert");
|
|
13
|
+
cmd.description(
|
|
14
|
+
"Replay a previous deploy manifest in reverse, executing reverseSql for each successful step."
|
|
15
|
+
).requiredOption(
|
|
16
|
+
"--manifest <path>",
|
|
17
|
+
"Path to a JSON deploy manifest from `sdt publish --apply --manifest`."
|
|
18
|
+
).requiredOption(
|
|
19
|
+
"--connection <name>",
|
|
20
|
+
"Connection profile to revert against. Should match the original deploy."
|
|
21
|
+
).requiredOption("--yes", "Explicit confirmation. Required because revert is destructive.").option(
|
|
22
|
+
"--continue-on-error",
|
|
23
|
+
"Continue past failed reverse statements. Default: stop on first failure.",
|
|
24
|
+
false
|
|
25
|
+
).option("--dry-run", "Print what would be reverted without executing.", false).action(async (opts) => {
|
|
26
|
+
const raw = await fs.readFile(String(opts.manifest), "utf8");
|
|
27
|
+
const manifest = JSON.parse(raw);
|
|
28
|
+
if (manifest.version !== 1) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Unsupported manifest version: ${manifest.version}. This CLI understands v1.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const profile = await getProfile(String(opts.connection));
|
|
34
|
+
if (profile.account !== manifest.account) {
|
|
35
|
+
console.error("");
|
|
36
|
+
console.error(
|
|
37
|
+
`WARNING: manifest's account (${manifest.account}) doesn't match the profile's account (${profile.account}).`
|
|
38
|
+
);
|
|
39
|
+
console.error(
|
|
40
|
+
"Reverting against a different account than the original deploy may have unintended effects."
|
|
41
|
+
);
|
|
42
|
+
console.error("Re-run with the correct --connection, or proceed carefully.");
|
|
43
|
+
console.error("");
|
|
44
|
+
}
|
|
45
|
+
const reverseSteps = revertStepsFromManifest(manifest);
|
|
46
|
+
console.log(`Reverting ${reverseSteps.length} step(s) from ${opts.manifest}`);
|
|
47
|
+
console.log(`Original deploy: ${manifest.deployedAt} \u2192 ${manifest.account}`);
|
|
48
|
+
console.log("");
|
|
49
|
+
if (opts.dryRun) {
|
|
50
|
+
for (const s of reverseSteps) {
|
|
51
|
+
const tag = s.reverseSql ? "\u21A9" : "\u26A0";
|
|
52
|
+
console.log(`${tag} ${s.objectType} ${s.fqn}`);
|
|
53
|
+
if (s.reverseSql) {
|
|
54
|
+
console.log(` ${s.reverseSql.split("\n")[0]?.slice(0, 100) ?? ""}`);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` (no reverseSql \u2014 IRREVERSIBLE; would be skipped)`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const skippable = reverseSteps.filter((s) => !s.reverseSql).length;
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(
|
|
62
|
+
`Dry-run summary: ${reverseSteps.length - skippable} reversible, ${skippable} irreversible.`
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const conn = new SnowflakeConnection(profile);
|
|
67
|
+
let succeeded = 0;
|
|
68
|
+
let failed = 0;
|
|
69
|
+
let irreversible = 0;
|
|
70
|
+
try {
|
|
71
|
+
await conn.connect();
|
|
72
|
+
const executor = new SnowflakeExecutor(conn);
|
|
73
|
+
for (const s of reverseSteps) {
|
|
74
|
+
if (!s.reverseSql) {
|
|
75
|
+
console.log(`\u26A0 skip ${s.objectType} ${s.fqn} \u2014 irreversible (no reverseSql captured)`);
|
|
76
|
+
irreversible++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`\u21A9 ${s.objectType} ${s.fqn} \u2026`);
|
|
80
|
+
const t0 = Date.now();
|
|
81
|
+
try {
|
|
82
|
+
await executor.execute(s.reverseSql);
|
|
83
|
+
console.log(` \u2713 (${Date.now() - t0}ms)`);
|
|
84
|
+
succeeded++;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
console.log(` \u2717 ${msg}`);
|
|
88
|
+
failed++;
|
|
89
|
+
if (!opts.continueOnError) {
|
|
90
|
+
console.error("");
|
|
91
|
+
console.error("Stopping on first failure. Use --continue-on-error to push through.");
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
await conn.disconnect();
|
|
98
|
+
}
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(
|
|
101
|
+
`Summary: ${succeeded} reverted, ${failed} failed, ${irreversible} irreversible.`
|
|
102
|
+
);
|
|
103
|
+
if (failed > 0 || irreversible > 0) process.exitCode = 1;
|
|
104
|
+
});
|
|
105
|
+
return cmd;
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
revertCommand
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=revert-ODMUVJW6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/revert.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport { Command } from 'commander';\nimport { getProfile, SnowflakeConnection } from '@sdt-tools/core/connection';\nimport {\n SnowflakeExecutor,\n revertStepsFromManifest,\n type DeployManifest,\n} from '@sdt-tools/core/deploy';\n\n/**\n * `sdt revert --manifest <path>` — undo a previous deploy by executing\n * each step's `reverseSql` in reverse order. Manifests come from\n * `sdt publish --apply --manifest <path>` (the JSON deploy manifest\n * captured after `--apply`).\n *\n * Steps without `reverseSql` are inherently irreversible (DROP, etc.)\n * and skipped with a clear warning. The command exits non-zero if any\n * reverse fails or if any step was irreversible (the user must inspect\n * by hand).\n *\n * Mirrors `Databricks/packages/cli/src/commands/revert.ts`. Use case: a\n * deploy succeeded but something downstream broke; revert it. Use case:\n * drift is detected and you need to roll back to the last-known-good\n * state.\n */\nexport function revertCommand(): Command {\n const cmd = new Command('revert');\n cmd\n .description(\n 'Replay a previous deploy manifest in reverse, executing reverseSql for each successful step.',\n )\n .requiredOption(\n '--manifest <path>',\n 'Path to a JSON deploy manifest from `sdt publish --apply --manifest`.',\n )\n .requiredOption(\n '--connection <name>',\n 'Connection profile to revert against. Should match the original deploy.',\n )\n .requiredOption('--yes', 'Explicit confirmation. Required because revert is destructive.')\n .option(\n '--continue-on-error',\n 'Continue past failed reverse statements. Default: stop on first failure.',\n false,\n )\n .option('--dry-run', 'Print what would be reverted without executing.', false)\n .action(async (opts) => {\n const raw = await fs.readFile(String(opts.manifest), 'utf8');\n const manifest = JSON.parse(raw) as DeployManifest;\n if (manifest.version !== 1) {\n throw new Error(\n `Unsupported manifest version: ${manifest.version}. This CLI understands v1.`,\n );\n }\n\n const profile = await getProfile(String(opts.connection));\n if (profile.account !== manifest.account) {\n console.error('');\n console.error(\n `WARNING: manifest's account (${manifest.account}) doesn't match the profile's account (${profile.account}).`,\n );\n console.error(\n 'Reverting against a different account than the original deploy may have unintended effects.',\n );\n console.error('Re-run with the correct --connection, or proceed carefully.');\n console.error('');\n }\n\n // Reverse-iterate; only successful steps need undoing.\n const reverseSteps = revertStepsFromManifest(manifest);\n\n console.log(`Reverting ${reverseSteps.length} step(s) from ${opts.manifest}`);\n console.log(`Original deploy: ${manifest.deployedAt} → ${manifest.account}`);\n console.log('');\n\n if (opts.dryRun) {\n for (const s of reverseSteps) {\n const tag = s.reverseSql ? '↩' : '⚠';\n console.log(`${tag} ${s.objectType} ${s.fqn}`);\n if (s.reverseSql) {\n console.log(` ${s.reverseSql.split('\\n')[0]?.slice(0, 100) ?? ''}`);\n } else {\n console.log(` (no reverseSql — IRREVERSIBLE; would be skipped)`);\n }\n }\n const skippable = reverseSteps.filter((s) => !s.reverseSql).length;\n console.log('');\n console.log(\n `Dry-run summary: ${reverseSteps.length - skippable} reversible, ${skippable} irreversible.`,\n );\n return;\n }\n\n const conn = new SnowflakeConnection(profile);\n let succeeded = 0;\n let failed = 0;\n let irreversible = 0;\n try {\n await conn.connect();\n const executor = new SnowflakeExecutor(conn);\n for (const s of reverseSteps) {\n if (!s.reverseSql) {\n console.log(`⚠ skip ${s.objectType} ${s.fqn} — irreversible (no reverseSql captured)`);\n irreversible++;\n continue;\n }\n process.stdout.write(`↩ ${s.objectType} ${s.fqn} …`);\n const t0 = Date.now();\n try {\n await executor.execute(s.reverseSql);\n console.log(` ✓ (${Date.now() - t0}ms)`);\n succeeded++;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.log(` ✗ ${msg}`);\n failed++;\n if (!opts.continueOnError) {\n console.error('');\n console.error('Stopping on first failure. Use --continue-on-error to push through.');\n break;\n }\n }\n }\n } finally {\n await conn.disconnect();\n }\n\n console.log('');\n console.log(\n `Summary: ${succeeded} reverted, ${failed} failed, ${irreversible} irreversible.`,\n );\n if (failed > 0 || irreversible > 0) process.exitCode = 1;\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachExplainFlag,
|
|
3
|
+
runExplain
|
|
4
|
+
} from "./chunk-ZWY4ZRHL.js";
|
|
5
|
+
import "./chunk-VM2H4LAO.js";
|
|
6
|
+
import "./chunk-DGUM43GV.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/review.ts
|
|
9
|
+
import { promises as fs } from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
import { ai, aiReview, pac, project, review } from "@sdt-tools/core";
|
|
13
|
+
function reviewCommand() {
|
|
14
|
+
const cmd = new Command("review");
|
|
15
|
+
cmd.description(
|
|
16
|
+
"Senior-DBA-style health report (lint + lineage + smell + cost + safety, with reasoning)."
|
|
17
|
+
).option("--source <path>", ".sdtproj or .sdtpac to analyze (project-health mode).").option("-o, --out <path>", "Output file path. Defaults to stdout.").option(
|
|
18
|
+
"--senior-dba",
|
|
19
|
+
"AI-driven senior-DBA review of a deploy diff (requires --diff and --safety).",
|
|
20
|
+
false
|
|
21
|
+
).option(
|
|
22
|
+
"--diff <path>",
|
|
23
|
+
"JSON file with compare summary { added, removed, modified, addedSample, removedSample, modifiedSample }."
|
|
24
|
+
).option(
|
|
25
|
+
"--safety <path>",
|
|
26
|
+
"JSON file with safety summary { unrecoverable, destructive, expensive, warning, sample }."
|
|
27
|
+
).option(
|
|
28
|
+
"--target-meta <path>",
|
|
29
|
+
"Optional file with target-metadata prose (existing tables, recent deploys, role grants)."
|
|
30
|
+
).option("--ddl <path>", "Optional truncated DDL preview file.").option("--format <fmt>", "Output format: markdown | json. Default markdown.", "markdown").option(
|
|
31
|
+
"--ai-max-spend <usd>",
|
|
32
|
+
"Refuse the AI call if today's estimated spend \u2265 this (USD). 0 = no cap.",
|
|
33
|
+
"0"
|
|
34
|
+
).action(
|
|
35
|
+
async (opts) => {
|
|
36
|
+
if (opts.seniorDba) {
|
|
37
|
+
await runSeniorDbaReview(opts, "sdt");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!opts.source) {
|
|
41
|
+
throw new Error("--source is required in the default (project-health) mode.");
|
|
42
|
+
}
|
|
43
|
+
const sourcePath = String(opts.source);
|
|
44
|
+
const model = await loadModel(sourcePath);
|
|
45
|
+
const md = review.renderReviewReport(model, { source: sourcePath });
|
|
46
|
+
await emit(md, opts.out);
|
|
47
|
+
await runExplain(
|
|
48
|
+
{
|
|
49
|
+
feature: "review.explain",
|
|
50
|
+
systemPrompt: "You are a Snowflake principal engineer giving a senior-architect verbal walkthrough of an automated review report. Summarize the headline themes, recommend the top 3 follow-ups in order of leverage, and call out anything a junior reviewer might miss."
|
|
51
|
+
},
|
|
52
|
+
opts,
|
|
53
|
+
() => `Review report follows:
|
|
54
|
+
|
|
55
|
+
${md}
|
|
56
|
+
|
|
57
|
+
Narrate this report for a teammate who has not read it.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
attachExplainFlag(cmd);
|
|
62
|
+
return cmd;
|
|
63
|
+
}
|
|
64
|
+
async function runSeniorDbaReview(opts, toolName) {
|
|
65
|
+
if (!opts.diff || !opts.safety) {
|
|
66
|
+
throw new Error("--senior-dba requires both --diff and --safety (JSON files).");
|
|
67
|
+
}
|
|
68
|
+
const [diff, safety, targetMeta, ddl] = await Promise.all([
|
|
69
|
+
readJson(opts.diff),
|
|
70
|
+
readJson(opts.safety),
|
|
71
|
+
opts.targetMeta ? fs.readFile(path.resolve(opts.targetMeta), "utf8") : Promise.resolve(void 0),
|
|
72
|
+
opts.ddl ? fs.readFile(path.resolve(opts.ddl), "utf8") : Promise.resolve(void 0)
|
|
73
|
+
]);
|
|
74
|
+
const compareSummary = normalizeCompareSummary(diff);
|
|
75
|
+
const safetySummary = normalizeSafetySummary(safety);
|
|
76
|
+
const result = await aiReview.runSeniorDbaReview(
|
|
77
|
+
{
|
|
78
|
+
compareSummary,
|
|
79
|
+
safetySummary,
|
|
80
|
+
...targetMeta ? { targetMetadata: targetMeta } : {},
|
|
81
|
+
...ddl ? { ddlPreview: ddl } : {}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
completeFn: async (user, system) => {
|
|
85
|
+
const r = await ai.complete(
|
|
86
|
+
[
|
|
87
|
+
{ role: "system", content: system },
|
|
88
|
+
{ role: "user", content: user }
|
|
89
|
+
],
|
|
90
|
+
{
|
|
91
|
+
feature: "review.senior-dba",
|
|
92
|
+
maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
return r.text;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
toolName
|
|
99
|
+
);
|
|
100
|
+
const format = String(opts.format ?? "markdown").toLowerCase();
|
|
101
|
+
const payload = format === "json" ? JSON.stringify({ ...result, rawModelText: void 0 }, null, 2) : aiReview.renderSeniorDbaReviewMarkdown(result, toolName);
|
|
102
|
+
await emit(payload, opts.out);
|
|
103
|
+
if (result.verdict === "request_changes" && !opts.out) {
|
|
104
|
+
process.exitCode = 2;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function normalizeCompareSummary(raw) {
|
|
108
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
109
|
+
const num = (k) => typeof o[k] === "number" ? o[k] : 0;
|
|
110
|
+
const arr = (k) => Array.isArray(o[k]) ? o[k].filter((x) => typeof x === "string") : void 0;
|
|
111
|
+
return {
|
|
112
|
+
added: num("added"),
|
|
113
|
+
removed: num("removed"),
|
|
114
|
+
modified: num("modified"),
|
|
115
|
+
...arr("addedSample") ? { addedSample: arr("addedSample") } : {},
|
|
116
|
+
...arr("removedSample") ? { removedSample: arr("removedSample") } : {},
|
|
117
|
+
...arr("modifiedSample") ? { modifiedSample: arr("modifiedSample") } : {}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function normalizeSafetySummary(raw) {
|
|
121
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
122
|
+
const num = (k) => typeof o[k] === "number" ? o[k] : 0;
|
|
123
|
+
const sample = Array.isArray(o.sample) ? o.sample.filter((x) => typeof x === "string") : void 0;
|
|
124
|
+
return {
|
|
125
|
+
unrecoverable: num("unrecoverable"),
|
|
126
|
+
destructive: num("destructive"),
|
|
127
|
+
expensive: num("expensive"),
|
|
128
|
+
warning: num("warning"),
|
|
129
|
+
...sample ? { sample } : {}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function readJson(p) {
|
|
133
|
+
const raw = await fs.readFile(path.resolve(p), "utf8");
|
|
134
|
+
return JSON.parse(raw);
|
|
135
|
+
}
|
|
136
|
+
async function loadModel(sourcePath) {
|
|
137
|
+
if (sourcePath.endsWith(".sdtpac")) {
|
|
138
|
+
const c = await pac.readPac(sourcePath);
|
|
139
|
+
return c.model;
|
|
140
|
+
}
|
|
141
|
+
const loaded = await project.loadProject(sourcePath);
|
|
142
|
+
return await project.parseProjectModel(loaded);
|
|
143
|
+
}
|
|
144
|
+
async function emit(payload, out) {
|
|
145
|
+
if (out) {
|
|
146
|
+
const p = path.resolve(String(out));
|
|
147
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
148
|
+
await fs.writeFile(p, payload + (payload.endsWith("\n") ? "" : "\n"), "utf8");
|
|
149
|
+
console.error(`Wrote ${p} (${payload.length} bytes).`);
|
|
150
|
+
} else {
|
|
151
|
+
process.stdout.write(payload + (payload.endsWith("\n") ? "" : "\n"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
reviewCommand,
|
|
156
|
+
runSeniorDbaReview
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=review-XXPWOBFP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { ai, aiReview, pac, project, review } from '@sdt-tools/core';\nimport { attachExplainFlag, runExplain } from '../util/ai-explain.js';\n\n/**\n * `sdt review` — produce a senior-DBA Markdown report combining lint,\n * lineage findings, smell findings, cost findings, and safety\n * reasoning. The deliverable a reviewer can paste straight into a PR\n * or an architecture-review meeting.\n *\n * Default mode (project health): pure composition of existing modules —\n * every finding's reasoning + suggestion is generated by\n * `@sdt-tools/core/diagnostics`; the format is the value-add.\n *\n * `--senior-dba` mode (AI Phase 4): takes a compare-result JSON\n * (typically the output of `sdt compare --json`) + a safety-summary\n * JSON + optional target-metadata snapshot + truncated DDL preview,\n * asks the configured AI provider for a senior-DBA verdict, and emits\n * the parsed result as Markdown (or JSON via `--format json`).\n */\nexport function reviewCommand(): Command {\n const cmd = new Command('review');\n cmd\n .description(\n 'Senior-DBA-style health report (lint + lineage + smell + cost + safety, with reasoning).',\n )\n .option('--source <path>', '.sdtproj or .sdtpac to analyze (project-health mode).')\n .option('-o, --out <path>', 'Output file path. Defaults to stdout.')\n .option(\n '--senior-dba',\n 'AI-driven senior-DBA review of a deploy diff (requires --diff and --safety).',\n false,\n )\n .option(\n '--diff <path>',\n 'JSON file with compare summary { added, removed, modified, addedSample, removedSample, modifiedSample }.',\n )\n .option(\n '--safety <path>',\n 'JSON file with safety summary { unrecoverable, destructive, expensive, warning, sample }.',\n )\n .option(\n '--target-meta <path>',\n 'Optional file with target-metadata prose (existing tables, recent deploys, role grants).',\n )\n .option('--ddl <path>', 'Optional truncated DDL preview file.')\n .option('--format <fmt>', 'Output format: markdown | json. Default markdown.', 'markdown')\n .option(\n '--ai-max-spend <usd>',\n \"Refuse the AI call if today's estimated spend ≥ this (USD). 0 = no cap.\",\n '0',\n )\n .action(\n async (opts: {\n source?: string;\n out?: string;\n explain?: boolean;\n seniorDba?: boolean;\n diff?: string;\n safety?: string;\n targetMeta?: string;\n ddl?: string;\n format?: string;\n aiMaxSpend?: string;\n }) => {\n if (opts.seniorDba) {\n await runSeniorDbaReview(opts, 'sdt');\n return;\n }\n if (!opts.source) {\n throw new Error('--source is required in the default (project-health) mode.');\n }\n const sourcePath = String(opts.source);\n const model = await loadModel(sourcePath);\n const md = review.renderReviewReport(model, { source: sourcePath });\n await emit(md, opts.out);\n await runExplain(\n {\n feature: 'review.explain',\n systemPrompt:\n 'You are a Snowflake principal engineer giving a senior-architect verbal walkthrough of an automated review report. Summarize the headline themes, recommend the top 3 follow-ups in order of leverage, and call out anything a junior reviewer might miss.',\n },\n opts,\n () =>\n `Review report follows:\\n\\n${md}\\n\\nNarrate this report for a teammate who has not read it.`,\n );\n },\n );\n attachExplainFlag(cmd);\n return cmd;\n}\n\nexport async function runSeniorDbaReview(\n opts: {\n diff?: string;\n safety?: string;\n targetMeta?: string;\n ddl?: string;\n format?: string;\n out?: string;\n aiMaxSpend?: string;\n },\n toolName: 'sdt' | 'ddt',\n): Promise<void> {\n if (!opts.diff || !opts.safety) {\n throw new Error('--senior-dba requires both --diff and --safety (JSON files).');\n }\n const [diff, safety, targetMeta, ddl] = await Promise.all([\n readJson(opts.diff),\n readJson(opts.safety),\n opts.targetMeta\n ? fs.readFile(path.resolve(opts.targetMeta), 'utf8')\n : Promise.resolve<string | undefined>(undefined),\n opts.ddl\n ? fs.readFile(path.resolve(opts.ddl), 'utf8')\n : Promise.resolve<string | undefined>(undefined),\n ]);\n\n const compareSummary = normalizeCompareSummary(diff);\n const safetySummary = normalizeSafetySummary(safety);\n\n const result = await aiReview.runSeniorDbaReview(\n {\n compareSummary,\n safetySummary,\n ...(targetMeta ? { targetMetadata: targetMeta } : {}),\n ...(ddl ? { ddlPreview: ddl } : {}),\n },\n {\n completeFn: async (user, system) => {\n const r = await ai.complete(\n [\n { role: 'system', content: system },\n { role: 'user', content: user },\n ],\n {\n feature: 'review.senior-dba',\n maxSpendUsd: Number(opts.aiMaxSpend ?? '0') || 0,\n },\n );\n return r.text;\n },\n },\n toolName,\n );\n\n const format = String(opts.format ?? 'markdown').toLowerCase();\n const payload =\n format === 'json'\n ? JSON.stringify({ ...result, rawModelText: undefined }, null, 2)\n : aiReview.renderSeniorDbaReviewMarkdown(result, toolName);\n await emit(payload, opts.out);\n // Set exit code on request_changes so CI gates can rely on it.\n if (result.verdict === 'request_changes' && !opts.out) {\n process.exitCode = 2;\n }\n}\n\nfunction normalizeCompareSummary(raw: unknown): aiReview.CompareSummaryInput {\n const o = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;\n const num = (k: string) => (typeof o[k] === 'number' ? (o[k] as number) : 0);\n const arr = (k: string) =>\n Array.isArray(o[k])\n ? (o[k] as unknown[]).filter((x): x is string => typeof x === 'string')\n : undefined;\n return {\n added: num('added'),\n removed: num('removed'),\n modified: num('modified'),\n ...(arr('addedSample') ? { addedSample: arr('addedSample') } : {}),\n ...(arr('removedSample') ? { removedSample: arr('removedSample') } : {}),\n ...(arr('modifiedSample') ? { modifiedSample: arr('modifiedSample') } : {}),\n };\n}\n\nfunction normalizeSafetySummary(raw: unknown): aiReview.SafetySummaryInput {\n const o = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;\n const num = (k: string) => (typeof o[k] === 'number' ? (o[k] as number) : 0);\n const sample = Array.isArray(o.sample)\n ? (o.sample as unknown[]).filter((x): x is string => typeof x === 'string')\n : undefined;\n return {\n unrecoverable: num('unrecoverable'),\n destructive: num('destructive'),\n expensive: num('expensive'),\n warning: num('warning'),\n ...(sample ? { sample } : {}),\n };\n}\n\nasync function readJson(p: string): Promise<unknown> {\n const raw = await fs.readFile(path.resolve(p), 'utf8');\n return JSON.parse(raw);\n}\n\nasync function loadModel(sourcePath: string) {\n if (sourcePath.endsWith('.sdtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await project.loadProject(sourcePath);\n return await project.parseProjectModel(loaded);\n}\n\nasync function emit(payload: string, out: unknown): Promise<void> {\n if (out) {\n const p = path.resolve(String(out));\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, payload + (payload.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${p} (${payload.length} bytes).`);\n } else {\n process.stdout.write(payload + (payload.endsWith('\\n') ? '' : '\\n'));\n }\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,IAAI,UAAU,KAAK,SAAS,cAAc;AAmB5C,SAAS,gBAAyB;AACvC,QAAM,MAAM,IAAI,QAAQ,QAAQ;AAChC,MACG;AAAA,IACC;AAAA,EACF,EACC,OAAO,mBAAmB,uDAAuD,EACjF,OAAO,oBAAoB,uCAAuC,EAClE;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,gBAAgB,sCAAsC,EAC7D,OAAO,kBAAkB,qDAAqD,UAAU,EACxF;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC,OAAO,SAWD;AACJ,UAAI,KAAK,WAAW;AAClB,cAAM,mBAAmB,MAAM,KAAK;AACpC;AAAA,MACF;AACA,UAAI,CAAC,KAAK,QAAQ;AAChB,cAAM,IAAI,MAAM,4DAA4D;AAAA,MAC9E;AACA,YAAM,aAAa,OAAO,KAAK,MAAM;AACrC,YAAM,QAAQ,MAAM,UAAU,UAAU;AACxC,YAAM,KAAK,OAAO,mBAAmB,OAAO,EAAE,QAAQ,WAAW,CAAC;AAClE,YAAM,KAAK,IAAI,KAAK,GAAG;AACvB,YAAM;AAAA,QACJ;AAAA,UACE,SAAS;AAAA,UACT,cACE;AAAA,QACJ;AAAA,QACA;AAAA,QACA,MACE;AAAA;AAAA,EAA6B,EAAE;AAAA;AAAA;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AACF,oBAAkB,GAAG;AACrB,SAAO;AACT;AAEA,eAAsB,mBACpB,MASA,UACe;AACf,MAAI,CAAC,KAAK,QAAQ,CAAC,KAAK,QAAQ;AAC9B,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF;AACA,QAAM,CAAC,MAAM,QAAQ,YAAY,GAAG,IAAI,MAAM,QAAQ,IAAI;AAAA,IACxD,SAAS,KAAK,IAAI;AAAA,IAClB,SAAS,KAAK,MAAM;AAAA,IACpB,KAAK,aACD,GAAG,SAAS,KAAK,QAAQ,KAAK,UAAU,GAAG,MAAM,IACjD,QAAQ,QAA4B,MAAS;AAAA,IACjD,KAAK,MACD,GAAG,SAAS,KAAK,QAAQ,KAAK,GAAG,GAAG,MAAM,IAC1C,QAAQ,QAA4B,MAAS;AAAA,EACnD,CAAC;AAED,QAAM,iBAAiB,wBAAwB,IAAI;AACnD,QAAM,gBAAgB,uBAAuB,MAAM;AAEnD,QAAM,SAAS,MAAM,SAAS;AAAA,IAC5B;AAAA,MACE;AAAA,MACA;AAAA,MACA,GAAI,aAAa,EAAE,gBAAgB,WAAW,IAAI,CAAC;AAAA,MACnD,GAAI,MAAM,EAAE,YAAY,IAAI,IAAI,CAAC;AAAA,IACnC;AAAA,IACA;AAAA,MACE,YAAY,OAAO,MAAM,WAAW;AAClC,cAAM,IAAI,MAAM,GAAG;AAAA,UACjB;AAAA,YACE,EAAE,MAAM,UAAU,SAAS,OAAO;AAAA,YAClC,EAAE,MAAM,QAAQ,SAAS,KAAK;AAAA,UAChC;AAAA,UACA;AAAA,YACE,SAAS;AAAA,YACT,aAAa,OAAO,KAAK,cAAc,GAAG,KAAK;AAAA,UACjD;AAAA,QACF;AACA,eAAO,EAAE;AAAA,MACX;AAAA,IACF;AAAA,IACA;AAAA,EACF;AAEA,QAAM,SAAS,OAAO,KAAK,UAAU,UAAU,EAAE,YAAY;AAC7D,QAAM,UACJ,WAAW,SACP,KAAK,UAAU,EAAE,GAAG,QAAQ,cAAc,OAAU,GAAG,MAAM,CAAC,IAC9D,SAAS,8BAA8B,QAAQ,QAAQ;AAC7D,QAAM,KAAK,SAAS,KAAK,GAAG;AAE5B,MAAI,OAAO,YAAY,qBAAqB,CAAC,KAAK,KAAK;AACrD,YAAQ,WAAW;AAAA,EACrB;AACF;AAEA,SAAS,wBAAwB,KAA4C;AAC3E,QAAM,IAAK,OAAO,OAAO,QAAQ,WAAW,MAAM,CAAC;AACnD,QAAM,MAAM,CAAC,MAAe,OAAO,EAAE,CAAC,MAAM,WAAY,EAAE,CAAC,IAAe;AAC1E,QAAM,MAAM,CAAC,MACX,MAAM,QAAQ,EAAE,CAAC,CAAC,IACb,EAAE,CAAC,EAAgB,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IACpE;AACN,SAAO;AAAA,IACL,OAAO,IAAI,OAAO;AAAA,IAClB,SAAS,IAAI,SAAS;AAAA,IACtB,UAAU,IAAI,UAAU;AAAA,IACxB,GAAI,IAAI,aAAa,IAAI,EAAE,aAAa,IAAI,aAAa,EAAE,IAAI,CAAC;AAAA,IAChE,GAAI,IAAI,eAAe,IAAI,EAAE,eAAe,IAAI,eAAe,EAAE,IAAI,CAAC;AAAA,IACtE,GAAI,IAAI,gBAAgB,IAAI,EAAE,gBAAgB,IAAI,gBAAgB,EAAE,IAAI,CAAC;AAAA,EAC3E;AACF;AAEA,SAAS,uBAAuB,KAA2C;AACzE,QAAM,IAAK,OAAO,OAAO,QAAQ,WAAW,MAAM,CAAC;AACnD,QAAM,MAAM,CAAC,MAAe,OAAO,EAAE,CAAC,MAAM,WAAY,EAAE,CAAC,IAAe;AAC1E,QAAM,SAAS,MAAM,QAAQ,EAAE,MAAM,IAChC,EAAE,OAAqB,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IACxE;AACJ,SAAO;AAAA,IACL,eAAe,IAAI,eAAe;AAAA,IAClC,aAAa,IAAI,aAAa;AAAA,IAC9B,WAAW,IAAI,WAAW;AAAA,IAC1B,SAAS,IAAI,SAAS;AAAA,IACtB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,EAC7B;AACF;AAEA,eAAe,SAAS,GAA6B;AACnD,QAAM,MAAM,MAAM,GAAG,SAAS,KAAK,QAAQ,CAAC,GAAG,MAAM;AACrD,SAAO,KAAK,MAAM,GAAG;AACvB;AAEA,eAAe,UAAU,YAAoB;AAC3C,MAAI,WAAW,SAAS,SAAS,GAAG;AAClC,UAAM,IAAI,MAAM,IAAI,QAAQ,UAAU;AACtC,WAAO,EAAE;AAAA,EACX;AACA,QAAM,SAAS,MAAM,QAAQ,YAAY,UAAU;AACnD,SAAO,MAAM,QAAQ,kBAAkB,MAAM;AAC/C;AAEA,eAAe,KAAK,SAAiB,KAA6B;AAChE,MAAI,KAAK;AACP,UAAM,IAAI,KAAK,QAAQ,OAAO,GAAG,CAAC;AAClC,UAAM,GAAG,MAAM,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACnD,UAAM,GAAG,UAAU,GAAG,WAAW,QAAQ,SAAS,IAAI,IAAI,KAAK,OAAO,MAAM;AAC5E,YAAQ,MAAM,SAAS,CAAC,KAAK,QAAQ,MAAM,UAAU;AAAA,EACvD,OAAO;AACL,YAAQ,OAAO,MAAM,WAAW,QAAQ,SAAS,IAAI,IAAI,KAAK,KAAK;AAAA,EACrE;AACF;","names":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-VM2H4LAO.js";
|
|
4
|
+
import "./chunk-DGUM43GV.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/rollback-suggest.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { ai, aiRollback } from "@sdt-tools/core";
|
|
10
|
+
function rollbackSuggestCommand() {
|
|
11
|
+
const cmd = new Command("rollback-suggest");
|
|
12
|
+
cmd.description(
|
|
13
|
+
"AI-assist: propose a reverse SQL for a forward DDL when the deterministic plan-to-steps bridge could not invert it. Output always carries a REVIEW BEFORE APPLY header. Standalone \u2014 not auto-wired into `revert`."
|
|
14
|
+
).requiredOption(
|
|
15
|
+
"--forward-sql <path>",
|
|
16
|
+
'Path to a file with the forward DDL. Use "-" for stdin.'
|
|
17
|
+
).option("--fqn <fqn>", "Optional object FQN for context.").option("--object-type <type>", "Optional object type for context (e.g. TABLE, STREAM).").option(
|
|
18
|
+
"--finding <code>",
|
|
19
|
+
"Optional SafetyFindingCode that motivated the rollback (e.g. DROP_DATA_TABLE)."
|
|
20
|
+
).option("--intent <text>", "Optional one-sentence operator intent.").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 (opts) => {
|
|
25
|
+
const forwardSql = await readInput(
|
|
26
|
+
opts.forwardSql,
|
|
27
|
+
'--forward-sql is required (path or "-" for stdin).'
|
|
28
|
+
);
|
|
29
|
+
const result = await aiRollback.suggestRollback(
|
|
30
|
+
{
|
|
31
|
+
forwardSql,
|
|
32
|
+
...opts.fqn ? { fqn: String(opts.fqn) } : {},
|
|
33
|
+
...opts.objectType ? { objectType: String(opts.objectType) } : {},
|
|
34
|
+
...opts.finding ? { findingCode: String(opts.finding) } : {},
|
|
35
|
+
...opts.intent ? { intentNotes: String(opts.intent) } : {}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
completeFn: async (prompt) => {
|
|
39
|
+
const r = await ai.complete([{ role: "user", content: prompt }], {
|
|
40
|
+
feature: "rollback-suggest",
|
|
41
|
+
maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
|
|
42
|
+
});
|
|
43
|
+
return r.text;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
if (String(opts.format).toLowerCase() === "json") {
|
|
48
|
+
const { rawModelText: _omit, ...keep } = result;
|
|
49
|
+
console.log(JSON.stringify(keep, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
logger.success(
|
|
53
|
+
`Confidence: ${result.confidence}${result.unrecoverable ? " (UNRECOVERABLE)" : ""}${result.parseFailed ? " \u2014 parse failed" : ""}`
|
|
54
|
+
);
|
|
55
|
+
logger.dim(`Reasoning: ${result.reasoning}`);
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(result.reverseSql);
|
|
58
|
+
if (result.confidence === "unrecoverable" || result.unrecoverable) {
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return cmd;
|
|
63
|
+
}
|
|
64
|
+
async function readInput(pathOrDash, missingMessage) {
|
|
65
|
+
if (!pathOrDash) throw new Error(missingMessage);
|
|
66
|
+
const p = String(pathOrDash);
|
|
67
|
+
if (p === "-") {
|
|
68
|
+
const chunks = [];
|
|
69
|
+
for await (const chunk of process.stdin) {
|
|
70
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
71
|
+
}
|
|
72
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
73
|
+
}
|
|
74
|
+
return fs.readFile(p, "utf8");
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
rollbackSuggestCommand
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=rollback-suggest-6G2HEKFR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/rollback-suggest.ts"],"sourcesContent":["/**\n * `sdt rollback-suggest` — AI Phase 6 #4. Propose a reverse-SQL\n * skeleton for a forward DDL that the deterministic plan-to-steps\n * bridge couldn't invert.\n *\n * The output is intended for review, NOT for automatic execution.\n * Every printed proposal carries a REVIEW BEFORE APPLY header.\n * Wiring this into `sdt revert --manifest` automatically is gated to\n * a future opt-in flag — applying AI-proposed reverse-SQL against a\n * live warehouse needs more guardrails than this command provides.\n *\n * Mirrors `ddt rollback-suggest`.\n */\nimport { promises as fs } from 'node:fs';\nimport { Command } from 'commander';\nimport { ai, aiRollback } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\n\nexport function rollbackSuggestCommand(): Command {\n const cmd = new Command('rollback-suggest');\n cmd\n .description(\n 'AI-assist: propose a reverse SQL for a forward DDL when the deterministic ' +\n 'plan-to-steps bridge could not invert it. Output always carries a ' +\n 'REVIEW BEFORE APPLY header. Standalone — not auto-wired into `revert`.',\n )\n .requiredOption(\n '--forward-sql <path>',\n 'Path to a file with the forward DDL. Use \"-\" for stdin.',\n )\n .option('--fqn <fqn>', 'Optional object FQN for context.')\n .option('--object-type <type>', 'Optional object type for context (e.g. TABLE, STREAM).')\n .option(\n '--finding <code>',\n 'Optional SafetyFindingCode that motivated the rollback (e.g. DROP_DATA_TABLE).',\n )\n .option('--intent <text>', 'Optional one-sentence operator intent.')\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 (opts) => {\n const forwardSql = await readInput(\n opts.forwardSql,\n '--forward-sql is required (path or \"-\" for stdin).',\n );\n\n const result = await aiRollback.suggestRollback(\n {\n forwardSql,\n ...(opts.fqn ? { fqn: String(opts.fqn) } : {}),\n ...(opts.objectType ? { objectType: String(opts.objectType) } : {}),\n ...(opts.finding ? { findingCode: String(opts.finding) } : {}),\n ...(opts.intent ? { intentNotes: String(opts.intent) } : {}),\n },\n {\n completeFn: async (prompt) => {\n const r = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'rollback-suggest',\n maxSpendUsd: Number(opts.aiMaxSpend ?? '0') || 0,\n });\n return r.text;\n },\n },\n );\n\n if (String(opts.format).toLowerCase() === 'json') {\n const { rawModelText: _omit, ...keep } = result;\n console.log(JSON.stringify(keep, null, 2));\n return;\n }\n\n logger.success(\n `Confidence: ${result.confidence}${result.unrecoverable ? ' (UNRECOVERABLE)' : ''}${result.parseFailed ? ' — parse failed' : ''}`,\n );\n logger.dim(`Reasoning: ${result.reasoning}`);\n console.log('');\n console.log(result.reverseSql);\n if (result.confidence === 'unrecoverable' || result.unrecoverable) {\n process.exitCode = 1;\n }\n });\n return cmd;\n}\n\nasync function readInput(pathOrDash: unknown, missingMessage: string): Promise<string> {\n if (!pathOrDash) throw new Error(missingMessage);\n const p = String(pathOrDash);\n if (p === '-') {\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 return fs.readFile(p, 'utf8');\n}\n"],"mappings":";;;;;;AAaA,SAAS,YAAY,UAAU;AAC/B,SAAS,eAAe;AACxB,SAAS,IAAI,kBAAkB;AAGxB,SAAS,yBAAkC;AAChD,QAAM,MAAM,IAAI,QAAQ,kBAAkB;AAC1C,MACG;AAAA,IACC;AAAA,EAGF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,eAAe,kCAAkC,EACxD,OAAO,wBAAwB,wDAAwD,EACvF;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,mBAAmB,wCAAwC,EAClE,OAAO,kBAAkB,6CAA6C,MAAM,EAC5E;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,OAAO,OAAO,SAAS;AACtB,UAAM,aAAa,MAAM;AAAA,MACvB,KAAK;AAAA,MACL;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,WAAW;AAAA,MAC9B;AAAA,QACE;AAAA,QACA,GAAI,KAAK,MAAM,EAAE,KAAK,OAAO,KAAK,GAAG,EAAE,IAAI,CAAC;AAAA,QAC5C,GAAI,KAAK,aAAa,EAAE,YAAY,OAAO,KAAK,UAAU,EAAE,IAAI,CAAC;AAAA,QACjE,GAAI,KAAK,UAAU,EAAE,aAAa,OAAO,KAAK,OAAO,EAAE,IAAI,CAAC;AAAA,QAC5D,GAAI,KAAK,SAAS,EAAE,aAAa,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;AAAA,MAC5D;AAAA,MACA;AAAA,QACE,YAAY,OAAO,WAAW;AAC5B,gBAAM,IAAI,MAAM,GAAG,SAAS,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC,GAAG;AAAA,YAC/D,SAAS;AAAA,YACT,aAAa,OAAO,KAAK,cAAc,GAAG,KAAK;AAAA,UACjD,CAAC;AACD,iBAAO,EAAE;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAEA,QAAI,OAAO,KAAK,MAAM,EAAE,YAAY,MAAM,QAAQ;AAChD,YAAM,EAAE,cAAc,OAAO,GAAG,KAAK,IAAI;AACzC,cAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AACzC;AAAA,IACF;AAEA,WAAO;AAAA,MACL,eAAe,OAAO,UAAU,GAAG,OAAO,gBAAgB,qBAAqB,EAAE,GAAG,OAAO,cAAc,yBAAoB,EAAE;AAAA,IACjI;AACA,WAAO,IAAI,cAAc,OAAO,SAAS,EAAE;AAC3C,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,OAAO,UAAU;AAC7B,QAAI,OAAO,eAAe,mBAAmB,OAAO,eAAe;AACjE,cAAQ,WAAW;AAAA,IACrB;AAAA,EACF,CAAC;AACH,SAAO;AACT;AAEA,eAAe,UAAU,YAAqB,gBAAyC;AACrF,MAAI,CAAC,WAAY,OAAM,IAAI,MAAM,cAAc;AAC/C,QAAM,IAAI,OAAO,UAAU;AAC3B,MAAI,MAAM,KAAK;AACb,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAK,KAAgB;AAAA,IAChF;AACA,WAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AAAA,EAC9C;AACA,SAAO,GAAG,SAAS,GAAG,MAAM;AAC9B;","names":[]}
|