@voltro/cli 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/apiBuild-BWo8KI6n.js +2 -0
  3. package/dist/{apiBuild-Dz8TJSxp.js → apiBuild-CFYXo06U.js} +1 -1
  4. package/dist/bin.js +1 -1
  5. package/dist/{build-DLme2ppa.js → build-CnSH-zru.js} +6 -6
  6. package/dist/{checkCommand-Dals7g-I.js → checkCommand-Do3SQbLx.js} +3 -3
  7. package/dist/{checkCommand-BUBqPBlI.js → checkCommand-OL41BMIb.js} +1 -1
  8. package/dist/{codegenCommand-L6vacVRW.js → codegenCommand-BJzaRz6q.js} +39 -47
  9. package/dist/{codemodRunner-DSZjgyCL.js → codemodRunner-De3k0pRd.js} +859 -629
  10. package/dist/{commands-CAxFLqG4.js → commands-By23Ljny.js} +68 -60
  11. package/dist/{dashboardCommand-CJC8Kg-s.js → dashboardCommand-DN1rAABE.js} +1 -1
  12. package/dist/dataCommand-B93TUBIx.js +1306 -0
  13. package/dist/dataProfile-Du3ztYxv.js +51 -0
  14. package/dist/{dbCommand-ZGNItjoJ.js → dbCommand-Bf7Cy0mn.js} +2 -2
  15. package/dist/dbCommand-Bnl7MSGC.js +2 -0
  16. package/dist/{dev-lvnVq5RD.js → dev-1yu42ubu.js} +2 -2
  17. package/dist/{dev-cGGsLNn6.js → dev-BWwE0QjK.js} +1 -1
  18. package/dist/doctorCommand-BmovAwb7.js +2 -0
  19. package/dist/{doctorCommand-BK0qu7eD.js → doctorCommand-y-EbP8st.js} +12 -12
  20. package/dist/{dormancyCommand-CyIk-zq4.js → dormancyCommand-Dc7KRNCG.js} +1 -1
  21. package/dist/{embeddingsCommand-D7TLklps.js → embeddingsCommand-kPs132bB.js} +1 -1
  22. package/dist/{envCommand-BHxKkGXq.js → envCommand-E12zSt51.js} +1 -1
  23. package/dist/{evolveCommand-CQh6dw21.js → evolveCommand-B3nagPVc.js} +2 -2
  24. package/dist/{frameworkTableAssembly-BNod_DKN.js → frameworkTableAssembly-BwJVEKLr.js} +3 -3
  25. package/dist/frameworkTableAssembly-CVDB2hCq.js +2 -0
  26. package/dist/index.js +1 -1
  27. package/dist/{infoCommand-CuNl9cbh.js → infoCommand-Cq0Yz98u.js} +1 -1
  28. package/dist/{interruptedReplace-C1ZI84xi.js → interruptedReplace-C3O3M1MM.js} +2 -1
  29. package/dist/interruptedReplace-CvmiAM9K.js +2 -0
  30. package/dist/{migrate-CL5Ed01M.js → migrate-D3MK9BpK.js} +2 -2
  31. package/dist/{runtimeTrace-C0SlIoQY.js → runtimeTrace-BpLUcrfy.js} +1 -1
  32. package/dist/{sdkgen-BXm7zx7F.js → sdkgen-dl4-WKF6.js} +1 -1
  33. package/dist/serveCommand-8CTzQCY0.js +2362 -0
  34. package/dist/serveCommand-C_WI4TTG.js +2 -0
  35. package/dist/serveEntry.js +5 -5
  36. package/dist/{subcommandNames-CKG5Dz3a.js → subcommandNames-DpYs3DXr.js} +12 -2
  37. package/dist/updateCommand-D91-TZ7E.js +2 -0
  38. package/dist/{updateCommand-BlyXavoG.js → updateCommand-i8JbrLAA.js} +2 -1
  39. package/dist/{webhooksCommand-QlAk_tED.js → webhooksCommand-B0bdyuQ4.js} +1 -1
  40. package/package.json +47 -17
  41. package/templates/AGENTS.md +1 -1
  42. package/templates/agent-docs/_index.md +1 -1
  43. package/templates/agent-docs/cli.md +124 -16
  44. package/templates/agent-docs/whats-new.md +30 -52
  45. package/templates/apps/api-ai/package.json +7 -7
  46. package/templates/apps/api-auth/package.json +8 -8
  47. package/templates/apps/api-backend/package.json +7 -7
  48. package/templates/apps/api-backend-deactivation/package.json +7 -7
  49. package/templates/apps/api-backend-mail/package.json +8 -8
  50. package/templates/apps/api-backend-mariadb/package.json +9 -9
  51. package/templates/apps/api-backend-sqlite/package.json +8 -8
  52. package/templates/apps/api-backend-storage/package.json +8 -8
  53. package/templates/apps/api-cms/package.json +10 -10
  54. package/templates/apps/api-collab/package.json +8 -8
  55. package/templates/apps/api-data-advanced/package.json +8 -8
  56. package/templates/apps/api-durable/package.json +8 -8
  57. package/templates/apps/api-feature-flags/package.json +9 -9
  58. package/templates/apps/api-governance/package.json +8 -8
  59. package/templates/apps/api-kv/package.json +8 -8
  60. package/templates/apps/api-moderation/package.json +8 -8
  61. package/templates/apps/api-observability/package.json +8 -8
  62. package/templates/apps/api-ratelimit/package.json +8 -8
  63. package/templates/apps/api-rbac/package.json +8 -8
  64. package/templates/apps/api-rest/package.json +7 -7
  65. package/templates/apps/api-saas/package.json +11 -11
  66. package/templates/apps/api-saas-starter/package.json +10 -10
  67. package/templates/apps/api-search/package.json +8 -8
  68. package/templates/apps/api-status/package.json +8 -8
  69. package/templates/apps/api-versioning/package.json +8 -8
  70. package/templates/apps/api-webhooks/package.json +9 -9
  71. package/templates/apps/changelog/package.json +6 -6
  72. package/templates/apps/edge-functions/package.json +2 -2
  73. package/templates/apps/frontend-admin/package.json +8 -8
  74. package/templates/apps/frontend-app/package.json +9 -9
  75. package/templates/apps/frontend-auth/package.json +8 -8
  76. package/templates/apps/frontend-blank/package.json +7 -7
  77. package/templates/apps/frontend-cms/package.json +9 -9
  78. package/templates/apps/frontend-collab/package.json +10 -10
  79. package/templates/apps/frontend-contact/package.json +7 -7
  80. package/templates/apps/frontend-dashboard/package.json +7 -7
  81. package/templates/apps/frontend-docs/package.json +7 -7
  82. package/templates/apps/frontend-i18n/package.json +6 -6
  83. package/templates/apps/frontend-landing/package.json +7 -7
  84. package/templates/apps/frontend-portal/package.json +8 -8
  85. package/templates/apps/frontend-saas/package.json +8 -8
  86. package/templates/apps/frontend-spa/package.json +7 -7
  87. package/templates/apps/frontend-ssr/package.json +7 -7
  88. package/templates/apps/frontend-ssr-api/package.json +8 -8
  89. package/templates/apps/frontend-static-blog/package.json +6 -6
  90. package/templates/apps/frontend-status/package.json +8 -8
  91. package/templates/apps/mobile-app/package.json +4 -4
  92. package/dist/apiBuild-DOnvi2zm.js +0 -2
  93. package/dist/dataCommand-hIaq2iKY.js +0 -1052
  94. package/dist/dataProfile-Cm0YVKSy.js +0 -18
  95. package/dist/dbCommand-D1Q33ktl.js +0 -2
  96. package/dist/doctorCommand-CWgUPye-.js +0 -2
  97. package/dist/frameworkTableAssembly-Rft-DPUg.js +0 -2
  98. package/dist/interruptedReplace-DNJ7MKph.js +0 -2
  99. package/dist/serveCommand-CV3YO9L3.js +0 -2035
  100. package/dist/serveCommand-ZP_Cb-yz.js +0 -2
  101. package/dist/updateCommand-CT5AvVg7.js +0 -2
