@voltro/cli 0.43.1 → 0.44.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 (90) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/{apiBuild-DF0f69Gf.js → apiBuild-DvWreIQP.js} +1 -1
  3. package/dist/apiBuild-XaW40W8x.js +2 -0
  4. package/dist/bin.js +1 -1
  5. package/dist/{build-LC_IJaHL.js → build-Ckc-FA7d.js} +6 -6
  6. package/dist/{checkCommand-DBpTSK8d.js → checkCommand-AlbawIal.js} +1 -1
  7. package/dist/{checkCommand-DDxwJCja.js → checkCommand-BNfEs4KG.js} +2 -2
  8. package/dist/{codegenCommand-LjxfK4Am.js → codegenCommand-iClXoRCW.js} +1 -1
  9. package/dist/{codemodRunner-Cnnz4-I7.js → codemodRunner-BKTnaGOz.js} +22 -0
  10. package/dist/{commands-CbQcuWsE.js → commands-fsnezHLH.js} +18 -18
  11. package/dist/{dashboardCommand-CAHjCe_9.js → dashboardCommand-DHDmlo9A.js} +1 -1
  12. package/dist/dataCommand-sfpGV694.js +867 -0
  13. package/dist/{dev-D0E5x2Ue.js → dev-BZO5aLdr.js} +1 -1
  14. package/dist/{dev-B2rZfqxw.js → dev-D-JZ6CGQ.js} +930 -916
  15. package/dist/doctorCommand-BeLOwq7S.js +2 -0
  16. package/dist/{doctorCommand-Ct_3BHgd.js → doctorCommand-BlUyESpD.js} +9 -9
  17. package/dist/{dormancyCommand-D1DKnYce.js → dormancyCommand-lQxdrU8C.js} +1 -1
  18. package/dist/{embeddingsCommand-1sZRBKlM.js → embeddingsCommand-ByBTju6X.js} +1 -1
  19. package/dist/{envCommand-7qjlQgLD.js → envCommand-CpCbYAgR.js} +1 -1
  20. package/dist/{evolveCommand-D5opB48I.js → evolveCommand-CBV3-TtK.js} +2 -2
  21. package/dist/index.js +1 -1
  22. package/dist/{infoCommand-Bs_8RU22.js → infoCommand-DJoA9cKD.js} +1 -1
  23. package/dist/{runtimeTrace-Wmi7V-wN.js → runtimeTrace-DJ9tPhrc.js} +1 -1
  24. package/dist/{sdkgen-DbTFGCjg.js → sdkgen-7dr8-Onw.js} +1 -1
  25. package/dist/serveCommand-BlnAlNQg.js +1891 -0
  26. package/dist/serveCommand-Dg_E1ev4.js +2 -0
  27. package/dist/serveEntry.js +1 -1
  28. package/dist/{updateCommand-CrI9B4d7.js → updateCommand-BvRidHp6.js} +1 -1
  29. package/dist/updateCommand-DcKVmLVN.js +2 -0
  30. package/dist/{webhooksCommand-BXUP5YjW.js → webhooksCommand-BfO_JF2b.js} +1 -1
  31. package/package.json +23 -17
  32. package/templates/AGENTS.md +1 -1
  33. package/templates/agent-docs/_index.md +1 -1
  34. package/templates/agent-docs/cli.md +144 -4
  35. package/templates/agent-docs/database/seedsdialects.md +12 -0
  36. package/templates/agent-docs/plugins/versioning.md +2 -0
  37. package/templates/agent-docs/whats-new.md +16 -51
  38. package/templates/apps/api-ai/package.json +7 -7
  39. package/templates/apps/api-auth/package.json +8 -8
  40. package/templates/apps/api-backend/package.json +7 -7
  41. package/templates/apps/api-backend-deactivation/package.json +7 -7
  42. package/templates/apps/api-backend-mail/package.json +8 -8
  43. package/templates/apps/api-backend-mariadb/package.json +9 -9
  44. package/templates/apps/api-backend-sqlite/package.json +8 -8
  45. package/templates/apps/api-backend-storage/package.json +8 -8
  46. package/templates/apps/api-cms/package.json +10 -10
  47. package/templates/apps/api-collab/package.json +8 -8
  48. package/templates/apps/api-data-advanced/package.json +8 -8
  49. package/templates/apps/api-durable/package.json +8 -8
  50. package/templates/apps/api-feature-flags/package.json +9 -9
  51. package/templates/apps/api-governance/package.json +8 -8
  52. package/templates/apps/api-kv/package.json +8 -8
  53. package/templates/apps/api-moderation/package.json +8 -8
  54. package/templates/apps/api-observability/package.json +8 -8
  55. package/templates/apps/api-ratelimit/package.json +8 -8
  56. package/templates/apps/api-rbac/package.json +8 -8
  57. package/templates/apps/api-rest/package.json +7 -7
  58. package/templates/apps/api-saas/package.json +11 -11
  59. package/templates/apps/api-saas-starter/package.json +10 -10
  60. package/templates/apps/api-search/package.json +8 -8
  61. package/templates/apps/api-status/package.json +8 -8
  62. package/templates/apps/api-versioning/package.json +8 -8
  63. package/templates/apps/api-webhooks/package.json +9 -9
  64. package/templates/apps/changelog/package.json +6 -6
  65. package/templates/apps/edge-functions/package.json +2 -2
  66. package/templates/apps/frontend-admin/package.json +8 -8
  67. package/templates/apps/frontend-app/package.json +9 -9
  68. package/templates/apps/frontend-auth/package.json +8 -8
  69. package/templates/apps/frontend-blank/package.json +7 -7
  70. package/templates/apps/frontend-cms/package.json +9 -9
  71. package/templates/apps/frontend-collab/package.json +10 -10
  72. package/templates/apps/frontend-contact/package.json +7 -7
  73. package/templates/apps/frontend-dashboard/package.json +7 -7
  74. package/templates/apps/frontend-docs/package.json +7 -7
  75. package/templates/apps/frontend-i18n/package.json +6 -6
  76. package/templates/apps/frontend-landing/package.json +7 -7
  77. package/templates/apps/frontend-portal/package.json +8 -8
  78. package/templates/apps/frontend-saas/package.json +8 -8
  79. package/templates/apps/frontend-spa/package.json +7 -7
  80. package/templates/apps/frontend-ssr/package.json +7 -7
  81. package/templates/apps/frontend-ssr-api/package.json +8 -8
  82. package/templates/apps/frontend-static-blog/package.json +6 -6
  83. package/templates/apps/frontend-status/package.json +8 -8
  84. package/templates/apps/mobile-app/package.json +4 -4
  85. package/dist/apiBuild-B7NgSMhX.js +0 -2
  86. package/dist/dataCommand-CrqvbVOv.js +0 -679
  87. package/dist/doctorCommand-CdsctSwg.js +0 -2
  88. package/dist/serveCommand-C8xzYCwa.js +0 -1776
  89. package/dist/serveCommand-CGNTXafv.js +0 -2
  90. package/dist/updateCommand-BvkKlV7D.js +0 -2
