@voltro/cli 0.31.0 → 0.33.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 (81) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/THIRD-PARTY-NOTICES.md +1 -29
  3. package/dist/apiBuild-C-x9YacA.js +2 -0
  4. package/dist/{apiBuild-CLPt-pX6.js → apiBuild-h9VHtnlw.js} +2 -2
  5. package/dist/bin.js +3 -3
  6. package/dist/checkCommand-DRovTKza.js +2 -0
  7. package/dist/{checkCommand-D7r6_pk7.js → checkCommand-xGhRFFg2.js} +330 -290
  8. package/dist/{commands-CNVVv03h.js → commands-CJfepbm4.js} +139 -13
  9. package/dist/{dbCommand-BH8GAYuf.js → dbCommand-DTLKAfbA.js} +431 -263
  10. package/dist/dbCommand-b1gum4td.js +2 -0
  11. package/dist/{dev-DdQEn2Ia.js → dev-C_P8FLSx.js} +2141 -2081
  12. package/dist/{dev-DOZVMVfi.js → dev-iiMtlkfs.js} +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/{inspectMetrics-BeEAnjqw.js → inspectMetrics-1xzTKAFx.js} +750 -685
  15. package/dist/{sdkgen-v9WV8zRi.js → sdkgen-C81QIkiL.js} +1 -1
  16. package/dist/serveCommand-DdaM4Hup.js +1608 -0
  17. package/dist/serveEntry.js +2 -2
  18. package/dist/{start-CI2QdKy9.js → start-C0koT0UO.js} +1 -1
  19. package/dist/startEntry.js +2 -2
  20. package/package.json +431 -18
  21. package/templates/AGENTS.md +1 -1
  22. package/templates/agent-docs/_index.md +1 -1
  23. package/templates/agent-docs/database/misc.md +37 -0
  24. package/templates/agent-docs/database/schema.md +4 -2
  25. package/templates/agent-docs/deployment.md +87 -1
  26. package/templates/agent-docs/plugins/audit.md +24 -0
  27. package/templates/agent-docs/plugins.md +50 -0
  28. package/templates/agent-docs/scheduling.md +4 -1
  29. package/templates/agent-docs/security.md +31 -1
  30. package/templates/agent-docs/whats-new.md +149 -59
  31. package/templates/apps/api-ai/package.json +7 -7
  32. package/templates/apps/api-auth/package.json +8 -8
  33. package/templates/apps/api-backend/package.json +7 -7
  34. package/templates/apps/api-backend-deactivation/package.json +7 -7
  35. package/templates/apps/api-backend-mail/package.json +8 -8
  36. package/templates/apps/api-backend-mariadb/package.json +9 -9
  37. package/templates/apps/api-backend-sqlite/package.json +8 -8
  38. package/templates/apps/api-backend-storage/package.json +8 -8
  39. package/templates/apps/api-cms/package.json +10 -10
  40. package/templates/apps/api-collab/package.json +8 -8
  41. package/templates/apps/api-data-advanced/package.json +8 -8
  42. package/templates/apps/api-durable/package.json +8 -8
  43. package/templates/apps/api-feature-flags/package.json +9 -9
  44. package/templates/apps/api-governance/package.json +8 -8
  45. package/templates/apps/api-kv/package.json +8 -8
  46. package/templates/apps/api-moderation/package.json +8 -8
  47. package/templates/apps/api-observability/package.json +8 -8
  48. package/templates/apps/api-ratelimit/package.json +8 -8
  49. package/templates/apps/api-rbac/package.json +8 -8
  50. package/templates/apps/api-rest/package.json +7 -7
  51. package/templates/apps/api-saas/package.json +11 -11
  52. package/templates/apps/api-saas-starter/package.json +10 -10
  53. package/templates/apps/api-search/package.json +8 -8
  54. package/templates/apps/api-status/package.json +8 -8
  55. package/templates/apps/api-versioning/package.json +8 -8
  56. package/templates/apps/api-webhooks/package.json +9 -9
  57. package/templates/apps/changelog/package.json +6 -6
  58. package/templates/apps/edge-functions/package.json +2 -2
  59. package/templates/apps/frontend-admin/package.json +8 -8
  60. package/templates/apps/frontend-app/package.json +8 -8
  61. package/templates/apps/frontend-auth/package.json +8 -8
  62. package/templates/apps/frontend-blank/package.json +7 -7
  63. package/templates/apps/frontend-cms/package.json +9 -9
  64. package/templates/apps/frontend-collab/package.json +9 -9
  65. package/templates/apps/frontend-contact/package.json +7 -7
  66. package/templates/apps/frontend-dashboard/package.json +7 -7
  67. package/templates/apps/frontend-docs/package.json +7 -7
  68. package/templates/apps/frontend-i18n/package.json +6 -6
  69. package/templates/apps/frontend-landing/package.json +7 -7
  70. package/templates/apps/frontend-portal/package.json +8 -8
  71. package/templates/apps/frontend-saas/package.json +8 -8
  72. package/templates/apps/frontend-spa/package.json +7 -7
  73. package/templates/apps/frontend-ssr/package.json +7 -7
  74. package/templates/apps/frontend-ssr-api/package.json +8 -8
  75. package/templates/apps/frontend-static-blog/package.json +6 -6
  76. package/templates/apps/frontend-status/package.json +8 -8
  77. package/templates/apps/mobile-app/package.json +2 -2
  78. package/dist/apiBuild-BOqL1dlg.js +0 -2
  79. package/dist/checkCommand-ClRvH_SN.js +0 -2
  80. package/dist/dbCommand-BwQ_Jl98.js +0 -2
  81. package/dist/serveCommand-KsKXXWn4.js +0 -1578
@@ -8,19 +8,19 @@ import { pathToFileURL as h } from "node:url";
8
8
  import { Effect as g } from "effect";
9
9
  import { createLogger as _ } from "@voltro/logger";
10
10
  import { promises as v, writeSync as y } from "node:fs";
11
- import { InMemoryDataStore as ee } from "@voltro/runtime";
12
- import { SqlClient as b } from "@effect/sql";
13
- import { actorsTable as te, frameworkTables as x, isMigrationDefinition as ne, quoteIdent as S, registerCoreTables as re } from "@voltro/database";
14
- import { createHash as ie } from "node:crypto";
15
- import { DEFAULT_CDC_CHANNEL as ae, applyPlan as C, applySchema as w, assessRollingDeployGate as oe, declaredSnapshot as se, destructiveScope as ce, detectReactiveTriggerDrift as le, fingerprintSchema as T, ignoreTablesFromEnv as E, introspectSchema as ue, pendingFileMigrationIds as de, planMigrations as D, reactiveTriggerRepairSql as fe, rollbackFileBasedMigration as pe, rollingDeployUnsafeOps as me, runFileBasedMigrations as he, shortFingerprint as O, squashMigrationPlans as ge, unblockLossy as _e } from "@voltro/database/sql";
11
+ import { InMemoryDataStore as b, makeFieldCipher as ee } from "@voltro/runtime";
12
+ import { SqlClient as x } from "@effect/sql";
13
+ import { actorsTable as te, frameworkTables as S, isEncrypted as C, isMigrationDefinition as ne, quoteIdent as re, registerCoreTables as ie } from "@voltro/database";
14
+ import { createHash as ae } from "node:crypto";
15
+ import { DEFAULT_CDC_CHANNEL as oe, applyPlan as se, applySchema as w, assessRollingDeployGate as ce, declaredSnapshot as le, destructiveScope as ue, detectReactiveTriggerDrift as de, fingerprintSchema as fe, ignoreTablesFromEnv as T, introspectSchema as pe, pendingFileMigrationIds as me, planMigrations as E, reactiveTriggerRepairSql as he, rollbackFileBasedMigration as ge, rollingDeployUnsafeOps as _e, runFileBasedMigrations as ve, shortFingerprint as D, squashMigrationPlans as ye, unblockLossy as be } from "@voltro/database/sql";
16
16
  //#region src/cliError.ts
