@voltro/cli 0.32.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 (80) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/THIRD-PARTY-NOTICES.md +1 -29
  3. package/dist/apiBuild-C-x9YacA.js +2 -0
  4. package/dist/{apiBuild-C4uEbs3s.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-dm7OHtPt.js → checkCommand-xGhRFFg2.js} +15 -15
  8. package/dist/{commands-C0nEePif.js → commands-CJfepbm4.js} +97 -13
  9. package/dist/{dbCommand-ifOMafuG.js → dbCommand-DTLKAfbA.js} +401 -241
  10. package/dist/dbCommand-b1gum4td.js +2 -0
  11. package/dist/{dev-5ficNnvF.js → dev-C_P8FLSx.js} +1539 -1487
  12. package/dist/{dev-rc3fwPSZ.js → dev-iiMtlkfs.js} +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/{inspectMetrics-BqO4E9G0.js → inspectMetrics-1xzTKAFx.js} +573 -550
  15. package/dist/{sdkgen-B_5mHQS2.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-D1-8eKrO.js → start-C0koT0UO.js} +1 -1
  19. package/dist/startEntry.js +2 -2
  20. package/package.json +31 -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.md +50 -0
  27. package/templates/agent-docs/scheduling.md +4 -1
  28. package/templates/agent-docs/security.md +23 -2
  29. package/templates/agent-docs/whats-new.md +128 -86
  30. package/templates/apps/api-ai/package.json +7 -7
  31. package/templates/apps/api-auth/package.json +8 -8
  32. package/templates/apps/api-backend/package.json +7 -7
  33. package/templates/apps/api-backend-deactivation/package.json +7 -7
  34. package/templates/apps/api-backend-mail/package.json +8 -8
  35. package/templates/apps/api-backend-mariadb/package.json +9 -9
  36. package/templates/apps/api-backend-sqlite/package.json +8 -8
  37. package/templates/apps/api-backend-storage/package.json +8 -8
  38. package/templates/apps/api-cms/package.json +10 -10
  39. package/templates/apps/api-collab/package.json +8 -8
  40. package/templates/apps/api-data-advanced/package.json +8 -8
  41. package/templates/apps/api-durable/package.json +8 -8
  42. package/templates/apps/api-feature-flags/package.json +9 -9
  43. package/templates/apps/api-governance/package.json +8 -8
  44. package/templates/apps/api-kv/package.json +8 -8
  45. package/templates/apps/api-moderation/package.json +8 -8
  46. package/templates/apps/api-observability/package.json +8 -8
  47. package/templates/apps/api-ratelimit/package.json +8 -8
  48. package/templates/apps/api-rbac/package.json +8 -8
  49. package/templates/apps/api-rest/package.json +7 -7
  50. package/templates/apps/api-saas/package.json +11 -11
  51. package/templates/apps/api-saas-starter/package.json +10 -10
  52. package/templates/apps/api-search/package.json +8 -8
  53. package/templates/apps/api-status/package.json +8 -8
  54. package/templates/apps/api-versioning/package.json +8 -8
  55. package/templates/apps/api-webhooks/package.json +9 -9
  56. package/templates/apps/changelog/package.json +6 -6
  57. package/templates/apps/edge-functions/package.json +2 -2
  58. package/templates/apps/frontend-admin/package.json +8 -8
  59. package/templates/apps/frontend-app/package.json +8 -8
  60. package/templates/apps/frontend-auth/package.json +8 -8
  61. package/templates/apps/frontend-blank/package.json +7 -7
  62. package/templates/apps/frontend-cms/package.json +9 -9
  63. package/templates/apps/frontend-collab/package.json +9 -9
  64. package/templates/apps/frontend-contact/package.json +7 -7
  65. package/templates/apps/frontend-dashboard/package.json +7 -7
  66. package/templates/apps/frontend-docs/package.json +7 -7
  67. package/templates/apps/frontend-i18n/package.json +6 -6
  68. package/templates/apps/frontend-landing/package.json +7 -7
  69. package/templates/apps/frontend-portal/package.json +8 -8
  70. package/templates/apps/frontend-saas/package.json +8 -8
  71. package/templates/apps/frontend-spa/package.json +7 -7
  72. package/templates/apps/frontend-ssr/package.json +7 -7
  73. package/templates/apps/frontend-ssr-api/package.json +8 -8
  74. package/templates/apps/frontend-static-blog/package.json +6 -6
  75. package/templates/apps/frontend-status/package.json +8 -8
  76. package/templates/apps/mobile-app/package.json +2 -2
  77. package/dist/apiBuild-OJEjtwcn.js +0 -2
  78. package/dist/checkCommand-CwMrzAgV.js +0 -2
  79. package/dist/dbCommand-By__Ev0R.js +0 -2
  80. package/dist/serveCommand-PxMmn96o.js +0 -1578
@@ -8,11 +8,11 @@ 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 S, quoteIdent as ne, registerCoreTables as re } from "@voltro/database";
14
- import { createHash as ie } from "node:crypto";
15
- import { DEFAULT_CDC_CHANNEL as ae, applyPlan as oe, applySchema as C, assessRollingDeployGate as se, declaredSnapshot as ce, destructiveScope as le, detectReactiveTriggerDrift as ue, fingerprintSchema as w, ignoreTablesFromEnv as T, introspectSchema as de, pendingFileMigrationIds as fe, planMigrations as E, reactiveTriggerRepairSql as pe, rollbackFileBasedMigration as me, rollingDeployUnsafeOps as he, runFileBasedMigrations as ge, shortFingerprint as D, squashMigrationPlans as _e, unblockLossy as ve } 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
17
  var O = class extends Error {
18
18
  isCliError = !0;
@@ -20,7 +20,7 @@ var O = class extends Error {
20
20
  constructor(e, t = 1) {
21
21
  super(e), this.name = "CliError", this.code = t;
22
22
  }
23
- }, ye = (e) => typeof e == "object" && !!e && e.isCliError === !0, be = (e) => typeof e != "string", xe = (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 O = class extends Error {
37
37
  });
38
38
  continue;
39
39
  }