@@ -0,0 +1,867 @@
1
+ import { t as e } from "./cliError-BmdYnghb.js";
2
+ import { c as t, i as n, o as r } from "./dev-D-JZ6CGQ.js";
3
+ import { n as i, t as a } from "./cliArgs-D4p8n7EE.js";
4
+ import { r as o } from "./dialectDriver-czCHYpeH.js";
5
+ import { t as s } from "./dataProfile-Cm0YVKSy.js";
6
+ import { o as c, t as l } from "./inspectFetch-EMuhTG_9.js";
7
+ import { r as u } from "./dbCommand-B237qI-j.js";
8
+ import { createLogger as d } from "@voltro/logger";
9
+ import { Effect as f, Stream as p } from "effect";
10
+ import { SqlClient as m } from "@effect/sql";
11
+ import { declaredSnapshot as h, fingerprintSchema as g, introspectSchema as _ } from "@voltro/database/sql";
12
+ import { connectionIdentity as v, normaliseTarget as y, sameDatabase as b, streamTable as x } from "@voltro/database";
13
+ import { STORAGE_REFS_TABLE as ee, resolveStorageProvider as S } from "@voltro/plugin-storage";
14
+ import { createHash as te } from "node:crypto";
15
+ import { assessLiveImport as ne, backupArtifactName as C, backupCommand as re, classificationFromSnapshot as ie, decryptStream as w, encryptStream as ae, packBundle as T, peekBundle as E, previewMasking as oe, restoreCommand as D, runExport as se, runImport as ce, runNativeStep as O, storageAssetSink as le, storageAssetSource as ue, storageBlobSink as de, storageBlobSource as fe, unpackBundle as pe, unpackBundleToDir as k } from "@voltro/data-transfer";
16
+ import { request as me } from "node:http";
17
+ import { request as he } from "node:https";
18
+ //#region src/longRequest.ts
19
+ var A = class extends Error {
20
+ elapsedMs;
21
+ url;
22
+ constructor(e, t) {
23
+ super(`no response after ${Math.round(e / 1e3)}s`), this.elapsedMs = e, this.url = t, this.name = "LongRequestTimeout";
24
+ }
25
+ }, j = async (e, t, n, r) => {
26
+ let i = new URL(e), a = i.protocol === "https:" ? he : me, o = Date.now();
27
+ return await new Promise((s, c) => {
28
+ let l = a({
29
+ protocol: i.protocol,
30
+ hostname: i.hostname,
31
+ port: i.port === "" ? void 0 : Number(i.port),
32
+ path: `${i.pathname}${i.search}`,
33
+ method: "POST",
34
+ headers: n
35
+ }, (e) => {
36
+ let t = [];
37
+ e.on("data", (e) => t.push(e)), e.on("end", () => s({
38
+ status: e.statusCode ?? 0,
39
+ headers: Object.fromEntries(Object.entries(e.headers).map(([e, t]) => [e, Array.isArray(t) ? t.join(", ") : String(t ?? "")])),
40
+ body: Buffer.concat(t)
41
+ })), e.on("error", c);
42
+ });
43
+ if (r?.timeoutMs !== void 0 && r.timeoutMs > 0 && l.setTimeout(r.timeoutMs, () => {
44
+ l.destroy(new A(Date.now() - o, e));
45
+ }), l.on("error", c), typeof t == "string" || Buffer.isBuffer(t)) {
46
+ l.end(t);
47
+ return;
48
+ }
49
+ (async () => {
50
+ try {
51
+ for await (let e of t) l.write(Buffer.from(e)) || await new Promise((e) => l.once("drain", () => e()));
52
+ l.end();
53
+ } catch (e) {
54
+ l.destroy(e);
55
+ }
56
+ })();
57
+ });
58
+ }, ge = (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.", _e = 16777216, ve = async function* (e, t) {
59
+ let n = [], r = 0, i = 0, a = function* (e) {
60
+ let t = Buffer.concat(n);
61
+ n = [], r = 0;
62
+ let a = {
63
+ bytes: t,
64
+ offset: i,
65
+ final: e
66
+ };
67
+ i += t.byteLength, yield a;
68
+ };
69
+ for await (let a of e) for (n.push(Buffer.from(a)), r += a.byteLength; r >= t;) {
70
+ let e = Buffer.concat(n), a = e.subarray(0, t), o = e.subarray(t);
71
+ n = o.byteLength > 0 ? [Buffer.from(o)] : [], r = o.byteLength;
72
+ let s = {
73
+ bytes: Buffer.from(a),
74
+ offset: i,
75
+ final: !1
76
+ };
77
+ i += a.byteLength, yield s;
78
+ }
79
+ yield* a(!0);
80
+ }, ye = (e) => te("sha256").update(Buffer.from(e)).digest("hex"), be = async (e) => {
81
+ let t = 0, n = 0, r = {
82
+ status: 0,
83
+ body: {}
84
+ };
85
+ for await (let i of ve(e.stream, e.chunkBytes)) {
86
+ if (i.offset + i.bytes.byteLength <= e.alreadyHeld && !i.final) {
87
+ n++;
88
+ continue;
89
+ }
90
+ let a = Math.max(0, e.alreadyHeld - i.offset), o = a > 0 ? i.bytes.subarray(a) : i.bytes, s = i.offset + a;
91
+ if (o.byteLength === 0 && !i.final) {
92
+ n++;
93
+ continue;
94
+ }
95
+ if (i.final && e.onFinalChunk?.(), r = await e.target.send(Buffer.from(o), {
96
+ ...e.baseHeaders,
97
+ "content-type": "application/octet-stream",
98
+ "x-import-upload-id": e.uploadId,
99
+ "x-import-upload-key": e.key,
100
+ "x-import-chunk-offset": String(s),
101
+ ...i.final ? { "x-import-chunk-final": "1" } : {}
102
+ }), n++, t += o.byteLength, e.onProgress?.(t), r.status >= 400) break;
103
+ }
104
+ return {
105
+ status: r.status,
106
+ body: r.body,
107
+ chunks: n,
108
+ bytes: t,
109
+ resumedFrom: e.alreadyHeld
110
+ };
111
+ }, M = "voltro-backup-stamp.json", xe = (e) => ({
112
+ stampVersion: 1,
113
+ dialect: e.dialect,
114
+ schemaFingerprint: e.schemaFingerprint,
115
+ voltroVersion: e.voltroVersion,
116
+ createdAt: e.createdAt
117
+ }), N = (e) => {
118
+ let t;
119
+ try {
120
+ t = JSON.parse(e);
121
+ } catch {
122
+ return null;
123
+ }
124
+ if (typeof t != "object" || !t) return null;
125
+ let n = t;
126
+ if (n.stampVersion !== 1 || typeof n.dialect != "string" || typeof n.voltroVersion != "string" || typeof n.createdAt != "string") return null;
127
+ let r = n.schemaFingerprint;
128
+ return r !== null && typeof r != "string" ? null : {
129
+ stampVersion: 1,
130
+ dialect: n.dialect,
131
+ schemaFingerprint: r,
132
+ voltroVersion: n.voltroVersion,
133
+ createdAt: n.createdAt
134
+ };
135
+ }, Se = (e) => {
136
+ let { stamp: t, currentDialect: n, currentFingerprint: r } = e;
137
+ if (t === null) return {
138
+ warn: `no ${M} beside the dump — an older/handmade backup. Cannot verify dialect or schema version; make sure this dump is a ${n} dump.`,
139
+ info: []
140
+ };
141
+ let i = [`backup produced by @voltro/cli ${t.voltroVersion} at ${t.createdAt}`, `schema fingerprint: ${t.schemaFingerprint ?? "<none recorded>"}`];
142
+ return t.dialect === n ? t.schemaFingerprint !== null && r !== null && t.schemaFingerprint !== r ? {
143
+ 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.`,
144
+ info: i
145
+ } : { info: i } : {
146
+ refuse: `dialect mismatch — backup is a ${t.dialect} dump, but DB_DIALECT is ${n}. A cross-dialect restore is never valid.`,
147
+ info: i
148
+ };
149
+ }, P = (e) => {
150
+ try {
151
+ let t = new URL(e), n = t.pathname.replace(/^\//, "");
152
+ return `${t.hostname.toLowerCase()}:${t.port || "(default)"}/${n}`;
153
+ } catch {
154
+ return e.trim();
155
+ }
156
+ }, Ce = (e) => {
157
+ let t = e.flagUrl ?? e.envUrl;
158
+ if (t === void 0 || t.trim() === "") return {
159
+ ok: !1,
160
+ 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."
161
+ };
162
+ let n = P(t);
163
+ return e.liveKeys.some((e) => e === n) ? {
164
+ ok: !1,
165
+ error: `drill target (${n}) is the LIVE database — a restore drill must target a throwaway DB, never production. It would --clean and overwrite it.`
166
+ } : {
167
+ ok: !0,
168
+ url: t
169
+ };
170
+ }, we = (e) => {
171
+ let { stampFingerprint: t, restoredFingerprint: n, restoredTableCount: r } = e;
172
+ return r === 0 ? {
173
+ pass: !1,
174
+ lines: ["FAIL — the restore produced ZERO tables. The dump is empty or unreadable; this backup would NOT recover your app."]
175
+ } : t !== null && n !== null && t !== n ? {
176
+ pass: !1,
177
+ 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."]
178
+ } : t === null || n === null ? {
179
+ pass: !0,
180
+ 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."]
181
+ } : {
182
+ pass: !0,
183
+ lines: [`PASS — restored ${r} table(s); schema fingerprint matches the backup stamp (${n.slice(0, 12)}).`]
184
+ };
185
+ }, F = d({ scope: "voltro:data" }), Te = async () => {
186
+ try {
187
+ 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");
188
+ return JSON.parse(n(r, "utf8")).version ?? "unknown";
189
+ } catch {
190
+ return "unknown";
191
+ }
192
+ }, I = async (e) => {
193
+ try {
194
+ return g(await U(e));
195
+ } catch {
196
+ return null;
197
+ }
198
+ }, L = (e) => {
199
+ if (e.event !== "done") return;
200
+ let t = e.total === void 0 ? "" : ` / ${e.total}`;
201
+ F.info(`${e.phase} [${e.index + 1}/${e.tableCount}] ${e.table}`, { rows: `${e.rowsDone}${t}` });
202
+ }, Ee = async (e) => {
203
+ let t = (e) => e?.meta?.databaseId;
204
+ if (e) {
205
+ let n = await l({
206
+ name: "target",
207
+ url: e
208
+ }, "/_voltro/inspect/app");
209
+ if (n.kind === "ok") return {
210
+ label: e,
211
+ databaseId: t(n.data)
212
+ };
213
+ }
214
+ for (let e of await c()) {
215
+ let n = await l(e, "/_voltro/inspect/app");
216
+ if (n.kind === "ok") return {
217
+ label: `${e.name} (${e.url})`,
218
+ databaseId: t(n.data)
219
+ };
220
+ }
221
+ return null;
222
+ }, R = async (e) => {
223
+ let t = e.includes("--allow-live"), n = t ? null : await Ee(z(e, "--api-url")), i = v({
224
+ dialect: V(),
225
+ ...r()
226
+ }), a = y({
227
+ dialect: V(),
228
+ ...r()
229
+ }), { refuse: o, message: s } = ne({
230
+ liveInstance: n?.label ?? null,
231
+ allowLive: t,
232
+ sameTarget: b(i, n?.databaseId),
233
+ ...a === void 0 ? {} : { targetLabel: `${a.host}:${a.port}/${a.database}` }
234
+ });
235
+ return !o || (F.error(s ?? "refusing: live instance detected"), !1);
236
+ }, z = i, De = [
237
+ "--api-url",
238
+ "--bundle-key",
239
+ "--compression",
240
+ "--consistency",
241
+ "--drill-url",
242
+ "--chunk-size",
243
+ "--exclude",
244
+ "--mode",
245
+ "--no-atomic",
246
+ "--on-conflict",
247
+ "--passphrase",
248
+ "--profile",
249
+ "--tables",
250
+ "--target",
251
+ "--timeout",
252
+ "--tenant",
253
+ "--token"
254
+ ], B = (e) => a(e, De), V = () => {
255
+ let t = (process.env.DB_DIALECT ?? "postgres").toLowerCase();
256
+ if (t === "memory") throw new e("voltro data: requires a real dialect, not DB_DIALECT=memory.");
257
+ return t;
258
+ }, H = async (e, t) => {
259
+ let n = (await o(e)).makeSqlLayer(t);
260
+ return f.runPromise(m.SqlClient.pipe(f.flatMap(_), f.provide(n)));
261
+ }, U = (e) => H(e, r()), Oe = () => {
262
+ let e = r(), t = [];
263
+ return e.url && t.push(P(e.url)), (e.host || e.database) && t.push(`${(e.host ?? "localhost").toLowerCase()}:${e.port ?? "(default)"}/${e.database ?? ""}`), t;
264
+ }, W = async () => {
265
+ let r = process.cwd(), i = await t(r);
266
+ if (!i) throw new e("voltro data: no app.config.ts found in the current directory.");
267
+ let a = await n(i, void 0), o = V();
268
+ return {
269
+ store: a,
270
+ snapshot: await U(o),
271
+ dialect: o
272
+ };
273
+ }, G = (e) => x(e, {
274
+ table: ee,
275
+ chunkSize: 500
276
+ }).pipe(p.map((e) => ({
277
+ key: String(e.key),
278
+ contentType: String(e.contentType ?? "application/octet-stream"),
279
+ size: Number(e.size ?? 0)
280
+ }))), K = (e) => !!e && e.endsWith(".vbundle"), q = (e) => z(e, "--passphrase") ?? process.env.VOLTRO_BUNDLE_KEY, ke = (e) => e.includes("--encrypt") || z(e, "--passphrase") !== void 0 || process.env.VOLTRO_BUNDLE_KEY !== void 0, Ae = async (e, t) => {
281
+ let { createWriteStream: n } = await import("node:fs"), { pipeline: r } = await import("node:stream/promises"), { Readable: i } = await import("node:stream");
282
+ await r(i.from(e), n(t));
283
+ }, J = (e) => ({ async *[Symbol.asyncIterator]() {
284
+ let { createReadStream: t } = await import("node:fs");
285
+ for await (let n of t(e)) yield n instanceof Uint8Array ? n : new Uint8Array(n);
286
+ } }), Y = (e) => (e ?? "").split(",").map((e) => e.trim()).filter(Boolean), je = (t, n) => {
287
+ let r = z(t, "--tenant"), i = z(t, "--tables"), a = z(t, "--exclude");
288
+ if (a !== void 0) {
289
+ if (i !== void 0) throw new e("voltro data export: --exclude and --tables are two ways to say the same thing; use one.");
290
+ if (r !== void 0) throw new e("voltro data export: --exclude does not apply to --tenant, whose table set is derived from the tenant closure.");
291
+ let t = Y(a), o = new Set(n), s = t.filter((e) => !o.has(e));
292
+ if (s.length > 0) throw new e(`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.`);
293
+ let c = new Set(t);
294
+ return {
295
+ kind: "tables",
296
+ tables: n.filter((e) => !c.has(e))
297
+ };
298
+ }
299
+ return r ? {
300
+ kind: "tenant",
301
+ tenantId: r
302
+ } : i ? {
303
+ kind: "tables",
304
+ tables: Y(i)
305
+ } : { kind: "all" };
306
+ }, Me = async () => {
307
+ let e = await u(process.cwd());
308
+ return ie(h(e));
309
+ }, Ne = "voltro/tenant", Pe = async () => {
310
+ let e = (await u(process.cwd())).filter((e) => (e.appliedMixins ?? []).some((e) => e.id === Ne)).map((e) => e.tableName);
311
+ return e.length > 0 ? e : void 0;
312
+ }, Fe = (e) => {
313
+ for (let t of e.tables) if (t.samples.length !== 0) {
314
+ console.log(`\n ${t.table}`);
315
+ for (let e of t.samples) console.log(` ${e.column} [${e.action}] ${JSON.stringify(e.before)} → ${JSON.stringify(e.after)}`);
316
+ }
317
+ if (e.leaks.length > 0) {
318
+ console.log("\n ⚠ possible leaks in KEPT columns:");
319
+ for (let t of e.leaks) console.log(` ${t.table}.${t.column} looks like ${t.pattern}: ${JSON.stringify(t.sample)}`);
320
+ }
321
+ if (e.unclassified.length > 0) {
322
+ console.log("\n ✗ unclassified (would BLOCK a real export — add .sensitive()/.safe() or a policy override):");
323
+ for (let t of e.unclassified) console.log(` ${t}`);
324
+ } else console.log("\n ✓ every exported column is classified — safe to run.");
325
+ }, Ie = async (t) => {
326
+ let { resolve: n, join: r } = await import("node:path"), i = await import("node:fs/promises"), a = z(t.args, "--chunk-size"), o = a === void 0 ? _e : Math.round(Number(a) * 1024 * 1024);
327
+ if (!Number.isFinite(o) || o <= 0) throw new e(`--chunk-size expects a positive number of megabytes, got '${String(a)}'`);
328
+ let s = t.stream[Symbol.asyncIterator](), c = [], l = 0, u = !1;
329
+ for (; l <= o;) {
330
+ let e = await s.next();
331
+ if (e.done === !0) {
332
+ u = !0;
333
+ break;
334
+ }
335
+ c.push(Buffer.from(e.value)), l += e.value.byteLength;
336
+ }
337
+ if (u && l <= o) return {
338
+ kind: "small",
339
+ body: Buffer.concat(c)
340
+ };
341
+ let d = n(process.cwd(), t.bundleDir), f = ye(K(t.bundleDir) ? await i.readFile(d) : await i.readFile(r(d, "manifest.json"))), p = `up-${f.slice(0, 32)}`, m = await j(t.url, Buffer.alloc(0), {
342
+ ...t.headers,
343
+ "x-import-upload-id": p,
344
+ "x-import-probe": "1"
345
+ }, Q(t.args)), h = m.status === 200 ? Number(JSON.parse(m.body.toString("utf8")).bytes ?? 0) : 0;
346
+ h > 0 && t.log.info("api import: resuming a chunked upload the instance already holds", { bytes: h });
347
+ let g = (async function* () {
348
+ for (let e of c) yield e;
349
+ for (;;) {
350
+ let e = await s.next();
351
+ if (e.done === !0) return;
352
+ yield e.value;
353
+ }
354
+ })(), _ = 0, v = await be({
355
+ stream: g,
356
+ chunkBytes: o,
357
+ uploadId: p,
358
+ key: f,
359
+ alreadyHeld: h,
360
+ baseHeaders: t.headers,
361
+ onProgress: (e) => {
362
+ _++, t.log.info(`api import: uploading — chunk ${_}, ${Math.round(e / 1024)} KiB sent`);
363
+ },
364
+ onFinalChunk: () => {
365
+ t.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.");
366
+ },
367
+ target: { send: async (e, n) => {
368
+ let r = await j(t.url, e, n, Q(t.args)), i = {};
369
+ try {
370
+ i = JSON.parse(r.body.toString("utf8") || "{}");
371
+ } catch {}
372
+ return {
373
+ status: r.status,
374
+ body: i
375
+ };
376
+ } }
377
+ });
378
+ return t.log.info("api import: uploaded in chunks", {
379
+ chunks: v.chunks,
380
+ bytes: v.bytes,
381
+ ...v.resumedFrom > 0 ? { resumedFrom: v.resumedFrom } : {}
382
+ }), {
383
+ kind: "sent",
384
+ res: {
385
+ status: v.status,
386
+ headers: {},
387
+ body: Buffer.from(JSON.stringify(v.body))
388
+ }
389
+ };
390
+ }, X = (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", Q = (t) => {
391
+ let n = z(t, "--timeout");
392
+ if (n === void 0) return {};
393
+ let r = Number(n);
394
+ if (!Number.isFinite(r) || r <= 0) throw new e(`--timeout expects a positive number of seconds, got '${n}'`);
395
+ return { timeoutMs: Math.round(r * 1e3) };
396
+ }, Le = async (e) => {
397
+ let t = z(e, "--target") === "api", n = z(e, "--bundle-key"), r = B(e);
398
+ if (!r && !(t && n)) return F.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;
399
+ if (t) {
400
+ let t = z(e, "--api-url");
401
+ if (!t) return F.error("--target api requires --api-url <url> (the running instance)"), 1;
402
+ let i = z(e, "--token") ?? process.env.VOLTRO_DATA_TRANSFER_SECRET;
403
+ if (!i) return F.error("--target api requires --token <secret> (or VOLTRO_DATA_TRANSFER_SECRET) matching the instance secret"), 1;
404
+ if (z(e, "--exclude") !== void 0) return F.error("--exclude is a direct-target flag: it expands against the live table list, which --target api does not expose. Use --tables with the explicit set."), 1;
405
+ let a = z(e, "--tables"), o = {
406
+ ...z(e, "--profile") ? { profile: z(e, "--profile") } : {},
407
+ ...z(e, "--tenant") ? { tenant: z(e, "--tenant") } : {},
408
+ ...a ? { tables: a.split(",").map((e) => e.trim()).filter(Boolean) } : {},
409
+ ...z(e, "--consistency") === "snapshot" ? { consistency: "snapshot" } : {},
410
+ ...z(e, "--compression") ? { compression: z(e, "--compression") } : {},
411
+ ...e.includes("--assets") ? { assets: !0 } : {},
412
+ ...n ? { bundleKey: n } : {}
413
+ }, s;
414
+ try {
415
+ s = await j(`${t.replace(/\/+$/, "")}/_voltro/admin/export`, JSON.stringify(o), {
416
+ "content-type": "application/json",
417
+ authorization: `Bearer ${i}`
418
+ }, Q(e));
419
+ } catch (e) {
420
+ if (e instanceof A) return F.error(`api export: the instance did not answer within ${Math.round(e.elapsedMs / 1e3)}s. That is a client-side deadline, not a server failure — the response arrives only when the export does. Nothing was written locally; re-run, or pass --timeout <seconds> (the default is to wait).`), 1;
421
+ throw e;
422
+ }
423
+ if (s.status < 200 || s.status >= 300) return F.error(`api export failed: HTTP ${s.status} — ${s.body.toString("utf8")}`), 1;
424
+ if (n) return F.info("api export complete (pushed to storage)", {
425
+ via: t,
426
+ ...JSON.parse(s.body.toString("utf8"))
427
+ }), 0;
428
+ let { resolve: c } = await import("node:path"), l = new Uint8Array(s.body);
429
+ return await k((async function* () {
430
+ yield l;
431
+ })(), c(process.cwd(), r)), F.info("api export complete (downloaded from the running instance)", {
432
+ outDir: r,
433
+ tables: s.headers["x-export-tables"],
434
+ rows: s.headers["x-export-rows"],
435
+ masked: s.headers["x-export-masked"]
436
+ }), 0;
437
+ }
438
+ if (!r) return F.error("a <outDir> is required for a direct export"), 1;
439
+ let i = z(e, "--profile"), a = i ? await s(i, { allowPath: !0 }) : {}, { store: o, snapshot: c, dialect: l } = await W(), u = e.includes("--assets") || a.assets === !0, d = u ? S({}) : void 0, p = z(e, "--compression") ?? a.compression, m = z(e, "--consistency") === "snapshot" ? "snapshot" : a.consistency ?? "live", h = a.masking, g = a.subset, _ = a.scope ?? je(e, c.tables.map((e) => e.name)), v = h ? await Me() : void 0, y = _.kind === "tenant" ? await Pe() : void 0, b = y ? { tenantTables: y } : {};
440
+ if (e.includes("--dry-run")) {
441
+ let e = _.kind === "tables" && _.tables ? [..._.tables] : c.tables.map((e) => e.name);
442
+ if (F.info("export DRY RUN — nothing was written", {
443
+ outDir: r,
444
+ tables: e.length,
445
+ scope: _.kind,
446
+ consistency: m,
447
+ compression: p ?? "zstd",
448
+ assets: u,
449
+ masking: h ? "profile policy applies" : "none"
450
+ }), F.info(`would export: ${e.join(", ")}`), !h) return 0;
451
+ let t = await f.runPromise(oe({
452
+ store: o,
453
+ snapshot: c,
454
+ masking: h,
455
+ ...v ? { classification: v } : {},
456
+ ...g ? {} : {
457
+ scope: _,
458
+ ...b
459
+ }
460
+ }));
461
+ return Fe(t), +(t.unclassified.length > 0);
462
+ }
463
+ if (K(r)) {
464
+ let t = await import("node:os"), n = await import("node:fs/promises"), { resolve: i, join: a } = await import("node:path"), s = await n.mkdtemp(a(t.tmpdir(), "voltro-export-"));
465
+ try {
466
+ let t = await f.runPromise(se({
467
+ store: o,
468
+ snapshot: c,
469
+ dialect: l,
470
+ outDir: s,
471
+ ...g ? { subset: g } : {
472
+ scope: _,
473
+ ...b
474
+ },
475
+ consistency: m,
476
+ ...p ? { compression: p } : {},
477
+ ...h ? { masking: h } : {},
478
+ ...v ? { classification: v } : {},
479
+ onProgress: L
480
+ })), n = T({
481
+ dir: s,
482
+ ...d ? { blobs: fe(d, () => G(o)) } : {}
483
+ }), a = ke(e);
484
+ if (a) {
485
+ let t = q(e);
486
+ if (!t) return F.error("--encrypt requires --passphrase <p> or VOLTRO_BUNDLE_KEY (a dedicated key, NOT the transfer secret)"), 1;
487
+ n = ae(n, t);
488
+ }
489
+ return await Ae(n, i(process.cwd(), r)), F.info("export complete (single file)", {
490
+ file: r,
491
+ encrypted: a,
492
+ assets: u,
493
+ tables: t.tables.length,
494
+ rows: t.tables.reduce((e, t) => e + t.rowCount, 0)
495
+ }), 0;
496
+ } finally {
497
+ await n.rm(s, {
498
+ recursive: !0,
499
+ force: !0
500
+ });
501
+ }
502
+ }
503
+ let x = await f.runPromise(se({
504
+ store: o,
505
+ snapshot: c,
506
+ dialect: l,
507
+ outDir: r,
508
+ ...g ? { subset: g } : {
509
+ scope: _,
510
+ ...b
511
+ },
512
+ consistency: m,
513
+ ...p ? { compression: p } : {},
514
+ ...h ? { masking: h } : {},
515
+ ...v ? { classification: v } : {},
516
+ ...d ? { assets: ue(d, () => G(o)) } : {},
517
+ onProgress: L
518
+ }));
519
+ return F.info("export complete", {
520
+ outDir: r,
521
+ tables: x.tables.length,
522
+ rows: x.tables.reduce((e, t) => e + t.rowCount, 0),
523
+ assets: x.assets.count,
524
+ compression: x.compression,
525
+ ...x.masking ? { maskedColumns: x.masking.transformed.length } : {}
526
+ }), 0;
527
+ }, Re = () => "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.", ze = (e) => {
528
+ let t = z(e, "--tables");
529
+ return {
530
+ ...t ? { tables: t.split(",").map((e) => e.trim()).filter(Boolean) } : {},
531
+ ...e.includes("--dry-run") ? { dryRun: !0 } : {}
532
+ };
533
+ }, Be = (e) => ({
534
+ tables: e.tablesWritten,
535
+ rows: e.rowsWritten,
536
+ carried: e.manifest.tables.reduce((e, t) => e + t.rowCount, 0),
537
+ ...e.tablesSkipped > 0 ? {
538
+ skippedTables: e.tablesSkipped,
539
+ skippedRows: e.rowsSkipped
540
+ } : {}
541
+ }), Ve = (e, t) => {
542
+ if (e.includes("--dry-run")) {
543
+ F.info("import DRY RUN — nothing was written", t), F.info(Re());
544
+ return;
545
+ }
546
+ if (t.rows === 0 && typeof t.skippedRows == "number" && t.skippedRows !== 0) {
547
+ F.info("import complete — 0 rows written, every table was already applied by an earlier run of this bundle directory", t);
548
+ return;
549
+ }
550
+ F.info("import complete", t);
551
+ }, He = (e) => {
552
+ let t = e;
553
+ if (t?._tag !== "RowsRefusedError" || !Array.isArray(t.rows)) return;
554
+ let n = t.rows, r = n.filter((e) => !e.derived), i = Number(t.totalCount ?? n.length), a = Number(t.primaryCount ?? r.length);
555
+ F.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.`);
556
+ for (let e of r) F.error(` ${e.table} ${e.id} ${e.reason}`);
557
+ let o = i - a;
558
+ o > 0 && F.error(` … and ${o} row(s) behind them. Fix the ${a} above and re-run; they resolve with their parents.`);
559
+ }, Ue = async (e) => {
560
+ let t = z(e, "--target") === "api", n = z(e, "--bundle-key"), i = B(e);
561
+ if (!i && !(t && n)) return F.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;
562
+ let a = z(e, "--mode") ?? "upsert", o = z(e, "--on-conflict") ?? "skip";
563
+ if (t) {
564
+ let t = z(e, "--api-url");
565
+ if (!t) return F.error("--target api requires --api-url <url> (the running instance)"), 1;
566
+ let r = z(e, "--token") ?? process.env.VOLTRO_DATA_TRANSFER_SECRET;
567
+ if (!r) return F.error("--target api requires --token <secret> (or VOLTRO_DATA_TRANSFER_SECRET) matching the instance secret"), 1;
568
+ let s = z(e, "--tables"), c = {
569
+ authorization: `Bearer ${r}`,
570
+ "x-import-mode": a,
571
+ "x-import-on-conflict": o,
572
+ ...Z(e, a) ? { "x-import-atomic": "1" } : {},
573
+ ...e.includes("--no-atomic") ? { "x-import-atomic": "0" } : {},
574
+ ...e.includes("--force") ? { "x-import-force": "1" } : {},
575
+ ...e.includes("--dry-run") ? { "x-import-dry-run": "1" } : {},
576
+ ...s ? { "x-import-tables": s } : {}
577
+ }, { resolve: l } = await import("node:path"), u;
578
+ if (n) c["content-type"] = "application/json", u = JSON.stringify({ bundleKey: n });
579
+ else if (K(i)) {
580
+ let t = J(l(process.cwd(), i)), n = q(e);
581
+ n && (t = w(t, n)), c["content-type"] = "application/octet-stream", u = t;
582
+ } else await (await import("node:fs/promises")).access(l(process.cwd(), i, "assets", "index.ndjson")).then(() => !0, () => !1) && F.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."), c["content-type"] = "application/octet-stream", u = T({ dir: l(process.cwd(), i) });
583
+ a === "replace" && !Z(e, a) && F.warn(X(!0));
584
+ let d = `${t.replace(/\/+$/, "")}/_voltro/admin/import`, f;
585
+ try {
586
+ let t = typeof u == "string" ? null : await Ie({
587
+ stream: u,
588
+ url: d,
589
+ headers: c,
590
+ bundleDir: i,
591
+ args: e,
592
+ log: F
593
+ });
594
+ (t === null || t.kind === "small") && F.info("api import: uploading the bundle — this request stays open until the instance has imported it"), f = t === null ? await j(d, u, c, Q(e)) : t.kind === "sent" ? t.res : await j(d, t.body, c, Q(e));
595
+ } catch (e) {
596
+ if (e instanceof A) return F.error(`api import: ${ge(e, a)}`), 1;
597
+ throw e;
598
+ }
599
+ if (f.status < 200 || f.status >= 300) return F.error(`api import failed: HTTP ${f.status} — ${f.body.toString("utf8")}`), 1;
600
+ let p = JSON.parse(f.body.toString("utf8"));
601
+ return e.includes("--dry-run") ? (F.info("api import DRY RUN — nothing was written", {
602
+ via: t,
603
+ ...p
604
+ }), F.info(Re()), 0) : (F.info("api import complete (imported by the running instance)", {
605
+ via: t,
606
+ ...p
607
+ }), 0);
608
+ }
609
+ if (!i) return F.error("a <bundleDir> is required for a direct import"), 1;
610
+ if (!await R(e)) return 1;
611
+ a === "replace" && e.includes("--allow-live") && !Z(e, a) && F.warn(X(!1));
612
+ let { store: s, snapshot: c } = await W(), l = e.includes("--force"), u = (e) => F.warn(e), d = e.includes("--assets") ? S({}) : void 0, p = V() === "postgres" ? (await import("@voltro/sql-postgres")).makePgCopySession(r()) : void 0;
613
+ if (K(i)) {
614
+ let t = await import("node:os"), n = await import("node:fs/promises"), { resolve: r, join: m } = await import("node:path"), h = await n.mkdtemp(m(t.tmpdir(), "voltro-import-"));
615
+ try {
616
+ let t = J(r(process.cwd(), i)), n = q(e);
617
+ return n && (t = w(t, n)), await pe(t, {
618
+ dir: h,
619
+ blobs: de(d ?? S({}))
620
+ }), Ve(e, {
621
+ file: i,
622
+ ...Be(await f.runPromise(ce({
623
+ store: s,
624
+ bundleDir: h,
625
+ mode: a,
626
+ onConflict: o,
627
+ atomic: Z(e, a),
628
+ verify: !e.includes("--no-verify"),
629
+ targetDialect: V(),
630
+ allowIncompatible: l,
631
+ targetSnapshot: c,
632
+ force: l,
633
+ onWarn: u,
634
+ onProgress: L,
635
+ assetsAlreadyRestored: !0,
636
+ ...ze(e),
637
+ ...p ? { copyLoader: p } : {}
638
+ })))
639
+ }), 0;
640
+ } finally {
641
+ await p?.close(), await n.rm(h, {
642
+ recursive: !0,
643
+ force: !0
644
+ });
645
+ }
646
+ }
647
+ let m = await f.runPromise(ce({
648
+ store: s,
649
+ bundleDir: i,
650
+ mode: a,
651
+ onConflict: o,
652
+ atomic: Z(e, a),
653
+ verify: !e.includes("--no-verify"),
654
+ targetDialect: V(),
655
+ allowIncompatible: l,
656
+ targetSnapshot: c,
657
+ force: l,
658
+ onWarn: u,
659
+ onProgress: L,
660
+ ...ze(e),
661
+ ...d ? { assets: le(d) } : {},
662
+ ...p ? { copyLoader: p } : {}
663
+ })).catch((e) => {
664
+ throw He(e), e;
665
+ }).finally(() => p?.close());
666
+ return Ve(e, {
667
+ bundleDir: i,
668
+ ...Be(m),
669
+ assets: m.manifest.assets.count
670
+ }), 0;
671
+ }, We = () => {
672
+ let e = r();
673
+ return {
674
+ ...e.url ? { url: e.url } : {},
675
+ ...e.host ? { host: e.host } : {},
676
+ ...e.port ? { port: e.port } : {},
677
+ ...e.username ? { username: e.username } : {},
678
+ ...e.password ? { password: e.password } : {},
679
+ ...e.database ? { database: e.database } : {}
680
+ };
681
+ }, Ge = async (e) => {
682
+ let t = B(e);
683
+ if (!t) return F.error("usage: voltro data backup <outDir> (native pg_dump/mysqldump/… + consistent artifact)"), 1;
684
+ let n = V(), { mkdir: r, writeFile: i } = await import("node:fs/promises"), { join: a } = await import("node:path");
685
+ await r(t, { recursive: !0 });
686
+ let o = a(t, C(n));
687
+ await f.runPromise(O(re(n, We(), o)));
688
+ let s = xe({
689
+ dialect: n,
690
+ schemaFingerprint: await I(n),
691
+ voltroVersion: await Te(),
692
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
693
+ });
694
+ return await i(a(t, M), `${JSON.stringify(s, null, 2)}\n`, "utf8"), F.info("native backup complete", {
695
+ dialect: n,
696
+ artifact: o,
697
+ stamp: M,
698
+ schemaFingerprint: s.schemaFingerprint ?? "<none>",
699
+ assets: "NOT included — use `voltro data export --assets` for rows + blobs"
700
+ }), 0;
701
+ }, Ke = async (e, t) => {
702
+ let n = V(), { join: r } = await import("node:path"), { readFile: i } = await import("node:fs/promises"), a = r(e, C(n)), o = Ce({
703
+ flagUrl: z(t, "--drill-url"),
704
+ envUrl: process.env.DRILL_DB_URL,
705
+ liveKeys: Oe()
706
+ });
707
+ if (!o.ok) return F.error(`restore --drill: ${o.error}`), 1;
708
+ let s = await i(r(e, M), "utf8").then(N, () => null);
709
+ if (s && s.dialect !== n) return F.error(`restore --drill: backup is a ${s.dialect} dump but DB_DIALECT is ${n} — cross-dialect restore is never valid.`), 1;
710
+ F.info("restore drill: restoring into throwaway DB", {
711
+ dialect: n,
712
+ target: P(o.url)
713
+ }), await f.runPromise(O(D(n, { url: o.url }, a)));
714
+ let c = null, l = 0;
715
+ try {
716
+ let e = await H(n, { url: o.url });
717
+ c = g(e), l = e.tables.length;
718
+ } catch (e) {
719
+ return F.error(`restore --drill: could not introspect the restored DB — ${String(e)}`), 1;
720
+ }
721
+ let u = we({
722
+ stampFingerprint: s?.schemaFingerprint ?? null,
723
+ restoredFingerprint: c,
724
+ restoredTableCount: l
725
+ });
726
+ for (let e of u.lines) u.pass ? F.info(`restore drill: ${e}`) : F.error(`restore drill: ${e}`);
727
+ return +!u.pass;
728
+ }, qe = async (e) => {
729
+ let t = B(e);
730
+ if (!t) return F.error("usage: voltro data restore <backupDir> [--allow-live] [--api-url <url>] [--force] [--drill [--drill-url <url>]] (native pg_restore/mysql/…)"), 1;
731
+ if (e.includes("--drill")) return Ke(t, e);
732
+ if (!await R(e)) return 1;
733
+ let n = V(), { join: r } = await import("node:path"), { readFile: i } = await import("node:fs/promises"), a = r(t, C(n)), o = Se({
734
+ stamp: await i(r(t, M), "utf8").then(N, () => null),
735
+ currentDialect: n,
736
+ currentFingerprint: await I(n)
737
+ });
738
+ for (let e of o.info) F.info(`backup stamp: ${e}`);
739
+ if (o.warn && F.warn(o.warn), o.refuse) {
740
+ if (!e.includes("--force")) return F.error(`restore refused: ${o.refuse} (pass --force to override)`), 1;
741
+ F.warn(`restore proceeding despite: ${o.refuse} (--force)`);
742
+ }
743
+ return await f.runPromise(O(D(n, We(), a))), F.info("native restore complete", {
744
+ dialect: n,
745
+ artifact: a
746
+ }), 0;
747
+ }, Je = async (e) => {
748
+ let t = B(e);
749
+ if (!t) return F.error("usage: voltro data inspect <file.vbundle|dir> [--json] [--passphrase <p>]"), 1;
750
+ let { resolve: n, join: r } = await import("node:path"), i = await import("node:fs/promises"), a = n(process.cwd(), t);
751
+ if ((await i.stat(a).catch(() => void 0))?.isDirectory()) {
752
+ let n = r(a, "manifest.json"), o = r(a, "README.md");
753
+ if (!e.includes("--json")) {
754
+ let e = await i.readFile(o, "utf8").catch(() => void 0);
755
+ if (e !== void 0) return console.log(e), 0;
756
+ }
757
+ let s = await i.readFile(n, "utf8").catch(() => void 0);
758
+ return s === void 0 ? (F.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);
759
+ }
760
+ let o = q(e), s = () => {
761
+ let e = J(a);
762
+ return o ? w(e, o) : e;
763
+ };
764
+ if (e.includes("--json")) {
765
+ let e = (await E(s(), ["manifest.json"])).get("manifest.json");
766
+ return e ? (console.log(e.toString("utf8")), 0) : (F.error("no manifest.json found in the bundle"), 1);
767
+ }
768
+ let c = (await E(s(), ["README.md"])).get("README.md");
769
+ if (c) return console.log(c.toString("utf8")), 0;
770
+ let l = (await E(s(), ["manifest.json"])).get("manifest.json");
771
+ return l ? (console.log(l.toString("utf8")), 0) : (F.error("bundle has no README.md or manifest.json"), 1);
772
+ }, Ye = async (e) => {
773
+ let t = B(e), n = B(e.filter((e) => e !== t));
774
+ if (!t || !n) return F.error("usage: voltro data unpack <file.vbundle> <dir> [--passphrase <p>]"), 1;
775
+ let { resolve: r } = await import("node:path"), i = J(r(process.cwd(), t)), a = q(e);
776
+ return a && (i = w(i, a)), await k(i, r(process.cwd(), n)), F.info("unpack complete", {
777
+ file: t,
778
+ dir: n
779
+ }), 0;
780
+ }, $ = {
781
+ export: {
782
+ common: [
783
+ "--target",
784
+ "--tables",
785
+ "--profile",
786
+ "--tenant",
787
+ "--assets",
788
+ "--compression",
789
+ "--consistency",
790
+ "--json"
791
+ ],
792
+ directOnly: [
793
+ "--exclude",
794
+ "--passphrase",
795
+ "--dry-run",
796
+ "--encrypt"
797
+ ],
798
+ apiOnly: [
799
+ "--api-url",
800
+ "--token",
801
+ "--bundle-key",
802
+ "--timeout"
803
+ ]
804
+ },
805
+ import: {
806
+ common: [
807
+ "--target",
808
+ "--tables",
809
+ "--mode",
810
+ "--on-conflict",
811
+ "--atomic",
812
+ "--no-atomic",
813
+ "--assets",
814
+ "--force",
815
+ "--dry-run",
816
+ "--json"
817
+ ],
818
+ directOnly: [
819
+ "--no-verify",
820
+ "--allow-live",
821
+ "--passphrase"
822
+ ],
823
+ apiOnly: [
824
+ "--api-url",
825
+ "--token",
826
+ "--bundle-key",
827
+ "--timeout",
828
+ "--chunk-size"
829
+ ]
830
+ },
831
+ unpack: { common: ["--passphrase", "--json"] },
832
+ inspect: { common: ["--json", "--passphrase"] },
833
+ backup: { common: ["--json", "--assets"] },
834
+ restore: { common: [
835
+ "--json",
836
+ "--allow-live",
837
+ "--force",
838
+ "--drill",
839
+ "--drill-url",
840
+ "--api-url"
841
+ ] }
842
+ }, Xe = (e) => e.split("=")[0], Ze = (e, t) => {
843
+ let n = $[e];
844
+ if (!n) return;
845
+ let r = z(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 ?? []);
846
+ for (let n of t) {
847
+ if (!n.startsWith("--")) continue;
848
+ let t = Xe(n);
849
+ if (i.has(t)) continue;
850
+ 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.`;
851
+ let o = Object.entries($).filter(([n, r]) => n !== e && [
852
+ ...r.common,
853
+ ...r.directOnly ?? [],
854
+ ...r.apiOnly ?? []
855
+ ].includes(t)).map(([e]) => e);
856
+ 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.`;
857
+ }
858
+ }, Qe = async (e) => {
859
+ let [t, ...n] = e;
860
+ if (t !== void 0 && $[t] !== void 0) {
861
+ let e = Ze(t, n);
862
+ if (e) return F.error(e), 2;
863
+ }
864
+ return t === "export" ? Le(n) : t === "import" ? Ue(n) : t === "unpack" ? Ye(n) : t === "inspect" ? Je(n) : t === "backup" ? Ge(n) : t === "restore" ? qe(n) : (F.error("usage: voltro data <export|import|unpack|inspect|backup|restore> … (logical: export/import; native: backup/restore)"), t ? 2 : 1);
865
+ };
866
+ //#endregion
867
+ export { Qe as runDataCommand };