17
- var k = class extends Error {
17
+ var O = class extends Error {
18
18
  isCliError = !0;
19
19
  code;
20
20
  constructor(e, t = 1) {
21
21
  super(e), this.name = "CliError", this.code = t;
22
22
  }
23
- }, ve = (e) => typeof e == "object" && !!e && e.isCliError === !0, ye = (e) => typeof e != "string", be = (e, t, n, r) => {
23
+ }, xe = (e) => typeof e == "object" && !!e && e.isCliError === !0, Se = (e) => typeof e != "string", Ce = (e, t, n, r) => {
24
24
  let i = [], a = new Map(t.map((e) => [e.name, e])), o = new Map(n.map((e) => [e.name, e])), s = new Set(e.leaveUnset ?? []);
25
25
  r > 0 && i.push({
26
26
  kind: "target-not-empty",
@@ -37,7 +37,7 @@ var k = class extends Error {
37
37
  });
38
38
  continue;
39
39
  }
40
- if (ye(n)) {
40
+ if (Se(n)) {
41
41
  c.push({
42
42
  target: t,
43
43
  source: n.expr,
@@ -80,23 +80,23 @@ var k = class extends Error {
80
80
  remintIds: f,
81
81
  fatal: i.some((e) => e.fatal)
82
82
  };
83
- }, xe = (e, t) => [
83
+ }, we = (e, t) => [
84
84
  `back up '${e.from}' to a restorable dump`,
85
85
  `copy rows: INSERT INTO ${e.into} (${t.assignments.map((e) => e.target).join(", ")}) SELECT … FROM ${e.from}`,
86
86
  ...t.remintIds ? ["record old id → new id in a translation table", `rewrite every reference to '${e.from}', JSON columns included`] : [],
87
87
  "verify: row counts match and a sample round-trips",
88
88
  `drop '${e.from}' — LAST, and only after the verify`
89
- ], Se = (e, t, n) => {
89
+ ], Te = (e, t, n) => {
90
90
  let r = [`adopt · ${e.from} (${n} row(s)) → ${e.into}`, ""];
91
91
  for (let e of t.assignments) r.push(` ${e.target.padEnd(24)} ← ${e.raw ? `${e.source} (raw)` : e.source}`);
92
92
  t.problems.length > 0 && r.push("");
93
93
  for (let e of t.problems) r.push(` ${e.fatal ? "✗" : "⚠"} ${e.detail}`);
94
94
  if (r.push(""), t.fatal) return r.push(" REFUSED — fix the problems above. Nothing was written."), r;
95
95
  r.push(" would run, in this order:");
96
- for (let [n, i] of xe(e, t).entries()) r.push(` ${n + 1}. ${i}`);
96
+ for (let [n, i] of we(e, t).entries()) r.push(` ${n + 1}. ${i}`);
97
97
  return r;
98
- }, Ce = (e) => e === "mysql" || e === "mariadb" ? (e) => `\`${e}\`` : e === "mssql" ? (e) => `[${e}]` : (e) => `"${e}"`, we = (e) => `${e}__adopt_snapshot`, Te = (e, t, n, r, i) => {
99
- let a = Ce(n), o = we(e.from), s = t.assignments.map((e) => e.target), c = t.assignments.map((e) => e.raw ? e.source : a(e.source));
98
+ }, Ee = (e) => e === "mysql" || e === "mariadb" ? (e) => `\`${e}\`` : e === "mssql" ? (e) => `[${e}]` : (e) => `"${e}"`, De = (e) => `${e}__adopt_snapshot`, Oe = (e, t, n, r, i) => {
99
+ let a = Ee(n), o = De(e.from), s = t.assignments.map((e) => e.target), c = t.assignments.map((e) => e.raw ? e.source : a(e.source));
100
100
  return i !== void 0 && (s.unshift(r), c.unshift(i)), {
101
101
  snapshot: n === "mssql" ? `SELECT * INTO ${a(o)} FROM ${a(e.from)}` : `CREATE TABLE ${a(o)} AS SELECT * FROM ${a(e.from)}`,
102
102
  copy: `INSERT INTO ${a(e.into)} (${s.map(a).join(", ")}) SELECT ${c.join(", ")} FROM ${a(e.from)}`,
@@ -106,18 +106,18 @@ var k = class extends Error {
106
106
  drop: `DROP TABLE ${a(e.from)}`,
107
107
  dropSnapshot: `DROP TABLE ${a(o)}`
108
108
  };
109
- }, A = (e) => {
109
+ }, ke = (e) => {
110
110
  let t = e[0];
111
111
  if (t === void 0) return 0;
112
112
  let n = t.n ?? t.N ?? Object.values(t)[0];
113
113
  return Number(n ?? 0);
114
- }, Ee = async (e, t, n, r, i, a, o = {}) => {
114
+ }, Ae = async (e, t, n, r, i, a, o = {}) => {
115
115
  if (t.fatal) throw Error("adopt: the plan was refused — nothing was run");
116
- let s = Te(e, t, n, r, i), c = we(e.from), l = A(await a(s.verifySource));
116
+ let s = Oe(e, t, n, r, i), c = De(e.from), l = ke(await a(s.verifySource));
117
117
  await a(s.snapshot);
118
118
  for (let e of s.translations) await a(e);
119
119
  await a(s.copy);
120
- let u = A(await a(s.verifyTarget));
120
+ let u = ke(await a(s.verifyTarget));
121
121
  return u === l ? o.keepSource === !0 ? {
122
122
  copied: u,
123
123
  sourceRows: l,
@@ -137,7 +137,7 @@ var k = class extends Error {
137
137
  snapshotTable: c,
138
138
  sourceDropped: !1
139
139
  };
140
- }, De = [
140
+ }, k = [
141
141
  "token",
142
142
  "secret",
143
143
  "password",
@@ -146,7 +146,7 @@ var k = class extends Error {
146
146
  "credential",
147
147
  "privatekey",
148
148
  "private_key"
149
- ], Oe = [
149
+ ], je = [
150
150
  {
151
151
  table: "_voltro_audit_log",
152
152
  column: "subject"
@@ -191,7 +191,7 @@ var k = class extends Error {
191
191
  table: "_voltro_row_history",
192
192
  column: "subjectId"
193
193
  }
194
- ], ke = (e, t) => {
194
+ ], Me = (e, t) => {
195
195
  switch (t) {
196
196
  case "postgres": return `LOWER(CAST(${e} AS text))`;
197
197
  case "mysql":
@@ -200,15 +200,55 @@ var k = class extends Error {
200
200
  case "sqlite":
201
201
  case "turso": return `LOWER(CAST(${e} AS TEXT))`;
202
202
  }
203
- }, Ae = (e, t, n = De) => n.map((n) => `${ke(e, t)} LIKE '%${n}%'`).join("\n OR "), je = async (e, t) => {
203
+ }, A = (e, t, n = k) => n.map((n) => `${Me(e, t)} LIKE '%${n}%'`).join("\n OR "), Ne = ["_omitted", "__redacted"], Pe = (e, t) => {
204
+ let n = e.toLowerCase();
205
+ return t.some((e) => n.includes(e));
206
+ }, j = (e, t = []) => {
207
+ if (Array.isArray(e)) {
208
+ for (let n of e) j(n, t);
209
+ return t;
210
+ }
211
+ if (typeof e == "object" && e) for (let [n, r] of Object.entries(e)) t.push(n), j(r, t);
212
+ return t;
213
+ }, M = (e) => Array.isArray(e) ? e.map(M) : typeof e == "object" && e ? Object.fromEntries(Object.entries(e).map(([e, t]) => [e, Ne.includes(e) ? null : M(t)])) : e, Fe = (e, t = k) => {
214
+ let n;
215
+ try {
216
+ n = JSON.parse(e);
217
+ } catch {
218
+ return {
219
+ asKey: !1,
220
+ redactionMarkerOnly: !1,
221
+ undecidable: !0
222
+ };
223
+ }
224
+ return {
225
+ asKey: j(n).some((e) => Pe(e, t)),
226
+ redactionMarkerOnly: !Pe(JSON.stringify(M(n)) ?? "", t),
227
+ undecidable: !1
228
+ };
229
+ }, Ie = (e) => ({
230
+ examined: e.length,
231
+ asKey: e.filter((e) => e.asKey).length,
232
+ redactionMarkerOnly: e.filter((e) => e.redactionMarkerOnly && !e.asKey).length,
233
+ undecidable: e.filter((e) => e.undecidable).length
234
+ }), N = (e) => {
235
+ let t = e.classification;
236
+ return t === void 0 || e.hits === void 0 || e.hits === 0 ? !1 : t.examined === e.hits && t.undecidable === 0 && t.asKey === 0 && t.redactionMarkerOnly === e.hits;
237
+ }, Le = (e, t, n, r) => {
238
+ let i = `WHERE ${A(t, n)}`, a = Me(t, n).replace(/^LOWER\((.*)\)$/s, "$1");
239
+ return n === "mssql" ? `SELECT TOP (${r}) ${a} AS v FROM ${e}\n ${i}` : `SELECT ${a} AS v FROM ${e}\n ${i}\n LIMIT ${r}`;
240
+ }, Re = async (e, t, n, r, i) => (await g.runPromise(e.unsafe(Le(t, n, r, i)))).map((e) => {
241
+ let t = Object.values(e)[0];
242
+ return typeof t == "string" ? t : JSON.stringify(t ?? null);
243
+ }), P = async (e, t) => {
204
244
  let n = await g.runPromise(e.unsafe(t));
205
245
  return Number(Object.values(n[0] ?? { n: 0 })[0] ?? 0);
206
- }, Me = async (e, t, n, r) => {
246
+ }, ze = async (e, t, n, r) => {
207
247
  let i = [];
208
248
  for (let a of n) {
209
249
  let n = r(a.table), o;
210
250
  try {
211
- o = await je(e, `SELECT COUNT(*) AS n FROM ${n}`);
251
+ o = await P(e, `SELECT COUNT(*) AS n FROM ${n}`);
212
252
  } catch {
213
253
  i.push({
214
254
  table: a.table,
@@ -220,12 +260,25 @@ var k = class extends Error {
220
260
  continue;
221
261
  }
222
262
  try {
223
- let s = await je(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${Ae(r(a.column), t)}`);
263
+ let s = await P(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${A(r(a.column), t)}`), c = [];
264
+ if (s > 0) for (let i of k) try {
265
+ let o = await P(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${A(r(a.column), t, [i])}`);
266
+ o > 0 && c.push({
267
+ needle: i,
268
+ rows: o
269
+ });
270
+ } catch {}
271
+ let l;
272
+ if (s > 0) try {
273
+ l = Ie((await Re(e, n, r(a.column), t, 500)).map((e) => Fe(e)));
274
+ } catch {}
224
275
  i.push({
225
276
  table: a.table,
226
277
  column: a.column,
227
278
  scanned: o,
228
- hits: s
279
+ hits: s,
280
+ matchedNeedles: c,
281
+ ...l === void 0 ? {} : { classification: l }
229
282
  });
230
283
  } catch {
231
284
  i.push({
@@ -238,27 +291,29 @@ var k = class extends Error {
238
291
  }
239
292
  }
240
293
  return i;
241
- }, Ne = (e, t) => {
242
- let n = [`credential scan · dialect ${t}`, ""], r = /* @__PURE__ */ new Set();
294
+ }, Be = (e, t) => {
295
+ let n = k.length, r = [`credential scan · dialect ${t}`, ""], i = /* @__PURE__ */ new Set();
243
296
  for (let t of e) {
244
297
  if (t.unavailable === "column") {
245
- n.push(` · ${t.table}.${t.column} — column is not present on this deployment (${t.scanned} row(s) exist but this column was not scanned)`);
298
+ r.push(` · ${t.table}.${t.column} — column is not present on this deployment (${t.scanned} row(s) exist but this column was not scanned)`);
246
299
  continue;
247
300
  }
248
301
  if (t.scanned === void 0) {
249
- r.has(t.table) || (r.add(t.table), n.push(` · ${t.table} — table not present (nothing scanned)`));
302
+ i.has(t.table) || (i.add(t.table), r.push(` · ${t.table} — table not present (nothing scanned)`));
250
303
  continue;
251
304
  }
252
305
  if (t.scanned === 0) {
253
- n.push(` · ${t.table} — table is EMPTY (0 rows scanned; this is not a clean bill of health)`);
306
+ r.push(` · ${t.table} — table is EMPTY (0 rows scanned; this is not a clean bill of health)`);
254
307
  continue;
255
308
  }
256
- let e = (t.hits ?? 0) > 0 ? "" : "";
257
- n.push(` ${e} ${t.table}.${t.column} — ${t.hits} of ${t.scanned} row(s) match a credential-shaped key`);
309
+ let e = N(t), n = (t.hits ?? 0) === 0 ? "" : e ? "~" : "✗";
310
+ r.push(` ${n} ${t.table}.${t.column} — ${t.hits} of ${t.scanned} row(s) contain a credential-shaped NAME (anywhere in the value)`), (t.matchedNeedles?.length ?? 0) > 0 && r.push(` matched (rows per needle, may overlap): ${t.matchedNeedles.map((e) => `${e.needle} (${e.rows})`).join(", ")}`);
311
+ let a = t.classification;
312
+ a !== void 0 && (r.push(` examined ${a.examined} of ${t.hits} matched row(s)${a.examined < (t.hits ?? 0) ? " (capped at 500)" : ""}: ${a.asKey} as a JSON KEY, ${a.redactionMarkerOnly} only inside a redaction marker${a.undecidable > 0 ? `, ${a.undecidable} not parseable as JSON` : ""}`), e && r.push(` EXPLAINED — every match is \`${Ne.join("` / `")}\`, the framework's own`, " record that a `.sensitive()` column was left OUT. Nothing to purge, nothing to rotate."));
258
313
  }
259
- let i = e.reduce((e, t) => e + (t.hits ?? 0), 0);
260
- return n.push(""), i > 0 ? n.push(` ${i} row(s) carry something credential-shaped.`, " Purge them AND rotate the credentials — assume anything written to a log", " table has been read. Then stop putting the credential on the Subject:", " `connectionCredentials(...)` keeps it in the framework vault.") : e.every((e) => e.scanned === void 0 || e.scanned === 0 || e.unavailable !== void 0) ? n.push(" NOTHING WAS SCANNED. This run proves nothing about your data.") : n.push(" No credential-shaped keys found in the rows that exist."), n.push("", " Run this on EVERY environment. A dev database is not a sample of production."), n.join("\n");
261
- }, Pe = (e) => e.some((e) => (e.hits ?? 0) > 0) ? 1 : e.every((e) => e.unavailable !== void 0 || e.scanned === void 0 || e.scanned === 0) ? 2 : 0, j = /\.migration\.ts$/, M = async (e, t) => {
314
+ let a = e.reduce((e, t) => e + (N(t) ? 0 : t.hits ?? 0), 0), o = e.reduce((e, t) => e + (N(t) ? t.hits ?? 0 : 0), 0);
315
+ return r.push(""), o > 0 && r.push(` ${o} row(s) matched only the framework's own redaction markers and are NOT findings.`), a > 0 ? r.push(` ${a} row(s) carry something credential-shaped.`, " Purge them AND rotate the credentials — assume anything written to a log", " table has been read. Then stop putting the credential on the Subject:", " `connectionCredentials(...)` keeps it in the framework vault.") : e.every((e) => e.scanned === void 0 || e.scanned === 0 || e.unavailable !== void 0) ? r.push(" NOTHING WAS SCANNED. This run proves nothing about your data.") : r.push(" No credential-shaped keys found in the rows that exist."), r.push("", ` These are NAME heuristics (${n} of them), not proof. A credential in a`, " column whose key is not credential-shaped — `keyValue`, `pat`, `authz` —", " is invisible here. A clean run means \"no credential-shaped NAME found\",", " which is not the same as \"no credential\".", "", " Run this on EVERY environment. A dev database is not a sample of production."), r.join("\n");
316
+ }, Ve = (e) => e.some((e) => (e.hits ?? 0) > 0 && !N(e)) ? 1 : e.every((e) => e.unavailable !== void 0 || e.scanned === void 0 || e.scanned === 0) ? 2 : 0, F = /\.migration\.ts$/, I = async (e, t) => {
262
317
  let n = [];
263
318
  for (let r of e) {
264
319
  let e;
@@ -278,7 +333,7 @@ var k = class extends Error {
278
333
  idInFile: i.id,
279
334
  idFromFile: d(r).replace(/\.migration\.ts$/, "")
280
335
  });
281
- let a = await v.readFile(r, "utf8").catch(() => ""), o = ie("sha256").update(a).digest("hex").slice(0, 16);
336
+ let a = await v.readFile(r, "utf8").catch(() => ""), o = ae("sha256").update(a).digest("hex").slice(0, 16);
282
337
  n.push({
283
338
  file: r,
284
339
  definition: i,
@@ -286,14 +341,14 @@ var k = class extends Error {
286
341
  });
287
342
  }
288
343
  return [...n].sort((e, t) => e.definition.id < t.definition.id ? -1 : 1);
289
- }, N = (e) => {
344
+ }, L = (e) => {
290
345
  let t = (process.env.DB_DIALECT ?? "postgres").toLowerCase(), n = t === "memory" ? "postgres" : t;
291
- return g.runPromise(w(x, n).pipe(g.provide(e)));
292
- }, P = (e) => g.runPromise(g.gen(function* () {
293
- let e = yield* (yield* b.SqlClient)`SELECT id, status, hash, "appliedAt" FROM "_voltro_migrations"`.pipe(g.orElseSucceed(() => []));
346
+ return g.runPromise(w(S, n).pipe(g.provide(e)));
347
+ }, R = (e) => g.runPromise(g.gen(function* () {
348
+ let e = yield* (yield* x.SqlClient)`SELECT id, status, hash, "appliedAt" FROM "_voltro_migrations"`.pipe(g.orElseSucceed(() => []));
294
349
  return new Map(e.map((e) => [e.id, e]));
295
- }).pipe(g.provide(e))), F = (e, t) => g.runPromise(g.gen(function* () {
296
- yield* (yield* b.SqlClient)`
350
+ }).pipe(g.provide(e))), z = (e, t) => g.runPromise(g.gen(function* () {
351
+ yield* (yield* x.SqlClient)`
297
352
  INSERT INTO "_voltro_migrations"
298
353
  (id, name, hash, status, "appliedAt", "durationMs", "errorMessage", "errorStack")
299
354
  VALUES
@@ -308,14 +363,14 @@ var k = class extends Error {
308
363
  "errorStack" = EXCLUDED."errorStack",
309
364
  "updatedAt" = now()
310
365
  `;
311
- }).pipe(g.provide(e))), Fe = async (e, t, n) => {
366
+ }).pipe(g.provide(e))), He = async (e, t, n) => {
312
367
  if (e.length === 0) return {
313
368
  applied: [],
314
369
  failed: null,
315
370
  skipped: []
316
371
  };
317
- await N(t);
318
- let r = await P(t), i = [], a = [], o = null;
372
+ await L(t);
373
+ let r = await R(t), i = [], a = [], o = null;
319
374
  for (let s of e) {
320
375
  let e = r.get(s.definition.id);
321
376
  if (e && e.status === "applied") {
@@ -336,7 +391,7 @@ var k = class extends Error {
336
391
  name: s.definition.name
337
392
  });
338
393
  let c = Date.now(), l = [];
339
- await F(t, {
394
+ await z(t, {
340
395
  id: s.definition.id,
341
396
  name: s.definition.name,
342
397
  hash: s.hash,
@@ -377,7 +432,7 @@ var k = class extends Error {
377
432
  ...a.stack ? { stack: a.stack } : {}
378
433
  }
379
434
  };
380
- o = f, i.push(f), await F(t, {
435
+ o = f, i.push(f), await z(t, {
381
436
  id: s.definition.id,
382
437
  name: s.definition.name,
383
438
  hash: s.hash,
@@ -396,7 +451,7 @@ var k = class extends Error {
396
451
  status: "applied",
397
452
  durationMs: Date.now() - c,
398
453
  steps: l
399
- }), await F(t, {
454
+ }), await z(t, {
400
455
  id: s.definition.id,
401
456
  name: s.definition.name,
402
457
  hash: s.hash,
@@ -416,9 +471,9 @@ var k = class extends Error {
416
471
  failed: o,
417
472
  skipped: a
418
473
  };
419
- }, Ie = async (e, t, n, r = {}) => {
420
- await N(t);
421
- let i = await P(t), a = e.filter((e) => i.get(e.definition.id)?.status === "applied");
474
+ }, Ue = async (e, t, n, r = {}) => {
475
+ await L(t);
476
+ let i = await R(t), a = e.filter((e) => i.get(e.definition.id)?.status === "applied");
422
477
  if (a.length === 0) return n.info("migration: nothing to roll back"), {
423
478
  applied: [],
424
479
  failed: null,
@@ -471,7 +526,7 @@ var k = class extends Error {
471
526
  status: "applied",
472
527
  durationMs: Date.now() - r,
473
528
  steps: i
474
- }), await F(t, {
529
+ }), await z(t, {
475
530
  id: e.definition.id,
476
531
  name: e.definition.name,
477
532
  hash: e.hash,
@@ -487,9 +542,9 @@ var k = class extends Error {
487
542
  failed: c,
488
543
  skipped: []
489
544
  };
490
- }, I = async (e, t) => {
491
- await N(t);
492
- let n = await P(t);
545
+ }, We = async (e, t) => {
546
+ await L(t);
547
+ let n = await R(t);
493
548
  return e.map((e) => {
494
549
  let t = n.get(e.definition.id);
495
550
  return t ? {
@@ -506,7 +561,7 @@ var k = class extends Error {
506
561
  hashMatches: !0
507
562
  };
508
563
  });
509
- }, L = (e) => {
564
+ }, Ge = (e) => {
510
565
  if (!e.failed) return "";
511
566
  let t = e.failed, n = [
512
567
  `✗ Migration failed: ${t.migrationId} / step '${t.error?.stepName ?? "?"}'`,
@@ -519,7 +574,59 @@ var k = class extends Error {
519
574
  ""
520
575
  ];
521
576
  return e.applied.length > 0 && n.unshift("", `(${e.applied.length} migration(s) applied successfully before the failure.)`), n.join("\n");
522
- }, R = _({ scope: "voltro:db" }), Le = /* @__PURE__ */ new Set([
577
+ }, Ke = (e) => {
578
+ let t = e.split(".");
579
+ if (t.length !== 2 || !t[0] || !t[1]) throw Error(`voltro db encrypt-column: expected <table>.<column>, got "${e}"`);
580
+ return {
581
+ table: t[0],
582
+ column: t[1]
583
+ };
584
+ }, qe = (e) => 49 + 4 * Math.ceil(e / 3), Je = (e, t) => {
585
+ let n = qe(e);
586
+ return {
587
+ longestPlaintextBytes: e,
588
+ longestCiphertext: n,
589
+ declaredMaxLength: t,
590
+ fits: t === void 0 || n <= t
591
+ };
592
+ }, Ye = (e, t) => `${e.table}.${e.column} is too narrow: its longest value is ${t.longestPlaintextBytes} byte(s), which encrypts to ${t.longestCiphertext} characters, and the column is declared as ${t.declaredMaxLength}. Widen it first (\`.maxLength(${t.longestCiphertext})\` or more, then \`voltro db apply\`) and run this again. Nothing was written.`, Xe = (e, t) => {
593
+ let n = t ? "would encrypt" : "encrypted";
594
+ return [
595
+ ` ${e.table}.${e.column}`,
596
+ ` scanned ${e.scanned}`,
597
+ ` already encrypted ${e.alreadyEncrypted} (skipped — a re-run is safe)`,
598
+ ` null / empty ${e.empty}`,
599
+ ` ${n.padEnd(18)} ${e.encrypted}`
600
+ ].join("\n");
601
+ }, Ze = (e, t, n) => {
602
+ if (e == null) return;
603
+ let r = typeof e == "string" ? e : String(e);
604
+ if (r === "" || C(r)) return;
605
+ let i = t.encrypt(r), a;
606
+ try {
607
+ a = t.decrypt(i);
608
+ } catch (e) {
609
+ throw Error(`${n.table}.${n.column}: the cipher produced a value it cannot read back (${e.message ?? String(e)}). Nothing was written.`);
610
+ }
611
+ if (a !== r) throw Error(`${n.table}.${n.column}: round-trip mismatch — the cipher did not return what it was given. Nothing was written.`);
612
+ return i;
613
+ }, Qe = (e) => Buffer.byteLength(e, "utf8"), $e = (e, t) => {
614
+ if (e !== void 0) try {
615
+ return t.decrypt(e), !0;
616
+ } catch {
617
+ return !1;
618
+ }
619
+ }, et = (e, t) => `${e.table}.${e.column} already holds values encrypted with a DIFFERENT key than ${t} — the active key cannot decrypt them. Continuing would encrypt the remaining rows under a second key, and the column would then be readable with neither key alone. Nothing was written. Point ${t} at the key the existing rows were written with.`, tt = (e) => [
620
+ "",
621
+ " NEXT — this converted the rows that exist. New writes are still plaintext",
622
+ " until the column carries the modifier:",
623
+ "",
624
+ ...e.map((e) => ` ${e.column}: text().encrypted() // ${e.table}`),
625
+ "",
626
+ " Order does not matter: a read returns a non-ciphertext value unchanged, so",
627
+ " the column may hold a mix while you deploy. Run this again afterwards to",
628
+ " catch anything written in between — it skips what is already encrypted."
629
+ ].join("\n"), B = _({ scope: "voltro:db" }), nt = /* @__PURE__ */ new Set([
523
630
  "node_modules",
524
631
  "dist",
525
632
  "build",
@@ -527,17 +634,17 @@ var k = class extends Error {
527
634
  ".vite",
528
635
  ".next",
529
636
  ".git"
530
- ]), z = async (e, t) => {
637
+ ]), V = async (e, t) => {
531
638
  let n = [], r = async (e) => {
532
639
  let i = await v.readdir(e, { withFileTypes: !0 }).catch(() => []);
533
640
  for (let a of i) {
534
- if (Le.has(a.name)) continue;
641
+ if (nt.has(a.name)) continue;
535
642
  let i = f(e, a.name);
536
643
  a.isDirectory() ? await r(i) : a.isFile() && t.test(a.name) && n.push(i);
537
644
  }
538
645
  };
539
646
  return await r(e), n;
540
- }, B = (e) => m(e.find((e) => !e.startsWith("-")) ?? process.cwd()), V = () => {
647
+ }, H = (e) => m(e.find((e) => !e.startsWith("-")) ?? process.cwd()), U = () => {
541
648
  let e = process.env.DB_MAX_CONNECTIONS ?? process.env.PG_MAX_CONNECTIONS, t = e !== void 0 && e !== "" ? Number(e) : NaN, n = Number.isInteger(t) && t > 0 ? { maxConnections: t } : {}, r = process.env.DB_SCHEMA, i = r && r.length > 0 ? { schema: r } : {}, a = process.env.DB_DIRECT_URL ?? process.env.DB_MIGRATE_URL ?? process.env.DB_URL ?? process.env.DB_PRIMARY_URL;
542
649
  return a ? {
543
650
  url: a,
@@ -552,63 +659,63 @@ var k = class extends Error {
552
659
  ...n,
553
660
  ...i
554
661
  };
555
- }, H = (e) => {
662
+ }, rt = (e) => {
556
663
  let t = e;
557
664
  if (typeof t != "object" || !t) return !1;
558
665
  if (t.code === "ERR_OUT_OF_RANGE" || t.cause?.code === "ERR_OUT_OF_RANGE") return !0;
559
666
  let n = `${t.message ?? ""} ${t.cause?.message ?? ""} ${t.stack ?? ""}`;
560
667
  return /ERR_OUT_OF_RANGE|out of range/i.test(n) && /pg-protocol|parseDataRow|buffer-reader|BufferReader/i.test(n);
561
- }, U = "Live-DB introspection got a corrupt / misaligned response from the database. This happens when `voltro db plan` / `apply` / `drift` / `migrate` runs through a connection POOLER (Supabase Supavisor / PgBouncer) on a LARGE schema — the pooler mis-frames a big catalog response. The introspection already batches by table to keep responses small; if a batch still trips it:\n 1. Lower the batch: set `VOLTRO_INTROSPECT_BATCH` to a smaller number of tables (default 20 — try 5 or 1). This keeps `db plan` on the normal pooler, no direct connection needed.\n 2. Or use a DIRECT (non-pooler) connection: set `DB_DIRECT_URL` (or `DB_MIGRATE_URL`) to the direct database URL — the runtime keeps `DB_URL`, so only the migration path is affected. (Supabase: direct = `db.<project-ref>.supabase.co` [IPv6-only on newer projects]; pooler = `aws-0-<region>.pooler.supabase.com`.)", W = () => {
668
+ }, it = "Live-DB introspection got a corrupt / misaligned response from the database. This happens when `voltro db plan` / `apply` / `drift` / `migrate` runs through a connection POOLER (Supabase Supavisor / PgBouncer) on a LARGE schema — the pooler mis-frames a big catalog response. The introspection already batches by table to keep responses small; if a batch still trips it:\n 1. Lower the batch: set `VOLTRO_INTROSPECT_BATCH` to a smaller number of tables (default 20 — try 5 or 1). This keeps `db plan` on the normal pooler, no direct connection needed.\n 2. Or use a DIRECT (non-pooler) connection: set `DB_DIRECT_URL` (or `DB_MIGRATE_URL`) to the direct database URL — the runtime keeps `DB_URL`, so only the migration path is affected. (Supabase: direct = `db.<project-ref>.supabase.co` [IPv6-only on newer projects]; pooler = `aws-0-<region>.pooler.supabase.com`.)", W = () => {
562
669
  let e = (process.env.DB_DIALECT ?? "postgres").toLowerCase();
563
- if (e === "memory") throw new k("voltro db: subcommands require a real dialect, not DB_DIALECT=memory.");
670
+ if (e === "memory") throw new O("voltro db: subcommands require a real dialect, not DB_DIALECT=memory.");
564
671
  return e;
565
- }, G = (e) => S(e, W()), K = (e) => e === "mysql" || e === "mariadb" ? "table_schema = DATABASE()" : e === "mssql" ? "table_schema = SCHEMA_NAME()" : "table_schema = 'public'", q = async () => (await r(W())).makeSqlLayer(V()), Re = async (e) => {
566
- let t = B(e), n = e.includes("--dry-run"), r = await M(await z(t, j), R);
567
- if (r.length === 0) return R.info("no migrations found", { root: t }), 0;
672
+ }, G = (e) => re(e, W()), K = (e) => e === "mysql" || e === "mariadb" ? "table_schema = DATABASE()" : e === "mssql" ? "table_schema = SCHEMA_NAME()" : "table_schema = 'public'", q = async () => (await r(W())).makeSqlLayer(U()), at = async (e) => {
673
+ let t = H(e), n = e.includes("--dry-run"), r = await I(await V(t, F), B);
674
+ if (r.length === 0) return B.info("no migrations found", { root: t }), 0;
568
675
  if (n) {
569
676
  let e = /* @__PURE__ */ new Set();
570
677
  try {
571
- let t = await I(r, await q());
678
+ let t = await We(r, await q());
572
679
  e = new Set(t.filter((e) => e.status === "applied").map((e) => e.id));
573
680
  } catch {
574
- R.warn("migrate --dry-run: could not read the migration ledger — showing ALL as pending");
681
+ B.warn("migrate --dry-run: could not read the migration ledger — showing ALL as pending");
575
682
  }
576
683
  let t = r.filter((t) => !e.has(t.definition.id));
577
- if (t.length === 0) return R.info("migrate --dry-run: nothing pending — every migration is applied"), 0;
578
- R.info(`migrate --dry-run: ${t.length} pending (nothing applied)`);
684
+ if (t.length === 0) return B.info("migrate --dry-run: nothing pending — every migration is applied"), 0;
685
+ B.info(`migrate --dry-run: ${t.length} pending (nothing applied)`);
579
686
  for (let e of t) console.log(` pending ${e.definition.id}`);
580
687
  return 0;
581
688
  }
582
- let i = await Fe(r, await q(), R);
583
- return i.failed ? (process.stderr.write(L(i) + "\n"), 1) : (R.info("migrate complete", {
689
+ let i = await He(r, await q(), B);
690
+ return i.failed ? (process.stderr.write(Ge(i) + "\n"), 1) : (B.info("migrate complete", {
584
691
  applied: i.applied.length,
585
692
  skipped: i.skipped.length
586
693
  }), 0);
587
- }, ze = async (e) => {
588
- let t = B(e), n = l(e, "--to"), r = await M(await z(t, j), R);
589
- if (r.length === 0) return R.info("no migrations found", { root: t }), 0;
590
- let i = await Ie(r, await q(), R, n ? { to: n } : {});
591
- return i.failed ? (process.stderr.write(L(i) + "\n"), 1) : (R.info("rollback complete", { reverted: i.applied.length }), 0);
592
- }, Be = async (e) => {
593
- let t = await M(await z(B(e), j), R);
694
+ }, ot = async (e) => {
695
+ let t = H(e), n = l(e, "--to"), r = await I(await V(t, F), B);
696
+ if (r.length === 0) return B.info("no migrations found", { root: t }), 0;
697
+ let i = await Ue(r, await q(), B, n ? { to: n } : {});
698
+ return i.failed ? (process.stderr.write(Ge(i) + "\n"), 1) : (B.info("rollback complete", { reverted: i.applied.length }), 0);
699
+ }, st = async (e) => {
700
+ let t = await I(await V(H(e), F), B);
594
701
  if (t.length === 0) return console.log("no migrations found"), 0;
595
- let n = await I(t, await q());
702
+ let n = await We(t, await q());
596
703
  for (let e of n) {
597
704
  let t = e.status === "applied" ? "✓" : e.status === "failed" ? "✗" : e.status === "reverted" ? "↩" : "·", n = e.hashMatches ? "" : " (hash drift!)", r = e.appliedAt ? ` @ ${e.appliedAt.toISOString()}` : "";
598
705
  console.log(` ${t} ${e.id} [${e.status}] ${e.name}${r}${n}`);
599
706
  }
600
707
  return 0;
601
- }, Ve = async (e) => {
708
+ }, ct = async (e) => {
602
709
  let t = (l(e, "--store") ?? process.env.DB_DIALECT ?? "postgres").toLowerCase();
603
- if (t === "memory") return R.warn("voltro db seed: using in-memory store (explicit opt-in via --store memory / DB_DIALECT=memory)"), R.warn(" rows written by this run will NOT persist beyond the process. set DB_DIALECT=postgres or unset to seed the real DB."), {
604
- store: new ee(),
710
+ if (t === "memory") return B.warn("voltro db seed: using in-memory store (explicit opt-in via --store memory / DB_DIALECT=memory)"), B.warn(" rows written by this run will NOT persist beyond the process. set DB_DIALECT=postgres or unset to seed the real DB."), {
711
+ store: new b(),
605
712
  dispose: async () => {},
606
713
  dialectId: "memory"
607
714
  };
608
715
  let n = await r(t);
609
- R.info(`voltro db seed: target store=${n.id} — rows will persist`);
716
+ B.info(`voltro db seed: target store=${n.id} — rows will persist`);
610
717
  let i = await n.makeStore({
611
- sqlLayer: n.makeSqlLayer(V()),
718
+ sqlLayer: n.makeSqlLayer(U()),
612
719
  changeStrategy: "inline"
613
720
  }), a = i;
614
721
  return {
@@ -618,77 +725,77 @@ var k = class extends Error {
618
725
  },
619
726
  dialectId: n.id
620
727
  };
621
- }, He = async (r) => {
622
- let a = B(r), o = l(r, "--id"), s = l(r, "--lifecycle"), c = await t(await z(a, i), R);
623
- if (c.length === 0) return R.info("no seeds found", { root: a }), 0;
624
- let { store: u, dispose: d, dialectId: f } = await Ve(r);
728
+ }, lt = async (r) => {
729
+ let a = H(r), o = l(r, "--id"), s = l(r, "--lifecycle"), c = await t(await V(a, i), B);
730
+ if (c.length === 0) return B.info("no seeds found", { root: a }), 0;
731
+ let { store: u, dispose: d, dialectId: f } = await ct(r);
625
732
  try {
626
- return o ? +((await e(c, o, u, R)).status === "failed") : +((await n((s ? c.filter((e) => e.definition.lifecycle === s) : c.filter((e) => e.definition.lifecycle === "boot")).map((e) => ({
733
+ return o ? +((await e(c, o, u, B)).status === "failed") : +((await n((s ? c.filter((e) => e.definition.lifecycle === s) : c.filter((e) => e.definition.lifecycle === "boot")).map((e) => ({
627
734
  ...e,
628
735
  definition: {
629
736
  ...e.definition,
630
737
  lifecycle: "boot"
631
738
  }
632
- })), u, R, { force: !0 })).filter((e) => e.status === "failed").length > 0);
739
+ })), u, B, { force: !0 })).filter((e) => e.status === "failed").length > 0);
633
740
  } finally {
634
741
  await d().catch((e) => {
635
- R.warn("voltro db seed: store dispose failed", { error: String(e) });
742
+ B.warn("voltro db seed: store dispose failed", { error: String(e) });
636
743
  });
637
744
  }
638
- }, Ue = (e) => typeof e == "object" && !!e && "tableName" in e && "fields" in e && typeof e.tableName == "string", We = async (e) => {
639
- let t = await z(e, s), n = [];
745
+ }, ut = (e) => typeof e == "object" && !!e && "tableName" in e && "fields" in e && typeof e.tableName == "string", dt = async (e) => {
746
+ let t = await V(e, s), n = [];
640
747
  for (let r of t) {
641
748
  let t;
642
749
  try {
643
750
  t = await import(h(r).href);
644
751
  } catch (t) {
645
- R.warn("db: failed to import schema file", {
752
+ B.warn("db: failed to import schema file", {
646
753
  file: p(e, r),
647
754
  error: String(t)
648
755
  });
649
756
  continue;
650
757
  }
651
- for (let e of Object.values(t)) Ue(e) && n.push(e);
758
+ for (let e of Object.values(t)) ut(e) && n.push(e);
652
759
  }
653
760
  return n;
654
- }, Ge = (e) => {
761
+ }, ft = (e) => {
655
762
  let t = new Map(e.map((e) => [e.tableName, e]));
656
763
  t.has("actors") || t.set("actors", te);
657
764
  let n = t.get("actors"), r = t.get("tenants");
658
- return re({
765
+ return ie({
659
766
  actors: () => n,
660
767
  ...r === void 0 ? {} : { tenants: () => r }
661
768
  }), [...t.values()];
662
- }, Ke = (e) => {
769
+ }, pt = (e) => {
663
770
  let t = /* @__PURE__ */ new Map();
664
771
  for (let n of e) {
665
772
  let e = n.tableName;
666
773
  e !== void 0 && !t.has(e) && t.set(e, n);
667
774
  }
668
775
  return [...t.values()];
669
- }, qe = (e) => {
670
- let t = ce(process.env.VOLTRO_DESTRUCTIVE_OK);
776
+ }, mt = (e) => {
777
+ let t = ue(process.env.VOLTRO_DESTRUCTIVE_OK);
671
778
  if (t === void 0 || !e.operations.filter((e) => e.blocked).every((e) => e.classification === "lossy")) return {
672
779
  plan: e,
673
780
  acknowledged: !1
674
781
  };
675
- let n = _e(e, t);
782
+ let n = be(e, t);
676
783
  return {
677
784
  plan: n,
678
785
  acknowledged: !n.operations.some((e) => e.blocked)
679
786
  };
680
787
  }, J = async (e, t) => g.runPromise(t.pipe(g.provide(e))), Y = async (e) => {
681
788
  try {
682
- return await J(e, b.SqlClient.pipe(g.flatMap(ue)));
789
+ return await J(e, x.SqlClient.pipe(g.flatMap(pe)));
683
790
  } catch (e) {
684
791
  let t = String(e?.message ?? e);
685
- if (H(e) || /connection error/i.test(t)) {
686
- let n = /* @__PURE__ */ Error(`${U}\n\nOriginal error: ${t}`);
792
+ if (rt(e) || /connection error/i.test(t)) {
793
+ let n = /* @__PURE__ */ Error(`${it}\n\nOriginal error: ${t}`);
687
794
  throw n.cause = e, n;
688
795
  }
689
796
  throw e;
690
797
  }
691
- }, Je = {
798
+ }, ht = {
692
799
  safe: "\x1B[32m✓\x1B[0m",
693
800
  "needs-default": "\x1B[32m⊕\x1B[0m",
694
801
  "needs-backfill": "\x1B[33m⊕\x1B[0m",
@@ -696,7 +803,7 @@ var k = class extends Error {
696
803
  lossy: "\x1B[31m✗\x1B[0m",
697
804
  "online-required": "\x1B[36m◷\x1B[0m",
698
805
  "multi-step": "\x1B[35m⇉\x1B[0m"
699
- }, Ye = (e) => {
806
+ }, gt = (e) => {
700
807
  let t = e.op, n;
701
808
  switch (t.kind) {
702
809
  case "create-table":
@@ -760,9 +867,9 @@ var k = class extends Error {
760
867
  n = `ALTER TABLE ${t.table} DROP CHECK ${t.column}`;
761
868
  break;
762
869
  }
763
- let r = Je[e.classification], i = e.reason ? ` # ${e.reason}` : "", a = e.blocked ? `\n ! ${e.blocked.fix}` : "";
870
+ let r = ht[e.classification], i = e.reason ? ` # ${e.reason}` : "", a = e.blocked ? `\n ! ${e.blocked.fix}` : "";
764
871
  return ` ${r} ${n}${i}${a}`;
765
- }, Xe = async (e, t) => {
872
+ }, _t = async (e, t) => {
766
873
  let n = e.operations.filter((e) => e.classification === "needs-default" && e.op.kind === "add-column");
767
874
  if (n.length === 0) return;
768
875
  let r = [];
@@ -771,7 +878,7 @@ var k = class extends Error {
771
878
  if (i === void 0 || a === void 0) continue;
772
879
  let o = 0;
773
880
  try {
774
- let e = await J(t, b.SqlClient.pipe(g.flatMap((e) => e.unsafe(`SELECT COUNT(*) AS n FROM ${S(i, W())}`))));
881
+ let e = await J(t, x.SqlClient.pipe(g.flatMap((e) => e.unsafe(`SELECT COUNT(*) AS n FROM ${re(i, W())}`))));
775
882
  o = Number(Object.values(e[0] ?? { n: 0 })[0] ?? 0);
776
883
  } catch {
777
884
  continue;
@@ -785,13 +892,13 @@ var k = class extends Error {
785
892
  }
786
893
  }, X = async (e, t) => {
787
894
  if (W() !== "postgres") return;
788
- let n = ae, r;
895
+ let n = oe, r;
789
896
  try {
790
- r = await J(t, b.SqlClient.pipe(g.flatMap((t) => le(t, e, n))));
897
+ r = await J(t, x.SqlClient.pipe(g.flatMap((t) => de(t, e, n))));
791
898
  } catch {
792
899
  return;
793
900
  }
794
- let i = fe({
901
+ let i = he({
795
902
  tables: e,
796
903
  dialect: "postgres",
797
904
  missing: r.missing,
@@ -799,10 +906,10 @@ var k = class extends Error {
799
906
  channel: n
800
907
  });
801
908
  if (i.length === 0) return;
802
- R.info(`db apply: installing change triggers on ${r.missing.length} table(s)` + (r.stale.length > 0 ? `, removing ${r.stale.length} stale one(s)` : ""));
909
+ B.info(`db apply: installing change triggers on ${r.missing.length} table(s)` + (r.stale.length > 0 ? `, removing ${r.stale.length} stale one(s)` : ""));
803
910
  let a = 0;
804
- for (let e of i) await J(t, b.SqlClient.pipe(g.flatMap((t) => t.unsafe(e)))), a += 1;
805
- R.info(`db apply: change triggers converged (${a} statement(s))`, {
911
+ for (let e of i) await J(t, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(e)))), a += 1;
912
+ B.info(`db apply: change triggers converged (${a} statement(s))`, {
806
913
  installed: r.missing.length,
807
914
  removed: r.stale.length
808
915
  });
@@ -812,40 +919,40 @@ var k = class extends Error {
812
919
  console.log(" (schema is up to date)\n");
813
920
  return;
814
921
  }
815
- for (let t of e.operations) console.log(Ye(t));
816
- console.log(), console.log(` safe: ${t.safe} needs-default: ${t.needsDefault} needs-backfill: ${t.needsBackfill}`), console.log(` rename: ${t.needsRenameAnnotation} lossy: ${t.lossy} blocked: ${t.blocked}`), console.log(` fingerprint: ${e.fromFingerprint.slice(0, 16)} → ${e.toFingerprint.slice(0, 16)}\n`), Ze(e);
817
- }, Ze = (e) => {
818
- let t = me(e.operations);
922
+ for (let t of e.operations) console.log(gt(t));
923
+ console.log(), console.log(` safe: ${t.safe} needs-default: ${t.needsDefault} needs-backfill: ${t.needsBackfill}`), console.log(` rename: ${t.needsRenameAnnotation} lossy: ${t.lossy} blocked: ${t.blocked}`), console.log(` fingerprint: ${e.fromFingerprint.slice(0, 16)} → ${e.toFingerprint.slice(0, 16)}\n`), vt(e);
924
+ }, vt = (e) => {
925
+ let t = _e(e.operations);
819
926
  if (t.length !== 0) {
820
927
  console.log(` ⚠ ${t.length} operation(s) UNSAFE under a rolling deploy`), console.log(" (old + new instances overlap → old code breaks against the new schema):");
821
928
  for (let e of t) console.log(` • ${e.op.op.kind}: ${e.reason}`), console.log(` → ${e.remedy}`);
822
929
  console.log(" Deploy with a maintenance window / scale-to-zero, or split into"), console.log(" expand → deploy → contract. See docs: deployment/production-hardening.\n");
823
930
  }
824
- }, Qe = (e, t) => {
825
- let n = oe({
931
+ }, yt = (e, t) => {
932
+ let n = ce({
826
933
  enabled: process.env.VOLTRO_ROLLING_DEPLOY === "1",
827
934
  forced: t.includes("--force"),
828
- unsafeCount: me(e.operations).length
935
+ unsafeCount: _e(e.operations).length
829
936
  });
830
- return n.refuse ? (R.error(`db apply: ${n.message}`), !0) : (n.warnForced && R.warn(`db apply: ${n.message}`), !1);
831
- }, $e = (e) => {
937
+ return n.refuse ? (B.error(`db apply: ${n.message}`), !0) : (n.warnForced && B.warn(`db apply: ${n.message}`), !1);
938
+ }, bt = (e) => {
832
939
  let t = l(e, "--online-after") ?? process.env.VOLTRO_ONLINE_THRESHOLD;
833
940
  if (t === void 0) return;
834
941
  let n = Number(t);
835
942
  if (!Number.isInteger(n) || n < 0) throw Error(`--online-after / VOLTRO_ONLINE_THRESHOLD must be a non-negative integer, got "${t}"`);
836
943
  return n;
837
944
  }, Q = async (e) => {
838
- let t = await We(e);
945
+ let t = await dt(e);
839
946
  if (t.length === 0) return;
840
947
  let n = await o({ root: e });
841
- return Ge(Ke([
948
+ return ft(pt([
842
949
  ...t,
843
- ...x,
950
+ ...S,
844
951
  ...n
845
952
  ]));
846
- }, et = async (e) => {
847
- let t = $e(e), n = B(e), r = await Q(n);
848
- if (r === void 0) return R.warn("db plan: no schema files found", {
953
+ }, xt = async (e) => {
954
+ let t = bt(e), n = H(e), r = await Q(n);
955
+ if (r === void 0) return B.warn("db plan: no schema files found", {
849
956
  root: n,
850
957
  hint: "looked for *.entity.ts / *.schema.ts / schema.ts"
851
958
  }), 1;
@@ -855,115 +962,115 @@ var k = class extends Error {
855
962
  a && (o.authorization = `Bearer ${a}`);
856
963
  try {
857
964
  let e = n.endsWith("/_voltro/inspect/migrations") ? n : `${n.replace(/\/+$/, "")}/_voltro/inspect/migrations`, i = await fetch(e, { headers: o });
858
- if (!i.ok) return R.error(`db plan --against: remote returned ${i.status}`, { url: e }), 1;
965
+ if (!i.ok) return B.error(`db plan --against: remote returned ${i.status}`, { url: e }), 1;
859
966
  let a = (await i.json()).drift?.liveSnapshot;
860
- if (!a) return R.error("db plan --against: remote inspect payload missing drift.liveSnapshot", { url: e }), 1;
861
- let s = D({
967
+ if (!a) return B.error("db plan --against: remote inspect payload missing drift.liveSnapshot", { url: e }), 1;
968
+ let s = E({
862
969
  declared: r,
863
970
  live: a,
864
971
  dialect: W(),
865
- ...E(),
972
+ ...T(),
866
973
  ...t === void 0 ? {} : { onlineAfter: t }
867
974
  });
868
975
  return console.log(`\n[--against ${e}] — diff vs remote live schema:\n`), Z(s), s.summary.blocked > 0 ? 2 : 0;
869
976
  } catch (e) {
870
- return R.error(`db plan --against: ${e.message}`), 1;
977
+ return B.error(`db plan --against: ${e.message}`), 1;
871
978
  }
872
979
  }
873
- let a = D({
980
+ let a = E({
874
981
  declared: r,
875
982
  live: await Y(await q()),
876
983
  dialect: W(),
877
- ...E(),
984
+ ...T(),
878
985
  ...t === void 0 ? {} : { onlineAfter: t }
879
986
  });
880
987
  return e.includes("--json") ? (y(1, `${JSON.stringify(a, null, 2)}\n`), a.summary.blocked > 0 ? 2 : 0) : (Z(a), a.summary.blocked > 0 ? 2 : 0);
881
- }, tt = async (e, t, n) => {
988
+ }, St = async (e, t, n) => {
882
989
  let r;
883
990
  try {
884
991
  let t = await v.readFile(m(e), "utf8");
885
992
  r = JSON.parse(t);
886
993
  } catch (t) {
887
- return R.error(`db apply --plan: cannot read/parse plan file '${e}': ${t.message}`), 1;
994
+ return B.error(`db apply --plan: cannot read/parse plan file '${e}': ${t.message}`), 1;
888
995
  }
889
- if (!Array.isArray(r?.operations) || typeof r?.fromFingerprint != "string" || typeof r?.toFingerprint != "string") return R.error(`db apply --plan: '${e}' is not a valid plan file (expected the output of \`voltro db plan --json\`).`), 1;
996
+ if (!Array.isArray(r?.operations) || typeof r?.fromFingerprint != "string" || typeof r?.toFingerprint != "string") return B.error(`db apply --plan: '${e}' is not a valid plan file (expected the output of \`voltro db plan --json\`).`), 1;
890
997
  let i = await Q(n);
891
- if (i === void 0) return R.warn("db apply --plan: no schema files found", { root: n }), 1;
998
+ if (i === void 0) return B.warn("db apply --plan: no schema files found", { root: n }), 1;
892
999
  let a = await q();
893
- await J(a, w(x, W()));
894
- let o = await J(a, b.SqlClient.pipe(g.flatMap((e) => de(e, n)))).catch(() => []);
895
- if (o.length > 0) return R.error(`db apply --plan: refusing — ${o.length} pending file-based migration(s).`), console.error(` ${o.join("\n ")}`), console.error(""), console.error(" These perform the changes a state diff cannot infer, so they change the shape"), console.error(" this plan was computed against. Apply them and regenerate the plan:"), console.error(""), console.error(" voltro db files ."), console.error(" voltro db plan --json > plan.json"), console.error(" voltro db apply --plan plan.json"), 1;
896
- let s = D({
1000
+ await J(a, w(S, W()));
1001
+ let o = await J(a, x.SqlClient.pipe(g.flatMap((e) => me(e, n)))).catch(() => []);
1002
+ if (o.length > 0) return B.error(`db apply --plan: refusing — ${o.length} pending file-based migration(s).`), console.error(` ${o.join("\n ")}`), console.error(""), console.error(" These perform the changes a state diff cannot infer, so they change the shape"), console.error(" this plan was computed against. Apply them and regenerate the plan:"), console.error(""), console.error(" voltro db files ."), console.error(" voltro db plan --json > plan.json"), console.error(" voltro db apply --plan plan.json"), 1;
1003
+ let s = E({
897
1004
  declared: i,
898
1005
  live: await Y(a),
899
1006
  dialect: W(),
900
- ...E()
1007
+ ...T()
901
1008
  });
902
- if (s.fromFingerprint !== r.fromFingerprint) return R.error("db apply --plan: refusing — the live schema has drifted since this plan was generated."), console.error(` plan.from=${r.fromFingerprint.slice(0, 16)} live=${s.fromFingerprint.slice(0, 16)}`), console.error(" Regenerate against the current live schema: `voltro db plan --json > plan.json`"), 2;
903
- if (s.toFingerprint !== r.toFingerprint) return R.error("db apply --plan: refusing — the declared schema differs from what this plan targets."), console.error(` plan.to=${r.toFingerprint.slice(0, 16)} declared=${s.toFingerprint.slice(0, 16)}`), console.error(" The schema files changed since the plan was generated. Regenerate the plan."), 2;
904
- let c = qe(s), u = c.plan;
905
- if (u.summary.blocked > 0 && !c.acknowledged) return Z(u), R.error(`db apply --plan: refusing — ${s.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (R.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), R.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
906
- if (c.acknowledged && R.warn("db apply --plan: proceeding with acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)"), Qe(u, t)) return 2;
907
- if (u.operations.length === 0) return R.info("db apply --plan: schema is up to date — no DDL to apply (change triggers are reconciled separately below)"), await X(i, a), 0;
1009
+ if (s.fromFingerprint !== r.fromFingerprint) return B.error("db apply --plan: refusing — the live schema has drifted since this plan was generated."), console.error(` plan.from=${r.fromFingerprint.slice(0, 16)} live=${s.fromFingerprint.slice(0, 16)}`), console.error(" Regenerate against the current live schema: `voltro db plan --json > plan.json`"), 2;
1010
+ if (s.toFingerprint !== r.toFingerprint) return B.error("db apply --plan: refusing — the declared schema differs from what this plan targets."), console.error(` plan.to=${r.toFingerprint.slice(0, 16)} declared=${s.toFingerprint.slice(0, 16)}`), console.error(" The schema files changed since the plan was generated. Regenerate the plan."), 2;
1011
+ let c = mt(s), u = c.plan;
1012
+ if (u.summary.blocked > 0 && !c.acknowledged) return Z(u), B.error(`db apply --plan: refusing — ${s.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (B.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), B.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
1013
+ if (c.acknowledged && B.warn("db apply --plan: proceeding with acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)"), yt(u, t)) return 2;
1014
+ if (u.operations.length === 0) return B.info("db apply --plan: schema is up to date — no DDL to apply (change triggers are reconciled separately below)"), await X(i, a), 0;
908
1015
  Z(u);
909
- let d = (process.env.NODE_ENV ?? "dev").toLowerCase(), f = l(t, "--note"), p = await J(a, b.SqlClient.pipe(g.flatMap((e) => C(e, u, {
1016
+ let d = (process.env.NODE_ENV ?? "dev").toLowerCase(), f = l(t, "--note"), p = await J(a, x.SqlClient.pipe(g.flatMap((e) => se(e, u, {
910
1017
  declared: i,
911
1018
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:plan",
912
1019
  environment: d === "production" ? "prod" : d === "staging" ? "staging" : "dev",
913
1020
  source: "file",
914
1021
  ...f ? { notes: f } : {},
915
- replan: () => g.promise(() => Y(a)).pipe(g.map((e) => D({
1022
+ replan: () => g.promise(() => Y(a)).pipe(g.map((e) => E({
916
1023
  declared: i,
917
1024
  live: e,
918
1025
  dialect: W(),
919
- ...E()
1026
+ ...T()
920
1027
  })))
921
1028
  }))));
922
- return R.info(`db apply --plan: applied ${p.appliedOps} op(s) in ${p.durationMs}ms`, {
1029
+ return B.info(`db apply --plan: applied ${p.appliedOps} op(s) in ${p.durationMs}ms`, {
923
1030
  id: p.id,
924
1031
  fingerprint: p.fingerprint.slice(0, 16)
925
1032
  }), await X(i, a), 0;
926
- }, nt = async (e) => {
927
- let t = B(e), n = $e(e), r = l(e, "--plan");
928
- if (r) return tt(r, e, t);
929
- if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return R.error("voltro db apply: refusing — auto-apply on prod is not allowed."), console.error("In production, schema changes go through `voltro db plan --json > plan.json`"), console.error("then `voltro db apply --plan plan.json` as an explicit step in your deploy pipeline."), 3;
1033
+ }, Ct = async (e) => {
1034
+ let t = H(e), n = bt(e), r = l(e, "--plan");
1035
+ if (r) return St(r, e, t);
1036
+ if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return B.error("voltro db apply: refusing — auto-apply on prod is not allowed."), console.error("In production, schema changes go through `voltro db plan --json > plan.json`"), console.error("then `voltro db apply --plan plan.json` as an explicit step in your deploy pipeline."), 3;
930
1037
  let i = await Q(t);
931
- if (i === void 0) return R.warn("db apply: no schema files found", { root: t }), 1;
932
- let a = await q(), o = await dt(t, a);
933
- if ("failed" in o) return R.error("db apply: a file-based migration FAILED — the schema diff was NOT run"), console.error(o.failed), console.error("Fix the migration and re-run. `voltro db files` runs them alone."), 1;
934
- await J(a, w(x, W()));
935
- let s = D({
1038
+ if (i === void 0) return B.warn("db apply: no schema files found", { root: t }), 1;
1039
+ let a = await q(), o = await Mt(t, a);
1040
+ if ("failed" in o) return B.error("db apply: a file-based migration FAILED — the schema diff was NOT run"), console.error(o.failed), console.error("Fix the migration and re-run. `voltro db files` runs them alone."), 1;
1041
+ await J(a, w(S, W()));
1042
+ let s = E({
936
1043
  declared: i,
937
1044
  live: await Y(a),
938
1045
  dialect: W(),
939
- ...E(),
1046
+ ...T(),
940
1047
  ...n === void 0 ? {} : { onlineAfter: n }
941
1048
  });
942
- Z(s), await Xe(s, a);
943
- let c = qe(s), u = c.plan;
944
- if (u.summary.blocked > 0 && !c.acknowledged) return R.error(`db apply: refusing — ${u.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (R.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), R.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
945
- if (c.acknowledged && R.warn(`db apply: proceeding with ${s.summary.blocked} acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)`), Qe(u, e)) return 2;
946
- if (u.operations.length === 0) return R.info("db apply: schema is up to date — no DDL to apply (change triggers are reconciled separately below)"), await st(a), l(e, "--note") !== void 0 && (R.warn("db apply: --note was ignored — an empty plan writes no history row to attach it to."), console.error("To record that the CURRENT live schema is the accepted baseline, use `voltro db drift --accept`.")), await X(i, a), 0;
947
- let d = l(e, "--note"), f = await J(a, b.SqlClient.pipe(g.flatMap((e) => C(e, u, {
1049
+ Z(s), await _t(s, a);
1050
+ let c = mt(s), u = c.plan;
1051
+ if (u.summary.blocked > 0 && !c.acknowledged) return B.error(`db apply: refusing — ${u.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (B.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), B.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
1052
+ if (c.acknowledged && B.warn(`db apply: proceeding with ${s.summary.blocked} acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)`), yt(u, e)) return 2;
1053
+ if (u.operations.length === 0) return B.info("db apply: schema is up to date — no DDL to apply (change triggers are reconciled separately below)"), await Ot(a), l(e, "--note") !== void 0 && (B.warn("db apply: --note was ignored — an empty plan writes no history row to attach it to."), console.error("To record that the CURRENT live schema is the accepted baseline, use `voltro db drift --accept`.")), await X(i, a), 0;
1054
+ let d = l(e, "--note"), f = await J(a, x.SqlClient.pipe(g.flatMap((e) => se(e, u, {
948
1055
  declared: i,
949
1056
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user",
950
1057
  environment: (process.env.NODE_ENV ?? "dev").toLowerCase() === "staging" ? "staging" : "dev",
951
1058
  source: "auto-diff",
952
1059
  ...d ? { notes: d } : {},
953
- replan: () => g.promise(() => Y(a)).pipe(g.map((e) => D({
1060
+ replan: () => g.promise(() => Y(a)).pipe(g.map((e) => E({
954
1061
  declared: i,
955
1062
  live: e,
956
1063
  dialect: W(),
957
- ...E(),
1064
+ ...T(),
958
1065
  ...n === void 0 ? {} : { onlineAfter: n }
959
1066
  })))
960
1067
  }))));
961
- return R.info(`db apply: applied ${f.appliedOps} op(s) in ${f.durationMs}ms`, {
1068
+ return B.info(`db apply: applied ${f.appliedOps} op(s) in ${f.durationMs}ms`, {
962
1069
  id: f.id,
963
1070
  fingerprint: f.fingerprint.slice(0, 16)
964
1071
  }), await X(i, a), 0;
965
- }, rt = async (e) => {
966
- let t = Math.max(1, Number(l(e, "--limit") ?? "20")), n = await J(await q(), b.SqlClient.pipe(g.flatMap((e) => e`
1072
+ }, wt = async (e) => {
1073
+ let t = Math.max(1, Number(l(e, "--limit") ?? "20")), n = await J(await q(), x.SqlClient.pipe(g.flatMap((e) => e`
967
1074
  SELECT ${e("id")}, ${e("fingerprint")}, ${e("operations")},
968
1075
  ${e("appliedBy")}, ${e("environment")}, ${e("source")},
969
1076
  ${e("durationMs")}, ${e("notes")}, ${e("appliedAt")}
@@ -971,25 +1078,25 @@ var k = class extends Error {
971
1078
  ORDER BY ${e("appliedAt")} DESC
972
1079
  LIMIT ${t}
973
1080
  `)));
974
- if (n.length === 0) return R.info("db plans: no planner-based migrations applied yet"), console.log("Run `voltro db apply` after a schema change to start a plan history."), 0;
1081
+ if (n.length === 0) return B.info("db plans: no planner-based migrations applied yet"), console.log("Run `voltro db apply` after a schema change to start a plan history."), 0;
975
1082
  console.log(`\n${n.length} plan${n.length === 1 ? "" : "s"} (newest first):\n`);
976
1083
  for (let e of n) {
977
1084
  let t = 0;
978
1085
  try {
979
- t = JSON.parse(ct(e.operations)).length;
1086
+ t = JSON.parse(kt(e.operations)).length;
980
1087
  } catch {}
981
- console.log(` ${e.id} fp=${O(e.fingerprint)} env=${e.environment} src=${e.source} ${t} op(s) ${e.durationMs}ms ${$(e.appliedAt)} by=${e.appliedBy}`), e.notes && console.log(` [2mnote: ${e.notes}[0m`);
1088
+ console.log(` ${e.id} fp=${D(e.fingerprint)} env=${e.environment} src=${e.source} ${t} op(s) ${e.durationMs}ms ${$(e.appliedAt)} by=${e.appliedBy}`), e.notes && console.log(` [2mnote: ${e.notes}[0m`);
982
1089
  }
983
1090
  return console.log(), 0;
984
- }, it = async (e) => {
1091
+ }, Tt = async (e) => {
985
1092
  let t = e[0];
986
1093
  if (!t) return console.error("voltro db restore-snapshot <plan-id>"), console.error(" Renames soft-dropped columns/tables from a plan back to their original names."), console.error(" Requires the plan to have been applied with VOLTRO_SOFT_DROP=1."), 2;
987
- let n = await q(), r = await J(n, b.SqlClient.pipe(g.flatMap((e) => e`SELECT ${e("id")}, ${e("operations")}
1094
+ let n = await q(), r = await J(n, x.SqlClient.pipe(g.flatMap((e) => e`SELECT ${e("id")}, ${e("operations")}
988
1095
  FROM ${e("_voltro_migration_plans")} WHERE ${e("id")} = ${t}`)));
989
1096
  if (r.length === 0) return console.error(`restore-snapshot: plan '${t}' not found`), 1;
990
1097
  let i;
991
1098
  try {
992
- i = JSON.parse(ct(r[0].operations));
1099
+ i = JSON.parse(kt(r[0].operations));
993
1100
  } catch {
994
1101
  return console.error(`restore-snapshot: plan '${t}' has corrupted operations JSON`), 1;
995
1102
  }
@@ -1003,7 +1110,7 @@ var k = class extends Error {
1003
1110
  if (a.length === 0 && o.length === 0) return console.log(`restore-snapshot: plan '${t}' has no drop-column / drop-table ops to restore`), 0;
1004
1111
  let s = 0;
1005
1112
  for (let e of a) {
1006
- let t = await J(n, b.SqlClient.pipe(g.flatMap((t) => t`
1113
+ let t = await J(n, x.SqlClient.pipe(g.flatMap((t) => t`
1007
1114
  SELECT column_name FROM information_schema.columns
1008
1115
  WHERE ${t.unsafe(K(W()))}
1009
1116
  AND table_name = ${e.table}
@@ -1015,11 +1122,11 @@ var k = class extends Error {
1015
1122
  continue;
1016
1123
  }
1017
1124
  let r = t[0].column_name;
1018
- await J(n, b.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table)} RENAME COLUMN ${G(r)} TO ${G(e.column)}`)))), s++, console.log(` ↩ restored ${e.table}.${e.column} (from ${r})`);
1125
+ await J(n, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table)} RENAME COLUMN ${G(r)} TO ${G(e.column)}`)))), s++, console.log(` ↩ restored ${e.table}.${e.column} (from ${r})`);
1019
1126
  }
1020
1127
  let c = 0;
1021
1128
  for (let e of o) {
1022
- let t = await J(n, b.SqlClient.pipe(g.flatMap((t) => t`
1129
+ let t = await J(n, x.SqlClient.pipe(g.flatMap((t) => t`
1023
1130
  SELECT table_name FROM information_schema.tables
1024
1131
  WHERE ${t.unsafe(K(W()))}
1025
1132
  AND table_name LIKE ${e.table + "__dropped_%"}
@@ -1030,18 +1137,18 @@ var k = class extends Error {
1030
1137
  continue;
1031
1138
  }
1032
1139
  let r = t[0].table_name;
1033
- await J(n, b.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(r)} RENAME TO ${G(e.table)}`)))), c++, console.log(` ↩ restored table ${e.table} (from ${r})`);
1140
+ await J(n, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(r)} RENAME TO ${G(e.table)}`)))), c++, console.log(` ↩ restored table ${e.table} (from ${r})`);
1034
1141
  }
1035
1142
  return console.log(`\n✓ restored ${s} column(s) + ${c} table(s) from plan '${t}'\n`), 0;
1036
- }, at = async (e) => {
1143
+ }, Et = async (e) => {
1037
1144
  let t = l(e, "--before");
1038
1145
  if (!t) return console.error("voltro db gc-snapshots --before <date> [--dry-run]"), console.error(" Permanently drops soft-dropped columns/tables (<name>__dropped_<ts>) older than <date>."), console.error(" <date> is ISO — e.g. 2026-01-01 or 2026-01-01T00:00:00Z. NOT reversible."), 2;
1039
1146
  let n = t.replace(/[^0-9]/g, "");
1040
1147
  if (n.length < 8) return console.error(`gc-snapshots: --before must be at least a full date (YYYY-MM-DD), got "${t}"`), 2;
1041
- let r = (n + "00000000000000").slice(0, 14), i = e.includes("--dry-run"), a = await q(), o = await J(a, b.SqlClient.pipe(g.flatMap((e) => e`
1148
+ let r = (n + "00000000000000").slice(0, 14), i = e.includes("--dry-run"), a = await q(), o = await J(a, x.SqlClient.pipe(g.flatMap((e) => e`
1042
1149
  SELECT table_name, column_name FROM information_schema.columns
1043
1150
  WHERE ${e.unsafe(K(W()))} AND column_name LIKE ${"%__dropped_%"}
1044
- `))), s = await J(a, b.SqlClient.pipe(g.flatMap((e) => e`
1151
+ `))), s = await J(a, x.SqlClient.pipe(g.flatMap((e) => e`
1045
1152
  SELECT table_name FROM information_schema.tables
1046
1153
  WHERE ${e.unsafe(K(W()))} AND table_name LIKE ${"%__dropped_%"}
1047
1154
  `))), c = (e) => {
@@ -1050,37 +1157,37 @@ var k = class extends Error {
1050
1157
  }, u = 0;
1051
1158
  for (let e of o) {
1052
1159
  let t = c(e.column_name);
1053
- t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop column" : "dropping column"} ${e.table_name}.${e.column_name}`), i || await J(a, b.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table_name)} DROP COLUMN ${G(e.column_name)}`)))), u++);
1160
+ t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop column" : "dropping column"} ${e.table_name}.${e.column_name}`), i || await J(a, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table_name)} DROP COLUMN ${G(e.column_name)}`)))), u++);
1054
1161
  }
1055
1162
  for (let e of s) {
1056
1163
  let t = c(e.table_name);
1057
- t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop table" : "dropping table"} ${e.table_name}`), i || await J(a, b.SqlClient.pipe(g.flatMap((t) => t.unsafe(`DROP TABLE ${G(e.table_name)}`)))), u++);
1164
+ t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop table" : "dropping table"} ${e.table_name}`), i || await J(a, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`DROP TABLE ${G(e.table_name)}`)))), u++);
1058
1165
  }
1059
1166
  return console.log(`\n${i ? `[dry-run] ${u} snapshot(s) would be dropped` : `✓ dropped ${u} snapshot(s)`} (older than ${r}).\n`), 0;
1060
- }, ot = async (e) => {
1167
+ }, Dt = async (e) => {
1061
1168
  let t = l(e, "--before");
1062
1169
  if (!t) return console.error("voltro db squash --before <iso-date> [--note \"<text>\"]"), 2;
1063
1170
  let n = t;
1064
1171
  if (Number.isNaN(new Date(n).getTime())) return console.error(`voltro db squash: --before '${n}' is not a valid ISO date`), 2;
1065
- let r = l(e, "--note"), i = B(e), a = await Q(i);
1066
- if (a === void 0) return R.warn("db squash: no schema files found", { root: i }), 1;
1067
- let o = T(se(a)), s = await q();
1172
+ let r = l(e, "--note"), i = H(e), a = await Q(i);
1173
+ if (a === void 0) return B.warn("db squash: no schema files found", { root: i }), 1;
1174
+ let o = fe(le(a)), s = await q();
1068
1175
  try {
1069
- let e = await J(s, ge({
1176
+ let e = await J(s, ye({
1070
1177
  before: n,
1071
1178
  fingerprint: o,
1072
1179
  appliedBy: process.env.USER ?? "cli",
1073
1180
  environment: process.env.NODE_ENV === "production" ? "prod" : "dev",
1074
1181
  ...r === void 0 ? {} : { note: r }
1075
1182
  }));
1076
- return console.log(`\n✓ squashed ${e.squashedCount} migration plan(s)\n snapshot id: ${e.snapshotId}\n fingerprint: ${O(e.snapshotFingerprint)}\n`), 0;
1183
+ return console.log(`\n✓ squashed ${e.squashedCount} migration plan(s)\n snapshot id: ${e.snapshotId}\n fingerprint: ${D(e.snapshotFingerprint)}\n`), 0;
1077
1184
  } catch (e) {
1078
1185
  return console.error(`squash refused: ${e.message}`), 1;
1079
1186
  }
1080
- }, st = async (e) => {
1187
+ }, Ot = async (e) => {
1081
1188
  try {
1082
- let t = T(await Y(e));
1083
- await J(e, b.SqlClient.pipe(g.flatMap((e) => e`
1189
+ let t = fe(await Y(e));
1190
+ await J(e, x.SqlClient.pipe(g.flatMap((e) => e`
1084
1191
  UPDATE ${e("_voltro_migration_plans")}
1085
1192
  SET ${e("liveFingerprint")} = ${t}
1086
1193
  WHERE ${e("id")} = (
@@ -1092,7 +1199,7 @@ var k = class extends Error {
1092
1199
  AND ${e("liveFingerprint")} IS NULL
1093
1200
  `)));
1094
1201
  } catch {}
1095
- }, ct = (e) => typeof e == "string" ? e : e == null ? "" : JSON.stringify(e), $ = (e) => e instanceof Date ? e.toISOString() : String(e ?? ""), lt = (e) => e === void 0 ? {
1202
+ }, kt = (e) => typeof e == "string" ? e : e == null ? "" : JSON.stringify(e), $ = (e) => e instanceof Date ? e.toISOString() : String(e ?? ""), At = (e) => e === void 0 ? {
1096
1203
  accept: !1,
1097
1204
  reason: "no-declared-schema"
1098
1205
  } : e > 0 ? {
@@ -1101,38 +1208,38 @@ var k = class extends Error {
1101
1208
  } : {
1102
1209
  accept: !0,
1103
1210
  reason: "converged"
1104
- }, ut = async (e) => {
1105
- let t = e.includes("--accept"), n = await q(), r = await J(n, b.SqlClient.pipe(g.flatMap((e) => e`
1211
+ }, jt = async (e) => {
1212
+ let t = e.includes("--accept"), n = await q(), r = await J(n, x.SqlClient.pipe(g.flatMap((e) => e`
1106
1213
  SELECT * FROM ${e("_voltro_migration_plans")}
1107
1214
  ORDER BY ${e("appliedAt")} DESC
1108
1215
  LIMIT 1
1109
- `))), i = await Y(n), a = T(i);
1110
- if (r.length === 0) return R.info("db drift: no _voltro_migration_plans rows yet"), console.log(`live user-schema fingerprint: ${O(a)}`), console.log("No baseline to compare against. Run `voltro db apply` to lock in the current state."), 0;
1216
+ `))), i = await Y(n), a = fe(i);
1217
+ if (r.length === 0) return B.info("db drift: no _voltro_migration_plans rows yet"), console.log(`live user-schema fingerprint: ${D(a)}`), console.log("No baseline to compare against. Run `voltro db apply` to lock in the current state."), 0;
1111
1218
  let o = r[0];
1112
- if (o.liveFingerprint === null || o.liveFingerprint === void 0) return R.warn("db drift: no drift baseline recorded yet — cannot compare"), console.log(), console.log(` last applied: ${o.id} at ${$(o.appliedAt)}`), console.log(` live now: ${O(a)}`), console.log(), console.log("That row was written before the framework recorded a live-schema baseline,"), console.log("and the value it does carry is the DECLARED snapshot's hash — not comparable"), console.log("with a live one. Run `voltro db apply` once to establish the baseline (a no-op"), console.log("apply backfills it too); use `voltro db plan` meanwhile, which compares declared"), console.log("against live directly."), console.log(), console.log("Exit code 3 — NOT 0. \"Did not compare\" is not \"clean\", and a gate on the exit"), console.log("code has to be able to tell them apart."), 3;
1113
- if (o.liveFingerprint === a) return R.info(`db drift: live matches last applied (${O(a)})`), 0;
1219
+ if (o.liveFingerprint === null || o.liveFingerprint === void 0) return B.warn("db drift: no drift baseline recorded yet — cannot compare"), console.log(), console.log(` last applied: ${o.id} at ${$(o.appliedAt)}`), console.log(` live now: ${D(a)}`), console.log(), console.log("That row was written before the framework recorded a live-schema baseline,"), console.log("and the value it does carry is the DECLARED snapshot's hash — not comparable"), console.log("with a live one. Run `voltro db apply` once to establish the baseline (a no-op"), console.log("apply backfills it too); use `voltro db plan` meanwhile, which compares declared"), console.log("against live directly."), console.log(), console.log("Exit code 3 — NOT 0. \"Did not compare\" is not \"clean\", and a gate on the exit"), console.log("code has to be able to tell them apart."), 3;
1220
+ if (o.liveFingerprint === a) return B.info(`db drift: live matches last applied (${D(a)})`), 0;
1114
1221
  let s = await (async () => {
1115
1222
  try {
1116
- let t = await Q(B(e));
1117
- return t === void 0 ? void 0 : D({
1223
+ let t = await Q(H(e));
1224
+ return t === void 0 ? void 0 : E({
1118
1225
  declared: t,
1119
1226
  live: i,
1120
1227
  dialect: W(),
1121
- ...E()
1228
+ ...T()
1122
1229
  }).operations;
1123
1230
  } catch {
1124
1231
  return;
1125
1232
  }
1126
1233
  })();
1127
1234
  if (t) {
1128
- let e = lt(s?.length);
1129
- if (e.reason === "no-declared-schema") return R.error("db drift --accept: no schema files found — cannot verify the live schema is current."), 1;
1235
+ let e = At(s?.length);
1236
+ if (e.reason === "no-declared-schema") return B.error("db drift --accept: no schema files found — cannot verify the live schema is current."), 1;
1130
1237
  if (!e.accept && s !== void 0) {
1131
- R.error(`db drift --accept: refusing — ${s.length} operation(s) still pending against your declared schema.`), console.log();
1238
+ B.error(`db drift --accept: refusing — ${s.length} operation(s) still pending against your declared schema.`), console.log();
1132
1239
  for (let e of s.slice(0, 10)) console.log(` - ${e.op.kind} ${"table" in e.op ? `[${e.op.table}]` : ""}`);
1133
1240
  return s.length > 10 && console.log(` … and ${s.length - 10} more`), console.log(), console.log("`--accept` records the CURRENT live schema as the applied baseline. It is for a"), console.log("change that already landed out of band and left the schema correct — not for one"), console.log("that has not landed. Run `voltro db apply` instead; it writes a real baseline."), 4;
1134
1241
  }
1135
- return await J(n, b.SqlClient.pipe(g.flatMap((e) => e`
1242
+ return await J(n, x.SqlClient.pipe(g.flatMap((e) => e`
1136
1243
  UPDATE ${e("_voltro_migration_plans")}
1137
1244
  SET ${e("liveFingerprint")} = ${a}
1138
1245
  WHERE ${e("id")} = (
@@ -1141,43 +1248,43 @@ var k = class extends Error {
1141
1248
  ORDER BY ${e("appliedAt")} DESC LIMIT 1
1142
1249
  ) AS ${e("newest")}
1143
1250
  )
1144
- `))), R.info(`db drift: accepted the live schema as the baseline (${O(a)})`), console.log(), console.log("`voltro db plan` reports 0 operations against this schema, so the live state already"), console.log("matches your declaration — only the ledger did not know. It does now."), console.log(), console.log("No history entry was invented: the latest row's baseline was updated in place,"), console.log("because no migration ran."), 0;
1251
+ `))), B.info(`db drift: accepted the live schema as the baseline (${D(a)})`), console.log(), console.log("`voltro db plan` reports 0 operations against this schema, so the live state already"), console.log("matches your declaration — only the ledger did not know. It does now."), console.log(), console.log("No history entry was invented: the latest row's baseline was updated in place,"), console.log("because no migration ran."), 0;
1145
1252
  }
1146
- return R.error("db drift: live schema DIVERGED from last applied state"), console.log(), console.log(` baseline: ${O(o.liveFingerprint)} at ${$(o.appliedAt)} (${o.id})`), console.log(` live now: ${O(a)}`), console.log(), console.log("Something changed the live schema after the last apply. This command can see"), console.log("THAT it changed, not what or who — the fingerprints are hashes, not a diff."), console.log(), s === void 0 ? (console.log("No schema files found here, so the declared side could not be compared."), console.log("Run `voltro db plan` from your project root for the actual difference."), 4) : s.length === 0 ? (console.log("Your DECLARED schema is already satisfied — `voltro db plan` reports 0 operations"), console.log("against this database. So the live schema is not wrong, only unrecorded: something"), console.log("applied a change without going through the planner (a hand-run ALTER, a DBA"), console.log("window, a restored dump), or it touched a table your code does not declare."), console.log(), console.log("If that was deliberate and the schema is right, record it:"), console.log(), console.log(" voltro db drift --accept"), console.log(), console.log("It updates the latest ledger row's baseline to the live schema and invents no"), console.log("history entry. Drift then measures from here."), 4) : (console.log(`\`voltro db plan\` reports ${s.length} operation(s) still pending against your`), console.log("declared schema, so this is not merely an unrecorded change — run it for the diff,"), console.log("then `voltro db apply`, which writes a fresh baseline of its own."), 4);
1147
- }, dt = async (e, t) => {
1253
+ return B.error("db drift: live schema DIVERGED from last applied state"), console.log(), console.log(` baseline: ${D(o.liveFingerprint)} at ${$(o.appliedAt)} (${o.id})`), console.log(` live now: ${D(a)}`), console.log(), console.log("Something changed the live schema after the last apply. This command can see"), console.log("THAT it changed, not what or who — the fingerprints are hashes, not a diff."), console.log(), s === void 0 ? (console.log("No schema files found here, so the declared side could not be compared."), console.log("Run `voltro db plan` from your project root for the actual difference."), 4) : s.length === 0 ? (console.log("Your DECLARED schema is already satisfied — `voltro db plan` reports 0 operations"), console.log("against this database. So the live schema is not wrong, only unrecorded: something"), console.log("applied a change without going through the planner (a hand-run ALTER, a DBA"), console.log("window, a restored dump), or it touched a table your code does not declare."), console.log(), console.log("If that was deliberate and the schema is right, record it:"), console.log(), console.log(" voltro db drift --accept"), console.log(), console.log("It updates the latest ledger row's baseline to the live schema and invents no"), console.log("history entry. Drift then measures from here."), 4) : (console.log(`\`voltro db plan\` reports ${s.length} operation(s) still pending against your`), console.log("declared schema, so this is not merely an unrecorded change — run it for the diff,"), console.log("then `voltro db apply`, which writes a fresh baseline of its own."), 4);
1254
+ }, Mt = async (e, t) => {
1148
1255
  let n = (process.env.NODE_ENV ?? "").toLowerCase() === "production" ? "prod" : (process.env.NODE_ENV ?? "").toLowerCase() === "staging" ? "staging" : "dev";
1149
1256
  try {
1150
- let r = await J(t, b.SqlClient.pipe(g.flatMap((t) => he(t, {
1257
+ let r = await J(t, x.SqlClient.pipe(g.flatMap((t) => ve(t, {
1151
1258
  projectRoot: e,
1152
1259
  env: n,
1153
1260
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
1154
1261
  }))));
1155
- return r.applied.length > 0 && R.info(`db apply: applied ${r.applied.length} pending file-based migration(s) first`, { applied: r.applied.map((e) => `${e.id} (${e.durationMs}ms)`) }), { applied: r.applied.length };
1262
+ return r.applied.length > 0 && B.info(`db apply: applied ${r.applied.length} pending file-based migration(s) first`, { applied: r.applied.map((e) => `${e.id} (${e.durationMs}ms)`) }), { applied: r.applied.length };
1156
1263
  } catch (e) {
1157
1264
  return { failed: e };
1158
1265
  }
1159
- }, ft = async (e) => {
1160
- let t = B(e), n = await q(), r = (process.env.NODE_ENV ?? "").toLowerCase() === "production" ? "prod" : (process.env.NODE_ENV ?? "").toLowerCase() === "staging" ? "staging" : "dev";
1161
- await J(n, w(x, W()));
1162
- let i = await J(n, b.SqlClient.pipe(g.flatMap((e) => he(e, {
1266
+ }, Nt = async (e) => {
1267
+ let t = H(e), n = await q(), r = (process.env.NODE_ENV ?? "").toLowerCase() === "production" ? "prod" : (process.env.NODE_ENV ?? "").toLowerCase() === "staging" ? "staging" : "dev";
1268
+ await J(n, w(S, W()));
1269
+ let i = await J(n, x.SqlClient.pipe(g.flatMap((e) => ve(e, {
1163
1270
  projectRoot: t,
1164
1271
  env: r,
1165
1272
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
1166
1273
  }))));
1167
- return i.applied.length === 0 ? (R.info(`db files: no pending file-based migrations (${i.skipped.length} already applied)`), 0) : (R.info(`db files: applied ${i.applied.length} migration(s)`, {
1274
+ return i.applied.length === 0 ? (B.info(`db files: no pending file-based migrations (${i.skipped.length} already applied)`), 0) : (B.info(`db files: applied ${i.applied.length} migration(s)`, {
1168
1275
  applied: i.applied.map((e) => `${e.id} (${e.durationMs}ms)`),
1169
1276
  skipped: i.skipped.length
1170
1277
  }), 0);
1171
- }, pt = async (e) => {
1172
- let t = B(e), n = e.find((e) => !e.startsWith("--"));
1173
- if (!n) return R.error("voltro db rollback-file <migration-id>"), 2;
1174
- if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return R.error("voltro db rollback-file: refusing on prod — schema rollback runs as an explicit deploy step."), 3;
1175
- let r = await J(await q(), b.SqlClient.pipe(g.flatMap((e) => pe(e, {
1278
+ }, Pt = async (e) => {
1279
+ let t = H(e), n = e.find((e) => !e.startsWith("--"));
1280
+ if (!n) return B.error("voltro db rollback-file <migration-id>"), 2;
1281
+ if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return B.error("voltro db rollback-file: refusing on prod — schema rollback runs as an explicit deploy step."), 3;
1282
+ let r = await J(await q(), x.SqlClient.pipe(g.flatMap((e) => ge(e, {
1176
1283
  projectRoot: t,
1177
1284
  id: n
1178
1285
  }))));
1179
- return R.info(`db rollback-file: rolled back ${r.id} in ${r.durationMs}ms`), 0;
1180
- }, mt = () => {
1286
+ return B.info(`db rollback-file: rolled back ${r.id} in ${r.durationMs}ms`), 0;
1287
+ }, Ft = () => {
1181
1288
  let e = /* @__PURE__ */ new Date();
1182
1289
  return [
1183
1290
  e.getUTCFullYear(),
@@ -1187,7 +1294,7 @@ var k = class extends Error {
1187
1294
  String(e.getUTCMinutes()).padStart(2, "0"),
1188
1295
  String(e.getUTCSeconds()).padStart(2, "0")
1189
1296
  ].join("");
1190
- }, ht = (e) => e.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration", gt = (e, t) => `import { defineMigration, SqlClient } from '@voltro/database'
1297
+ }, It = (e) => e.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration", Lt = (e, t) => `import { defineMigration, SqlClient } from '@voltro/database'
1191
1298
  import { Effect } from 'effect'
1192
1299
 
1193
1300
  // A hand-written migration. Reach for this when the DECLARATIVE differ can't
@@ -1219,24 +1326,24 @@ export default defineMigration({
1219
1326
  }),
1220
1327
  ],
1221
1328
  })
1222
- `, _t = {
1223
- plan: et,
1224
- apply: nt,
1225
- plans: rt,
1226
- drift: ut,
1227
- files: ft,
1228
- "rollback-file": pt,
1229
- migrate: Re,
1230
- rollback: ze,
1231
- status: Be,
1232
- seed: He,
1329
+ `, Rt = {
1330
+ plan: xt,
1331
+ apply: Ct,
1332
+ plans: wt,
1333
+ drift: jt,
1334
+ files: Nt,
1335
+ "rollback-file": Pt,
1336
+ migrate: at,
1337
+ rollback: ot,
1338
+ status: st,
1339
+ seed: lt,
1233
1340
  generate: async (e) => {
1234
- let { value: t, rest: n } = c(e, "--root"), { value: r, rest: i } = c(n, "--name"), a = t ? m(t) : process.cwd(), o = ht(r ?? u(i) ?? "migration"), s = `${mt()}_${o}`, l = f(a, "migrations"), d = f(l, `${s}.migration.ts`);
1235
- return await v.mkdir(l, { recursive: !0 }), await v.writeFile(d, gt(s, o), "utf8"), R.info(`created ${p(a, d)}`), console.error("Fill in the apply/undo steps, then run `voltro db migrate` (rollback with `voltro db rollback`)."), console.error("For a plain schema shape change you may not need this — `voltro db apply` diffs declared vs live."), 0;
1341
+ let { value: t, rest: n } = c(e, "--root"), { value: r, rest: i } = c(n, "--name"), a = t ? m(t) : process.cwd(), o = It(r ?? u(i) ?? "migration"), s = `${Ft()}_${o}`, l = f(a, "migrations"), d = f(l, `${s}.migration.ts`);
1342
+ return await v.mkdir(l, { recursive: !0 }), await v.writeFile(d, Lt(s, o), "utf8"), B.info(`created ${p(a, d)}`), console.error("Fill in the apply/undo steps, then run `voltro db migrate` (rollback with `voltro db rollback`)."), console.error("For a plain schema shape change you may not need this — `voltro db apply` diffs declared vs live."), 0;
1236
1343
  },
1237
- squash: ot,
1238
- "restore-snapshot": it,
1239
- "gc-snapshots": at,
1344
+ squash: Dt,
1345
+ "restore-snapshot": Tt,
1346
+ "gc-snapshots": Et,
1240
1347
  adopt: async (e) => {
1241
1348
  let t = (t) => {
1242
1349
  let n = e.indexOf(`--${t}`);
@@ -1250,18 +1357,18 @@ export default defineMigration({
1250
1357
  into: r,
1251
1358
  map: o.map,
1252
1359
  ...o.leaveUnset ? { leaveUnset: o.leaveUnset } : {}
1253
- }, c = W(), l = await q(), u = await J(l, b.SqlClient.pipe(g.flatMap(ue))), d = (e) => u.tables.find((t) => t.name === e)?.columns.map((e) => ({
1360
+ }, c = W(), l = await q(), u = await J(l, x.SqlClient.pipe(g.flatMap(pe))), d = (e) => u.tables.find((t) => t.name === e)?.columns.map((e) => ({
1254
1361
  name: e.name,
1255
1362
  nullable: e.nullable,
1256
1363
  hasDefault: e.defaultValue !== void 0 && e.defaultValue !== null
1257
1364
  })), f = d(n), p = d(r);
1258
1365
  if (f === void 0) return console.error(`voltro db adopt: '${n}' does not exist in the database`), 1;
1259
1366
  if (p === void 0) return console.error(`voltro db adopt: '${r}' does not exist in the database — boot once so the plugin's table is created`), 1;
1260
- let h = async (e) => await J(l, b.SqlClient.pipe(g.flatMap((t) => t.unsafe(e)))), _ = async (e) => Number(Object.values((await h(`SELECT COUNT(*) AS n FROM "${e}"`))[0] ?? { n: 0 })[0] ?? 0), v = be(s, f, p, await _(r));
1261
- for (let e of Se(s, v, await _(n))) console.log(e);
1367
+ let h = async (e) => await J(l, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(e)))), _ = async (e) => Number(Object.values((await h(`SELECT COUNT(*) AS n FROM "${e}"`))[0] ?? { n: 0 })[0] ?? 0), v = Ce(s, f, p, await _(r));
1368
+ for (let e of Te(s, v, await _(n))) console.log(e);
1262
1369
  if (v.fatal) return 1;
1263
1370
  if (!e.includes("--apply")) return console.log(""), console.log(" (dry run — nothing written. Re-run with --apply.)"), 0;
1264
- let y = await Ee(s, v, c, "id", void 0, h, { keepSource: e.includes("--keep-source") });
1371
+ let y = await Ae(s, v, c, "id", void 0, h, { keepSource: e.includes("--keep-source") });
1265
1372
  return console.log(""), console.log(` copied ${y.copied}/${y.sourceRows} row(s)`), console.log(` snapshot: ${y.snapshotTable} — restore from here, then drop it yourself`), y.verified ? (console.log(y.sourceDropped ? ` dropped ${n}` : ` ${n} left in place (--keep-source)`), 0) : (console.error(" ✗ counts DIFFER — the source was NOT dropped. Compare the two tables before doing anything else."), 1);
1266
1373
  },
1267
1374
  "scan-credentials": async (e) => {
@@ -1269,28 +1376,89 @@ export default defineMigration({
1269
1376
  for (let t = 0; t < e.length; t += 1) {
1270
1377
  if (e[t] !== "--table") continue;
1271
1378
  let r = e[t + 1];
1272
- if (r === void 0) throw new k("voltro db scan-credentials: --table needs a value");
1379
+ if (r === void 0) throw new O("voltro db scan-credentials: --table needs a value");
1273
1380
  let [i, a = "subject"] = r.split(":");
1274
1381
  n.push({
1275
1382
  table: i,
1276
1383
  column: a
1277
1384
  });
1278
1385
  }
1279
- let r = [...Oe, ...n], i = await J(await q(), b.SqlClient.pipe(g.flatMap((e) => g.promise(() => Me(e, t, r, G)))));
1280
- return console.log(Ne(i, t)), Pe(i);
1386
+ let r = [...je, ...n], i = await J(await q(), x.SqlClient.pipe(g.flatMap((e) => g.promise(() => ze(e, t, r, G)))));
1387
+ return console.log(Be(i, t)), Ve(i);
1388
+ },
1389
+ "encrypt-column": async (e) => {
1390
+ let t = W(), n = e.includes("--dry-run"), r = e.includes("--yes"), i = (t) => {
1391
+ let n = e.indexOf(t);
1392
+ return n >= 0 ? e[n + 1] : void 0;
1393
+ }, a = Math.max(1, Number(i("--batch") ?? 500)), o = i("--key-env") ?? "FIELD_ENCRYPTION_KEY", s = process.env[o];
1394
+ if (!s) throw new O(`voltro db encrypt-column: ${o} is not set. It must be the SAME key your app runs with (\`governancePlugin({ fieldEncryption: { key } })\`) — encrypting with a different one produces rows the app cannot read. Use --key-env to name a different variable.`);
1395
+ let c = ee(s), l = e.filter((e) => !e.startsWith("--") && e.includes(".")).map(Ke);
1396
+ if (l.length === 0) throw new O("voltro db encrypt-column: name at least one <table>.<column>");
1397
+ let u = await q(), d = [];
1398
+ for (let e of l) {
1399
+ let i = G(e.table), s = G(e.column), l = await J(u, x.SqlClient.pipe(g.flatMap((n) => g.promise(async () => {
1400
+ let r = await g.runPromise(n.unsafe(`SELECT character_maximum_length AS n FROM information_schema.columns\n WHERE ${K(t)} AND table_name = '${e.table}' AND column_name = '${e.column}'`));
1401
+ if (r.length === 0) throw new O(`voltro db encrypt-column: ${e.table}.${e.column} does not exist on this database`);
1402
+ let i = Object.values(r[0])[0];
1403
+ return i == null ? void 0 : Number(i);
1404
+ })))), f = await J(u, x.SqlClient.pipe(g.flatMap((e) => g.promise(async () => await g.runPromise(e.unsafe(`SELECT ${G("id")} AS id, ${s} AS v FROM ${i}`)))))), p = 0;
1405
+ for (let e of f) typeof e.v != "string" || C(e.v) || (p = Math.max(p, Qe(e.v)));
1406
+ let m = Je(p, l);
1407
+ if (!m.fits) return console.error(` ✗ ${Ye(e, m)}`), 1;
1408
+ if ($e(f.map((e) => e.v).find((e) => typeof e == "string" && C(e)), c) === !1) return console.error(` ✗ ${et(e, o)}`), 1;
1409
+ let h = f.filter((t) => Ze(t.v, c, e) !== void 0).length;
1410
+ if (!n && !r) return console.error(` ✗ ${e.table}.${e.column}: ${h} row(s) would be rewritten in place. Re-run with --yes, or --dry-run to see the counts without writing.`), 1;
1411
+ let _ = 0, v = 0, y = 0, b = [];
1412
+ for (let t of f) {
1413
+ if (t.v === null || t.v === void 0 || t.v === "") {
1414
+ v += 1;
1415
+ continue;
1416
+ }
1417
+ if (typeof t.v == "string" && C(t.v)) {
1418
+ _ += 1;
1419
+ continue;
1420
+ }
1421
+ let n = Ze(t.v, c, e);
1422
+ if (n === void 0) {
1423
+ v += 1;
1424
+ continue;
1425
+ }
1426
+ b.push({
1427
+ id: t.id,
1428
+ value: n
1429
+ }), y += 1;
1430
+ }
1431
+ if (!n && b.length > 0) for (let t = 0; t < b.length; t += a) {
1432
+ let n = b.slice(t, t + a);
1433
+ await J(u, x.SqlClient.pipe(g.flatMap((t) => g.promise(async () => {
1434
+ for (let r of n) await g.runPromise(t`UPDATE ${t(e.table)} SET ${t(e.column)} = ${r.value} WHERE ${t("id")} = ${r.id}`);
1435
+ })))), B.info(`encrypt-column ${e.table}.${e.column}: ${Math.min(t + n.length, b.length)}/${b.length}`);
1436
+ }
1437
+ d.push({
1438
+ table: e.table,
1439
+ column: e.column,
1440
+ scanned: f.length,
1441
+ alreadyEncrypted: _,
1442
+ empty: v,
1443
+ encrypted: y
1444
+ });
1445
+ }
1446
+ console.log(`field encryption · dialect ${t}${n ? " · DRY RUN, nothing written" : ""}`), console.log("");
1447
+ for (let e of d) console.log(Xe(e, n));
1448
+ return n || console.log(tt(l)), 0;
1281
1449
  }
1282
- }, vt = async (e) => {
1450
+ }, zt = async (e) => {
1283
1451
  let [t, ...n] = e;
1284
1452
  if (!t) {
1285
1453
  console.error("voltro db <subcommand> [args]"), console.error("subcommands:");
1286
- for (let e of Object.keys(_t)) console.error(` ${e}`);
1454
+ for (let e of Object.keys(Rt)) console.error(` ${e}`);
1287
1455
  return 1;
1288
1456
  }
1289
- let r = _t[t];
1457
+ let r = Rt[t];
1290
1458
  if (!r) return console.error(`voltro db: unknown subcommand '${t}'`), 2;
1291
- (process.env.DB_DIRECT_URL !== void 0 || process.env.DB_MIGRATE_URL !== void 0) && R.info("using a DIRECT (non-pooler) connection for the db-command path (DB_DIRECT_URL/DB_MIGRATE_URL set)");
1459
+ (process.env.DB_DIRECT_URL !== void 0 || process.env.DB_MIGRATE_URL !== void 0) && B.info("using a DIRECT (non-pooler) connection for the db-command path (DB_DIRECT_URL/DB_MIGRATE_URL set)");
1292
1460
  let i = (e) => {
1293
- H(e) ? R.error(U) : console.error(e), process.exit(1);
1461
+ rt(e) ? B.error(it) : console.error(e), process.exit(1);
1294
1462
  };
1295
1463
  process.on("uncaughtException", i);
1296
1464
  try {
@@ -1300,4 +1468,4 @@ export default defineMigration({
1300
1468
  }
1301
1469
  };
1302
1470
  //#endregion
1303
- export { Ge as a, k as c, lt as i, ve as l, V as n, H as o, We as r, vt as s, U as t };
1471
+ export { ft as a, O as c, At as i, xe as l, U as n, rt as o, dt as r, zt as s, it as t };