40
- if (be(n)) {
40
+ if (Se(n)) {
41
41
  c.push({
42
42
  target: t,
43
43
  source: n.expr,
@@ -80,23 +80,23 @@ var O = class extends Error {
80
80
  remintIds: f,
81
81
  fatal: i.some((e) => e.fatal)
82
82
  };
83
- }, Se = (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
- ], Ce = (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 Se(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
- }, we = (e) => e === "mysql" || e === "mariadb" ? (e) => `\`${e}\`` : e === "mssql" ? (e) => `[${e}]` : (e) => `"${e}"`, Te = (e) => `${e}__adopt_snapshot`, Ee = (e, t, n, r, i) => {
99
- let a = we(n), o = Te(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 O = class extends Error {
106
106
  drop: `DROP TABLE ${a(e.from)}`,
107
107
  dropSnapshot: `DROP TABLE ${a(o)}`
108
108
  };
109
- }, De = (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
- }, Oe = 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 = Ee(e, t, n, r, i), c = Te(e.from), l = De(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 = De(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,
@@ -146,7 +146,7 @@ var O = class extends Error {
146
146
  "credential",
147
147
  "privatekey",
148
148
  "private_key"
149
- ], ke = [
149
+ ], je = [
150
150
  {
151
151
  table: "_voltro_audit_log",
152
152
  column: "subject"
@@ -191,7 +191,7 @@ var O = class extends Error {
191
191
  table: "_voltro_row_history",
192
192
  column: "subjectId"
193
193
  }
194
- ], Ae = (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 O = class extends Error {
200
200
  case "sqlite":
201
201
  case "turso": return `LOWER(CAST(${e} AS TEXT))`;
202
202
  }
203
- }, je = (e, t, n = k) => n.map((n) => `${Ae(e, t)} LIKE '%${n}%'`).join("\n OR "), A = 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 A(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,20 +260,25 @@ var O = class extends Error {
220
260
  continue;
221
261
  }
222
262
  try {
223
- let s = await A(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${je(r(a.column), t)}`), c = [];
263
+ let s = await P(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${A(r(a.column), t)}`), c = [];
224
264
  if (s > 0) for (let i of k) try {
225
- let o = await A(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${je(r(a.column), t, [i])}`);
265
+ let o = await P(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${A(r(a.column), t, [i])}`);
226
266
  o > 0 && c.push({
227
267
  needle: i,
228
268
  rows: o
229
269
  });
230
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 {}
231
275
  i.push({
232
276
  table: a.table,
233
277
  column: a.column,
234
278
  scanned: o,
235
279
  hits: s,
236
- matchedNeedles: c
280
+ matchedNeedles: c,
281
+ ...l === void 0 ? {} : { classification: l }
237
282
  });
238
283
  } catch {
239
284
  i.push({
@@ -246,7 +291,7 @@ var O = class extends Error {
246
291
  }
247
292
  }
248
293
  return i;
249
- }, Ne = (e, t) => {
294
+ }, Be = (e, t) => {
250
295
  let n = k.length, r = [`credential scan · dialect ${t}`, ""], i = /* @__PURE__ */ new Set();
251
296
  for (let t of e) {
252
297
  if (t.unavailable === "column") {
@@ -261,12 +306,14 @@ var O = class extends Error {
261
306
  r.push(` · ${t.table} — table is EMPTY (0 rows scanned; this is not a clean bill of health)`);
262
307
  continue;
263
308
  }
264
- let e = (t.hits ?? 0) > 0 ? "" : "";
265
- r.push(` ${e} ${t.table}.${t.column} — ${t.hits} of ${t.scanned} row(s) match a credential-shaped key`), (t.matchedNeedles?.length ?? 0) > 0 && r.push(` matched (rows per needle, may overlap): ${t.matchedNeedles.map((e) => `${e.needle} (${e.rows})`).join(", ")}`);
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."));
266
313
  }
267
- let a = e.reduce((e, t) => e + (t.hits ?? 0), 0);
268
- return r.push(""), 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 key 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");
269
- }, 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) => {
270
317
  let n = [];
271
318
  for (let r of e) {
272
319
  let e;
@@ -276,7 +323,7 @@ var O = class extends Error {
276
323
  t.warn("migration: failed to import", { file: r }, e);
277
324
  continue;
278
325
  }
279
- let i = (e.default && S(e.default) ? e.default : void 0) ?? Object.values(e).find(S);
326
+ let i = (e.default && ne(e.default) ? e.default : void 0) ?? Object.values(e).find(ne);
280
327
  if (!i) {
281
328
  t.warn("migration: no defineMigration export", { file: r });
282
329
  continue;
@@ -286,7 +333,7 @@ var O = class extends Error {
286
333
  idInFile: i.id,
287
334
  idFromFile: d(r).replace(/\.migration\.ts$/, "")
288
335
  });
289
- 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);
290
337
  n.push({
291
338
  file: r,
292
339
  definition: i,
@@ -294,14 +341,14 @@ var O = class extends Error {
294
341
  });
295
342
  }
296
343
  return [...n].sort((e, t) => e.definition.id < t.definition.id ? -1 : 1);
297
- }, N = (e) => {
344
+ }, L = (e) => {
298
345
  let t = (process.env.DB_DIALECT ?? "postgres").toLowerCase(), n = t === "memory" ? "postgres" : t;
299
- return g.runPromise(C(x, n).pipe(g.provide(e)));
300
- }, P = (e) => g.runPromise(g.gen(function* () {
301
- 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(() => []));
302
349
  return new Map(e.map((e) => [e.id, e]));
303
- }).pipe(g.provide(e))), F = (e, t) => g.runPromise(g.gen(function* () {
304
- yield* (yield* b.SqlClient)`
350
+ }).pipe(g.provide(e))), z = (e, t) => g.runPromise(g.gen(function* () {
351
+ yield* (yield* x.SqlClient)`
305
352
  INSERT INTO "_voltro_migrations"
306
353
  (id, name, hash, status, "appliedAt", "durationMs", "errorMessage", "errorStack")
307
354
  VALUES
@@ -316,14 +363,14 @@ var O = class extends Error {
316
363
  "errorStack" = EXCLUDED."errorStack",
317
364
  "updatedAt" = now()
318
365
  `;
319
- }).pipe(g.provide(e))), Fe = async (e, t, n) => {
366
+ }).pipe(g.provide(e))), He = async (e, t, n) => {
320
367
  if (e.length === 0) return {
321
368
  applied: [],
322
369
  failed: null,
323
370
  skipped: []
324
371
  };
325
- await N(t);
326
- let r = await P(t), i = [], a = [], o = null;
372
+ await L(t);
373
+ let r = await R(t), i = [], a = [], o = null;
327
374
  for (let s of e) {
328
375
  let e = r.get(s.definition.id);
329
376
  if (e && e.status === "applied") {
@@ -344,7 +391,7 @@ var O = class extends Error {
344
391
  name: s.definition.name
345
392
  });
346
393
  let c = Date.now(), l = [];
347
- await F(t, {
394
+ await z(t, {
348
395
  id: s.definition.id,
349
396
  name: s.definition.name,
350
397
  hash: s.hash,
@@ -385,7 +432,7 @@ var O = class extends Error {
385
432
  ...a.stack ? { stack: a.stack } : {}
386
433
  }
387
434
  };
388
- o = f, i.push(f), await F(t, {
435
+ o = f, i.push(f), await z(t, {
389
436
  id: s.definition.id,
390
437
  name: s.definition.name,
391
438
  hash: s.hash,
@@ -404,7 +451,7 @@ var O = class extends Error {
404
451
  status: "applied",
405
452
  durationMs: Date.now() - c,
406
453
  steps: l
407
- }), await F(t, {
454
+ }), await z(t, {
408
455
  id: s.definition.id,
409
456
  name: s.definition.name,
410
457
  hash: s.hash,
@@ -424,9 +471,9 @@ var O = class extends Error {
424
471
  failed: o,
425
472
  skipped: a
426
473
  };
427
- }, Ie = async (e, t, n, r = {}) => {
428
- await N(t);
429
- 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");
430
477
  if (a.length === 0) return n.info("migration: nothing to roll back"), {
431
478
  applied: [],
432
479
  failed: null,
@@ -479,7 +526,7 @@ var O = class extends Error {
479
526
  status: "applied",
480
527
  durationMs: Date.now() - r,
481
528
  steps: i
482
- }), await F(t, {
529
+ }), await z(t, {
483
530
  id: e.definition.id,
484
531
  name: e.definition.name,
485
532
  hash: e.hash,
@@ -495,9 +542,9 @@ var O = class extends Error {
495
542
  failed: c,
496
543
  skipped: []
497
544
  };
498
- }, I = async (e, t) => {
499
- await N(t);
500
- let n = await P(t);
545
+ }, We = async (e, t) => {
546
+ await L(t);
547
+ let n = await R(t);
501
548
  return e.map((e) => {
502
549
  let t = n.get(e.definition.id);
503
550
  return t ? {
@@ -514,7 +561,7 @@ var O = class extends Error {
514
561
  hashMatches: !0
515
562
  };
516
563
  });
517
- }, L = (e) => {
564
+ }, Ge = (e) => {
518
565
  if (!e.failed) return "";
519
566
  let t = e.failed, n = [
520
567
  `✗ Migration failed: ${t.migrationId} / step '${t.error?.stepName ?? "?"}'`,
@@ -527,7 +574,59 @@ var O = class extends Error {
527
574
  ""
528
575
  ];
529
576
  return e.applied.length > 0 && n.unshift("", `(${e.applied.length} migration(s) applied successfully before the failure.)`), n.join("\n");
530
- }, 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([
531
630
  "node_modules",
532
631
  "dist",
533
632
  "build",
@@ -535,17 +634,17 @@ var O = class extends Error {
535
634
  ".vite",
536
635
  ".next",
537
636
  ".git"
538
- ]), z = async (e, t) => {
637
+ ]), V = async (e, t) => {
539
638
  let n = [], r = async (e) => {
540
639
  let i = await v.readdir(e, { withFileTypes: !0 }).catch(() => []);
541
640
  for (let a of i) {
542
- if (Le.has(a.name)) continue;
641
+ if (nt.has(a.name)) continue;
543
642
  let i = f(e, a.name);
544
643
  a.isDirectory() ? await r(i) : a.isFile() && t.test(a.name) && n.push(i);
545
644
  }
546
645
  };
547
646
  return await r(e), n;
548
- }, B = (e) => m(e.find((e) => !e.startsWith("-")) ?? process.cwd()), V = () => {
647
+ }, H = (e) => m(e.find((e) => !e.startsWith("-")) ?? process.cwd()), U = () => {
549
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;
550
649
  return a ? {
551
650
  url: a,
@@ -560,63 +659,63 @@ var O = class extends Error {
560
659
  ...n,
561
660
  ...i
562
661
  };
563
- }, H = (e) => {
662
+ }, rt = (e) => {
564
663
  let t = e;
565
664
  if (typeof t != "object" || !t) return !1;
566
665
  if (t.code === "ERR_OUT_OF_RANGE" || t.cause?.code === "ERR_OUT_OF_RANGE") return !0;
567
666
  let n = `${t.message ?? ""} ${t.cause?.message ?? ""} ${t.stack ?? ""}`;
568
667
  return /ERR_OUT_OF_RANGE|out of range/i.test(n) && /pg-protocol|parseDataRow|buffer-reader|BufferReader/i.test(n);
569
- }, 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 = () => {
570
669
  let e = (process.env.DB_DIALECT ?? "postgres").toLowerCase();
571
670
  if (e === "memory") throw new O("voltro db: subcommands require a real dialect, not DB_DIALECT=memory.");
572
671
  return e;
573
- }, G = (e) => ne(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) => {
574
- let t = B(e), n = e.includes("--dry-run"), r = await M(await z(t, j), R);
575
- 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;
576
675
  if (n) {
577
676
  let e = /* @__PURE__ */ new Set();
578
677
  try {
579
- let t = await I(r, await q());
678
+ let t = await We(r, await q());
580
679
  e = new Set(t.filter((e) => e.status === "applied").map((e) => e.id));
581
680
  } catch {
582
- 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");
583
682
  }
584
683
  let t = r.filter((t) => !e.has(t.definition.id));
585
- if (t.length === 0) return R.info("migrate --dry-run: nothing pending — every migration is applied"), 0;
586
- 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)`);
587
686
  for (let e of t) console.log(` pending ${e.definition.id}`);
588
687
  return 0;
589
688
  }
590
- let i = await Fe(r, await q(), R);
591
- 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", {
592
691
  applied: i.applied.length,
593
692
  skipped: i.skipped.length
594
693
  }), 0);
595
- }, ze = async (e) => {
596
- let t = B(e), n = l(e, "--to"), r = await M(await z(t, j), R);
597
- if (r.length === 0) return R.info("no migrations found", { root: t }), 0;
598
- let i = await Ie(r, await q(), R, n ? { to: n } : {});
599
- return i.failed ? (process.stderr.write(L(i) + "\n"), 1) : (R.info("rollback complete", { reverted: i.applied.length }), 0);
600
- }, Be = async (e) => {
601
- 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);
602
701
  if (t.length === 0) return console.log("no migrations found"), 0;
603
- let n = await I(t, await q());
702
+ let n = await We(t, await q());
604
703
  for (let e of n) {
605
704
  let t = e.status === "applied" ? "✓" : e.status === "failed" ? "✗" : e.status === "reverted" ? "↩" : "·", n = e.hashMatches ? "" : " (hash drift!)", r = e.appliedAt ? ` @ ${e.appliedAt.toISOString()}` : "";
606
705
  console.log(` ${t} ${e.id} [${e.status}] ${e.name}${r}${n}`);
607
706
  }
608
707
  return 0;
609
- }, Ve = async (e) => {
708
+ }, ct = async (e) => {
610
709
  let t = (l(e, "--store") ?? process.env.DB_DIALECT ?? "postgres").toLowerCase();
611
- 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."), {
612
- 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(),
613
712
  dispose: async () => {},
614
713
  dialectId: "memory"
615
714
  };
616
715
  let n = await r(t);
617
- 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`);
618
717
  let i = await n.makeStore({
619
- sqlLayer: n.makeSqlLayer(V()),
718
+ sqlLayer: n.makeSqlLayer(U()),
620
719
  changeStrategy: "inline"
621
720
  }), a = i;
622
721
  return {
@@ -626,77 +725,77 @@ var O = class extends Error {
626
725
  },
627
726
  dialectId: n.id
628
727
  };
629
- }, He = async (r) => {
630
- let a = B(r), o = l(r, "--id"), s = l(r, "--lifecycle"), c = await t(await z(a, i), R);
631
- if (c.length === 0) return R.info("no seeds found", { root: a }), 0;
632
- 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);
633
732
  try {
634
- 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) => ({
635
734
  ...e,
636
735
  definition: {
637
736
  ...e.definition,
638
737
  lifecycle: "boot"
639
738
  }
640
- })), u, R, { force: !0 })).filter((e) => e.status === "failed").length > 0);
739
+ })), u, B, { force: !0 })).filter((e) => e.status === "failed").length > 0);
641
740
  } finally {
642
741
  await d().catch((e) => {
643
- R.warn("voltro db seed: store dispose failed", { error: String(e) });
742
+ B.warn("voltro db seed: store dispose failed", { error: String(e) });
644
743
  });
645
744
  }
646
- }, Ue = (e) => typeof e == "object" && !!e && "tableName" in e && "fields" in e && typeof e.tableName == "string", We = async (e) => {
647
- 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 = [];
648
747
  for (let r of t) {
649
748
  let t;
650
749
  try {
651
750
  t = await import(h(r).href);
652
751
  } catch (t) {
653
- R.warn("db: failed to import schema file", {
752
+ B.warn("db: failed to import schema file", {
654
753
  file: p(e, r),
655
754
  error: String(t)
656
755
  });
657
756
  continue;
658
757
  }
659
- 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);
660
759
  }
661
760
  return n;
662
- }, Ge = (e) => {
761
+ }, ft = (e) => {
663
762
  let t = new Map(e.map((e) => [e.tableName, e]));
664
763
  t.has("actors") || t.set("actors", te);
665
764
  let n = t.get("actors"), r = t.get("tenants");
666
- return re({
765
+ return ie({
667
766
  actors: () => n,
668
767
  ...r === void 0 ? {} : { tenants: () => r }
669
768
  }), [...t.values()];
670
- }, Ke = (e) => {
769
+ }, pt = (e) => {
671
770
  let t = /* @__PURE__ */ new Map();
672
771
  for (let n of e) {
673
772
  let e = n.tableName;
674
773
  e !== void 0 && !t.has(e) && t.set(e, n);
675
774
  }
676
775
  return [...t.values()];
677
- }, qe = (e) => {
678
- let t = le(process.env.VOLTRO_DESTRUCTIVE_OK);
776
+ }, mt = (e) => {
777
+ let t = ue(process.env.VOLTRO_DESTRUCTIVE_OK);
679
778
  if (t === void 0 || !e.operations.filter((e) => e.blocked).every((e) => e.classification === "lossy")) return {
680
779
  plan: e,
681
780
  acknowledged: !1
682
781
  };
683
- let n = ve(e, t);
782
+ let n = be(e, t);
684
783
  return {
685
784
  plan: n,
686
785
  acknowledged: !n.operations.some((e) => e.blocked)
687
786
  };
688
787
  }, J = async (e, t) => g.runPromise(t.pipe(g.provide(e))), Y = async (e) => {
689
788
  try {
690
- return await J(e, b.SqlClient.pipe(g.flatMap(de)));
789
+ return await J(e, x.SqlClient.pipe(g.flatMap(pe)));
691
790
  } catch (e) {
692
791
  let t = String(e?.message ?? e);
693
- if (H(e) || /connection error/i.test(t)) {
694
- 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}`);
695
794
  throw n.cause = e, n;
696
795
  }
697
796
  throw e;
698
797
  }
699
- }, Je = {
798
+ }, ht = {
700
799
  safe: "\x1B[32m✓\x1B[0m",
701
800
  "needs-default": "\x1B[32m⊕\x1B[0m",
702
801
  "needs-backfill": "\x1B[33m⊕\x1B[0m",
@@ -704,7 +803,7 @@ var O = class extends Error {
704
803
  lossy: "\x1B[31m✗\x1B[0m",
705
804
  "online-required": "\x1B[36m◷\x1B[0m",
706
805
  "multi-step": "\x1B[35m⇉\x1B[0m"
707
- }, Ye = (e) => {
806
+ }, gt = (e) => {
708
807
  let t = e.op, n;
709
808
  switch (t.kind) {
710
809
  case "create-table":
@@ -768,9 +867,9 @@ var O = class extends Error {
768
867
  n = `ALTER TABLE ${t.table} DROP CHECK ${t.column}`;
769
868
  break;
770
869
  }
771
- 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}` : "";
772
871
  return ` ${r} ${n}${i}${a}`;
773
- }, Xe = async (e, t) => {
872
+ }, _t = async (e, t) => {
774
873
  let n = e.operations.filter((e) => e.classification === "needs-default" && e.op.kind === "add-column");
775
874
  if (n.length === 0) return;
776
875
  let r = [];
@@ -779,7 +878,7 @@ var O = class extends Error {
779
878
  if (i === void 0 || a === void 0) continue;
780
879
  let o = 0;
781
880
  try {
782
- let e = await J(t, b.SqlClient.pipe(g.flatMap((e) => e.unsafe(`SELECT COUNT(*) AS n FROM ${ne(i, W())}`))));
881
+ let e = await J(t, x.SqlClient.pipe(g.flatMap((e) => e.unsafe(`SELECT COUNT(*) AS n FROM ${re(i, W())}`))));
783
882
  o = Number(Object.values(e[0] ?? { n: 0 })[0] ?? 0);
784
883
  } catch {
785
884
  continue;
@@ -793,13 +892,13 @@ var O = class extends Error {
793
892
  }
794
893
  }, X = async (e, t) => {
795
894
  if (W() !== "postgres") return;
796
- let n = ae, r;
895
+ let n = oe, r;
797
896
  try {
798
- r = await J(t, b.SqlClient.pipe(g.flatMap((t) => ue(t, e, n))));
897
+ r = await J(t, x.SqlClient.pipe(g.flatMap((t) => de(t, e, n))));
799
898
  } catch {
800
899
  return;
801
900
  }
802
- let i = pe({
901
+ let i = he({
803
902
  tables: e,
804
903
  dialect: "postgres",
805
904
  missing: r.missing,
@@ -807,10 +906,10 @@ var O = class extends Error {
807
906
  channel: n
808
907
  });
809
908
  if (i.length === 0) return;
810
- 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)` : ""));
811
910
  let a = 0;
812
- for (let e of i) await J(t, b.SqlClient.pipe(g.flatMap((t) => t.unsafe(e)))), a += 1;
813
- 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))`, {
814
913
  installed: r.missing.length,
815
914
  removed: r.stale.length
816
915
  });
@@ -820,40 +919,40 @@ var O = class extends Error {
820
919
  console.log(" (schema is up to date)\n");
821
920
  return;
822
921
  }
823
- for (let t of e.operations) console.log(Ye(t));
824
- 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);
825
- }, Ze = (e) => {
826
- let t = he(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);
827
926
  if (t.length !== 0) {
828
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):");
829
928
  for (let e of t) console.log(` • ${e.op.op.kind}: ${e.reason}`), console.log(` → ${e.remedy}`);
830
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");
831
930
  }
832
- }, Qe = (e, t) => {
833
- let n = se({
931
+ }, yt = (e, t) => {
932
+ let n = ce({
834
933
  enabled: process.env.VOLTRO_ROLLING_DEPLOY === "1",
835
934
  forced: t.includes("--force"),
836
- unsafeCount: he(e.operations).length
935
+ unsafeCount: _e(e.operations).length
837
936
  });
838
- return n.refuse ? (R.error(`db apply: ${n.message}`), !0) : (n.warnForced && R.warn(`db apply: ${n.message}`), !1);
839
- }, $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) => {
840
939
  let t = l(e, "--online-after") ?? process.env.VOLTRO_ONLINE_THRESHOLD;
841
940
  if (t === void 0) return;
842
941
  let n = Number(t);
843
942
  if (!Number.isInteger(n) || n < 0) throw Error(`--online-after / VOLTRO_ONLINE_THRESHOLD must be a non-negative integer, got "${t}"`);
844
943
  return n;
845
944
  }, Q = async (e) => {
846
- let t = await We(e);
945
+ let t = await dt(e);
847
946
  if (t.length === 0) return;
848
947
  let n = await o({ root: e });
849
- return Ge(Ke([
948
+ return ft(pt([
850
949
  ...t,
851
- ...x,
950
+ ...S,
852
951
  ...n
853
952
  ]));
854
- }, et = async (e) => {
855
- let t = $e(e), n = B(e), r = await Q(n);
856
- 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", {
857
956
  root: n,
858
957
  hint: "looked for *.entity.ts / *.schema.ts / schema.ts"
859
958
  }), 1;
@@ -863,9 +962,9 @@ var O = class extends Error {
863
962
  a && (o.authorization = `Bearer ${a}`);
864
963
  try {
865
964
  let e = n.endsWith("/_voltro/inspect/migrations") ? n : `${n.replace(/\/+$/, "")}/_voltro/inspect/migrations`, i = await fetch(e, { headers: o });
866
- 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;
867
966
  let a = (await i.json()).drift?.liveSnapshot;
868
- if (!a) return R.error("db plan --against: remote inspect payload missing drift.liveSnapshot", { url: e }), 1;
967
+ if (!a) return B.error("db plan --against: remote inspect payload missing drift.liveSnapshot", { url: e }), 1;
869
968
  let s = E({
870
969
  declared: r,
871
970
  live: a,
@@ -875,7 +974,7 @@ var O = class extends Error {
875
974
  });
876
975
  return console.log(`\n[--against ${e}] — diff vs remote live schema:\n`), Z(s), s.summary.blocked > 0 ? 2 : 0;
877
976
  } catch (e) {
878
- return R.error(`db plan --against: ${e.message}`), 1;
977
+ return B.error(`db plan --against: ${e.message}`), 1;
879
978
  }
880
979
  }
881
980
  let a = E({
@@ -886,35 +985,35 @@ var O = class extends Error {
886
985
  ...t === void 0 ? {} : { onlineAfter: t }
887
986
  });
888
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);
889
- }, tt = async (e, t, n) => {
988
+ }, St = async (e, t, n) => {
890
989
  let r;
891
990
  try {
892
991
  let t = await v.readFile(m(e), "utf8");
893
992
  r = JSON.parse(t);
894
993
  } catch (t) {
895
- 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;
896
995
  }
897
- 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;
898
997
  let i = await Q(n);
899
- 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;
900
999
  let a = await q();
901
- await J(a, C(x, W()));
902
- let o = await J(a, b.SqlClient.pipe(g.flatMap((e) => fe(e, n)))).catch(() => []);
903
- 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;
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;
904
1003
  let s = E({
905
1004
  declared: i,
906
1005
  live: await Y(a),
907
1006
  dialect: W(),
908
1007
  ...T()
909
1008
  });
910
- 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;
911
- 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;
912
- let c = qe(s), u = c.plan;
913
- 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;
914
- if (c.acknowledged && R.warn("db apply --plan: proceeding with acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)"), Qe(u, t)) return 2;
915
- 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;
916
1015
  Z(u);
917
- let d = (process.env.NODE_ENV ?? "dev").toLowerCase(), f = l(t, "--note"), p = await J(a, b.SqlClient.pipe(g.flatMap((e) => oe(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, {
918
1017
  declared: i,
919
1018
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:plan",
920
1019
  environment: d === "production" ? "prod" : d === "staging" ? "staging" : "dev",
@@ -927,19 +1026,19 @@ var O = class extends Error {
927
1026
  ...T()
928
1027
  })))
929
1028
  }))));
930
- 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`, {
931
1030
  id: p.id,
932
1031
  fingerprint: p.fingerprint.slice(0, 16)
933
1032
  }), await X(i, a), 0;
