@ddt-tools/cli 0.2.0 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advise-tests-YNMKVJCD.js +87 -0
- package/dist/advise-tests-YNMKVJCD.js.map +1 -0
- package/dist/ai-NTNPYEKZ.js +86 -0
- package/dist/ai-NTNPYEKZ.js.map +1 -0
- package/dist/anonymize-LERTWUQO.js +139 -0
- package/dist/anonymize-LERTWUQO.js.map +1 -0
- package/dist/approval-GGZGKIU4.js +73 -0
- package/dist/approval-GGZGKIU4.js.map +1 -0
- package/dist/approval-chain-GWJKZHVU.js +118 -0
- package/dist/approval-chain-GWJKZHVU.js.map +1 -0
- package/dist/audit-log-2PH55BU4.js +159 -0
- package/dist/audit-log-2PH55BU4.js.map +1 -0
- package/dist/backlog-QNXGOUF4.js +76 -0
- package/dist/backlog-QNXGOUF4.js.map +1 -0
- package/dist/bisect-W3XKKRWG.js +111 -0
- package/dist/bisect-W3XKKRWG.js.map +1 -0
- package/dist/bookmarks-XVOGXGMC.js +107 -0
- package/dist/bookmarks-XVOGXGMC.js.map +1 -0
- package/dist/branch-S3I2IJGQ.js +103 -0
- package/dist/branch-S3I2IJGQ.js.map +1 -0
- package/dist/build-MP3JQEFO.js +20 -0
- package/dist/build-MP3JQEFO.js.map +1 -0
- package/dist/catalog-3J3NFNXP.js +137 -0
- package/dist/catalog-3J3NFNXP.js.map +1 -0
- package/dist/changelog-ZQAH3ULB.js +216 -0
- package/dist/changelog-ZQAH3ULB.js.map +1 -0
- package/dist/chunk-2FT6HXKS.js +55 -0
- package/dist/chunk-2FT6HXKS.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-DL3V7UJ2.js +25 -0
- package/dist/chunk-DL3V7UJ2.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-XFXG347C.js +40 -0
- package/dist/chunk-XFXG347C.js.map +1 -0
- package/dist/cli.js +499 -19402
- package/dist/cli.js.map +1 -1
- package/dist/compare-P7JOV76O.js +379 -0
- package/dist/compare-P7JOV76O.js.map +1 -0
- package/dist/compare-profiles-H33CXZPD.js +219 -0
- package/dist/compare-profiles-H33CXZPD.js.map +1 -0
- package/dist/completion-ZSNCQKJ2.js +89 -0
- package/dist/completion-ZSNCQKJ2.js.map +1 -0
- package/dist/connection-CDGVEFUC.js +148 -0
- package/dist/connection-CDGVEFUC.js.map +1 -0
- package/dist/cost-estimate-S2MKHT2H.js +321 -0
- package/dist/cost-estimate-S2MKHT2H.js.map +1 -0
- package/dist/data-compare-46ZI7KHL.js +128 -0
- package/dist/data-compare-46ZI7KHL.js.map +1 -0
- package/dist/data-fit-WGEPLD5S.js +127 -0
- package/dist/data-fit-WGEPLD5S.js.map +1 -0
- package/dist/deploy-status-4H5KJFRC.js +58 -0
- package/dist/deploy-status-4H5KJFRC.js.map +1 -0
- package/dist/design-ILX3ZSWW.js +135 -0
- package/dist/design-ILX3ZSWW.js.map +1 -0
- package/dist/diagnose-WPUL67E4.js +150 -0
- package/dist/diagnose-WPUL67E4.js.map +1 -0
- package/dist/discover-DEO2R5T6.js +78 -0
- package/dist/discover-DEO2R5T6.js.map +1 -0
- package/dist/docs-QNY3MUVO.js +183 -0
- package/dist/docs-QNY3MUVO.js.map +1 -0
- package/dist/drift-FDRNPWQA.js +233 -0
- package/dist/drift-FDRNPWQA.js.map +1 -0
- package/dist/drift-gate-6BWWWMHW.js +103 -0
- package/dist/drift-gate-6BWWWMHW.js.map +1 -0
- package/dist/error-lookup-4R3Y4RBC.js +56 -0
- package/dist/error-lookup-4R3Y4RBC.js.map +1 -0
- package/dist/errorReporting-3LPE2IJY.js +109 -0
- package/dist/errorReporting-3LPE2IJY.js.map +1 -0
- package/dist/exec-JOLH5LPT.js +122 -0
- package/dist/exec-JOLH5LPT.js.map +1 -0
- package/dist/explain-NS26WE2Y.js +189 -0
- package/dist/explain-NS26WE2Y.js.map +1 -0
- package/dist/explorer-GSYYYOAL.js +58 -0
- package/dist/explorer-GSYYYOAL.js.map +1 -0
- package/dist/extract-4LWEZG4O.js +152 -0
- package/dist/extract-4LWEZG4O.js.map +1 -0
- package/dist/features-KQV4OFIZ.js +54 -0
- package/dist/features-KQV4OFIZ.js.map +1 -0
- package/dist/feedback-CBLGXUEG.js +158 -0
- package/dist/feedback-CBLGXUEG.js.map +1 -0
- package/dist/find-SMXRCZ76.js +176 -0
- package/dist/find-SMXRCZ76.js.map +1 -0
- package/dist/format-HMGG6MY3.js +277 -0
- package/dist/format-HMGG6MY3.js.map +1 -0
- package/dist/generate-W7VLBDLI.js +160 -0
- package/dist/generate-W7VLBDLI.js.map +1 -0
- package/dist/graph-YYL5UYCJ.js +168 -0
- package/dist/graph-YYL5UYCJ.js.map +1 -0
- package/dist/history-GDRFP4PG.js +184 -0
- package/dist/history-GDRFP4PG.js.map +1 -0
- package/dist/hosts-DRFZTMIJ.js +45 -0
- package/dist/hosts-DRFZTMIJ.js.map +1 -0
- package/dist/impact-A4NU6CB2.js +63 -0
- package/dist/impact-A4NU6CB2.js.map +1 -0
- package/dist/import-2RNYDL4E.js +79 -0
- package/dist/import-2RNYDL4E.js.map +1 -0
- package/dist/index.cjs +11 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/init-EAOGNGXI.js +54 -0
- package/dist/init-EAOGNGXI.js.map +1 -0
- package/dist/install-hooks-G3Y5LVXK.js +109 -0
- package/dist/install-hooks-G3Y5LVXK.js.map +1 -0
- package/dist/license-Z5YSC7XQ.js +43 -0
- package/dist/license-Z5YSC7XQ.js.map +1 -0
- package/dist/lineage-C5CGVP36.js +555 -0
- package/dist/lineage-C5CGVP36.js.map +1 -0
- package/dist/lint-AQFPZ3WG.js +144 -0
- package/dist/lint-AQFPZ3WG.js.map +1 -0
- package/dist/mcp-F7FND5X7.js +343 -0
- package/dist/mcp-F7FND5X7.js.map +1 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js +156 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js.map +1 -0
- package/dist/migrate-platform-E7VZFPO5.js +91 -0
- package/dist/migrate-platform-E7VZFPO5.js.map +1 -0
- package/dist/optimize-WUJ5ZN5Y.js +109 -0
- package/dist/optimize-WUJ5ZN5Y.js.map +1 -0
- package/dist/perf-UULZSREY.js +200 -0
- package/dist/perf-UULZSREY.js.map +1 -0
- package/dist/pii-QHU32VML.js +146 -0
- package/dist/pii-QHU32VML.js.map +1 -0
- package/dist/pilot-BR6GVK32.js +29 -0
- package/dist/pilot-BR6GVK32.js.map +1 -0
- package/dist/pr-comment-2FOA3EXG.js +81 -0
- package/dist/pr-comment-2FOA3EXG.js.map +1 -0
- package/dist/preview-XNY422OU.js +46 -0
- package/dist/preview-XNY422OU.js.map +1 -0
- package/dist/profile-SQTBNKYS.js +98 -0
- package/dist/profile-SQTBNKYS.js.map +1 -0
- package/dist/promote-FSGUPIPD.js +417 -0
- package/dist/promote-FSGUPIPD.js.map +1 -0
- package/dist/publish-AYCRMCE2.js +739 -0
- package/dist/publish-AYCRMCE2.js.map +1 -0
- package/dist/purge-Y5IOTXKA.js +56 -0
- package/dist/purge-Y5IOTXKA.js.map +1 -0
- package/dist/query-log-SDDGMJLJ.js +112 -0
- package/dist/query-log-SDDGMJLJ.js.map +1 -0
- package/dist/refactor-TC7S43F2.js +5809 -0
- package/dist/refactor-TC7S43F2.js.map +1 -0
- package/dist/refresh-MDJYOYV5.js +39 -0
- package/dist/refresh-MDJYOYV5.js.map +1 -0
- package/dist/replay-E4664A5K.js +118 -0
- package/dist/replay-E4664A5K.js.map +1 -0
- package/dist/revert-QWQWCJJB.js +111 -0
- package/dist/revert-QWQWCJJB.js.map +1 -0
- package/dist/review-7CAVLD67.js +164 -0
- package/dist/review-7CAVLD67.js.map +1 -0
- package/dist/rollback-suggest-C6D5YFCA.js +79 -0
- package/dist/rollback-suggest-C6D5YFCA.js.map +1 -0
- package/dist/safer-alternative-QR4QEFUV.js +84 -0
- package/dist/safer-alternative-QR4QEFUV.js.map +1 -0
- package/dist/safety-OFWUFLK4.js +165 -0
- package/dist/safety-OFWUFLK4.js.map +1 -0
- package/dist/savings-MEBE4TXI.js +95 -0
- package/dist/savings-MEBE4TXI.js.map +1 -0
- package/dist/scan-secrets-XCUBMLHL.js +54 -0
- package/dist/scan-secrets-XCUBMLHL.js.map +1 -0
- package/dist/schema-7JZIG6QR.js +447 -0
- package/dist/schema-7JZIG6QR.js.map +1 -0
- package/dist/script-BMYVBHFR.js +167 -0
- package/dist/script-BMYVBHFR.js.map +1 -0
- package/dist/search-TA3C3AZT.js +151 -0
- package/dist/search-TA3C3AZT.js.map +1 -0
- package/dist/seed-W4Q3L2IU.js +101 -0
- package/dist/seed-W4Q3L2IU.js.map +1 -0
- package/dist/sketch-6B2V6FJV.js +83 -0
- package/dist/sketch-6B2V6FJV.js.map +1 -0
- package/dist/snapshot-YMVS322L.js +171 -0
- package/dist/snapshot-YMVS322L.js.map +1 -0
- package/dist/snippets-EVTN63OU.js +74 -0
- package/dist/snippets-EVTN63OU.js.map +1 -0
- package/dist/standards-FGJW3CQL.js +238 -0
- package/dist/standards-FGJW3CQL.js.map +1 -0
- package/dist/suggest-V3LVIFZ5.js +44 -0
- package/dist/suggest-V3LVIFZ5.js.map +1 -0
- package/dist/suggest-constraints-EX2FCWOQ.js +154 -0
- package/dist/suggest-constraints-EX2FCWOQ.js.map +1 -0
- package/dist/suite-YTQ3CNX5.js +85 -0
- package/dist/suite-YTQ3CNX5.js.map +1 -0
- package/dist/telemetry-KOIY3NEQ.js +90 -0
- package/dist/telemetry-KOIY3NEQ.js.map +1 -0
- package/dist/template-MUJ6X6LN.js +396 -0
- package/dist/template-MUJ6X6LN.js.map +1 -0
- package/dist/test-XFSQHR2S.js +169 -0
- package/dist/test-XFSQHR2S.js.map +1 -0
- package/dist/trial-GFTGYCR3.js +31 -0
- package/dist/trial-GFTGYCR3.js.map +1 -0
- package/dist/validate-LFDEZFFH.js +107 -0
- package/dist/validate-LFDEZFFH.js.map +1 -0
- package/dist/verify-KRDYOJCR.js +76 -0
- package/dist/verify-KRDYOJCR.js.map +1 -0
- package/dist/watch-FSG23RR3.js +80 -0
- package/dist/watch-FSG23RR3.js.map +1 -0
- package/dist/xcompare-U4TXTTIR.js +87 -0
- package/dist/xcompare-U4TXTTIR.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19298
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
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 { refresh, ProjectSource } from "@ddt-tools/core";
|
|
8
|
+
function refreshCommand() {
|
|
9
|
+
return new Command("refresh").description(
|
|
10
|
+
"Generate a REFRESH script for streaming tables + materialized views + pipeline API calls. Operator runs the output script + makes the pipeline API calls explicitly."
|
|
11
|
+
).requiredOption("--source <path>", ".ddtproj or .ddtpac defining the objects.").option("--no-streaming-tables", "Skip REFRESH STREAMING TABLE entries.", false).option("--no-materialized-views", "Skip REFRESH MATERIALIZED VIEW entries.", false).option("--no-pipelines", "Skip pipeline API-call comments.", 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 ProjectSource(sourcePath);
|
|
14
|
+
const model = await source.load();
|
|
15
|
+
const result = refresh.generateRefreshScript(
|
|
16
|
+
model,
|
|
17
|
+
{
|
|
18
|
+
streamingTables: opts.streamingTables !== false,
|
|
19
|
+
materializedViews: opts.materializedViews !== false,
|
|
20
|
+
pipelines: opts.pipelines !== false
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
const header = `-- ${result.summary.streamingTablesRefreshed} streaming table(s) \xB7 ${result.summary.materializedViewsRefreshed} materialized view(s) \xB7 ${result.summary.pipelinesNoted} pipeline(s)
|
|
24
|
+
|
|
25
|
+
`;
|
|
26
|
+
const out = header + result.sql;
|
|
27
|
+
if (opts.output) {
|
|
28
|
+
const outPath = path.resolve(String(opts.output));
|
|
29
|
+
await fs.writeFile(outPath, out, "utf8");
|
|
30
|
+
console.error(`refresh: wrote script to ${outPath}`);
|
|
31
|
+
} else {
|
|
32
|
+
process.stdout.write(out);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
refreshCommand
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=refresh-MDJYOYV5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/refresh.ts"],"sourcesContent":["/**\n * `ddt refresh` — DCM compatibility item 4 (mirrors `sdt refresh`).\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { refresh, ProjectSource } from '@ddt-tools/core';\n\nexport function refreshCommand(): Command {\n return new Command('refresh')\n .description(\n 'Generate a REFRESH script for streaming tables + materialized views + pipeline API calls. ' +\n 'Operator runs the output script + makes the pipeline API calls explicitly.',\n )\n .requiredOption('--source <path>', '.ddtproj or .ddtpac defining the objects.')\n .option('--no-streaming-tables', 'Skip REFRESH STREAMING TABLE entries.', false)\n .option('--no-materialized-views', 'Skip REFRESH MATERIALIZED VIEW entries.', false)\n .option('--no-pipelines', 'Skip pipeline API-call comments.', 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 ProjectSource(sourcePath);\n const model = await source.load();\n const result = refresh.generateRefreshScript(\n model as Parameters<typeof refresh.generateRefreshScript>[0],\n {\n streamingTables: opts.streamingTables !== false,\n materializedViews: opts.materializedViews !== false,\n pipelines: opts.pipelines !== false,\n },\n );\n const header = `-- ${result.summary.streamingTablesRefreshed} streaming table(s) · ${result.summary.materializedViewsRefreshed} materialized view(s) · ${result.summary.pipelinesNoted} pipeline(s)\\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":";;;AAGA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,SAAS,qBAAqB;AAEhC,SAAS,iBAA0B;AACxC,SAAO,IAAI,QAAQ,SAAS,EACzB;AAAA,IACC;AAAA,EAEF,EACC,eAAe,mBAAmB,2CAA2C,EAC7E,OAAO,yBAAyB,yCAAyC,KAAK,EAC9E,OAAO,2BAA2B,2CAA2C,KAAK,EAClF,OAAO,kBAAkB,oCAAoC,KAAK,EAClE,OAAO,uBAAuB,2CAA2C,EACzE,OAAO,OAAO,SAAkC;AAC/C,UAAM,aAAa,KAAK,QAAQ,OAAO,KAAK,MAAM,CAAC;AACnD,UAAM,SAAS,IAAI,cAAc,UAAU;AAC3C,UAAM,QAAQ,MAAM,OAAO,KAAK;AAChC,UAAM,SAAS,QAAQ;AAAA,MACrB;AAAA,MACA;AAAA,QACE,iBAAiB,KAAK,oBAAoB;AAAA,QAC1C,mBAAmB,KAAK,sBAAsB;AAAA,QAC9C,WAAW,KAAK,cAAc;AAAA,MAChC;AAAA,IACF;AACA,UAAM,SAAS,MAAM,OAAO,QAAQ,wBAAwB,4BAAyB,OAAO,QAAQ,0BAA0B,8BAA2B,OAAO,QAAQ,cAAc;AAAA;AAAA;AACtL,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,118 @@
|
|
|
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 {
|
|
8
|
+
DatabricksExecutor,
|
|
9
|
+
createConnection,
|
|
10
|
+
getProfile
|
|
11
|
+
} from "@ddt-tools/core";
|
|
12
|
+
async function discoverManifests(input) {
|
|
13
|
+
const stat = await fs.stat(input).catch(() => null);
|
|
14
|
+
if (!stat) throw new Error(`Manifest path not found: ${input}`);
|
|
15
|
+
if (stat.isFile()) return [path.resolve(input)];
|
|
16
|
+
const entries = await fs.readdir(input, { withFileTypes: true });
|
|
17
|
+
const files = entries.filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".json")).map((e) => path.join(input, e.name)).sort();
|
|
18
|
+
if (files.length === 0) throw new Error(`No .json manifests under ${input}.`);
|
|
19
|
+
return files;
|
|
20
|
+
}
|
|
21
|
+
function replayCommand() {
|
|
22
|
+
const cmd = new Command("replay");
|
|
23
|
+
cmd.description(
|
|
24
|
+
"Re-execute forwardSql from one or more deploy manifests against a target workspace."
|
|
25
|
+
).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(
|
|
26
|
+
"--continue-on-error",
|
|
27
|
+
"Continue past failed statements. Default: stop on first failure.",
|
|
28
|
+
false
|
|
29
|
+
).option("--dry-run", "Print what would be replayed without executing.", false).action(async (opts) => {
|
|
30
|
+
const manifestPaths = await discoverManifests(String(opts.manifest));
|
|
31
|
+
const manifests = [];
|
|
32
|
+
for (const p of manifestPaths) {
|
|
33
|
+
const raw = await fs.readFile(p, "utf8");
|
|
34
|
+
const m = JSON.parse(raw);
|
|
35
|
+
if (m.version !== 1) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${p}: unsupported manifest version ${m.version} (this CLI understands v1)`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
manifests.push({ path: p, manifest: m });
|
|
41
|
+
}
|
|
42
|
+
manifests.sort((a, b) => a.manifest.deployedAt < b.manifest.deployedAt ? -1 : 1);
|
|
43
|
+
const profile = await getProfile(String(opts.connection));
|
|
44
|
+
const filter = opts.filter ? String(opts.filter) : "";
|
|
45
|
+
const fromId = opts.fromStep ? String(opts.fromStep) : "";
|
|
46
|
+
const toId = opts.toStep ? String(opts.toStep) : "";
|
|
47
|
+
const flatSteps = [];
|
|
48
|
+
let inRange = !fromId;
|
|
49
|
+
for (const { path: p, manifest } of manifests) {
|
|
50
|
+
for (const s of manifest.steps) {
|
|
51
|
+
if (!inRange) {
|
|
52
|
+
if (s.id === fromId) inRange = true;
|
|
53
|
+
else continue;
|
|
54
|
+
}
|
|
55
|
+
if (filter && !s.fqn.includes(filter)) continue;
|
|
56
|
+
if (s.status !== "SUCCESS" && s.status !== "ROLLED_BACK") continue;
|
|
57
|
+
flatSteps.push({ step: s, manifestPath: p, deployedAt: manifest.deployedAt });
|
|
58
|
+
if (toId && s.id === toId) {
|
|
59
|
+
inRange = false;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
console.log(
|
|
65
|
+
`Replay plan: ${flatSteps.length} step(s) across ${manifests.length} manifest(s) \u2192 ${profile.auth.host}`
|
|
66
|
+
);
|
|
67
|
+
console.log("");
|
|
68
|
+
if (opts.dryRun) {
|
|
69
|
+
for (const { step, deployedAt } of flatSteps) {
|
|
70
|
+
console.log(`\u25B6 ${deployedAt} ${step.objectType} ${step.fqn}`);
|
|
71
|
+
console.log(` ${step.forwardSql.split("\n")[0]?.slice(0, 100) ?? ""}`);
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log(`Dry-run summary: ${flatSteps.length} step(s) would replay.`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!opts.yes) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Refusing to replay without --yes. Replay re-executes historical DDL against the target."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const conn = createConnection(profile);
|
|
83
|
+
let succeeded = 0;
|
|
84
|
+
let failed = 0;
|
|
85
|
+
try {
|
|
86
|
+
await conn.connect();
|
|
87
|
+
const executor = new DatabricksExecutor(conn);
|
|
88
|
+
for (const { step, deployedAt } of flatSteps) {
|
|
89
|
+
process.stdout.write(`\u25B6 ${deployedAt} ${step.objectType} ${step.fqn} \u2026`);
|
|
90
|
+
const t0 = Date.now();
|
|
91
|
+
try {
|
|
92
|
+
await executor.execute(step.forwardSql);
|
|
93
|
+
console.log(` \u2713 (${Date.now() - t0}ms)`);
|
|
94
|
+
succeeded++;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
97
|
+
console.log(` \u2717 ${msg}`);
|
|
98
|
+
failed++;
|
|
99
|
+
if (!opts.continueOnError) {
|
|
100
|
+
console.error("");
|
|
101
|
+
console.error("Stopping on first failure. Use --continue-on-error to push through.");
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
await conn.disconnect();
|
|
108
|
+
}
|
|
109
|
+
console.log("");
|
|
110
|
+
console.log(`Summary: ${succeeded} replayed, ${failed} failed.`);
|
|
111
|
+
if (failed > 0) process.exitCode = 1;
|
|
112
|
+
});
|
|
113
|
+
return cmd;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
replayCommand
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=replay-E4664A5K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/replay.ts"],"sourcesContent":["/**\n * `ddt replay --manifest <path>` — re-execute a previous deploy's\n * forwardSql against a fresh target.\n *\n * Use cases:\n * - Rebuild a dev/stage workspace from production's manifest history\n * (DDL replay against a clean catalog).\n * - Reproduce a customer's failure by replaying their support-bundle\n * manifest against an internal sandbox.\n *\n * Symmetric to `ddt revert` — same manifest format, same executor, but\n * forward instead of reverse.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n DatabricksExecutor,\n createConnection,\n getProfile,\n type DeployManifest,\n type DeployManifestStep,\n} from '@ddt-tools/core';\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 // Directory — chronological by manifest deployedAt (filename usually\n // contains the timestamp so a lexical sort works).\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 workspace.',\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.auth.host}`,\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 = createConnection(profile);\n let succeeded = 0;\n let failed = 0;\n try {\n await conn.connect();\n const executor = new DatabricksExecutor(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,111 @@
|
|
|
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 {
|
|
7
|
+
DatabricksExecutor,
|
|
8
|
+
createConnection,
|
|
9
|
+
getProfile,
|
|
10
|
+
revertStepsFromManifest
|
|
11
|
+
} from "@ddt-tools/core";
|
|
12
|
+
function revertCommand() {
|
|
13
|
+
const cmd = new Command("revert");
|
|
14
|
+
cmd.description(
|
|
15
|
+
"Replay a previous deploy manifest in reverse, executing reverseSql for each successful step."
|
|
16
|
+
).requiredOption(
|
|
17
|
+
"--manifest <path>",
|
|
18
|
+
"Path to a JSON deploy manifest from `ddt publish --apply --manifest`."
|
|
19
|
+
).requiredOption(
|
|
20
|
+
"--connection <name>",
|
|
21
|
+
"Connection profile to revert against. Should match the original deploy."
|
|
22
|
+
).requiredOption("--yes", "Explicit confirmation. Required because revert is destructive.").option(
|
|
23
|
+
"--continue-on-error",
|
|
24
|
+
"Continue past failed reverse statements. Default: stop on first failure.",
|
|
25
|
+
false
|
|
26
|
+
).option("--dry-run", "Print what would be reverted without executing.", false).action(async (opts) => {
|
|
27
|
+
const raw = await fs.readFile(String(opts.manifest), "utf8");
|
|
28
|
+
const manifest = JSON.parse(raw);
|
|
29
|
+
if (manifest.version !== 1) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unsupported manifest version: ${manifest.version}. This CLI understands v1.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const profile = await getProfile(String(opts.connection));
|
|
35
|
+
if (profile.auth.host !== manifest.workspaceHost) {
|
|
36
|
+
console.error("");
|
|
37
|
+
console.error(
|
|
38
|
+
`WARNING: manifest's workspaceHost (${manifest.workspaceHost}) doesn't match the profile's host (${profile.auth.host}).`
|
|
39
|
+
);
|
|
40
|
+
console.error(
|
|
41
|
+
"Reverting against a different workspace than the original deploy may have unintended effects."
|
|
42
|
+
);
|
|
43
|
+
console.error("Re-run with the correct --connection, or proceed carefully.");
|
|
44
|
+
console.error("");
|
|
45
|
+
}
|
|
46
|
+
const reverseSteps = revertStepsFromManifest(manifest);
|
|
47
|
+
console.log(`Reverting ${reverseSteps.length} step(s) from ${opts.manifest}`);
|
|
48
|
+
console.log(`Original deploy: ${manifest.deployedAt} \u2192 ${manifest.workspaceHost}`);
|
|
49
|
+
console.log("");
|
|
50
|
+
if (opts.dryRun) {
|
|
51
|
+
for (const s of reverseSteps) {
|
|
52
|
+
const tag = s.reverseSql ? "\u21A9" : "\u26A0";
|
|
53
|
+
console.log(`${tag} ${s.objectType} ${s.fqn}`);
|
|
54
|
+
if (s.reverseSql) {
|
|
55
|
+
console.log(` ${s.reverseSql.split("\n")[0]?.slice(0, 100) ?? ""}`);
|
|
56
|
+
} else {
|
|
57
|
+
console.log(` (no reverseSql \u2014 IRREVERSIBLE; would be skipped)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const skippable = reverseSteps.filter((s) => !s.reverseSql).length;
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log(
|
|
63
|
+
`Dry-run summary: ${reverseSteps.length - skippable} reversible, ${skippable} irreversible.`
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const conn = createConnection(profile);
|
|
68
|
+
let succeeded = 0;
|
|
69
|
+
let failed = 0;
|
|
70
|
+
let irreversible = 0;
|
|
71
|
+
try {
|
|
72
|
+
await conn.connect();
|
|
73
|
+
const executor = new DatabricksExecutor(conn);
|
|
74
|
+
for (const s of reverseSteps) {
|
|
75
|
+
if (!s.reverseSql) {
|
|
76
|
+
console.log(`\u26A0 skip ${s.objectType} ${s.fqn} \u2014 irreversible (no reverseSql captured)`);
|
|
77
|
+
irreversible++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
process.stdout.write(`\u21A9 ${s.objectType} ${s.fqn} \u2026`);
|
|
81
|
+
const t0 = Date.now();
|
|
82
|
+
try {
|
|
83
|
+
await executor.execute(s.reverseSql);
|
|
84
|
+
console.log(` \u2713 (${Date.now() - t0}ms)`);
|
|
85
|
+
succeeded++;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
console.log(` \u2717 ${msg}`);
|
|
89
|
+
failed++;
|
|
90
|
+
if (!opts.continueOnError) {
|
|
91
|
+
console.error("");
|
|
92
|
+
console.error("Stopping on first failure. Use --continue-on-error to push through.");
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
await conn.disconnect();
|
|
99
|
+
}
|
|
100
|
+
console.log("");
|
|
101
|
+
console.log(
|
|
102
|
+
`Summary: ${succeeded} reverted, ${failed} failed, ${irreversible} irreversible.`
|
|
103
|
+
);
|
|
104
|
+
if (failed > 0 || irreversible > 0) process.exitCode = 1;
|
|
105
|
+
});
|
|
106
|
+
return cmd;
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
revertCommand
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=revert-QWQWCJJB.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 {\n DatabricksExecutor,\n createConnection,\n getProfile,\n revertStepsFromManifest,\n type DeployManifest,\n} from '@ddt-tools/core';\n\n/**\n * `ddt revert --manifest <path>` — undo a previous deploy by executing\n * each step's `reverseSql` in reverse order. Manifests come from\n * `ddt publish --manifest <path>` (the JSON deploy manifest captured\n * 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 * Sqitch-equivalent revert flow. Use case: a deploy succeeded but\n * something downstream broke; revert it. Use case: drift is detected\n * and you need to roll back to the last-known-good state.\n *\n * The manifest shape (`DeployManifest`) lives in `@ddt-tools/core` so the\n * `publish` writer, this reader, and `replay` share one contract.\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 `ddt 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.auth.host !== manifest.workspaceHost) {\n console.error('');\n console.error(\n `WARNING: manifest's workspaceHost (${manifest.workspaceHost}) doesn't match the profile's host (${profile.auth.host}).`,\n );\n console.error(\n 'Reverting against a different workspace 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.workspaceHost}`);\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 = createConnection(profile);\n let succeeded = 0;\n let failed = 0;\n let irreversible = 0;\n try {\n await conn.connect();\n const executor = new DatabricksExecutor(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,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachExplainFlag,
|
|
3
|
+
runExplain
|
|
4
|
+
} from "./chunk-XFXG347C.js";
|
|
5
|
+
import "./chunk-DGUM43GV.js";
|
|
6
|
+
|
|
7
|
+
// src/commands/review.ts
|
|
8
|
+
import { promises as fs } from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import {
|
|
12
|
+
ai,
|
|
13
|
+
aiReview,
|
|
14
|
+
loadProject,
|
|
15
|
+
pac,
|
|
16
|
+
parseProjectModel,
|
|
17
|
+
review
|
|
18
|
+
} from "@ddt-tools/core";
|
|
19
|
+
function reviewCommand() {
|
|
20
|
+
const cmd = new Command("review");
|
|
21
|
+
cmd.description(
|
|
22
|
+
"Senior-DBA-style health report (lint + lineage + smell + cost + safety, with reasoning)."
|
|
23
|
+
).option("--source <path>", ".ddtproj or .ddtpac to analyze (project-health mode).").option("-o, --out <path>", "Output file path. Defaults to stdout.").option(
|
|
24
|
+
"--senior-dba",
|
|
25
|
+
"AI-driven senior-DBA review of a deploy diff (requires --diff and --safety).",
|
|
26
|
+
false
|
|
27
|
+
).option(
|
|
28
|
+
"--diff <path>",
|
|
29
|
+
"JSON file with compare summary { added, removed, modified, addedSample, removedSample, modifiedSample }."
|
|
30
|
+
).option(
|
|
31
|
+
"--safety <path>",
|
|
32
|
+
"JSON file with safety summary { unrecoverable, destructive, expensive, warning, sample }."
|
|
33
|
+
).option(
|
|
34
|
+
"--target-meta <path>",
|
|
35
|
+
"Optional file with target-metadata prose (existing tables, recent deploys, role grants)."
|
|
36
|
+
).option("--ddl <path>", "Optional truncated DDL preview file.").option("--format <fmt>", "Output format: markdown | json. Default markdown.", "markdown").option(
|
|
37
|
+
"--ai-max-spend <usd>",
|
|
38
|
+
"Refuse the AI call if today's estimated spend \u2265 this (USD). 0 = no cap.",
|
|
39
|
+
"0"
|
|
40
|
+
).action(
|
|
41
|
+
async (opts) => {
|
|
42
|
+
if (opts.seniorDba) {
|
|
43
|
+
await runSeniorDbaReview(opts, "ddt");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!opts.source) {
|
|
47
|
+
throw new Error("--source is required in the default (project-health) mode.");
|
|
48
|
+
}
|
|
49
|
+
const sourcePath = String(opts.source);
|
|
50
|
+
const model = await loadModel(sourcePath);
|
|
51
|
+
const md = review.renderReviewReport(model, { source: sourcePath });
|
|
52
|
+
await emit(md, opts.out);
|
|
53
|
+
await runExplain(
|
|
54
|
+
{
|
|
55
|
+
feature: "review.explain",
|
|
56
|
+
systemPrompt: "You are a Databricks principal engineer giving a senior-architect 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."
|
|
57
|
+
},
|
|
58
|
+
opts,
|
|
59
|
+
() => `Review report follows:
|
|
60
|
+
|
|
61
|
+
${md}
|
|
62
|
+
|
|
63
|
+
Narrate this report for a teammate who has not read it.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
attachExplainFlag(cmd);
|
|
68
|
+
return cmd;
|
|
69
|
+
}
|
|
70
|
+
async function runSeniorDbaReview(opts, toolName) {
|
|
71
|
+
if (!opts.diff || !opts.safety) {
|
|
72
|
+
throw new Error("--senior-dba requires both --diff and --safety (JSON files).");
|
|
73
|
+
}
|
|
74
|
+
const [diff, safety, targetMeta, ddl] = await Promise.all([
|
|
75
|
+
readJson(opts.diff),
|
|
76
|
+
readJson(opts.safety),
|
|
77
|
+
opts.targetMeta ? fs.readFile(path.resolve(opts.targetMeta), "utf8") : Promise.resolve(void 0),
|
|
78
|
+
opts.ddl ? fs.readFile(path.resolve(opts.ddl), "utf8") : Promise.resolve(void 0)
|
|
79
|
+
]);
|
|
80
|
+
const compareSummary = normalizeCompareSummary(diff);
|
|
81
|
+
const safetySummary = normalizeSafetySummary(safety);
|
|
82
|
+
const result = await aiReview.runSeniorDbaReview(
|
|
83
|
+
{
|
|
84
|
+
compareSummary,
|
|
85
|
+
safetySummary,
|
|
86
|
+
...targetMeta ? { targetMetadata: targetMeta } : {},
|
|
87
|
+
...ddl ? { ddlPreview: ddl } : {}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
completeFn: async (user, system) => {
|
|
91
|
+
const r = await ai.complete(
|
|
92
|
+
[
|
|
93
|
+
{ role: "system", content: system },
|
|
94
|
+
{ role: "user", content: user }
|
|
95
|
+
],
|
|
96
|
+
{
|
|
97
|
+
feature: "review.senior-dba",
|
|
98
|
+
maxSpendUsd: Number(opts.aiMaxSpend ?? "0") || 0
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
return r.text;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
toolName
|
|
105
|
+
);
|
|
106
|
+
const format = String(opts.format ?? "markdown").toLowerCase();
|
|
107
|
+
const payload = format === "json" ? JSON.stringify({ ...result, rawModelText: void 0 }, null, 2) : aiReview.renderSeniorDbaReviewMarkdown(result, toolName);
|
|
108
|
+
await emit(payload, opts.out);
|
|
109
|
+
if (result.verdict === "request_changes" && !opts.out) {
|
|
110
|
+
process.exitCode = 2;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function normalizeCompareSummary(raw) {
|
|
114
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
115
|
+
const num = (k) => typeof o[k] === "number" ? o[k] : 0;
|
|
116
|
+
const arr = (k) => Array.isArray(o[k]) ? o[k].filter((x) => typeof x === "string") : void 0;
|
|
117
|
+
return {
|
|
118
|
+
added: num("added"),
|
|
119
|
+
removed: num("removed"),
|
|
120
|
+
modified: num("modified"),
|
|
121
|
+
...arr("addedSample") ? { addedSample: arr("addedSample") } : {},
|
|
122
|
+
...arr("removedSample") ? { removedSample: arr("removedSample") } : {},
|
|
123
|
+
...arr("modifiedSample") ? { modifiedSample: arr("modifiedSample") } : {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function normalizeSafetySummary(raw) {
|
|
127
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
128
|
+
const num = (k) => typeof o[k] === "number" ? o[k] : 0;
|
|
129
|
+
const sample = Array.isArray(o.sample) ? o.sample.filter((x) => typeof x === "string") : void 0;
|
|
130
|
+
return {
|
|
131
|
+
unrecoverable: num("unrecoverable"),
|
|
132
|
+
destructive: num("destructive"),
|
|
133
|
+
expensive: num("expensive"),
|
|
134
|
+
warning: num("warning"),
|
|
135
|
+
...sample ? { sample } : {}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function readJson(p) {
|
|
139
|
+
const raw = await fs.readFile(path.resolve(p), "utf8");
|
|
140
|
+
return JSON.parse(raw);
|
|
141
|
+
}
|
|
142
|
+
async function loadModel(sourcePath) {
|
|
143
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
144
|
+
const c = await pac.readPac(sourcePath);
|
|
145
|
+
return c.model;
|
|
146
|
+
}
|
|
147
|
+
const loaded = await loadProject(sourcePath);
|
|
148
|
+
return await parseProjectModel(loaded);
|
|
149
|
+
}
|
|
150
|
+
async function emit(payload, out) {
|
|
151
|
+
if (out) {
|
|
152
|
+
const p = path.resolve(String(out));
|
|
153
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
154
|
+
await fs.writeFile(p, payload + (payload.endsWith("\n") ? "" : "\n"), "utf8");
|
|
155
|
+
console.error(`Wrote ${p} (${payload.length} bytes).`);
|
|
156
|
+
} else {
|
|
157
|
+
process.stdout.write(payload + (payload.endsWith("\n") ? "" : "\n"));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
reviewCommand,
|
|
162
|
+
runSeniorDbaReview
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=review-7CAVLD67.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 {\n ai,\n aiReview,\n loadProject,\n pac,\n parseProjectModel,\n review,\n type DatabricksObject,\n} from '@ddt-tools/core';\nimport { attachExplainFlag, runExplain } from '../util/ai-explain.js';\n\n/**\n * `ddt review` — senior-DBA Markdown report. Mirrors `sdt review`.\n *\n * Default mode (project health): pure composition of existing modules.\n *\n * `--senior-dba` mode (AI Phase 4): takes a compare-result JSON + a\n * safety-summary JSON + optional target-metadata snapshot + truncated\n * DDL preview, asks the configured AI provider for a senior-DBA\n * verdict, and emits the parsed result as Markdown (or JSON via\n * `--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>', '.ddtproj or .ddtpac 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, 'ddt');\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 Databricks principal engineer giving a senior-architect 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 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): Promise<DatabricksObject[]> {\n if (sourcePath.endsWith('.ddtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await loadProject(sourcePath);\n return await parseProjectModel(loaded);\n}\n\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":"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","names":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/rollback-suggest.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { ai, aiRollback } from "@ddt-tools/core";
|
|
7
|
+
function rollbackSuggestCommand() {
|
|
8
|
+
const cmd = new Command("rollback-suggest");
|
|
9
|
+
cmd.description(
|
|
10
|
+
"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`."
|
|
11
|
+
).requiredOption(
|
|
12
|
+
"--forward-sql <path>",
|
|
13
|
+
'Path to a file with the forward DDL. Use "-" for stdin.'
|
|
14
|
+
).option("--fqn <fqn>", "Optional object FQN for context.").option(
|
|
15
|
+
"--object-type <type>",
|
|
16
|
+
"Optional object type for context (e.g. MANAGED_TABLE, STREAMING_TABLE)."
|
|
17
|
+
).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
|
+
console.error(
|
|
53
|
+
`Confidence: ${result.confidence}${result.unrecoverable ? " (UNRECOVERABLE)" : ""}${result.parseFailed ? " \u2014 parse failed" : ""}`
|
|
54
|
+
);
|
|
55
|
+
console.error(`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-C6D5YFCA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/rollback-suggest.ts"],"sourcesContent":["/**\n * `ddt 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 `ddt 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 `sdt rollback-suggest`.\n */\nimport { promises as fs } from 'node:fs';\nimport { Command } from 'commander';\nimport { ai, aiRollback } from '@ddt-tools/core';\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(\n '--object-type <type>',\n 'Optional object type for context (e.g. MANAGED_TABLE, STREAMING_TABLE).',\n )\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 console.error(\n `Confidence: ${result.confidence}${result.unrecoverable ? ' (UNRECOVERABLE)' : ''}${result.parseFailed ? ' — parse failed' : ''}`,\n );\n console.error(`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;AAExB,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;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;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,YAAQ;AAAA,MACN,eAAe,OAAO,UAAU,GAAG,OAAO,gBAAgB,qBAAqB,EAAE,GAAG,OAAO,cAAc,yBAAoB,EAAE;AAAA,IACjI;AACA,YAAQ,MAAM,cAAc,OAAO,SAAS,EAAE;AAC9C,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":[]}
|