@@ -0,0 +1,1306 @@
1
+ import { i as e, t } from "./subcommandNames-DpYs3DXr.js";
2
+ import { t as n } from "./cliError-BmdYnghb.js";
3
+ import { c as r, i, o as a, z as o } from "./dev-1yu42ubu.js";
4
+ import { n as s, t as c } from "./cliArgs-D4p8n7EE.js";
5
+ import { r as l } from "./dialectDriver-czCHYpeH.js";
6
+ import { i as u, n as d, r as f, t as p } from "./dataProfile-Du3ztYxv.js";
7
+ import { o as m, t as h } from "./inspectFetch-EMuhTG_9.js";
8
+ import { r as g } from "./dbCommand-Bf7Cy0mn.js";
9
+ import { resolve as _ } from "node:path";
10
+ import { createLogger as v } from "@voltro/logger";
11
+ import { DATA_TRANSFERS_TABLE as y, REPLACE_IN_PROGRESS_TABLE as b, _voltroDataTransfersTable as x, _voltroReplaceInProgressTable as S, connectionIdentity as C, derivedRowId as ee, ensureTableRegistered as te, normaliseTarget as ne, sameDatabase as re, streamTable as ie } from "@voltro/database";
12
+ import { Cause as w, Effect as T, Exit as ae, Option as oe, Stream as se } from "effect";
13
+ import { SqlClient as ce } from "@effect/sql";
14
+ import { declaredSnapshot as le, fingerprintSchema as ue, introspectSchema as de } from "@voltro/database/sql";
15
+ import { STORAGE_REFS_TABLE as fe, resolveStorageProvider as E } from "@voltro/plugin-storage";
16
+ import { createHash as pe } from "node:crypto";
17
+ import { ASSETS_DIR as D, ASSET_INDEX_FILE as O, BundleError as me, asImportError as he, assessLiveImport as ge, backupArtifactName as k, backupCommand as _e, captureAssetsToCas as ve, classificationFromSnapshot as ye, decryptStream as A, encryptStream as be, packBundle as xe, peekBundle as j, previewMasking as Se, restoreAssetsFromCas as Ce, restoreCommand as we, runExport as Te, runImport as Ee, runNativeStep as M, storageAssetSink as De, storageAssetSource as Oe, storageBlobSink as ke, storageBlobSource as Ae, unpackBundle as je, unpackBundleToDir as Me } from "@voltro/data-transfer";
18
+ import { request as Ne } from "node:http";
19
+ import { request as Pe } from "node:https";
20
+ //#region src/longRequest.ts
21
+ var Fe = /* @__PURE__ */ new Set([
22
+ "ECONNREFUSED",
23
+ "ETIMEDOUT",
24
+ "EHOSTUNREACH",
25
+ "ENETUNREACH",
26
+ "ENOTFOUND",
27
+ "ECONNRESET",
28
+ "EAI_AGAIN"
29
+ ]), N = class extends Error {
30
+ url;
31
+ cause;
32
+ constructor(e, t) {
33
+ let n = (() => {
34
+ try {
35
+ return new URL(e).host;
36
+ } catch {
37
+ return e;
38
+ }
39
+ })();
40
+ super(`the instance at ${n} is not answering (${t.code ?? "connect failed"}).`), this.url = e, this.cause = t, this.name = "InstanceUnreachable";
41
+ }
42
+ }, P = class extends Error {
43
+ elapsedMs;
44
+ url;
45
+ constructor(e, t) {
46
+ super(`no response after ${Math.round(e / 1e3)}s`), this.elapsedMs = e, this.url = t, this.name = "LongRequestTimeout";
47
+ }
48
+ }, F = async (e, t, n, r) => {
49
+ let i = new URL(e), a = i.protocol === "https:" ? Pe : Ne, o = Date.now();
50
+ return await new Promise((s, c) => {
51
+ let l = a({
52
+ protocol: i.protocol,
53
+ hostname: i.hostname,
54
+ port: i.port === "" ? void 0 : Number(i.port),
55
+ path: `${i.pathname}${i.search}`,
56
+ method: "POST",
57
+ headers: n
58
+ }, (e) => {
59
+ let t = [];
60
+ e.on("data", (e) => t.push(e)), e.on("end", () => s({
61
+ status: e.statusCode ?? 0,
62
+ headers: Object.fromEntries(Object.entries(e.headers).map(([e, t]) => [e, Array.isArray(t) ? t.join(", ") : String(t ?? "")])),
63
+ body: Buffer.concat(t)
64
+ })), e.on("error", c);
65
+ });
66
+ if (r?.timeoutMs !== void 0 && r.timeoutMs > 0 && l.setTimeout(r.timeoutMs, () => {
67
+ l.destroy(new P(Date.now() - o, e));
68
+ }), l.on("error", (t) => c(Fe.has(t.code ?? "") ? new N(e, t) : t)), typeof t == "string" || Buffer.isBuffer(t)) {
69
+ l.end(t);
70
+ return;
71
+ }
72
+ (async () => {
73
+ try {
74
+ for await (let e of t) l.write(Buffer.from(e)) || await new Promise((e) => l.once("drain", () => e()));
75
+ l.end();
76
+ } catch (e) {
77
+ l.destroy(e);
78
+ }
79
+ })();
80
+ });
81
+ }, I = (e) => `${e.message}\n That address came from --api-url (or VOLTRO_API_URL) — it is the running instance, not the database.\n Nothing was sent, so nothing was imported. Check the instance is up and reachable from here\n (a VPN, an ingress or a stopped deployment are the usual three), e.g. curl -sS ${e.url}/_voltro/inspect/app`, Ie = (e, t) => `the instance did not answer within ${Math.round(e.elapsedMs / 1e3)}s. This is a client-side deadline, not a failure on the server: the upload itself finished long before, and the response arrives only when the import does. The instance is very probably STILL IMPORTING and will finish.\n` + (t === "replace" ? " Do NOT simply re-run: --mode replace empties the target first, so a second run started while the first is mid-flight would empty it under the first one. Check the instance's log for the import's completion, and check the target's row counts, before deciding." : ` Re-running the same bundle is safe once the first run has finished (--mode ${t} is idempotent), but check the instance's log first — the work may already be done.`) + "\n To keep a deadline anyway, pass --timeout <seconds>; the default is to wait.", Le = 16777216, Re = async function* (e, t) {
82
+ let n = [], r = 0, i = 0, a = function* (e) {
83
+ let t = Buffer.concat(n);
84
+ n = [], r = 0;
85
+ let a = {
86
+ bytes: t,
87
+ offset: i,
88
+ final: e
89
+ };
90
+ i += t.byteLength, yield a;
91
+ };
92
+ for await (let a of e) for (n.push(Buffer.from(a)), r += a.byteLength; r >= t;) {
93
+ let e = Buffer.concat(n), a = e.subarray(0, t), o = e.subarray(t);
94
+ n = o.byteLength > 0 ? [Buffer.from(o)] : [], r = o.byteLength;
95
+ let s = {
96
+ bytes: Buffer.from(a),
97
+ offset: i,
98
+ final: !1
99
+ };
100
+ i += a.byteLength, yield s;
101
+ }
102
+ yield* a(!0);
103
+ }, ze = (e) => pe("sha256").update(Buffer.from(e)).digest("hex"), Be = async (e) => {
104
+ let t = 0, n = 0, r = {
105
+ status: 0,
106
+ body: {}
107
+ };
108
+ for await (let i of Re(e.stream, e.chunkBytes)) {
109
+ if (i.offset + i.bytes.byteLength <= e.alreadyHeld && !i.final) {
110
+ n++;
111
+ continue;
112
+ }
113
+ let a = Math.max(0, e.alreadyHeld - i.offset), o = a > 0 ? i.bytes.subarray(a) : i.bytes, s = i.offset + a;
114
+ if (o.byteLength === 0 && !i.final) {
115
+ n++;
116
+ continue;
117
+ }
118
+ if (i.final && e.onFinalChunk?.(), r = await e.target.send(Buffer.from(o), {
119
+ ...e.baseHeaders,
120
+ "content-type": "application/octet-stream",
121
+ "x-import-upload-id": e.uploadId,
122
+ "x-import-upload-key": e.key,
123
+ "x-import-chunk-offset": String(s),
124
+ ...i.final ? { "x-import-chunk-final": "1" } : {}
125
+ }), n++, t += o.byteLength, e.onProgress?.(t), r.status >= 400) break;
126
+ }
127
+ return {
128
+ status: r.status,
129
+ body: r.body,
130
+ chunks: n,
131
+ bytes: t,
132
+ resumedFrom: e.alreadyHeld
133
+ };
134
+ }, L = "voltro-backup-stamp.json", Ve = (e) => ({
135
+ stampVersion: 1,
136
+ dialect: e.dialect,
137
+ schemaFingerprint: e.schemaFingerprint,
138
+ voltroVersion: e.voltroVersion,
139
+ createdAt: e.createdAt,
140
+ assets: e.assets ?? null
141
+ }), He = (e) => {
142
+ let t;
143
+ try {
144
+ t = JSON.parse(e);
145
+ } catch {
146
+ return null;
147
+ }
148
+ if (typeof t != "object" || !t) return null;
149
+ let n = t;
150
+ if (n.stampVersion !== 1 || typeof n.dialect != "string" || typeof n.voltroVersion != "string" || typeof n.createdAt != "string") return null;
151
+ let r = n.schemaFingerprint;
152
+ if (r !== null && typeof r != "string") return null;
153
+ let i = n.assets, a = typeof i == "object" && i && typeof i.count == "number" && typeof i.totalBytes == "number" ? {
154
+ count: i.count,
155
+ totalBytes: i.totalBytes
156
+ } : null;
157
+ return {
158
+ stampVersion: 1,
159
+ dialect: n.dialect,
160
+ schemaFingerprint: r,
161
+ voltroVersion: n.voltroVersion,
162
+ createdAt: n.createdAt,
163
+ assets: a
164
+ };
165
+ }, Ue = (e) => {
166
+ let { stamp: t, currentDialect: n, currentFingerprint: r } = e;
167
+ if (t === null) return {
168
+ warn: `no ${L} beside the dump — an older/handmade backup. Cannot verify dialect or schema version; make sure this dump is a ${n} dump.`,
169
+ info: []
170
+ };
171
+ let i = [`backup produced by @voltro/cli ${t.voltroVersion} at ${t.createdAt}`, `schema fingerprint: ${t.schemaFingerprint ?? "<none recorded>"}`];
172
+ return t.dialect === n ? t.schemaFingerprint !== null && r !== null && t.schemaFingerprint !== r ? {
173
+ warn: `schema skew — the backup's schema (${t.schemaFingerprint.slice(0, 12)}) differs from what this code declares (${r.slice(0, 12)}). Run \`voltro db apply\` after the restore, or the app may boot against a schema it doesn't understand.`,
174
+ info: i
175
+ } : { info: i } : {
176
+ refuse: `dialect mismatch — backup is a ${t.dialect} dump, but DB_DIALECT is ${n}. A cross-dialect restore is never valid.`,
177
+ info: i
178
+ };
179
+ }, We = async (e, t) => {
180
+ let n = ee("dimp", `${t.direction}-${t.artifact}-${Date.now()}-${Math.random()}`);
181
+ try {
182
+ return te(x), await e.insert(y, {
183
+ id: n,
184
+ direction: t.direction,
185
+ startedAt: /* @__PURE__ */ new Date(),
186
+ finishedAt: null,
187
+ failure: null,
188
+ mode: `native ${t.dialect}${t.assets ? " + assets" : ""}`,
189
+ via: "cli",
190
+ bundle: t.artifact,
191
+ sourceFingerprint: null,
192
+ tablesWritten: 0,
193
+ rowsWritten: 0,
194
+ staged: null
195
+ }), {
196
+ runId: n,
197
+ recorded: !0
198
+ };
199
+ } catch {
200
+ return {
201
+ runId: n,
202
+ recorded: !1
203
+ };
204
+ }
205
+ }, R = async (e, t, n) => {
206
+ t.recorded && await e.update(y, t.runId, {
207
+ finishedAt: /* @__PURE__ */ new Date(),
208
+ failure: n.failure ?? null,
209
+ ...n.assets === void 0 ? {} : { rowsWritten: n.assets }
210
+ }).then(() => void 0, () => void 0);
211
+ }, Ge = async (e, t) => {
212
+ let n = ee("repl", `restore-${t.artifact}-${Date.now()}-${Math.random()}`);
213
+ try {
214
+ te(S), await e.insert(b, {
215
+ id: n,
216
+ startedAt: /* @__PURE__ */ new Date(),
217
+ kind: "restore",
218
+ tables: null,
219
+ capture: t.artifact,
220
+ via: "cli"
221
+ });
222
+ } catch {
223
+ return;
224
+ }
225
+ return async () => {
226
+ await e.deleteMany(b, { where: {
227
+ column: "id",
228
+ op: "eq",
229
+ value: n
230
+ } }).then(() => void 0, () => void 0);
231
+ };
232
+ }, Ke = async (e) => {
233
+ let t = await T.runPromiseExit(e);
234
+ if (ae.isSuccess(t)) return;
235
+ let n = w.failureOption(t.cause);
236
+ if (oe.isSome(n)) {
237
+ let e = n.value;
238
+ return String(e?.message ?? e);
239
+ }
240
+ return w.pretty(t.cause);
241
+ }, z = (e) => {
242
+ try {
243
+ let t = new URL(e), n = t.pathname.replace(/^\//, "");
244
+ return `${t.hostname.toLowerCase()}:${t.port || "(default)"}/${n}`;
245
+ } catch {
246
+ return e.trim();
247
+ }
248
+ }, qe = (e) => {
249
+ let t = e.flagUrl ?? e.envUrl;
250
+ if (t === void 0 || t.trim() === "") return {
251
+ ok: !1,
252
+ error: "no drill target — pass `--drill-url <url>` or set `DRILL_DB_URL`. The drill restores into a THROWAWAY database (never the live one), so it needs its own connection."
253
+ };
254
+ let n = z(t);
255
+ return e.liveKeys.some((e) => e === n) ? {
256
+ ok: !1,
257
+ error: `drill target (${n}) is the LIVE database — a restore drill must target a throwaway DB, never production. It would --clean and overwrite it.`
258
+ } : {
259
+ ok: !0,
260
+ url: t
261
+ };
262
+ }, Je = (e) => {
263
+ let { stampFingerprint: t, restoredFingerprint: n, restoredTableCount: r } = e;
264
+ return r === 0 ? {
265
+ pass: !1,
266
+ lines: ["FAIL — the restore produced ZERO tables. The dump is empty or unreadable; this backup would NOT recover your app."]
267
+ } : t !== null && n !== null && t !== n ? {
268
+ pass: !1,
269
+ lines: [`FAIL — restored ${r} table(s), but the schema fingerprint (${n.slice(0, 12)}) does NOT match the backup's stamp (${t.slice(0, 12)}).`, "The restore did not reproduce the schema that was backed up — the artifact is inconsistent."]
270
+ } : t === null || n === null ? {
271
+ pass: !0,
272
+ lines: [`PASS (partial) — restored ${r} table(s) into the throwaway DB.`, "No stamped fingerprint to cross-check the schema shape (older/hand-made backup) — the bytes restore, but the shape was not verified."]
273
+ } : {
274
+ pass: !0,
275
+ lines: [`PASS — restored ${r} table(s); schema fingerprint matches the backup stamp (${n.slice(0, 12)}).`]
276
+ };
277
+ }, B = v({ scope: "voltro:data" }), Ye = async () => {
278
+ try {
279
+ let { createRequire: e } = await import("node:module"), { join: t } = await import("node:path"), { readFileSync: n } = await import("node:fs"), r = e(t(process.cwd(), "package.json")).resolve("@voltro/cli/package.json");
280
+ return JSON.parse(n(r, "utf8")).version ?? "unknown";
281
+ } catch {
282
+ return "unknown";
283
+ }
284
+ }, Xe = async (e) => {
285
+ try {
286
+ return ue(await tt(e));
287
+ } catch {
288
+ return null;
289
+ }
290
+ }, V = (e) => {
291
+ if (e.event !== "done") return;
292
+ let t = e.total === void 0 ? "" : ` / ${e.total}`;
293
+ B.info(`${e.phase} [${e.index + 1}/${e.tableCount}] ${e.table}`, { rows: `${e.rowsDone}${t}` });
294
+ }, Ze = async (e) => {
295
+ let t = (e) => e?.meta?.databaseId;
296
+ if (e) {
297
+ let n = await h({
298
+ name: "target",
299
+ url: e
300
+ }, "/_voltro/inspect/app");
301
+ if (n.kind === "ok") return {
302
+ label: e,
303
+ databaseId: t(n.data)
304
+ };
305
+ }
306
+ for (let e of await m()) {
307
+ let n = await h(e, "/_voltro/inspect/app");
308
+ if (n.kind === "ok") return {
309
+ label: `${e.name} (${e.url})`,
310
+ databaseId: t(n.data)
311
+ };
312
+ }
313
+ return null;
314
+ }, Qe = async (e) => {
315
+ let t = e.includes("--allow-live"), n = t ? null : await Ze(H(e, "--api-url")), r = C({
316
+ dialect: W(),
317
+ ...a()
318
+ }), i = ne({
319
+ dialect: W(),
320
+ ...a()
321
+ }), { refuse: o, message: s } = ge({
322
+ liveInstance: n?.label ?? null,
323
+ allowLive: t,
324
+ sameTarget: re(r, n?.databaseId),
325
+ ...i === void 0 ? {} : { targetLabel: `${i.host}:${i.port}/${i.database}` }
326
+ });
327
+ return !o || (B.error(s ?? "refusing: live instance detected"), !1);
328
+ }, H = s, $e = [
329
+ "--api-url",
330
+ "--bundle-key",
331
+ "--compression",
332
+ "--consistency",
333
+ "--drill-url",
334
+ "--chunk-size",
335
+ "--exclude",
336
+ "--limit",
337
+ "--max-request-seconds",
338
+ "--mode",
339
+ "--no-atomic",
340
+ "--on-conflict",
341
+ "--passphrase",
342
+ "--profile",
343
+ "--tables",
344
+ "--target",
345
+ "--timeout",
346
+ "--tenant",
347
+ "--token"
348
+ ], U = (e) => c(e, $e), W = () => {
349
+ let e = (process.env.DB_DIALECT ?? "postgres").toLowerCase();
350
+ if (e === "memory") throw new n("voltro data: requires a real dialect, not DB_DIALECT=memory.");
351
+ return e;
352
+ }, et = async (e, t) => {
353
+ let n = (await l(e)).makeSqlLayer(t);
354
+ return T.runPromise(ce.SqlClient.pipe(T.flatMap(de), T.provide(n)));
355
+ }, tt = (e) => et(e, a()), nt = () => {
356
+ let e = a(), t = [];
357
+ return e.url && t.push(z(e.url)), (e.host || e.database) && t.push(`${(e.host ?? "localhost").toLowerCase()}:${e.port ?? "(default)"}/${e.database ?? ""}`), t;
358
+ }, G = async () => {
359
+ let e = process.cwd(), t = await r(e);
360
+ if (!t) throw new n("voltro data: no app.config.ts found in the current directory.");
361
+ let a = await i(t, void 0), o = W();
362
+ return {
363
+ store: a,
364
+ snapshot: await tt(o),
365
+ dialect: o
366
+ };
367
+ }, K = (e) => ie(e, {
368
+ table: fe,
369
+ chunkSize: 500
370
+ }).pipe(se.map((e) => ({
371
+ key: String(e.key),
372
+ contentType: String(e.contentType ?? "application/octet-stream"),
373
+ size: Number(e.size ?? 0)
374
+ }))), q = (e) => !!e && e.endsWith(".vbundle"), J = (e) => H(e, "--passphrase") ?? process.env.VOLTRO_BUNDLE_KEY, rt = (e) => e.includes("--encrypt") || H(e, "--passphrase") !== void 0 || process.env.VOLTRO_BUNDLE_KEY !== void 0, it = async (e, t) => {
375
+ let { createWriteStream: n } = await import("node:fs"), { pipeline: r } = await import("node:stream/promises"), { Readable: i } = await import("node:stream");
376
+ await r(i.from(e), n(t));
377
+ }, Y = (e) => ({ async *[Symbol.asyncIterator]() {
378
+ let { createReadStream: t } = await import("node:fs");
379
+ for await (let n of t(e)) yield n instanceof Uint8Array ? n : new Uint8Array(n);
380
+ } }), at = async (e, t) => {
381
+ if (t !== void 0 || e === void 0) return null;
382
+ try {
383
+ let { resolve: t, join: n } = await import("node:path"), r = await import("node:fs/promises"), i = t(process.cwd(), e), a = q(e) ? (await j(Y(i), ["manifest.json"])).get("manifest.json") : await r.readFile(n(i, "manifest.json"));
384
+ if (a === void 0) return null;
385
+ let o = (JSON.parse(Buffer.from(a).toString("utf8")).tables ?? []).map((e) => e.name).filter((e) => typeof e == "string");
386
+ return o.length > 0 ? o : null;
387
+ } catch {
388
+ return null;
389
+ }
390
+ }, ot = (e) => {
391
+ let t = H(e, "--max-request-seconds") ?? process.env.VOLTRO_MAX_REQUEST_SECONDS;
392
+ if (t === void 0) return { pollMs: 2e3 };
393
+ let r = Number(t);
394
+ if (!Number.isFinite(r) || r <= 0) throw new n(`--max-request-seconds expects a positive number of seconds, got '${t}'`);
395
+ let i = Math.round(r * 1e3);
396
+ return {
397
+ perRequest: { timeoutMs: i },
398
+ pollMs: Math.max(500, Math.min(2e3, Math.floor(i / 2)))
399
+ };
400
+ }, st = async (e, t, n) => {
401
+ let r = await fetch(`${e.replace(/\/+$/, "")}/_voltro/admin/transfers?limit=50`, { headers: { authorization: `Bearer ${t}` } }).catch(() => null);
402
+ if (r === null || r.status !== 200) return null;
403
+ let i = ((await r.json().catch(() => null))?.runs ?? []).find((e) => e.id === n);
404
+ return i === void 0 ? null : {
405
+ ...i,
406
+ startedAt: new Date(String(i.startedAt)),
407
+ finishedAt: i.finishedAt === null || i.finishedAt === void 0 ? null : new Date(String(i.finishedAt))
408
+ };
409
+ }, ct = async (e) => {
410
+ let t = e.apiUrl.replace(/\/+$/, ""), n = [], r = 0, i;
411
+ for (;;) {
412
+ let a = `${t}/_voltro/admin/export/download?runId=${encodeURIComponent(e.runId)}&offset=${r}`, o = new AbortController(), s = e.perRequest === void 0 ? void 0 : setTimeout(() => {
413
+ o.abort();
414
+ }, e.perRequest.timeoutMs), c;
415
+ try {
416
+ c = await fetch(a, {
417
+ headers: { authorization: `Bearer ${e.secret}` },
418
+ signal: o.signal
419
+ });
420
+ } catch (t) {
421
+ return e.log.error(`api export: fetching bytes at offset ${r} failed — ${t?.message ?? String(t)}. The bundle is still on the instance; re-run to resume from here.`), null;
422
+ } finally {
423
+ s !== void 0 && clearTimeout(s);
424
+ }
425
+ if (c.status !== 200) return e.log.error(`api export: the instance answered HTTP ${c.status} for the bytes at offset ${r} — ${await c.text()}`), null;
426
+ let l = Buffer.from(await c.arrayBuffer());
427
+ if (i ??= Number(c.headers.get("x-export-total-bytes") ?? "0"), n.push(l), r += l.byteLength, l.byteLength === 0 || i !== void 0 && r >= i) break;
428
+ e.log.info(` downloading — ${r}/${i ?? "?"} bytes`);
429
+ }
430
+ return new Uint8Array(Buffer.concat(n));
431
+ }, X = "transfers", lt = (e) => (e ?? "").split(",").map((e) => e.trim()).filter(Boolean), ut = (e, t) => {
432
+ let r = H(e, "--tenant"), i = H(e, "--tables"), a = H(e, "--exclude");
433
+ if (a !== void 0) {
434
+ if (i !== void 0) throw new n("voltro data export: --exclude and --tables are two ways to say the same thing; use one.");
435
+ if (r !== void 0) throw new n("voltro data export: --exclude does not apply to --tenant, whose table set is derived from the tenant closure.");
436
+ let e = lt(a), o = new Set(t), s = e.filter((e) => !o.has(e));
437
+ if (s.length > 0) throw new n(`voltro data export: --exclude names ${s.length} table(s) that do not exist: ${s.join(", ")}. An exclusion that excludes nothing would leave the run looking like it worked.`);
438
+ return {
439
+ kind: "all",
440
+ exclude: e
441
+ };
442
+ }
443
+ return r ? {
444
+ kind: "tenant",
445
+ tenantId: r
446
+ } : i ? {
447
+ kind: "tables",
448
+ tables: lt(i)
449
+ } : { kind: "all" };
450
+ }, dt = async () => {
451
+ let e = await g(process.cwd());
452
+ return ye(le(e));
453
+ }, ft = "voltro/tenant", pt = async () => {
454
+ let e = (await g(process.cwd())).filter((e) => (e.appliedMixins ?? []).some((e) => e.id === ft)).map((e) => e.tableName);
455
+ return e.length > 0 ? e : void 0;
456
+ }, mt = (e) => {
457
+ for (let t of e.tables) if (t.samples.length !== 0) {
458
+ console.log(`\n ${t.table}`);
459
+ for (let e of t.samples) console.log(` ${e.column} [${e.action}] ${JSON.stringify(e.before)} → ${JSON.stringify(e.after)}`);
460
+ }
461
+ if (e.leaks.length > 0) {
462
+ console.log("\n ⚠ possible leaks in KEPT columns:");
463
+ for (let t of e.leaks) console.log(` ${t.table}.${t.column} looks like ${t.pattern}: ${JSON.stringify(t.sample)}`);
464
+ }
465
+ if (e.unclassified.length > 0) {
466
+ console.log("\n ✗ unclassified (would BLOCK a real export — add .sensitive()/.safe() or a policy override):");
467
+ for (let t of e.unclassified) console.log(` ${t}`);
468
+ } else console.log("\n ✓ every exported column is classified — safe to run.");
469
+ }, ht = async (e) => {
470
+ let { resolve: t, join: r } = await import("node:path"), i = await import("node:fs/promises"), a = H(e.args, "--chunk-size"), o = a === void 0 ? Le : Math.round(Number(a) * 1024 * 1024);
471
+ if (!Number.isFinite(o) || o <= 0) throw new n(`--chunk-size expects a positive number of megabytes, got '${String(a)}'`);
472
+ let s = e.stream[Symbol.asyncIterator](), c = [], l = 0, u = !1;
473
+ for (; l <= o;) {
474
+ let e = await s.next();
475
+ if (e.done === !0) {
476
+ u = !0;
477
+ break;
478
+ }
479
+ c.push(Buffer.from(e.value)), l += e.value.byteLength;
480
+ }
481
+ if (u && l <= o) return {
482
+ kind: "small",
483
+ body: Buffer.concat(c)
484
+ };
485
+ let d = t(process.cwd(), e.bundleDir), f = ze(q(e.bundleDir) ? await i.readFile(d) : await i.readFile(r(d, "manifest.json"))), p = `up-${f.slice(0, 32)}`, m = await F(e.url, Buffer.alloc(0), {
486
+ ...e.headers,
487
+ "x-import-upload-id": p,
488
+ "x-import-probe": "1"
489
+ }, Q(e.args)), h = m.status === 200 ? Number(JSON.parse(m.body.toString("utf8")).bytes ?? 0) : 0;
490
+ h > 0 && e.log.info("api import: resuming a chunked upload the instance already holds", { bytes: h });
491
+ let g = (async function* () {
492
+ for (let e of c) yield e;
493
+ for (;;) {
494
+ let e = await s.next();
495
+ if (e.done === !0) return;
496
+ yield e.value;
497
+ }
498
+ })(), _ = 0, v = await Be({
499
+ stream: g,
500
+ chunkBytes: o,
501
+ uploadId: p,
502
+ key: f,
503
+ alreadyHeld: h,
504
+ baseHeaders: e.headers,
505
+ onProgress: (t) => {
506
+ _++, e.log.info(`api import: uploading — chunk ${_}, ${Math.round(t / 1024)} KiB sent`);
507
+ },
508
+ onFinalChunk: () => {
509
+ e.log.info("api import: upload done — the instance is importing now. This request stays open until it finishes, which on a full bundle is minutes, and nothing more is printed until then. It is not a hang; the instance logs the import itself.");
510
+ },
511
+ target: { send: async (t, n) => {
512
+ let r = await F(e.url, t, n, Q(e.args)), i = {};
513
+ try {
514
+ i = JSON.parse(r.body.toString("utf8") || "{}");
515
+ } catch {}
516
+ return {
517
+ status: r.status,
518
+ body: i
519
+ };
520
+ } }
521
+ });
522
+ return e.log.info("api import: uploaded in chunks", {
523
+ chunks: v.chunks,
524
+ bytes: v.bytes,
525
+ ...v.resumedFrom > 0 ? { resumedFrom: v.resumedFrom } : {}
526
+ }), {
527
+ kind: "sent",
528
+ res: {
529
+ status: v.status,
530
+ headers: {},
531
+ body: Buffer.from(JSON.stringify(v.body))
532
+ }
533
+ };
534
+ }, gt = (e) => `--mode replace with --no-atomic against a ${e ? "RUNNING instance" : "live database"}: the target is empty between the delete and the load, and anything the application writes in that window can collide with a row from the bundle — or survive the import as a row the bundle never carried. Drop --no-atomic to run the whole load in one transaction (concurrent writers then wait instead of racing), or stop the traffic for the duration.`, Z = (e, t) => e.includes("--no-atomic") ? !1 : e.includes("--atomic") || t === "replace", _t = (e, t, n) => {
535
+ if (t !== "replace" || e.includes("--no-rollback")) return;
536
+ let r = H(e, "--rollback-dir");
537
+ return r === void 0 ? o(n) : _(r);
538
+ }, vt = (e, t, n) => {
539
+ let r = _t(e, t, n);
540
+ return {
541
+ ...r === void 0 ? {} : { rollbackDir: r },
542
+ recordInterruption: !0,
543
+ via: "direct"
544
+ };
545
+ }, Q = (e) => {
546
+ let t = H(e, "--timeout");
547
+ if (t === void 0) return {};
548
+ let r = Number(t);
549
+ if (!Number.isFinite(r) || r <= 0) throw new n(`--timeout expects a positive number of seconds, got '${t}'`);
550
+ return { timeoutMs: Math.round(r * 1e3) };
551
+ }, yt = async (e) => {
552
+ let t = H(e, "--target") === "api", n = H(e, "--bundle-key"), r = U(e);
553
+ if (!r && !(t && n)) return B.error("usage: voltro data export <outDir> [--target direct|api] [--api-url <url>] [--token <secret>] [--timeout <seconds>] [--profile <name>] [--tenant <id>] [--tables a,b] [--exclude a,b] [--bundle-key <storage-key>] [--assets] [--compression zstd|gzip|none] [--consistency live|snapshot] [--dry-run]"), 1;
554
+ if (t) {
555
+ let t = H(e, "--api-url");
556
+ if (!t) return B.error("--target api requires --api-url <url> (the running instance)"), 1;
557
+ let i = H(e, "--token") ?? process.env.VOLTRO_DATA_TRANSFER_SECRET;
558
+ if (!i) return B.error("--target api requires --token <secret> (or VOLTRO_DATA_TRANSFER_SECRET) matching the instance secret"), 1;
559
+ let a = H(e, "--tables"), o = H(e, "--exclude");
560
+ if (o !== void 0 && a !== void 0) return B.error("voltro data export: --exclude and --tables are two ways to say the same thing; use one."), 1;
561
+ let s = {
562
+ ...H(e, "--profile") ? { profile: H(e, "--profile") } : {},
563
+ ...H(e, "--tenant") ? { tenant: H(e, "--tenant") } : {},
564
+ ...a ? { tables: a.split(",").map((e) => e.trim()).filter(Boolean) } : {},
565
+ ...o ? { exclude: o.split(",").map((e) => e.trim()).filter(Boolean) } : {},
566
+ ...H(e, "--consistency") === "snapshot" ? { consistency: "snapshot" } : {},
567
+ ...H(e, "--compression") ? { compression: H(e, "--compression") } : {},
568
+ ...e.includes("--assets") ? { assets: !0 } : {},
569
+ ...n ? { bundleKey: n } : {}
570
+ }, c = ot(e), l;
571
+ try {
572
+ let n = await F(`${t.replace(/\/+$/, "")}/_voltro/admin/export`, JSON.stringify(s), {
573
+ "content-type": "application/json",
574
+ authorization: `Bearer ${i}`
575
+ }, c.perRequest ?? Q(e));
576
+ if (n.status < 200 || n.status >= 300) return B.error(`api export failed: HTTP ${n.status} — ${n.body.toString("utf8")}`), 1;
577
+ l = JSON.parse(n.body.toString("utf8"));
578
+ } catch (e) {
579
+ if (e instanceof P) return B.error(`api export: the instance did not answer within ${Math.round(e.elapsedMs / 1e3)}s. That is a client-side deadline. Nothing was written locally; re-run, or raise --timeout.`), 1;
580
+ if (e instanceof N) return B.error(`api export: ${I(e)}`), 1;
581
+ throw e;
582
+ }
583
+ if (e.includes("--detach")) return B.info(`api export: started as ${l.runId}. NOT waiting for it.\n The outcome is not known yet — read it with: voltro data ${X} --target api --api-url ${t}`), 0;
584
+ if (l.recorded === !1) return B.warn(`api export: started as ${l.runId}, but this instance could not record the run — so its outcome\n cannot be read back. Run \`voltro db apply\` against it so the next one is watchable.`), 0;
585
+ let p = await d({
586
+ read: () => st(t, i, l.runId),
587
+ onTick: (e) => {
588
+ B.info(f(e));
589
+ },
590
+ intervalMs: c.pollMs,
591
+ ...c.total === void 0 ? {} : { timeoutMs: c.total }
592
+ }), m = u(p);
593
+ if (m !== 0) return B.error(m === 1 ? `api export FAILED (${l.runId}): ${String(p?.failure ?? "no reason recorded")}` : `api export: no outcome visible for ${l.runId}. The run is not cancelled; it is simply not readable from here.`), m;
594
+ if (n) return B.info("api export complete (pushed to storage)", {
595
+ via: t,
596
+ runId: l.runId,
597
+ bundleKey: n,
598
+ tables: p?.tablesWritten ?? 0,
599
+ rows: p?.rowsWritten ?? 0
600
+ }), 0;
601
+ let { resolve: h } = await import("node:path"), g = await ct({
602
+ apiUrl: t,
603
+ secret: i,
604
+ runId: l.runId,
605
+ log: B,
606
+ ...c.perRequest ? { perRequest: c.perRequest } : {}
607
+ });
608
+ return g === null ? 1 : (await Me((async function* () {
609
+ yield g;
610
+ })(), h(process.cwd(), r)), B.info("api export complete (downloaded from the running instance)", {
611
+ outDir: r,
612
+ runId: l.runId,
613
+ tables: p?.tablesWritten ?? 0,
614
+ rows: p?.rowsWritten ?? 0,
615
+ bytes: g.byteLength
616
+ }), 0);
617
+ }
618
+ if (!r) return B.error("a <outDir> is required for a direct export"), 1;
619
+ let i = H(e, "--profile"), a = i ? await p(i, { allowPath: !0 }) : {}, { store: o, snapshot: s, dialect: c } = await G(), l = e.includes("--assets") || a.assets === !0, m = l ? E({}) : void 0, h = H(e, "--compression") ?? a.compression, g = H(e, "--consistency") === "snapshot" ? "snapshot" : a.consistency ?? "live", _ = a.masking, v = a.subset, y = a.scope ?? ut(e, s.tables.map((e) => e.name)), b = _ ? await dt() : void 0, x = y.kind === "tenant" ? await pt() : void 0, S = x ? { tenantTables: x } : {};
620
+ if (e.includes("--dry-run")) {
621
+ let e = y.kind === "tables" && y.tables ? [...y.tables] : s.tables.map((e) => e.name);
622
+ if (B.info("export DRY RUN — nothing was written", {
623
+ outDir: r,
624
+ tables: e.length,
625
+ scope: y.kind,
626
+ consistency: g,
627
+ compression: h ?? "zstd",
628
+ assets: l,
629
+ masking: _ ? "profile policy applies" : "none"
630
+ }), B.info(`would export: ${e.join(", ")}`), !_) return 0;
631
+ let t = await T.runPromise(Se({
632
+ store: o,
633
+ snapshot: s,
634
+ masking: _,
635
+ ...b ? { classification: b } : {},
636
+ ...v ? {} : {
637
+ scope: y,
638
+ ...S
639
+ }
640
+ }));
641
+ return mt(t), +(t.unclassified.length > 0);
642
+ }
643
+ if (q(r)) {
644
+ let t = await import("node:os"), n = await import("node:fs/promises"), { resolve: i, join: a } = await import("node:path"), u = await n.mkdtemp(a(t.tmpdir(), "voltro-export-"));
645
+ try {
646
+ let t = await T.runPromise(Te({
647
+ store: o,
648
+ snapshot: s,
649
+ dialect: c,
650
+ outDir: u,
651
+ ...v ? { subset: v } : {
652
+ scope: y,
653
+ ...S
654
+ },
655
+ consistency: g,
656
+ ...h ? { compression: h } : {},
657
+ ..._ ? { masking: _ } : {},
658
+ ...b ? { classification: b } : {},
659
+ onProgress: V
660
+ })), n = xe({
661
+ dir: u,
662
+ ...m ? { blobs: Ae(m, () => K(o)) } : {}
663
+ }), a = rt(e);
664
+ if (a) {
665
+ let t = J(e);
666
+ if (!t) return B.error("--encrypt requires --passphrase <p> or VOLTRO_BUNDLE_KEY (a dedicated key, NOT the transfer secret)"), 1;
667
+ n = be(n, t);
668
+ }
669
+ return await it(n, i(process.cwd(), r)), B.info("export complete (single file)", {
670
+ file: r,
671
+ encrypted: a,
672
+ assets: l,
673
+ tables: t.tables.length,
674
+ rows: t.tables.reduce((e, t) => e + t.rowCount, 0)
675
+ }), 0;
676
+ } finally {
677
+ await n.rm(u, {
678
+ recursive: !0,
679
+ force: !0
680
+ });
681
+ }
682
+ }
683
+ let C = await T.runPromise(Te({
684
+ store: o,
685
+ snapshot: s,
686
+ dialect: c,
687
+ outDir: r,
688
+ ...v ? { subset: v } : {
689
+ scope: y,
690
+ ...S
691
+ },
692
+ consistency: g,
693
+ ...h ? { compression: h } : {},
694
+ ..._ ? { masking: _ } : {},
695
+ ...b ? { classification: b } : {},
696
+ ...m ? { assets: Oe(m, () => K(o)) } : {},
697
+ onProgress: V
698
+ }));
699
+ return B.info("export complete", {
700
+ outDir: r,
701
+ tables: C.tables.length,
702
+ rows: C.tables.reduce((e, t) => e + t.rowCount, 0),
703
+ assets: C.assets.count,
704
+ compression: C.compression,
705
+ ...C.masking ? { maskedColumns: C.masking.transformed.length } : {}
706
+ }), 0;
707
+ }, bt = () => "dry run checked: schema fit, cross-dialect portability, mode legality, the table selection. It did NOT read per-table checksums (those stream during the load) and cannot see conflicts that depend on the target's current rows.", xt = (e) => {
708
+ let t = H(e, "--tables");
709
+ return {
710
+ ...t ? { tables: t.split(",").map((e) => e.trim()).filter(Boolean) } : {},
711
+ ...e.includes("--dry-run") ? { dryRun: !0 } : {}
712
+ };
713
+ }, St = (e) => ({
714
+ tables: e.tablesWritten,
715
+ rows: e.rowsWritten,
716
+ carried: e.manifest.tables.reduce((e, t) => e + t.rowCount, 0),
717
+ ...e.tablesSkipped > 0 ? {
718
+ skippedTables: e.tablesSkipped,
719
+ skippedRows: e.rowsSkipped
720
+ } : {}
721
+ }), Ct = (e, t) => {
722
+ if (e.includes("--dry-run")) {
723
+ B.info("import DRY RUN — nothing was written", t), B.info(bt());
724
+ return;
725
+ }
726
+ if (t.rows === 0 && typeof t.skippedRows == "number" && t.skippedRows !== 0) {
727
+ B.info("import complete — 0 rows written, every table was already applied by an earlier run of this bundle directory", t);
728
+ return;
729
+ }
730
+ B.info("import complete", t);
731
+ }, wt = Symbol("refused"), Tt = (e) => {
732
+ let t = he(e) ?? e;
733
+ if (t?._tag !== "RowsRefusedError" || !Array.isArray(t.rows)) return !1;
734
+ let n = t.rows, r = n.filter((e) => !e.derived), i = Number(t.totalCount ?? n.length), a = Number(t.primaryCount ?? r.length);
735
+ B.error(`import refused ${i} row(s), of which ${a} are the actual failures — the rest could not land because a row they reference did not.`);
736
+ let o = Array.isArray(t.byTable) ? t.byTable : [];
737
+ o.length > 1 && B.error(` by table: ${o.map((e) => `${e.table} ${e.refused}`).join(", ")}`);
738
+ for (let e of r) B.error(` ${e.table} ${e.id} ${e.reason}`);
739
+ n.length < i && B.error(` (the list above is capped at ${n.length} of ${i} — the counts by table are complete)`);
740
+ let s = i - a;
741
+ return s > 0 && B.error(` … and ${s} row(s) behind them. Fix the ${a} above and re-run; they resolve with their parents.`), !0;
742
+ }, Et = (e) => {
743
+ let t = typeof e == "string" ? e : e.toString("utf8"), n = t;
744
+ try {
745
+ let e = JSON.parse(t), r = e.error ?? e.message;
746
+ typeof r == "string" && (n = r);
747
+ } catch {}
748
+ let r = n.indexOf("RowsRefusedError");
749
+ if (r < 0) return;
750
+ let i = n.indexOf("{", r);
751
+ if (i < 0) return;
752
+ let a = 0, o = !1, s = !1;
753
+ for (let e = i; e < n.length; e++) {
754
+ let t = n[e];
755
+ if (o) {
756
+ if (s) {
757
+ s = !1;
758
+ continue;
759
+ }
760
+ if (t === "\\") {
761
+ s = !0;
762
+ continue;
763
+ }
764
+ t === "\"" && (o = !1);
765
+ continue;
766
+ }
767
+ if (t === "\"") {
768
+ o = !0;
769
+ continue;
770
+ }
771
+ if (t === "{") a++;
772
+ else if (t === "}" && --a === 0) try {
773
+ let t = JSON.parse(n.slice(i, e + 1));
774
+ return Array.isArray(t.rows) ? {
775
+ _tag: "RowsRefusedError",
776
+ ...t
777
+ } : void 0;
778
+ } catch {
779
+ return;
780
+ }
781
+ }
782
+ }, Dt = async (e) => {
783
+ let t = H(e, "--target") === "api", n = H(e, "--bundle-key"), r = U(e);
784
+ if (!r && !(t && n)) return B.error("usage: voltro data import <bundleDir> [--target direct|api] [--api-url <url>] [--token <secret>] [--timeout <seconds>] [--bundle-key <storage-key>] [--tables a,b] [--mode upsert|append|replace] [--on-conflict skip|fail] [--atomic] [--assets] [--no-verify] [--force] [--allow-live] [--dry-run] (--force: import despite schema drift AND cross-dialect warnings; --dry-run: report what would move, write nothing)"), 1;
785
+ let i = H(e, "--mode") ?? "upsert", o = H(e, "--on-conflict") ?? "skip";
786
+ if (t) {
787
+ let t = H(e, "--api-url");
788
+ if (!t) return B.error("--target api requires --api-url <url> (the running instance)"), 1;
789
+ let a = H(e, "--token") ?? process.env.VOLTRO_DATA_TRANSFER_SECRET;
790
+ if (!a) return B.error("--target api requires --token <secret> (or VOLTRO_DATA_TRANSFER_SECRET) matching the instance secret"), 1;
791
+ let s = H(e, "--tables"), c = H(e, "--rollback-key");
792
+ i === "replace" && c === void 0 && B.warn("api replace: no rollback capture — this run empties the target first and nothing was asked to be kept.\n Pass --rollback-key <storage-key> and the INSTANCE stores the target's current rows in its own\n object storage before deleting them, which is the one place that survives the pod going away.");
793
+ let l = {
794
+ authorization: `Bearer ${a}`,
795
+ "x-import-mode": i,
796
+ "x-import-on-conflict": o,
797
+ ...Z(e, i) ? { "x-import-atomic": "1" } : {},
798
+ ...e.includes("--no-atomic") ? { "x-import-atomic": "0" } : {},
799
+ ...c === void 0 ? {} : { "x-import-rollback-key": c },
800
+ ...e.includes("--force") ? { "x-import-force": "1" } : {},
801
+ ...e.includes("--dry-run") ? { "x-import-dry-run": "1" } : {},
802
+ ...s ? { "x-import-tables": s } : {}
803
+ }, { resolve: p } = await import("node:path"), m;
804
+ if (n) l["content-type"] = "application/json", m = JSON.stringify({ bundleKey: n });
805
+ else if (q(r)) {
806
+ let t = Y(p(process.cwd(), r)), n = J(e);
807
+ n && (t = A(t, n)), l["content-type"] = "application/octet-stream", m = t;
808
+ } else await (await import("node:fs/promises")).access(p(process.cwd(), r, "assets", "index.ndjson")).then(() => !0, () => !1) && B.warn("this directory bundle has assets; --target api uploads DATA ONLY. Pack a single-file `.vbundle` (voltro data export <name>.vbundle --assets) to transfer assets over the API."), l["content-type"] = "application/octet-stream", m = xe({ dir: p(process.cwd(), r) });
809
+ if (i === "replace") {
810
+ let i = await at(r, n);
811
+ {
812
+ let r = i === null ? { bundleKey: n } : { tables: i }, a = await F(`${t.replace(/\/+$/, "")}/_voltro/admin/import`, JSON.stringify(r), {
813
+ ...l,
814
+ "content-type": "application/json",
815
+ "x-import-preflight": "1"
816
+ }, Q(e)).catch(() => null), o = a !== null && a.status === 200 ? JSON.parse(a.body.toString("utf8")) : null;
817
+ o === null ? B.warn("api replace: the instance did not answer the preflight (an older build does not have it). The run still works; the decision will only be visible in the pod log.") : o.staged === !0 ? B.info(`api replace: the instance WILL stage — ${o.stagedTables ?? 0} table(s) load into copies and the\n target keeps its rows until one short swap at the end. Interrupting the load leaves the target intact.`) : B.warn(`api replace: the instance will NOT stage — it empties the target first and refills it, so an
818
+ interrupted run leaves the target empty.\n Reason: ${o.reason ?? "not stated"}`);
819
+ }
820
+ }
821
+ i === "replace" && !Z(e, i) && B.warn(gt(!0));
822
+ let h = `${t.replace(/\/+$/, "")}/_voltro/admin/import`, g = ot(e), _;
823
+ try {
824
+ let t = typeof m == "string" ? null : await ht({
825
+ stream: m,
826
+ url: h,
827
+ headers: l,
828
+ bundleDir: r,
829
+ args: e,
830
+ log: B
831
+ }), n = t === null ? await F(h, m, l, g.perRequest ?? Q(e)) : t.kind === "sent" ? t.res : await F(h, t.body, l, g.perRequest ?? Q(e));
832
+ if (n.status < 200 || n.status >= 300) return Tt(Et(n.body)) || B.error(`api import failed: HTTP ${n.status} — ${n.body.toString("utf8")}`), 1;
833
+ _ = JSON.parse(n.body.toString("utf8"));
834
+ } catch (e) {
835
+ if (e instanceof P) return B.error(`api import: ${Ie(e, i)}`), 1;
836
+ if (e instanceof N) return B.error(`api import: ${I(e)}`), 1;
837
+ throw e;
838
+ }
839
+ let v = await F(`${t.replace(/\/+$/, "")}/_voltro/admin/import/start`, JSON.stringify(_.bundleKey === void 0 ? { uploadId: _.uploadId } : { bundleKey: _.bundleKey }), {
840
+ ...l,
841
+ "content-type": "application/json"
842
+ }, g.perRequest ?? Q(e)).catch((e) => {
843
+ if (e instanceof N) return B.error(`api import: ${I(e)}`), null;
844
+ throw e;
845
+ });
846
+ if (v === null) return 1;
847
+ if (v.status < 200 || v.status >= 300) return Tt(Et(v.body)) || B.error(`api import refused: HTTP ${v.status} — ${v.body.toString("utf8")}`), 1;
848
+ let y = JSON.parse(v.body.toString("utf8"));
849
+ if (y.alreadyStarted === !0 && B.info(`api import: this bundle is already being imported as ${y.runId} — watching that run instead of starting a second one`), e.includes("--detach")) return B.info(`api import: started as ${y.runId}. NOT waiting for it.\n The outcome is not known yet — read it with: voltro data ${X} --target api --api-url ${t}`), 0;
850
+ if (y.recorded === !1) return B.warn(`api import: started as ${y.runId}, but this instance could not record the run — so its outcome\n cannot be read back. Run \`voltro db apply\` against it so the next import is watchable.`), 0;
851
+ let b = await d({
852
+ read: () => st(t, a, y.runId),
853
+ onTick: (e) => {
854
+ B.info(f(e));
855
+ },
856
+ intervalMs: g.pollMs,
857
+ ...g.total === void 0 ? {} : { timeoutMs: g.total }
858
+ }), x = u(b);
859
+ return x === 2 ? (B.warn(b === null ? `api import: started as ${y.runId}, but its record could not be READ back from this instance.\n That is a channel problem, not a slow import — an older instance without the history route, a
860
+ proxy dropping the GET, or a target with no history table. The run itself is unaffected and
861
+ is NOT cancelled; its outcome is simply not visible from here.` : `api import: still running after the time allowed. The run is NOT cancelled — it is decoupled from\n this client by design. Watch it with: voltro data ${X} --target api --api-url ${t}`), 2) : x === 1 ? (B.error(`api import FAILED (${y.runId}): ${String(b?.failure ?? "no reason recorded")}`), 1) : e.includes("--dry-run") ? (B.info("api import DRY RUN — nothing was written", {
862
+ via: t,
863
+ runId: y.runId
864
+ }), B.info(bt()), 0) : (B.info("api import complete (imported by the running instance)", {
865
+ via: t,
866
+ runId: y.runId,
867
+ tables: b?.tablesWritten ?? 0,
868
+ rows: b?.rowsWritten ?? 0,
869
+ ...b?.staged !== null && b?.staged !== void 0 ? { staged: b.staged } : {}
870
+ }), 0);
871
+ }
872
+ if (!r) return B.error("a <bundleDir> is required for a direct import"), 1;
873
+ if (!await Qe(e)) return 1;
874
+ i === "replace" && e.includes("--allow-live") && !Z(e, i) && B.warn(gt(!1));
875
+ let { store: s, snapshot: c } = await G();
876
+ c.tables.some((e) => e.name === y) || B.warn(`this target has no \`${y}\` table, so this import will not be recorded.\n The import itself is unaffected. \`voltro db apply\` creates it.`);
877
+ let l = e.includes("--force"), p = (e) => B.warn(e), m = e.includes("--assets") ? E({}) : void 0, h = W() === "postgres" ? (await import("@voltro/sql-postgres")).makePgCopySession(a()) : void 0;
878
+ if (q(r)) {
879
+ let t = await import("node:os"), n = await import("node:fs/promises"), { resolve: a, join: u } = await import("node:path"), d = await n.mkdtemp(u(t.tmpdir(), "voltro-import-"));
880
+ try {
881
+ let t = Y(a(process.cwd(), r)), n = J(e);
882
+ return n && (t = A(t, n)), await je(t, {
883
+ dir: d,
884
+ blobs: ke(m ?? E({}))
885
+ }), Ct(e, {
886
+ file: r,
887
+ ...St(await T.runPromise(Ee({
888
+ store: s,
889
+ bundleDir: d,
890
+ mode: i,
891
+ onConflict: o,
892
+ atomic: Z(e, i),
893
+ ...vt(e, i, r),
894
+ verify: !e.includes("--no-verify"),
895
+ targetDialect: W(),
896
+ allowIncompatible: l,
897
+ targetSnapshot: c,
898
+ force: l,
899
+ onWarn: p,
900
+ onProgress: V,
901
+ assetsAlreadyRestored: !0,
902
+ ...xt(e),
903
+ ...h ? { copyLoader: h } : {}
904
+ })))
905
+ }), 0;
906
+ } finally {
907
+ await h?.close(), await n.rm(d, {
908
+ recursive: !0,
909
+ force: !0
910
+ });
911
+ }
912
+ }
913
+ let g = await T.runPromise(Ee({
914
+ ...vt(e, i, r),
915
+ store: s,
916
+ bundleDir: r,
917
+ mode: i,
918
+ onConflict: o,
919
+ atomic: Z(e, i),
920
+ verify: !e.includes("--no-verify"),
921
+ targetDialect: W(),
922
+ allowIncompatible: l,
923
+ targetSnapshot: c,
924
+ force: l,
925
+ onWarn: p,
926
+ onProgress: V,
927
+ ...xt(e),
928
+ ...m ? { assets: De(m) } : {},
929
+ ...h ? { copyLoader: h } : {}
930
+ })).catch((e) => {
931
+ if (Tt(e)) return wt;
932
+ throw e;
933
+ }).finally(() => h?.close());
934
+ return typeof g == "symbol" ? 1 : (Ct(e, {
935
+ bundleDir: r,
936
+ ...St(g),
937
+ assets: g.manifest.assets.count
938
+ }), 0);
939
+ }, Ot = () => {
940
+ let e = a();
941
+ return {
942
+ ...e.url ? { url: e.url } : {},
943
+ ...e.host ? { host: e.host } : {},
944
+ ...e.port ? { port: e.port } : {},
945
+ ...e.username ? { username: e.username } : {},
946
+ ...e.password ? { password: e.password } : {},
947
+ ...e.database ? { database: e.database } : {}
948
+ };
949
+ }, kt = async (e) => {
950
+ let t = U(e);
951
+ if (!t) return B.error("usage: voltro data backup <outDir> [--assets] (native pg_dump/mariadb-dump/… + consistent artifact)"), 1;
952
+ let n = W(), r = e.includes("--assets"), { mkdir: i, writeFile: a } = await import("node:fs/promises"), { join: o } = await import("node:path"), s = r ? E({}) : void 0;
953
+ if (r && !s) return B.error("--assets: this app has no storage provider configured, so there are no blobs to capture.\n Configure one in app.config.ts, or drop the flag to take the rows-only dump."), 1;
954
+ await i(t, { recursive: !0 });
955
+ let c = o(t, k(n)), l = await At(), u = l ? await We(l.store, {
956
+ direction: "backup",
957
+ dialect: n,
958
+ artifact: c,
959
+ assets: r
960
+ }) : void 0, d = await Ke(M(_e(n, Ot(), c)));
961
+ if (d !== void 0) return l && u && await R(l.store, u, { failure: d }), B.error(`native backup failed: ${d}`), 1;
962
+ let f = 0, p = 0;
963
+ if (s && l) {
964
+ let e = o(t, D);
965
+ await i(e, { recursive: !0 });
966
+ let n = o(t, O), r = l.store, a = await T.runPromiseExit(ve({
967
+ source: Oe(s, () => K(r)),
968
+ assetsDir: e,
969
+ progress: {
970
+ done: () => !1,
971
+ mark: (e, t) => T.tryPromise({
972
+ try: async () => {
973
+ await (await import("node:fs/promises")).appendFile(n, `${JSON.stringify(t)}\n`);
974
+ },
975
+ catch: (e) => new me({
976
+ reason: `asset index append failed: ${String(e)}`,
977
+ path: n
978
+ })
979
+ })
980
+ }
981
+ }));
982
+ if (a._tag === "Failure") {
983
+ let e = w.pretty(a.cause);
984
+ return u && await R(l.store, u, { failure: `assets: ${e}` }), B.error(`the rows were dumped to ${c}, but the asset capture failed: ${e}\n This directory holds a ROWS-ONLY backup. Re-run with --assets once the storage
985
+ backend is reachable, or restore without --assets and accept the dangling refs.`), 1;
986
+ }
987
+ f = a.value.count, p = a.value.totalBytes;
988
+ }
989
+ let m = Ve({
990
+ dialect: n,
991
+ schemaFingerprint: await Xe(n),
992
+ voltroVersion: await Ye(),
993
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
994
+ assets: r ? {
995
+ count: f,
996
+ totalBytes: p
997
+ } : null
998
+ });
999
+ return await a(o(t, L), `${JSON.stringify(m, null, 2)}\n`, "utf8"), l && u && await R(l.store, u, { assets: f }), B.info("native backup complete", {
1000
+ dialect: n,
1001
+ artifact: c,
1002
+ stamp: L,
1003
+ schemaFingerprint: m.schemaFingerprint ?? "<none>",
1004
+ assets: r ? `${f} blob(s), ${p} byte(s) under ${D}/` : "NOT included — re-run with --assets, or use `voltro data export --assets`",
1005
+ recorded: u?.recorded === !0 ? "voltro data transfers" : "NOT recorded (this target keeps no transfer history)"
1006
+ }), 0;
1007
+ }, At = async () => {
1008
+ try {
1009
+ let e = await r(process.cwd());
1010
+ return e ? { store: await i(e, void 0) } : void 0;
1011
+ } catch {
1012
+ return;
1013
+ }
1014
+ }, jt = async (e, t) => {
1015
+ let n = W(), { join: r } = await import("node:path"), { readFile: i } = await import("node:fs/promises"), a = r(e, k(n)), o = qe({
1016
+ flagUrl: H(t, "--drill-url"),
1017
+ envUrl: process.env.DRILL_DB_URL,
1018
+ liveKeys: nt()
1019
+ });
1020
+ if (!o.ok) return B.error(`restore --drill: ${o.error}`), 1;
1021
+ let s = await i(r(e, L), "utf8").then(He, () => null);
1022
+ if (s && s.dialect !== n) return B.error(`restore --drill: backup is a ${s.dialect} dump but DB_DIALECT is ${n} — cross-dialect restore is never valid.`), 1;
1023
+ B.info("restore drill: restoring into throwaway DB", {
1024
+ dialect: n,
1025
+ target: z(o.url)
1026
+ }), await T.runPromise(M(we(n, { url: o.url }, a)));
1027
+ let c = null, l = 0;
1028
+ try {
1029
+ let e = await et(n, { url: o.url });
1030
+ c = ue(e), l = e.tables.length;
1031
+ } catch (e) {
1032
+ return B.error(`restore --drill: could not introspect the restored DB — ${String(e)}`), 1;
1033
+ }
1034
+ let u = Je({
1035
+ stampFingerprint: s?.schemaFingerprint ?? null,
1036
+ restoredFingerprint: c,
1037
+ restoredTableCount: l
1038
+ });
1039
+ for (let e of u.lines) u.pass ? B.info(`restore drill: ${e}`) : B.error(`restore drill: ${e}`);
1040
+ return +!u.pass;
1041
+ }, Mt = async (e) => {
1042
+ let t = U(e);
1043
+ if (!t) return B.error("usage: voltro data restore <backupDir> [--assets] [--allow-live] [--api-url <url>] [--force] [--drill [--drill-url <url>]] (native pg_restore/mariadb/…)"), 1;
1044
+ if (e.includes("--drill")) return jt(t, e);
1045
+ if (!await Qe(e)) return 1;
1046
+ let n = W(), { join: r } = await import("node:path"), { readFile: i } = await import("node:fs/promises"), a = r(t, k(n)), o = e.includes("--assets"), s = Ue({
1047
+ stamp: await i(r(t, L), "utf8").then(He, () => null),
1048
+ currentDialect: n,
1049
+ currentFingerprint: await Xe(n)
1050
+ });
1051
+ for (let e of s.info) B.info(`backup stamp: ${e}`);
1052
+ if (s.warn && B.warn(s.warn), s.refuse) {
1053
+ if (!e.includes("--force")) return B.error(`restore refused: ${s.refuse} (pass --force to override)`), 1;
1054
+ B.warn(`restore proceeding despite: ${s.refuse} (--force)`);
1055
+ }
1056
+ let c = r(t, O), l = await i(c, "utf8").then(() => !0, () => !1);
1057
+ if (o && !l) return B.error(`--assets: this backup carries no assets — there is no ${O} in ${t}.\n It was taken without --assets, so its rows will reference blobs this restore cannot
1058
+ put back. Restore from a backup taken with --assets, or drop the flag to accept that.`), 1;
1059
+ !o && l && B.warn("this backup CARRIES assets and --assets was not passed: the rows will be restored and the blobs will not. Pass --assets to restore both.");
1060
+ let u = o ? E({}) : void 0;
1061
+ if (o && !u) return B.error("--assets: this app has no storage provider configured, so there is nowhere to put the blobs."), 1;
1062
+ let d = await At(), f = d ? await Ge(d.store, { artifact: a }) : void 0;
1063
+ if (d && f === void 0) return B.error(`refusing to restore: could not write the in-progress marker.
1064
+ It is one row in \`${b}\`, written before the first destructive\n statement so a killed restore refuses the next boot rather than serving a half-loaded
1065
+ database. Run \`voltro db apply\` so the table exists, then retry.`), 1;
1066
+ let p = d ? await We(d.store, {
1067
+ direction: "restore",
1068
+ dialect: n,
1069
+ artifact: a,
1070
+ assets: o
1071
+ }) : void 0, m = await Ke(M(we(n, Ot(), a)));
1072
+ if (m !== void 0) return d && p && await R(d.store, p, { failure: m }), B.error(`native restore failed: ${m}\n This database is now in an unknown state and the next boot will REFUSE, by design.
1073
+ Re-run the restore to completion, or clear the marker deliberately with
1074
+ \`voltro data clear-replace-marker --yes\` once you have decided the state is correct.`), 1;
1075
+ let h = 0;
1076
+ if (u) {
1077
+ let e = await T.runPromiseExit(Ce({
1078
+ sink: De(u),
1079
+ assetsDir: r(t, D),
1080
+ indexPath: c
1081
+ }));
1082
+ if (e._tag === "Failure") {
1083
+ let t = w.pretty(e.cause);
1084
+ return d && p && await R(d.store, p, { failure: `assets: ${t}` }), B.error(`the rows were restored, but the asset restore failed: ${t}\n The in-progress marker is still set and the next boot will refuse. Re-run
1085
+ \`voltro data restore <dir> --assets\` once storage is reachable.`), 1;
1086
+ }
1087
+ h = e.value;
1088
+ }
1089
+ return f && await f(), d && p && await R(d.store, p, { assets: h }), B.info("native restore complete", {
1090
+ dialect: n,
1091
+ artifact: a,
1092
+ assets: o ? `${h} blob(s) restored` : "not restored (no --assets)",
1093
+ recorded: p?.recorded === !0 ? "voltro data transfers" : "NOT recorded (this target keeps no transfer history)"
1094
+ }), 0;
1095
+ }, Nt = async (e) => {
1096
+ let t = U(e);
1097
+ if (!t) return B.error("usage: voltro data inspect <file.vbundle|dir> [--json] [--passphrase <p>]"), 1;
1098
+ let { resolve: n, join: r } = await import("node:path"), i = await import("node:fs/promises"), a = n(process.cwd(), t);
1099
+ if ((await i.stat(a).catch(() => void 0))?.isDirectory()) {
1100
+ let n = r(a, "manifest.json"), o = r(a, "README.md");
1101
+ if (!e.includes("--json")) {
1102
+ let e = await i.readFile(o, "utf8").catch(() => void 0);
1103
+ if (e !== void 0) return console.log(e), 0;
1104
+ }
1105
+ let s = await i.readFile(n, "utf8").catch(() => void 0);
1106
+ return s === void 0 ? (B.error(`'${t}' is a directory but holds no manifest.json — is it an unpacked bundle? (a bundle exported with --target api is always unpacked into a directory, even when the path ends in .vbundle)`), 1) : (console.log(s), 0);
1107
+ }
1108
+ let o = J(e), s = () => {
1109
+ let e = Y(a);
1110
+ return o ? A(e, o) : e;
1111
+ };
1112
+ if (e.includes("--json")) {
1113
+ let e = (await j(s(), ["manifest.json"])).get("manifest.json");
1114
+ return e ? (console.log(e.toString("utf8")), 0) : (B.error("no manifest.json found in the bundle"), 1);
1115
+ }
1116
+ let c = (await j(s(), ["README.md"])).get("README.md");
1117
+ if (c) return console.log(c.toString("utf8")), 0;
1118
+ let l = (await j(s(), ["manifest.json"])).get("manifest.json");
1119
+ return l ? (console.log(l.toString("utf8")), 0) : (B.error("bundle has no README.md or manifest.json"), 1);
1120
+ }, Pt = async (e) => {
1121
+ let { REPLACE_IN_PROGRESS_TABLE: t } = await import("@voltro/database"), { interruptedReplaceAtBoot: n } = await import("./interruptedReplace-CvmiAM9K.js"), { store: r } = await G(), i = await n(r);
1122
+ if (i === void 0) return B.info("no interrupted replace is recorded — nothing to clear"), 0;
1123
+ if (!e.includes("--yes")) return B.error(`${i}\n\n Re-run with --yes to clear it.`), 1;
1124
+ let a = await r.query({
1125
+ table: t,
1126
+ predicate: void 0,
1127
+ order: [],
1128
+ take: void 0,
1129
+ skip: void 0,
1130
+ projection: void 0
1131
+ });
1132
+ for (let e of a) await r.delete(t, String(e.id));
1133
+ return B.info(`cleared ${a.length} interrupted-replace marker(s) — the next boot will start`), 0;
1134
+ }, Ft = async (e) => {
1135
+ let { STAGING_PREFIX: t, dropStagingSql: n, execStatements: r } = await import("@voltro/data-transfer"), { store: i } = await G(), a = W();
1136
+ if (a !== "postgres" && a !== "mysql" && a !== "mariadb" && a !== "sqlite" && a !== "mssql") return B.error(`this store has no staging tables to clear (dialect: ${a})`), 1;
1137
+ let { introspectSchema: o } = await import("@voltro/database/sql"), s = (await i.run((await import("@effect/sql")).SqlClient.SqlClient.pipe((await import("effect")).Effect.flatMap(o)))).tables.map((e) => e.name).filter((e) => e.startsWith(t));
1138
+ return s.length === 0 ? (B.info("no staging tables are present — nothing to clear"), 0) : e.includes("--yes") ? (await r(i, s.map((e) => n(e.slice(t.length), a))), B.info(`dropped ${s.length} staging table(s)`), 0) : (B.error(`${s.length} staging table(s) from an earlier \`--mode replace\`:\n` + s.map((e) => ` ${e}`).join("\n") + "\n\n Each holds a copy of what a run was loading. Nothing else removes them.\n Make sure no import is running right now — this cannot tell a leftover from one in flight.\n Re-run with --yes to drop them."), 1);
1139
+ }, It = async (e) => {
1140
+ let { describeTransferRun: t, DATA_TRANSFERS_TABLE: n } = await import("@voltro/database"), r = Number(H(e, "--limit") ?? "20"), i = Number.isFinite(r) ? Math.max(1, Math.min(200, Math.trunc(r))) : 20;
1141
+ if (H(e, "--target") === "api") {
1142
+ let t = H(e, "--api-url");
1143
+ if (!t) return B.error("--target api requires --api-url <url> (the running instance)"), 1;
1144
+ let n = H(e, "--token") ?? process.env.VOLTRO_DATA_TRANSFER_SECRET;
1145
+ if (!n) return B.error("--target api requires --token <secret> (or VOLTRO_DATA_TRANSFER_SECRET)"), 1;
1146
+ let r = await fetch(`${t.replace(/\/+$/, "")}/_voltro/admin/transfers?limit=${i}`, { headers: { authorization: `Bearer ${n}` } }), a = await r.text();
1147
+ if (r.status !== 200) return B.error(`api transfers: ${r.status} ${a}`), 1;
1148
+ let o = JSON.parse(a);
1149
+ if (e.includes("--json")) return console.log(a), 0;
1150
+ let s = o.runs ?? [];
1151
+ if (s.length === 0) return B.info("no data transfers recorded on this instance"), 0;
1152
+ (o.running ?? 0) > 0 && B.info(`${o.running} transfer(s) IN FLIGHT — re-run this to watch the counters move`);
1153
+ for (let e of s) B.info(` ${e.line ?? ""}`);
1154
+ return 0;
1155
+ }
1156
+ let { store: a } = await G(), o;
1157
+ try {
1158
+ o = await a.query({
1159
+ table: n,
1160
+ order: [{
1161
+ column: "startedAt",
1162
+ direction: "desc"
1163
+ }],
1164
+ predicate: void 0,
1165
+ projection: void 0,
1166
+ skip: 0,
1167
+ take: i
1168
+ });
1169
+ } catch {
1170
+ return B.error(`this target has no \`${n}\` table, so no transfer has been recorded here.\n Run \`voltro db apply\`; the transfers themselves are unaffected by its absence.`), 1;
1171
+ }
1172
+ if (o.length === 0) return B.info("no data transfers recorded against this target"), 0;
1173
+ for (let e of o) {
1174
+ let n = e;
1175
+ B.info(` ${t({
1176
+ ...e,
1177
+ startedAt: new Date(n.startedAt),
1178
+ finishedAt: n.finishedAt === null ? null : new Date(n.finishedAt)
1179
+ })}`);
1180
+ }
1181
+ return 0;
1182
+ }, Lt = async (e) => {
1183
+ let t = U(e), n = U(e.filter((e) => e !== t));
1184
+ if (!t || !n) return B.error("usage: voltro data unpack <file.vbundle> <dir> [--passphrase <p>]"), 1;
1185
+ let { resolve: r } = await import("node:path"), i = Y(r(process.cwd(), t)), a = J(e);
1186
+ return a && (i = A(i, a)), await Me(i, r(process.cwd(), n)), B.info("unpack complete", {
1187
+ file: t,
1188
+ dir: n
1189
+ }), 0;
1190
+ }, $ = {
1191
+ export: {
1192
+ common: [
1193
+ "--target",
1194
+ "--tables",
1195
+ "--exclude",
1196
+ "--profile",
1197
+ "--tenant",
1198
+ "--assets",
1199
+ "--compression",
1200
+ "--consistency",
1201
+ "--json"
1202
+ ],
1203
+ directOnly: [
1204
+ "--passphrase",
1205
+ "--dry-run",
1206
+ "--encrypt"
1207
+ ],
1208
+ apiOnly: [
1209
+ "--api-url",
1210
+ "--token",
1211
+ "--bundle-key",
1212
+ "--timeout",
1213
+ "--detach",
1214
+ "--max-request-seconds"
1215
+ ]
1216
+ },
1217
+ "clear-staging": { common: ["--yes"] },
1218
+ transfers: {
1219
+ common: [
1220
+ "--target",
1221
+ "--limit",
1222
+ "--json"
1223
+ ],
1224
+ apiOnly: ["--api-url", "--token"]
1225
+ },
1226
+ "clear-replace-marker": { common: ["--yes"] },
1227
+ import: {
1228
+ common: [
1229
+ "--target",
1230
+ "--tables",
1231
+ "--mode",
1232
+ "--on-conflict",
1233
+ "--atomic",
1234
+ "--no-atomic",
1235
+ "--assets",
1236
+ "--force",
1237
+ "--dry-run",
1238
+ "--json"
1239
+ ],
1240
+ directOnly: [
1241
+ "--no-verify",
1242
+ "--allow-live",
1243
+ "--passphrase",
1244
+ "--rollback-dir",
1245
+ "--no-rollback"
1246
+ ],
1247
+ apiOnly: [
1248
+ "--api-url",
1249
+ "--token",
1250
+ "--bundle-key",
1251
+ "--timeout",
1252
+ "--chunk-size",
1253
+ "--rollback-key",
1254
+ "--detach",
1255
+ "--max-request-seconds"
1256
+ ]
1257
+ },
1258
+ unpack: { common: ["--passphrase", "--json"] },
1259
+ inspect: { common: ["--json", "--passphrase"] },
1260
+ backup: { common: ["--json", "--assets"] },
1261
+ restore: { common: [
1262
+ "--json",
1263
+ "--assets",
1264
+ "--allow-live",
1265
+ "--force",
1266
+ "--drill",
1267
+ "--drill-url",
1268
+ "--api-url"
1269
+ ] }
1270
+ }, Rt = (e) => e.split("=")[0], zt = (e, t) => {
1271
+ let n = $[e];
1272
+ if (!n) return;
1273
+ let r = H(t, "--target") === "api" ? "api" : "direct", i = /* @__PURE__ */ new Set([...n.common, ...r === "api" ? n.apiOnly ?? [] : n.directOnly ?? []]), a = new Set(r === "api" ? n.directOnly ?? [] : n.apiOnly ?? []);
1274
+ for (let n of t) {
1275
+ if (!n.startsWith("--")) continue;
1276
+ let t = Rt(n);
1277
+ if (i.has(t)) continue;
1278
+ if (a.has(t)) return `voltro data ${e}: ${t} is not read with --target ${r} (it is a --target ${r === "api" ? "direct" : "api"} flag). Remove it, or change --target.`;
1279
+ let o = Object.entries($).filter(([n, r]) => n !== e && [
1280
+ ...r.common,
1281
+ ...r.directOnly ?? [],
1282
+ ...r.apiOnly ?? []
1283
+ ].includes(t)).map(([e]) => e);
1284
+ return o.length > 0 ? `voltro data ${e}: ${t} is not a \`${e}\` flag (it belongs to \`voltro data ${o.join("` / `")}\`). Remove it.` : `voltro data ${e}: unknown flag ${t}. Run \`voltro data --help\` for the flags this command reads.`;
1285
+ }
1286
+ }, Bt = {
1287
+ export: yt,
1288
+ import: Dt,
1289
+ transfers: It,
1290
+ inspect: Nt,
1291
+ unpack: Lt,
1292
+ backup: kt,
1293
+ restore: Mt,
1294
+ "clear-replace-marker": Pt,
1295
+ "clear-staging": Ft
1296
+ }, Vt = async (n) => {
1297
+ let [r, ...i] = n;
1298
+ if (r !== void 0 && $[r] !== void 0) {
1299
+ let e = zt(r, i);
1300
+ if (e) return B.error(e), 2;
1301
+ }
1302
+ let a = r === void 0 ? void 0 : Bt[r];
1303
+ return a === void 0 ? (B.error(`usage: voltro data ${e(t)} … (logical: export/import; native: backup/restore; \`imports\` reads the history, incl. a run in flight)`), r ? 2 : 1) : a(i);
1304
+ };
1305
+ //#endregion
1306
+ export { Vt as runDataCommand };