934
- }, nt = async (e) => {
935
- let t = B(e), n = $e(e), r = l(e, "--plan");
936
- if (r) return tt(r, e, t);
937
- 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;
938
1037
  let i = await Q(t);
939
- if (i === void 0) return R.warn("db apply: no schema files found", { root: t }), 1;
940
- let a = await q(), o = await dt(t, a);
941
- 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;
942
- await J(a, C(x, W()));
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()));
943
1042
  let s = E({
944
1043
  declared: i,
945
1044
  live: await Y(a),
@@ -947,12 +1046,12 @@ var O = class extends Error {
947
1046
  ...T(),
948
1047
  ...n === void 0 ? {} : { onlineAfter: n }
949
1048
  });
950
- Z(s), await Xe(s, a);
951
- let c = qe(s), u = c.plan;
952
- 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;
953
- if (c.acknowledged && R.warn(`db apply: proceeding with ${s.summary.blocked} acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)`), Qe(u, e)) return 2;
954
- 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;
955
- let d = l(e, "--note"), f = await J(a, b.SqlClient.pipe(g.flatMap((e) => oe(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, {
956
1055
  declared: i,
957
1056
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user",
958
1057
  environment: (process.env.NODE_ENV ?? "dev").toLowerCase() === "staging" ? "staging" : "dev",
@@ -966,12 +1065,12 @@ var O = class extends Error {
966
1065
  ...n === void 0 ? {} : { onlineAfter: n }
967
1066
  })))
968
1067
  }))));
969
- 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`, {
970
1069
  id: f.id,
971
1070
  fingerprint: f.fingerprint.slice(0, 16)
972
1071
  }), await X(i, a), 0;
973
- }, rt = async (e) => {
974
- 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`
975
1074
  SELECT ${e("id")}, ${e("fingerprint")}, ${e("operations")},
976
1075
  ${e("appliedBy")}, ${e("environment")}, ${e("source")},
977
1076
  ${e("durationMs")}, ${e("notes")}, ${e("appliedAt")}
@@ -979,25 +1078,25 @@ var O = class extends Error {
979
1078
  ORDER BY ${e("appliedAt")} DESC
980
1079
  LIMIT ${t}
981
1080
  `)));
982
- 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;
983
1082
  console.log(`\n${n.length} plan${n.length === 1 ? "" : "s"} (newest first):\n`);
984
1083
  for (let e of n) {
985
1084
  let t = 0;
986
1085
  try {
987
- t = JSON.parse(ct(e.operations)).length;
1086
+ t = JSON.parse(kt(e.operations)).length;
988
1087
  } catch {}
989
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`);
990
1089
  }
991
1090
  return console.log(), 0;
992
- }, it = async (e) => {
1091
+ }, Tt = async (e) => {
993
1092
  let t = e[0];
994
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;
995
- 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")}
996
1095
  FROM ${e("_voltro_migration_plans")} WHERE ${e("id")} = ${t}`)));
997
1096
  if (r.length === 0) return console.error(`restore-snapshot: plan '${t}' not found`), 1;
998
1097
  let i;
999
1098
  try {
1000
- i = JSON.parse(ct(r[0].operations));
1099
+ i = JSON.parse(kt(r[0].operations));
1001
1100
  } catch {
1002
1101
  return console.error(`restore-snapshot: plan '${t}' has corrupted operations JSON`), 1;
1003
1102
  }
@@ -1011,7 +1110,7 @@ var O = class extends Error {
1011
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;
1012
1111
  let s = 0;
1013
1112
  for (let e of a) {
1014
- 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`
1015
1114
  SELECT column_name FROM information_schema.columns
1016
1115
  WHERE ${t.unsafe(K(W()))}
1017
1116
  AND table_name = ${e.table}
@@ -1023,11 +1122,11 @@ var O = class extends Error {
1023
1122
  continue;
1024
1123
  }
1025
1124
  let r = t[0].column_name;
1026
- 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})`);
1027
1126
  }
1028
1127
  let c = 0;
1029
1128
  for (let e of o) {
1030
- 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`
1031
1130
  SELECT table_name FROM information_schema.tables
1032
1131
  WHERE ${t.unsafe(K(W()))}
1033
1132
  AND table_name LIKE ${e.table + "__dropped_%"}
@@ -1038,18 +1137,18 @@ var O = class extends Error {
1038
1137
  continue;
1039
1138
  }
1040
1139
  let r = t[0].table_name;
1041
- 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})`);
1042
1141
  }
1043
1142
  return console.log(`\n✓ restored ${s} column(s) + ${c} table(s) from plan '${t}'\n`), 0;
1044
- }, at = async (e) => {
1143
+ }, Et = async (e) => {
1045
1144
  let t = l(e, "--before");
1046
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;
1047
1146
  let n = t.replace(/[^0-9]/g, "");
1048
1147
  if (n.length < 8) return console.error(`gc-snapshots: --before must be at least a full date (YYYY-MM-DD), got "${t}"`), 2;
1049
- 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`
1050
1149
  SELECT table_name, column_name FROM information_schema.columns
1051
1150
  WHERE ${e.unsafe(K(W()))} AND column_name LIKE ${"%__dropped_%"}
1052
- `))), s = await J(a, b.SqlClient.pipe(g.flatMap((e) => e`
1151
+ `))), s = await J(a, x.SqlClient.pipe(g.flatMap((e) => e`
1053
1152
  SELECT table_name FROM information_schema.tables
1054
1153
  WHERE ${e.unsafe(K(W()))} AND table_name LIKE ${"%__dropped_%"}
1055
1154
  `))), c = (e) => {
@@ -1058,23 +1157,23 @@ var O = class extends Error {
1058
1157
  }, u = 0;
1059
1158
  for (let e of o) {
1060
1159
  let t = c(e.column_name);
1061
- 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++);
1062
1161
  }
1063
1162
  for (let e of s) {
1064
1163
  let t = c(e.table_name);
1065
- 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++);
1066
1165
  }
1067
1166
  return console.log(`\n${i ? `[dry-run] ${u} snapshot(s) would be dropped` : `✓ dropped ${u} snapshot(s)`} (older than ${r}).\n`), 0;
1068
- }, ot = async (e) => {
1167
+ }, Dt = async (e) => {
1069
1168
  let t = l(e, "--before");
1070
1169
  if (!t) return console.error("voltro db squash --before <iso-date> [--note \"<text>\"]"), 2;
1071
1170
  let n = t;
1072
1171
  if (Number.isNaN(new Date(n).getTime())) return console.error(`voltro db squash: --before '${n}' is not a valid ISO date`), 2;
1073
- let r = l(e, "--note"), i = B(e), a = await Q(i);
1074
- if (a === void 0) return R.warn("db squash: no schema files found", { root: i }), 1;
1075
- let o = w(ce(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();
1076
1175
  try {
1077
- let e = await J(s, _e({
1176
+ let e = await J(s, ye({
1078
1177
  before: n,
1079
1178
  fingerprint: o,
1080
1179
  appliedBy: process.env.USER ?? "cli",
@@ -1085,10 +1184,10 @@ var O = class extends Error {
1085
1184
  } catch (e) {
1086
1185
  return console.error(`squash refused: ${e.message}`), 1;
1087
1186
  }
1088
- }, st = async (e) => {
1187
+ }, Ot = async (e) => {
1089
1188
  try {
1090
- let t = w(await Y(e));
1091
- 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`
1092
1191
  UPDATE ${e("_voltro_migration_plans")}
1093
1192
  SET ${e("liveFingerprint")} = ${t}
1094
1193
  WHERE ${e("id")} = (
@@ -1100,7 +1199,7 @@ var O = class extends Error {
1100
1199
  AND ${e("liveFingerprint")} IS NULL
1101
1200
  `)));
1102
1201
  } catch {}
1103
- }, 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 ? {
1104
1203
  accept: !1,
1105
1204
  reason: "no-declared-schema"
1106
1205
  } : e > 0 ? {
@@ -1109,19 +1208,19 @@ var O = class extends Error {
1109
1208
  } : {
1110
1209
  accept: !0,
1111
1210
  reason: "converged"
1112
- }, ut = async (e) => {
1113
- 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`
1114
1213
  SELECT * FROM ${e("_voltro_migration_plans")}
1115
1214
  ORDER BY ${e("appliedAt")} DESC
1116
1215
  LIMIT 1
1117
- `))), i = await Y(n), a = w(i);
1118
- if (r.length === 0) return R.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;
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;
1119
1218
  let o = r[0];
1120
- 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: ${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;
1121
- if (o.liveFingerprint === a) return R.info(`db drift: live matches last applied (${D(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;
1122
1221
  let s = await (async () => {
1123
1222
  try {
1124
- let t = await Q(B(e));
1223
+ let t = await Q(H(e));
1125
1224
  return t === void 0 ? void 0 : E({
1126
1225
  declared: t,
1127
1226
  live: i,
@@ -1133,14 +1232,14 @@ var O = class extends Error {
1133
1232
  }
1134
1233
  })();
1135
1234
  if (t) {
1136
- let e = lt(s?.length);
1137
- 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;
1138
1237
  if (!e.accept && s !== void 0) {
1139
- 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();
1140
1239
  for (let e of s.slice(0, 10)) console.log(` - ${e.op.kind} ${"table" in e.op ? `[${e.op.table}]` : ""}`);
1141
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;
1142
1241
  }
1143
- return await J(n, b.SqlClient.pipe(g.flatMap((e) => e`
1242
+ return await J(n, x.SqlClient.pipe(g.flatMap((e) => e`
1144
1243
  UPDATE ${e("_voltro_migration_plans")}
1145
1244
  SET ${e("liveFingerprint")} = ${a}
1146
1245
  WHERE ${e("id")} = (
@@ -1149,43 +1248,43 @@ var O = class extends Error {
1149
1248
  ORDER BY ${e("appliedAt")} DESC LIMIT 1
1150
1249
  ) AS ${e("newest")}
1151
1250
  )
1152
- `))), R.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;
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;
1153
1252
  }
1154
- return R.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);
1155
- }, 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) => {
1156
1255
  let n = (process.env.NODE_ENV ?? "").toLowerCase() === "production" ? "prod" : (process.env.NODE_ENV ?? "").toLowerCase() === "staging" ? "staging" : "dev";
1157
1256
  try {
1158
- let r = await J(t, b.SqlClient.pipe(g.flatMap((t) => ge(t, {
1257
+ let r = await J(t, x.SqlClient.pipe(g.flatMap((t) => ve(t, {
1159
1258
  projectRoot: e,
1160
1259
  env: n,
1161
1260
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
1162
1261
  }))));
1163
- 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 };
1164
1263
  } catch (e) {
1165
1264
  return { failed: e };
1166
1265
  }
1167
- }, ft = async (e) => {
1168
- let t = B(e), n = await q(), r = (process.env.NODE_ENV ?? "").toLowerCase() === "production" ? "prod" : (process.env.NODE_ENV ?? "").toLowerCase() === "staging" ? "staging" : "dev";
1169
- await J(n, C(x, W()));
1170
- let i = await J(n, b.SqlClient.pipe(g.flatMap((e) => ge(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, {
1171
1270
  projectRoot: t,
1172
1271
  env: r,
1173
1272
  appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
1174
1273
  }))));
1175
- 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)`, {
1176
1275
  applied: i.applied.map((e) => `${e.id} (${e.durationMs}ms)`),
1177
1276
  skipped: i.skipped.length
1178
1277
  }), 0);
1179
- }, pt = async (e) => {
1180
- let t = B(e), n = e.find((e) => !e.startsWith("--"));
1181
- if (!n) return R.error("voltro db rollback-file <migration-id>"), 2;
1182
- 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;
1183
- let r = await J(await q(), b.SqlClient.pipe(g.flatMap((e) => me(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, {
1184
1283
  projectRoot: t,
1185
1284
  id: n
1186
1285
  }))));
1187
- return R.info(`db rollback-file: rolled back ${r.id} in ${r.durationMs}ms`), 0;
1188
- }, mt = () => {
1286
+ return B.info(`db rollback-file: rolled back ${r.id} in ${r.durationMs}ms`), 0;
1287
+ }, Ft = () => {
1189
1288
  let e = /* @__PURE__ */ new Date();
1190
1289
  return [
1191
1290
  e.getUTCFullYear(),
@@ -1195,7 +1294,7 @@ var O = class extends Error {
1195
1294
  String(e.getUTCMinutes()).padStart(2, "0"),
1196
1295
  String(e.getUTCSeconds()).padStart(2, "0")
1197
1296
  ].join("");
1198
- }, 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'
1199
1298
  import { Effect } from 'effect'
1200
1299
 
1201
1300
  // A hand-written migration. Reach for this when the DECLARATIVE differ can't
@@ -1227,24 +1326,24 @@ export default defineMigration({
1227
1326
  }),
1228
1327
  ],
1229
1328
  })
1230
- `, _t = {
1231
- plan: et,
1232
- apply: nt,
1233
- plans: rt,
1234
- drift: ut,
1235
- files: ft,
1236
- "rollback-file": pt,
1237
- migrate: Re,
1238
- rollback: ze,
1239
- status: Be,
1240
- 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,
1241
1340
  generate: async (e) => {
1242
- 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`);
1243
- 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;
1244
1343
  },
1245
- squash: ot,
1246
- "restore-snapshot": it,
1247
- "gc-snapshots": at,
1344
+ squash: Dt,
1345
+ "restore-snapshot": Tt,
1346
+ "gc-snapshots": Et,
1248
1347
  adopt: async (e) => {
1249
1348
  let t = (t) => {
1250
1349
  let n = e.indexOf(`--${t}`);
@@ -1258,18 +1357,18 @@ export default defineMigration({
1258
1357
  into: r,
1259
1358
  map: o.map,
1260
1359
  ...o.leaveUnset ? { leaveUnset: o.leaveUnset } : {}
1261
- }, c = W(), l = await q(), u = await J(l, b.SqlClient.pipe(g.flatMap(de))), 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) => ({
1262
1361
  name: e.name,
1263
1362
  nullable: e.nullable,
1264
1363
  hasDefault: e.defaultValue !== void 0 && e.defaultValue !== null
1265
1364
  })), f = d(n), p = d(r);
1266
1365
  if (f === void 0) return console.error(`voltro db adopt: '${n}' does not exist in the database`), 1;
1267
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;
1268
- 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 = xe(s, f, p, await _(r));
1269
- for (let e of Ce(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);
1270
1369
  if (v.fatal) return 1;
1271
1370
  if (!e.includes("--apply")) return console.log(""), console.log(" (dry run — nothing written. Re-run with --apply.)"), 0;
1272
- let y = await Oe(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") });
1273
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);
1274
1373
  },
1275
1374
  "scan-credentials": async (e) => {
@@ -1284,21 +1383,82 @@ export default defineMigration({
1284
1383
  column: a
1285
1384
  });
1286
1385
  }
1287
- let r = [...ke, ...n], i = await J(await q(), b.SqlClient.pipe(g.flatMap((e) => g.promise(() => Me(e, t, r, G)))));
1288
- 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;
1289
1449
  }
1290
- }, vt = async (e) => {
1450
+ }, zt = async (e) => {
1291
1451
  let [t, ...n] = e;
1292
1452
  if (!t) {
1293
1453
  console.error("voltro db <subcommand> [args]"), console.error("subcommands:");
1294
- for (let e of Object.keys(_t)) console.error(` ${e}`);
1454
+ for (let e of Object.keys(Rt)) console.error(` ${e}`);
1295
1455
  return 1;
1296
1456
  }
1297
- let r = _t[t];
1457
+ let r = Rt[t];
1298
1458
  if (!r) return console.error(`voltro db: unknown subcommand '${t}'`), 2;
1299
- (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)");
1300
1460
  let i = (e) => {
1301
- H(e) ? R.error(U) : console.error(e), process.exit(1);
1461
+ rt(e) ? B.error(it) : console.error(e), process.exit(1);
1302
1462
  };
1303
1463
  process.on("uncaughtException", i);
1304
1464
  try {
@@ -1308,4 +1468,4 @@ export default defineMigration({
1308
1468
  }
1309
1469
  };
1310
1470
  //#endregion
1311
- export { Ge as a, O as c, lt as i, ye 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 };