@voltro/cli 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/apiBuild-BWo8KI6n.js +2 -0
  3. package/dist/{apiBuild-QmYHaCF4.js → apiBuild-CFYXo06U.js} +1 -1
  4. package/dist/bin.js +1 -1
  5. package/dist/{build-Bi4O_g7T.js → build-CnSH-zru.js} +6 -6
  6. package/dist/{checkCommand-D8vjQXQt.js → checkCommand-Do3SQbLx.js} +2 -2
  7. package/dist/{checkCommand-udbQpphy.js → checkCommand-OL41BMIb.js} +1 -1
  8. package/dist/{codegenCommand-DApSH7hK.js → codegenCommand-BJzaRz6q.js} +1 -1
  9. package/dist/{codemodRunner-De1_316I.js → codemodRunner-De3k0pRd.js} +641 -481
  10. package/dist/{commands-a5PxCaIg.js → commands-By23Ljny.js} +19 -19
  11. package/dist/{dashboardCommand-DaoUUqJS.js → dashboardCommand-DN1rAABE.js} +1 -1
  12. package/dist/{dataCommand-CJv_OMoS.js → dataCommand-B93TUBIx.js} +473 -323
  13. package/dist/{dev-C31Nvlbv.js → dev-1yu42ubu.js} +1 -1
  14. package/dist/{dev-DFVuzOj7.js → dev-BWwE0QjK.js} +1 -1
  15. package/dist/doctorCommand-BmovAwb7.js +2 -0
  16. package/dist/{doctorCommand-B3uraeAe.js → doctorCommand-y-EbP8st.js} +10 -10
  17. package/dist/{dormancyCommand-QUXQ9yoz.js → dormancyCommand-Dc7KRNCG.js} +1 -1
  18. package/dist/{embeddingsCommand-Zw1-rYSF.js → embeddingsCommand-kPs132bB.js} +1 -1
  19. package/dist/{envCommand-DtaJ4xEf.js → envCommand-E12zSt51.js} +1 -1
  20. package/dist/{evolveCommand-JDD3hE1i.js → evolveCommand-B3nagPVc.js} +2 -2
  21. package/dist/index.js +1 -1
  22. package/dist/{infoCommand-COsUC1DB.js → infoCommand-Cq0Yz98u.js} +1 -1
  23. package/dist/{interruptedReplace-C1ZI84xi.js → interruptedReplace-C3O3M1MM.js} +2 -1
  24. package/dist/interruptedReplace-CvmiAM9K.js +2 -0
  25. package/dist/{runtimeTrace-Di6fcndp.js → runtimeTrace-BpLUcrfy.js} +1 -1
  26. package/dist/{sdkgen-CIi5gM17.js → sdkgen-dl4-WKF6.js} +1 -1
  27. package/dist/{serveCommand-N3udT49p.js → serveCommand-8CTzQCY0.js} +6 -6
  28. package/dist/serveCommand-C_WI4TTG.js +2 -0
  29. package/dist/serveEntry.js +1 -1
  30. package/dist/updateCommand-D91-TZ7E.js +2 -0
  31. package/dist/{updateCommand-2BfbeMvp.js → updateCommand-i8JbrLAA.js} +2 -1
  32. package/dist/{webhooksCommand-CwX9IzvJ.js → webhooksCommand-B0bdyuQ4.js} +1 -1
  33. package/package.json +35 -17
  34. package/templates/AGENTS.md +1 -1
  35. package/templates/agent-docs/_index.md +1 -1
  36. package/templates/agent-docs/cli.md +53 -3
  37. package/templates/agent-docs/whats-new.md +28 -37
  38. package/templates/apps/api-ai/package.json +7 -7
  39. package/templates/apps/api-auth/package.json +8 -8
  40. package/templates/apps/api-backend/package.json +7 -7
  41. package/templates/apps/api-backend-deactivation/package.json +7 -7
  42. package/templates/apps/api-backend-mail/package.json +8 -8
  43. package/templates/apps/api-backend-mariadb/package.json +9 -9
  44. package/templates/apps/api-backend-sqlite/package.json +8 -8
  45. package/templates/apps/api-backend-storage/package.json +8 -8
  46. package/templates/apps/api-cms/package.json +10 -10
  47. package/templates/apps/api-collab/package.json +8 -8
  48. package/templates/apps/api-data-advanced/package.json +8 -8
  49. package/templates/apps/api-durable/package.json +8 -8
  50. package/templates/apps/api-feature-flags/package.json +9 -9
  51. package/templates/apps/api-governance/package.json +8 -8
  52. package/templates/apps/api-kv/package.json +8 -8
  53. package/templates/apps/api-moderation/package.json +8 -8
  54. package/templates/apps/api-observability/package.json +8 -8
  55. package/templates/apps/api-ratelimit/package.json +8 -8
  56. package/templates/apps/api-rbac/package.json +8 -8
  57. package/templates/apps/api-rest/package.json +7 -7
  58. package/templates/apps/api-saas/package.json +11 -11
  59. package/templates/apps/api-saas-starter/package.json +10 -10
  60. package/templates/apps/api-search/package.json +8 -8
  61. package/templates/apps/api-status/package.json +8 -8
  62. package/templates/apps/api-versioning/package.json +8 -8
  63. package/templates/apps/api-webhooks/package.json +9 -9
  64. package/templates/apps/changelog/package.json +6 -6
  65. package/templates/apps/edge-functions/package.json +2 -2
  66. package/templates/apps/frontend-admin/package.json +8 -8
  67. package/templates/apps/frontend-app/package.json +9 -9
  68. package/templates/apps/frontend-auth/package.json +8 -8
  69. package/templates/apps/frontend-blank/package.json +7 -7
  70. package/templates/apps/frontend-cms/package.json +9 -9
  71. package/templates/apps/frontend-collab/package.json +10 -10
  72. package/templates/apps/frontend-contact/package.json +7 -7
  73. package/templates/apps/frontend-dashboard/package.json +7 -7
  74. package/templates/apps/frontend-docs/package.json +7 -7
  75. package/templates/apps/frontend-i18n/package.json +6 -6
  76. package/templates/apps/frontend-landing/package.json +7 -7
  77. package/templates/apps/frontend-portal/package.json +8 -8
  78. package/templates/apps/frontend-saas/package.json +8 -8
  79. package/templates/apps/frontend-spa/package.json +7 -7
  80. package/templates/apps/frontend-ssr/package.json +7 -7
  81. package/templates/apps/frontend-ssr-api/package.json +8 -8
  82. package/templates/apps/frontend-static-blog/package.json +6 -6
  83. package/templates/apps/frontend-status/package.json +8 -8
  84. package/templates/apps/mobile-app/package.json +4 -4
  85. package/dist/apiBuild-dZbHdqMg.js +0 -2
  86. package/dist/doctorCommand-CPL6EhrW.js +0 -2
  87. package/dist/interruptedReplace-DNJ7MKph.js +0 -2
  88. package/dist/serveCommand-CzSDngKm.js +0 -2
  89. package/dist/updateCommand-CtYm7aKH.js +0 -2
@@ -3,7 +3,7 @@ import { t } from "./frameworkInspectState-CX2250XB.js";
3
3
  import { n } from "./appModuleLoader-C9r9mxZt.js";
4
4
  import { t as r } from "./bootRefusal-pgvDxlrU.js";
5
5
  import { n as i, r as a } from "./startupRunner-CEqQq7ax.js";
6
- import { t as o } from "./interruptedReplace-C1ZI84xi.js";
6
+ import { t as o } from "./interruptedReplace-C3O3M1MM.js";
7
7
  import { r as s } from "./appPort-B_HpJ_ck.js";
8
8
  import { H as c, J as l, K as u, V as d, W as f, X as p, Y as m, Z as h, ct as g, ft as _, g as v, ht as ee, lt as y, pt as te, q as b, ut as x } from "./webDev-B4IEMB0n.js";
9
9
  import { n as ne, r as S } from "./dialectDriver-czCHYpeH.js";
@@ -1,3 +1,3 @@
1
- import { c as e, d as t, f as n, i as r, u as i } from "./dev-C31Nvlbv.js";
1
+ import { c as e, d as t, f as n, i as r, u as i } from "./dev-1yu42ubu.js";
2
2
  import "./dialectDriver-czCHYpeH.js";
3
3
  export { r as buildStore, e as loadApiConfig, i as loadDiscovered, t as runDev, n as walk };
@@ -0,0 +1,2 @@
1
+ import { b as e, y as t } from "./doctorCommand-y-EbP8st.js";
2
+ export { t as runDoctorCommand, e as runServePreflight };
@@ -1,8 +1,8 @@
1
- import { Ht as e, Ut as t, Vt as n } from "./dev-C31Nvlbv.js";
1
+ import { Ht as e, Ut as t, Vt as n } from "./dev-1yu42ubu.js";
2
2
  import { at as r, it as i, ot as a, s as o, st as s, t as c, w as l, y as u } from "./webDev-B4IEMB0n.js";
3
3
  import { x as d } from "./frameworkTableAssembly-BwJVEKLr.js";
4
4
  import { n as f } from "./pageConvention-cEiRxdab.js";
5
- import { r as p, u as m } from "./apiBuild-QmYHaCF4.js";
5
+ import { r as p, u as m } from "./apiBuild-CFYXo06U.js";
6
6
  import { n as h, r as g, t as ee } from "./cliOutput-D1tSBoRM.js";
7
7
  import { n as _, t as te } from "./tsconfigPaths-BWXBWgcl.js";
8
8
  import { n as ne, r as re } from "./fileTaxonomy-CJfgOllU.js";
@@ -1350,7 +1350,7 @@ var le = /* @__PURE__ */ new Set([
1350
1350
  let t = Wt.get(e);
1351
1351
  if (t !== void 0) return t;
1352
1352
  let n = (async () => {
1353
- let { walk: t, loadDiscovered: n } = await import("./dev-DFVuzOj7.js"), r = await t(e);
1353
+ let { walk: t, loadDiscovered: n } = await import("./dev-BWwE0QjK.js"), r = await t(e);
1354
1354
  return {
1355
1355
  files: r,
1356
1356
  discovered: await n(r)
@@ -1459,7 +1459,7 @@ var le = /* @__PURE__ */ new Set([
1459
1459
  }
1460
1460
  }, J = async (r) => {
1461
1461
  try {
1462
- let { loadApiConfig: i } = await import("./dev-DFVuzOj7.js"), { discovered: a } = await U(r), o = e(await i(r)), s = {
1462
+ let { loadApiConfig: i } = await import("./dev-BWwE0QjK.js"), { discovered: a } = await U(r), o = e(await i(r)), s = {
1463
1463
  queries: a.queries,
1464
1464
  mutations: a.mutations,
1465
1465
  actions: a.actions,
@@ -1543,7 +1543,7 @@ var le = /* @__PURE__ */ new Set([
1543
1543
  }, nn = async (e) => nt({
1544
1544
  hasWorkflows: async () => (await U(e)).discovered.workflows.length > 0,
1545
1545
  openStore: async () => {
1546
- let { loadApiConfig: t, buildStore: n } = await import("./dev-DFVuzOj7.js"), r = await t(e);
1546
+ let { loadApiConfig: t, buildStore: n } = await import("./dev-BWwE0QjK.js"), r = await t(e);
1547
1547
  if (r === null) throw Error("no app.config.ts in this directory");
1548
1548
  let i = await n(r, void 0);
1549
1549
  return {
@@ -1623,7 +1623,7 @@ var le = /* @__PURE__ */ new Set([
1623
1623
  }
1624
1624
  }, ln = async (e) => {
1625
1625
  try {
1626
- let { loadApiConfig: t } = await import("./dev-DFVuzOj7.js"), n = (await t(e))?.doctor?.scopeVocabulary;
1626
+ let { loadApiConfig: t } = await import("./dev-BWwE0QjK.js"), n = (await t(e))?.doctor?.scopeVocabulary;
1627
1627
  if (typeof n != "string") return [];
1628
1628
  let [r, i] = n.split("#");
1629
1629
  if (r === void 0 || i === void 0) return [];
@@ -1640,7 +1640,7 @@ var le = /* @__PURE__ */ new Set([
1640
1640
  findings: []
1641
1641
  });
1642
1642
  try {
1643
- let { offlineManifest: n, toInput: r } = await import("./checkCommand-udbQpphy.js"), { buildAppGraph: i, checkAppGraph: a } = await import("./appGraph-zuMGKVYX.js"), o = await n(e), s = r(o.manifest, o.tables), c = await ln(e), l = [.../* @__PURE__ */ new Set([...s.scopes ?? [], ...c])];
1643
+ let { offlineManifest: n, toInput: r } = await import("./checkCommand-OL41BMIb.js"), { buildAppGraph: i, checkAppGraph: a } = await import("./appGraph-zuMGKVYX.js"), o = await n(e), s = r(o.manifest, o.tables), c = await ln(e), l = [.../* @__PURE__ */ new Set([...s.scopes ?? [], ...c])];
1644
1644
  if (l.length === 0) {
1645
1645
  try {
1646
1646
  let { loadAppSchemaInputs: t } = await import("./frameworkTableAssembly-CVDB2hCq.js");
@@ -1694,7 +1694,7 @@ var le = /* @__PURE__ */ new Set([
1694
1694
  if (n === void 0 || r.length === 0) return;
1695
1695
  let i = [];
1696
1696
  try {
1697
- let { loadApiConfig: t } = await import("./dev-DFVuzOj7.js");
1697
+ let { loadApiConfig: t } = await import("./dev-BWwE0QjK.js");
1698
1698
  i = (await t(e))?.locales ?? [];
1699
1699
  } catch {
1700
1700
  return;
@@ -1725,7 +1725,7 @@ var le = /* @__PURE__ */ new Set([
1725
1725
  }, pn = async (e) => {
1726
1726
  let { discovered: t } = await U(e), n = [];
1727
1727
  try {
1728
- let { loadApiConfig: t } = await import("./dev-DFVuzOj7.js");
1728
+ let { loadApiConfig: t } = await import("./dev-BWwE0QjK.js");
1729
1729
  n = ((await t(e))?.plugins ?? []).map((e) => {
1730
1730
  let t = (e.extendSchema?.tables ?? []).map((e) => {
1731
1731
  let t = e;
@@ -1870,7 +1870,7 @@ var le = /* @__PURE__ */ new Set([
1870
1870
  return n.unchecked.length > $ && console.log(` … and ${n.unchecked.length - $} more — \`voltro doctor --json\` prints every one (\`authz.unchecked\`).`), i === void 0 ? (console.log(`\nNo ${X} yet, so this run is advisory. To start the ratchet:`), console.log("\n voltro doctor --write-authz-allowlist\n"), console.log("That records today's unchecked handlers as DEBT and fails every run after on"), console.log("anything NEW. Fix a handler and its line stops mattering — the scan reports it as"), console.log("guarded whether or not the line is still there."), !1) : (console.log(`\n${n.unchecked.length} executor(s) with no access check are not in ${X}.`), console.log("Add a guard, or add the tag to that file if it is genuinely open."), console.log("\nBefore hand-rolling another check: `guards: [{ action, resourceType, resource }]`"), console.log("answers \"may this subject act on THIS row\" declaratively, and an app whose"), console.log("relationships live in its own tables registers its own tuple source rather than"), console.log("copying data into a framework table. See the authorization docs."), !0);
1871
1871
  }, yn = async (e) => {
1872
1872
  try {
1873
- let { loadApiConfig: t } = await import("./dev-DFVuzOj7.js"), { decidedSchemaReport: n } = await import("./doctorDecidedSchema-BlbWgCD_.js"), r = await t(e);
1873
+ let { loadApiConfig: t } = await import("./dev-BWwE0QjK.js"), { decidedSchemaReport: n } = await import("./doctorDecidedSchema-BlbWgCD_.js"), r = await t(e);
1874
1874
  return r == null ? null : n({
1875
1875
  ...r.schema ? { declared: r.schema } : {},
1876
1876
  ...r.store === void 0 ? {} : { store: r.store },
@@ -1,4 +1,4 @@
1
- import { c as e, i as t } from "./dev-C31Nvlbv.js";
1
+ import { c as e, i as t } from "./dev-1yu42ubu.js";
2
2
  import { n, t as r } from "./cliArgs-D4p8n7EE.js";
3
3
  import { r as i } from "./appPort-B_HpJ_ck.js";
4
4
  import { resolve as a } from "node:path";
@@ -1,4 +1,4 @@
1
- import { c as e, i as t } from "./dev-C31Nvlbv.js";
1
+ import { c as e, i as t } from "./dev-1yu42ubu.js";
2
2
  import { Effect as n } from "effect";
3
3
  import { embedMany as r } from "@voltro/ai";
4
4
  //#region src/embeddingsBackfill.ts
@@ -1,4 +1,4 @@
1
- import { c as e } from "./dev-C31Nvlbv.js";
1
+ import { c as e } from "./dev-1yu42ubu.js";
2
2
  import { _t as t, gt as n, mt as r, pt as i } from "./webDev-B4IEMB0n.js";
3
3
  import { join as a } from "node:path";
4
4
  import { promises as o } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import { n as e } from "./cliOutput-D1tSBoRM.js";
2
- import { t } from "./checkCommand-D8vjQXQt.js";
3
- import { i as n, n as r } from "./codemodRunner-De1_316I.js";
2
+ import { t } from "./checkCommand-Do3SQbLx.js";
3
+ import { i as n, n as r } from "./codemodRunner-De3k0pRd.js";
4
4
  import { basename as i, join as a, resolve as o } from "node:path";
5
5
  import { branchNamespaceName as s, planBranchProvision as c } from "@voltro/database";
6
6
  import { readFile as l } from "node:fs/promises";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, i as t, n, o as r, r as i, t as a } from "./commands-a5PxCaIg.js";
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./commands-By23Ljny.js";
2
2
  import { r as o } from "./templates-BR-fb4SP.js";
3
3
  //#region src/index.ts
4
4
  var s = "framework";
@@ -1,5 +1,5 @@
1
1
  import { n as e, r as t, t as n } from "./cliOutput-D1tSBoRM.js";
2
- import { o as r } from "./updateCommand-2BfbeMvp.js";
2
+ import { o as r } from "./updateCommand-i8JbrLAA.js";
3
3
  import { join as i, resolve as a } from "node:path";
4
4
  import { readFileSync as o, readdirSync as s, statSync as c } from "node:fs";
5
5
  //#region src/infoCommand.ts
@@ -14,7 +14,8 @@ var n = async (n) => {
14
14
  let i = r.map((e) => ({
15
15
  id: String(e.id ?? ""),
16
16
  startedAt: e.startedAt instanceof Date ? e.startedAt : new Date(String(e.startedAt ?? "")),
17
- tables: Number(e.tables ?? 0),
17
+ kind: e.kind === void 0 || e.kind === null ? "replace" : String(e.kind),
18
+ tables: e.tables === null || e.tables === void 0 ? null : Number(e.tables),
18
19
  capture: e.capture === null || e.capture === void 0 ? null : String(e.capture),
19
20
  via: String(e.via ?? "unknown")
20
21
  }));
@@ -0,0 +1,2 @@
1
+ import { t as e } from "./interruptedReplace-C3O3M1MM.js";
2
+ export { e as interruptedReplaceAtBoot };
@@ -1,4 +1,4 @@
1
- import { n as e } from "./apiBuild-QmYHaCF4.js";
1
+ import { n as e } from "./apiBuild-CFYXo06U.js";
2
2
  import { join as t, resolve as n, sep as r } from "node:path";
3
3
  import { existsSync as i, promises as a } from "node:fs";
4
4
  import { createLogger as o } from "@voltro/logger";
@@ -422,7 +422,7 @@ let package = Package(
422
422
  await l.mkdir(o(e), { recursive: !0 }), await l.writeFile(e, n.content, "utf8");
423
423
  }
424
424
  }, X = async (e) => {
425
- let { offlineManifest: t } = await import("./checkCommand-udbQpphy.js"), { manifest: n } = await t(e);
425
+ let { offlineManifest: t } = await import("./checkCommand-OL41BMIb.js"), { manifest: n } = await t(e);
426
426
  return n;
427
427
  }, Z = async (e) => {
428
428
  let t = J(await (e.manifestProvider ?? X)(e.root), {
@@ -3,8 +3,8 @@ import { t } from "./frameworkInspectState-CX2250XB.js";
3
3
  import { r as n, t as r } from "./appModuleLoader-C9r9mxZt.js";
4
4
  import { t as i } from "./bootRefusal-pgvDxlrU.js";
5
5
  import { n as a, r as o } from "./startupRunner-CEqQq7ax.js";
6
- import { t as s } from "./interruptedReplace-C1ZI84xi.js";
7
- import { $ as c, $t as l, A as u, At as d, Bt as f, C as ee, Cn as te, Ct as ne, D as p, Dt as m, E as h, En as re, Et as g, F as ie, Ft as ae, G as oe, Gt as se, H as _, Ht as ce, I as le, It as ue, J as de, Jt as v, K as y, Kt as fe, L as b, Lt as pe, M as me, Mt as he, N as ge, Nt as _e, O as ve, Ot as ye, P as be, Pt as xe, Q as Se, Qt as Ce, R as x, Rt as we, S as Te, St as Ee, T as De, Tn as Oe, Tt as S, U as ke, V as Ae, W as je, Wt as Me, X as Ne, Xt as Pe, Y as Fe, Yt as C, Z as Ie, Zt as Le, _ as w, _n as Re, _t as T, an as ze, at as Be, b as Ve, bn as He, bt as Ue, c as We, cn as Ge, ct as Ke, dn as qe, dt as Je, en as Ye, et as Xe, f as Ze, fn as Qe, ft as $e, g as et, gt as tt, h as nt, hn as rt, ht as it, i as at, in as ot, it as st, j as ct, jt as lt, k as ut, kt as dt, ln as ft, lt as pt, m as mt, mn as ht, mt as gt, nn as _t, nt as E, o as D, on as vt, ot as O, p as k, pn as yt, pt as bt, q as xt, qt as St, r as Ct, rn as A, rt as wt, sn as j, st as Tt, t as Et, tn as Dt, tt as Ot, u as kt, un as At, ut as jt, v as Mt, vn as Nt, vt as M, w as N, wn as Pt, wt as Ft, x as It, xn as Lt, xt as Rt, y as zt, yn as Bt, yt as Vt, zt as Ht } from "./dev-C31Nvlbv.js";
6
+ import { t as s } from "./interruptedReplace-C3O3M1MM.js";
7
+ import { $ as c, $t as l, A as u, At as d, Bt as f, C as ee, Cn as te, Ct as ne, D as p, Dt as m, E as h, En as re, Et as g, F as ie, Ft as ae, G as oe, Gt as se, H as _, Ht as ce, I as le, It as ue, J as de, Jt as v, K as y, Kt as fe, L as b, Lt as pe, M as me, Mt as he, N as ge, Nt as _e, O as ve, Ot as ye, P as be, Pt as xe, Q as Se, Qt as Ce, R as x, Rt as we, S as Te, St as Ee, T as De, Tn as Oe, Tt as S, U as ke, V as Ae, W as je, Wt as Me, X as Ne, Xt as Pe, Y as Fe, Yt as C, Z as Ie, Zt as Le, _ as w, _n as Re, _t as T, an as ze, at as Be, b as Ve, bn as He, bt as Ue, c as We, cn as Ge, ct as Ke, dn as qe, dt as Je, en as Ye, et as Xe, f as Ze, fn as Qe, ft as $e, g as et, gt as tt, h as nt, hn as rt, ht as it, i as at, in as ot, it as st, j as ct, jt as lt, k as ut, kt as dt, ln as ft, lt as pt, m as mt, mn as ht, mt as gt, nn as _t, nt as E, o as D, on as vt, ot as O, p as k, pn as yt, pt as bt, q as xt, qt as St, r as Ct, rn as A, rt as wt, sn as j, st as Tt, t as Et, tn as Dt, tt as Ot, u as kt, un as At, ut as jt, v as Mt, vn as Nt, vt as M, w as N, wn as Pt, wt as Ft, x as It, xn as Lt, xt as Rt, y as zt, yn as Bt, yt as Vt, zt as Ht } from "./dev-1yu42ubu.js";
8
8
  import { n as Ut } from "./cliArgs-D4p8n7EE.js";
9
9
  import { r as P } from "./appPort-B_HpJ_ck.js";
10
10
  import { ht as Wt, pt as Gt } from "./webDev-B4IEMB0n.js";
@@ -18,7 +18,7 @@ import { h as pn, o as mn } from "./inspect-B7U7Cl_Z.js";
18
18
  import { n as hn } from "./mssqlClusterPatch-_4cE_nun.js";
19
19
  import { i as gn, n as _n, r as vn, t as yn } from "./webhookDiscovery-CphsQe59.js";
20
20
  import { t as bn } from "./inspectMetrics-CfdKLh6t.js";
21
- import { r as xn, u as Sn } from "./apiBuild-QmYHaCF4.js";
21
+ import { r as xn, u as Sn } from "./apiBuild-CFYXo06U.js";
22
22
  import { t as Cn } from "./bootTiming-BdyP9nYw.js";
23
23
  import { a as wn, o as Tn, t as En } from "./dataProfile-Du3ztYxv.js";
24
24
  import L, { basename as Dn, join as On } from "node:path";
@@ -97,7 +97,7 @@ var Zi = /* @__PURE__ */ ((e) => typeof require < "u" ? require : typeof Proxy <
97
97
  await a(e), await o();
98
98
  }
99
99
  };
100
- }, ta = "/_voltro/admin/import", na = "/_voltro/admin/import/start", ra = "/_voltro/admin/export", ia = "/_voltro/admin/export/download", aa = "/_voltro/admin/imports", Z = (e, t) => ({
100
+ }, ta = "/_voltro/admin/import", na = "/_voltro/admin/import/start", ra = "/_voltro/admin/export", ia = "/_voltro/admin/export/download", aa = "/_voltro/admin/transfers", Z = (e, t) => ({
101
101
  status: e,
102
102
  contentType: "application/json; charset=utf-8",
103
103
  body: JSON.stringify(t)
@@ -463,7 +463,7 @@ var Zi = /* @__PURE__ */ ((e) => typeof require < "u" ? require : typeof Proxy <
463
463
  atomic: r.atomic,
464
464
  ...r.dryRun ? { dryRun: !0 } : {},
465
465
  recorded: f,
466
- outcome: "poll /_voltro/admin/imports for this runId — a 202 means started, not finished"
466
+ outcome: "poll /_voltro/admin/transfers for this runId — a 202 means started, not finished"
467
467
  });
468
468
  }, La = async (e) => {
469
469
  let t = await Wi(e, ["manifest.json"]).then((e) => e.get("manifest.json"), () => null);
@@ -679,7 +679,7 @@ var Zi = /* @__PURE__ */ ((e) => typeof require < "u" ? require : typeof Proxy <
679
679
  transport: c ? "storage" : "download",
680
680
  ...c ? { bundleKey: r.bundleKey } : {},
681
681
  recorded: d,
682
- outcome: "poll /_voltro/admin/imports for this runId; fetch the bytes from /_voltro/admin/export/download"
682
+ outcome: "poll /_voltro/admin/transfers for this runId; fetch the bytes from /_voltro/admin/export/download"
683
683
  });
684
684
  }, to = async (e, t) => {
685
685
  try {
@@ -0,0 +1,2 @@
1
+ import { n as e } from "./serveCommand-8CTzQCY0.js";
2
+ export { e as runServeDispatch };
@@ -1,4 +1,4 @@
1
- import { t as e } from "./serveCommand-N3udT49p.js";
1
+ import { t as e } from "./serveCommand-8CTzQCY0.js";
2
2
  import { t } from "./loadEnv-D9nEOClM.js";
3
3
  import { r as n } from "./appModuleLoader-C9r9mxZt.js";
4
4
  import { i as r } from "./dialectDriver-czCHYpeH.js";
@@ -0,0 +1,2 @@
1
+ import { f as e, p as t } from "./updateCommand-i8JbrLAA.js";
2
+ export { e as runApplyCodemods, t as runUpdateCommand };
@@ -1,4 +1,4 @@
1
- import { n as e, r as t, t as n } from "./codemodRunner-De1_316I.js";
1
+ import { n as e, r as t, t as n } from "./codemodRunner-De3k0pRd.js";
2
2
  import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
3
3
  import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
4
4
  import { totalmem as p } from "node:os";
@@ -579,6 +579,7 @@ with \`--only\` / \`--root\`.`;
579
579
  for (let e of r.manual) {
580
580
  console.log(`\n▸ ${e.id} — ${e.title}`);
581
581
  for (let t of e.note.split("\n")) console.log(` ${t}`);
582
+ e.certain || (console.log(""), console.log(" ── nothing in this repository matched. That is NOT a clean bill of health:"), console.log(" `voltro update` reads the files git tracks here, and a CLI call also lives"), console.log(" in places it cannot reach — an inline script in a CI runner, a CronJob"), console.log(" spec in another repo, a runbook, a deploy job. Search those by hand."));
582
583
  }
583
584
  }
584
585
  return n.dryRun && console.log("\n(dry-run — no files written)"), 0;
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
222
222
  ...t === void 0 ? {} : { payload: t }
223
223
  };
224
224
  }, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
225
- let { walk: t, loadDiscovered: n } = await import("./dev-DFVuzOj7.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
225
+ let { walk: t, loadDiscovered: n } = await import("./dev-BWwE0QjK.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
226
226
  return r((await n(await t(e))).events.map((e) => ({
227
227
  file: e.file,
228
228
  descriptor: e.descriptor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -676,6 +676,24 @@
676
676
  "title": "store.query() now returns the row type — casts on row fields may stop compiling",
677
677
  "kind": "manual"
678
678
  },
679
+ {
680
+ "version": "0.50.0",
681
+ "id": "0.50.0/01_transfer-runs-endpoint",
682
+ "title": "`GET /_voltro/admin/imports` → `GET /_voltro/admin/transfers`",
683
+ "kind": "manual"
684
+ },
685
+ {
686
+ "version": "0.50.0",
687
+ "id": "0.50.0/02_reissue-data-imports-rename",
688
+ "title": "RE-CHECK: `voltro data imports` → `voltro data transfers` (0.49.0's note could not see your scripts)",
689
+ "kind": "manual"
690
+ },
691
+ {
692
+ "version": "0.50.0",
693
+ "id": "0.50.0/03_interrupted-replace-tables-nullable",
694
+ "title": "`InterruptedReplace.tables` is `number | null`",
695
+ "kind": "manual"
696
+ },
679
697
  {
680
698
  "version": "0.6.0",
681
699
  "id": "0.6.0/01_no-dev-session-secret",
@@ -751,22 +769,22 @@
751
769
  "@effect/platform-node": "^0.108.0",
752
770
  "@effect/sql": "^0.52.0",
753
771
  "@effect/workflow": "^0.19.0",
754
- "@voltro/ai": "0.49.0",
755
- "@voltro/cache": "0.49.0",
756
- "@voltro/data-transfer": "0.49.0",
757
- "@voltro/database": "0.49.0",
758
- "@voltro/env": "0.49.0",
759
- "@voltro/kv": "0.49.0",
760
- "@voltro/logger": "0.49.0",
761
- "@voltro/plugin-auth": "0.49.0",
762
- "@voltro/plugin-broadcast": "0.49.0",
763
- "@voltro/plugin-mail": "0.49.0",
764
- "@voltro/plugin-storage": "0.49.0",
765
- "@voltro/plugin-webhooks": "0.49.0",
766
- "@voltro/protocol": "0.49.0",
767
- "@voltro/runtime": "0.49.0",
768
- "@voltro/serverless": "0.49.0",
769
- "@voltro/workflow": "0.49.0",
772
+ "@voltro/ai": "0.50.0",
773
+ "@voltro/cache": "0.50.0",
774
+ "@voltro/data-transfer": "0.50.0",
775
+ "@voltro/database": "0.50.0",
776
+ "@voltro/env": "0.50.0",
777
+ "@voltro/kv": "0.50.0",
778
+ "@voltro/logger": "0.50.0",
779
+ "@voltro/plugin-auth": "0.50.0",
780
+ "@voltro/plugin-broadcast": "0.50.0",
781
+ "@voltro/plugin-mail": "0.50.0",
782
+ "@voltro/plugin-storage": "0.50.0",
783
+ "@voltro/plugin-webhooks": "0.50.0",
784
+ "@voltro/protocol": "0.50.0",
785
+ "@voltro/runtime": "0.50.0",
786
+ "@voltro/serverless": "0.50.0",
787
+ "@voltro/workflow": "0.50.0",
770
788
  "chokidar": "^5.0.0",
771
789
  "ioredis": "^5.11.1",
772
790
  "tinyglobby": "^0.2.17",
@@ -707,7 +707,7 @@ each plugin's own README.
707
707
 
708
708
  | Topic | Open | Summary |
709
709
  |---|---|---|
710
- | **What's new in 0.49.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
710
+ | **What's new in 0.50.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
711
711
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
712
712
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
713
713
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.49.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.50.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -2996,7 +2996,7 @@ $ voltro data transfers --target api --api-url https://app.example
2996
2996
  2026-08-23T08:00:00.000Z · export all — 228866 row(s) across 104 table(s) · via api · from backups/nightly.vbundle
2997
2997
  ```
2998
2998
 
2999
- Same data over `GET /_voltro/admin/imports?limit=20`, behind the same
2999
+ Same data over `GET /_voltro/admin/transfers?limit=20`, behind the same
3000
3000
  data-transfer secret (the row names bundles and schema fingerprints). It works
3001
3001
  from a different machine than the one that started the import, and through
3002
3002
  anything that forwards a GET.
@@ -3330,12 +3330,62 @@ yield* runImport({ store: target, bundleDir: './backup', targetSnapshot }).pipe(
3330
3330
  ## Native backup / restore
3331
3331
 
3332
3332
  ```bash
3333
- voltro data backup ./backups/2026-07-01 # pg_dump --format=custom / mysqldump --single-transaction / …
3334
- voltro data restore ./backups/2026-07-01 # pg_restore / mysql /
3333
+ voltro data backup ./backups/2026-07-01 # pg_dump --format=custom / mariadb-dump --single-transaction / …
3334
+ voltro data backup ./backups/2026-07-01 --assets # rows AND the stored blobs
3335
+ voltro data restore ./backups/2026-07-01 --assets # pg_restore / mariadb / … + the blobs
3335
3336
  ```
3336
3337
 
3337
3338
  These shell out to the vendor tools resolved from your `DB_DIALECT` + connection env. They produce a dialect-native artifact (`db.dump`, `db.sql`, `db.sqlite`, `db.bacpac`) that is lossless and point-in-time consistent for **same-dialect** restore — the right tool for disaster recovery. Secrets are passed via the tools' environment variables (`PGPASSWORD`, `MYSQL_PWD`), never on the command line, where the tool supports it. The named tool must be installed and on `PATH`.
3338
3339
 
3340
+ ### `--assets` — the blobs are not in the dump
3341
+
3342
+ A vendor dump contains rows. Your blobs are in object storage, and no `pg_dump` has ever seen them. So a rows-only backup restores a database whose rows reference objects that are not there — and the reference and the object are checked at different times, which is why that state is discovered by a user, months later, rather than by the restore.
3343
+
3344
+ `--assets` captures them alongside the dump, through the same content-addressed pipeline `voltro data export --assets` uses: each blob is streamed (never buffered whole), stored under `assets/<sha256>` so identical content is stored once, and listed in `assets/index.ndjson`. `restore --assets` streams them back and **re-hashes on the way**, so a corrupted artifact can never silently overwrite good bytes.
3345
+
3346
+ ```
3347
+ backups/2026-07-01/
3348
+ ├─ db.dump # the vendor artifact (rows)
3349
+ ├─ voltro-backup-stamp.json # provenance, incl. what --assets captured
3350
+ ├─ assets/index.ndjson # key → sha256 → size → contentType
3351
+ └─ assets/<sha256> # the blob bodies, deduped by content
3352
+ ```
3353
+
3354
+ Three refusals, each for a belief that is otherwise acted on silently:
3355
+
3356
+ - **`backup --assets` with no storage provider configured → refused.** There is nothing to capture, and a flag that is accepted and ignored lets you build a rollback story on an artifact that does not contain what you asked for.
3357
+ - **`restore --assets` on a rows-only backup → refused.** You believe the blobs are in there. Restoring the rows anyway produces exactly the dangling state this exists to prevent.
3358
+ - **restore *without* `--assets` on a backup that HAS them → warned, not refused.** Restoring rows without blobs is legitimate (a schema drill, a lower environment), and refusing it would push people at `--force`.
3359
+
3360
+ Resume is per blob key, so re-running a `--assets` capture that was interrupted transfers only what is missing. The **dump itself has no resume** — a vendor artifact is one opaque file with no offset to restart from. If you need a resumable, chunkable, observable transfer, that is the logical path (`export` / `import`), and it is why the logical path exists.
3361
+
3362
+ ### A restore that is interrupted refuses the next boot
3363
+
3364
+ `restore` writes one row into `_voltro_replace_in_progress` **before** the first destructive statement and removes it **after** the last write — the blobs included. Its presence at boot is a refusal naming the artifact that was going in.
3365
+
3366
+ This is the counterpart to `--allow-live`, and it guards from the other side: `--allow-live` asks you not to restore over a running instance, and this says *this database is mid-restore, do not serve it*. A half-restored database looks exactly like a normal one from the inside — every query answers, nothing errors.
3367
+
3368
+ Whether the marker survives the restore depends on the dialect's restore shape, and that is stated rather than implied:
3369
+
3370
+ | dialect | shape | effect |
3371
+ |---|---|---|
3372
+ | postgres, mysql, mariadb | drops only the objects the dump names | the marker survives, so a killed restore is caught at the next boot |
3373
+ | sqlite, turso | whole-file replacement — made **atomic** (temp file + rename) | there is no half-restored state to catch; a killed restore leaves the live file untouched |
3374
+ | mssql | `sqlpackage /Action:Import` replaces the database | a *failed* import is the one case not covered here — verify with `--drill` |
3375
+
3376
+ Clear a marker deliberately with `voltro data clear-replace-marker --yes` once you have decided the current state is correct.
3377
+
3378
+ ### Both directions are in the history
3379
+
3380
+ `backup` and `restore` write a row to the same `_voltro_data_transfers` record `import` and `export` use, so `voltro data transfers` answers "did last night's backup finish" from the instance that ran it:
3381
+
3382
+ ```
3383
+ 2026-08-23T02:00:00.000Z · backup native postgres + assets — finished (412 blob(s)) · via cli · from ./backups/2026-08-23/db.dump
3384
+ 2026-08-22T09:14:02.000Z · FAILED restore native mariadb — mariadb-dump: exited with code 2 · via cli · from ./backups/2026-08-21/db.sql
3385
+ ```
3386
+
3387
+ A native run reports **blobs**, not rows: the vendor tool reports no row count we can trust, and printing `0 row(s)` over a `pg_dump` that worked would be a measurement, and a wrong one. A target with no `_voltro_data_transfers` table still gets its backup — the closing line says it was not recorded, rather than implying it was.
3388
+
3339
3389
  ### The provenance stamp — a restore that refuses the wrong DB
3340
3390
 
3341
3391
  A native dump is opaque: it doesn't say which dialect made it, which schema shape it carries, or when. `backup` writes a sidecar `voltro-backup-stamp.json` next to the artifact recording exactly that — `dialect`, the live schema `fingerprint`, the `@voltro/cli` version, and the timestamp.
@@ -1,4 +1,4 @@
1
- # What's new in 0.49.0
1
+ # What's new in 0.50.0
2
2
 
3
3
  Read this FIRST when a task touches an area you have not worked in recently.
4
4
  It is the cheapest way to notice that the framework grew the thing you were
@@ -9,63 +9,54 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
10
  ### ⚠ BREAKING
11
11
 
12
- - **@voltro/cli, @voltro/data-transfer, @voltro/database, @voltro/sql-sqlite, @voltro/voltro** — A data transfer is a series of short requests now — no single one may outlive a caller's budget.
12
+ - **@voltro/cli** — The read-only run-history endpoint moved:
13
13
 
14
- **The rule:** *a request that carries bytes never runs a transfer; a request that starts a transfer never carries bytes.* It was broken in the worst available place. The upload was already chunked and resumable — many short requests, each abandonable — and then the FINAL chunk fell through and ran the whole import. So the longest request of the flow arrived AFTER the entire upload had succeeded, and a caller under a policy that caps a single request (a job runner that kills a client at ten minutes; a 30 s ingress ceiling) lost the most expensive thing they had already paid for. A single-request upload had the same shape without the excuse, and the export had no protocol at all: one request that read the whole database and streamed it back.
14
+ GET /_voltro/admin/imports GET /_voltro/admin/transfers
15
15
 
16
- **Import.** `POST /_voltro/admin/import` accumulates and answers `202`. `POST /_voltro/admin/import/start` begins the run and answers `202 { runId }` as soon as the run's first row exists. `start` is idempotent per upload — it is a short request and therefore a retryable one, and without a claim a retry would begin a second destructive run from the same bytes. A claim whose run has ENDED is taken over rather than honoured forever, so a process that dies holding one cannot poison a bundle.
16
+ Same secret, same query parameters, same response shape. Only the path changed.
17
17
 
18
- **Export.** `POST /_voltro/admin/export` answers `202 { runId }` and produces in the background; the bytes come back from `GET /_voltro/admin/export/download?runId=&offset=&length=` in ranges, resumable, with the total in a header. Object storage (`--bundle-key`) remains for a bundle you want to KEEP it is no longer the only way to get one out, because requiring it would leave an instance without storage unable to export at all.
18
+ It answers for four directions now — import, export, native backup, native restore and it named one of them. The subcommand and the table were renamed for exactly that reason in 0.49.0 and this path was left behind, which is worse than renaming none of them: a reader who follows a rename tries the matching path, gets a 404, and concludes the instance is too old.
19
19
 
20
- **The client.** `--max-request-seconds` (or `VOLTRO_MAX_REQUEST_SECONDS`) declares the budget declared rather than probed, because the thing that kills a request is a policy on the caller's side and only they know it. `--detach` returns once the run has started and says the outcome is NOT known. Attached, the CLI polls the record and prints per-table progress; Ctrl-C then loses the watching and never the run.
20
+ `voltro data transfers --target api` uses the new path for you. What needs a hand is anything calling it directly a monitoring check, an uptime probe, a curl in a runbook, a dashboard datasource. The codemod is `manual` and declares `reach: 'beyond-source'`, so it prints whether or not the path is found in your repository.
21
+ - **@voltro/database, @voltro/voltro** — `InterruptedReplace.tables` is `number | null`, and the interface gains `kind?: 'replace' | 'restore'`.
21
22
 
22
- **The trap, stated because it is the one way to get this wrong:** a failure used to arrive in the response (409 on drift, 409 on a refused mode, 500 otherwise). After the split the response is a `202`, so **a client deriving its exit code from the status line reports a failed import as a success.** The exit code comes from the polled record, in one shared function, and the drift check moved into `start` where it can still be a refusal that leaves no history.
23
+ `_voltro_replace_in_progress` records two destructive operations now an import's `--mode replace` and a native `voltro data restore`. A restore replaces the whole database from an artefact, so "how many tables was this going to empty and refill" has no answer: not known up front, not meaningful after.
23
24
 
24
- **Two knobs that were constants.** `VOLTRO_IMPORT_UPLOAD_DIR` and `VOLTRO_EXPORT_ARTIFACT_DIR` move the staging areas off the default temp filesystem — which on a container is frequently a small tmpfs, so an instance simply could not accept a bundle the size a grown database produces, and the failure arrived as a write error halfway through an upload somebody had been waiting on.
25
+ Writing `0` would have preserved the type and been worse. That number is rendered into the boot refusal, which is read under pressure, and it would have said `began emptying 0 table(s)` a measurement that was never taken. The framework's own text says "this database" instead; `Number(null)` is `0`, so coercing it reintroduces exactly the sentence being avoided.
25
26
 
26
- **Renamed:** `voltro data imports` `voltro data transfers`, and `_voltro_data_imports` `_voltro_data_transfers` with a `direction` column. The command showed one direction and now shows both; the table rename carries its rows via the declarative differ on every dialect and needs nothing from you. The CLI rename ships a `manual` codemod — the command lives in scripts, CI jobs and runbooks, which `voltro update` cannot see or rewrite. The four PUBLIC exports that named the same record were renamed with it (`ImportRun`, `describeImportRun`, `IMPORT_RUNS_TABLE`, `_voltroImportRunsTable`); those are application source, they ship their own `transform` codemod, and they have their own entry.
27
- - **@voltro/database, @voltro/voltro** — The run-history exports say `transfer`, not `import`.
27
+ `DataTransferRun.direction` also widens to name `'backup'` and `'restore'` explicitly. That one is documentary the union already ended in `| string`, so no assignability changes.
28
28
 
29
- `@voltro/database` (and `voltro/database`) renamed four public exports along with the table behind them:
30
-
31
- | was | is | |---|---| | `ImportRun` | `DataTransferRun` | | `describeImportRun` | `describeTransferRun` | | `IMPORT_RUNS_TABLE` | `DATA_TRANSFERS_TABLE` | | `_voltroImportRunsTable` | `_voltroDataTransfersTable` |
32
-
33
- An EXPORT writes to this record now — the row carries a `direction` — so every one of those names described half of what it holds.
34
-
35
- A `transform` codemod rewrites all four, alias-aware, from either module spelling. It also rewrites a hand-spelled `_voltro_data_imports` in a string, template or raw-SQL fragment, and that is the half worth stating: the four identifiers announce themselves as compile errors, while a query that addresses the table by name has nothing to fail on. The table itself moves with its rows via the declarative differ on every dialect.
36
-
37
- This is filed apart from the transfer-protocol entry beside it deliberately. That one's codemod is `manual` and is about a CLI invocation living in scripts and CI jobs; this one is application source and is rewritten for you. Reading the first as covering both is what would leave a build broken with a note saying nothing in your source was affected.
29
+ **`voltro update` carries you across this** codemod `0.50.0/03_interrupted-replace-tables-nullable`.
38
30
 
39
31
  ### Added
40
32
 
41
- - **@voltro/database, @voltro/data-transfer, @voltro/cli** — An import records WHETHER it staged, and a soft-dropped column says so in a drift refusal.
33
+ - **@voltro/cli, @voltro/data-transfer, @voltro/database** — `voltro data backup --assets` and `voltro data restore --assets` now move the stored blobs alongside the vendor dump, through the same content-addressed phase the logical `export` / `import` path uses — streamed, deduped by sha256, verified on the way back, resumable per key.
42
34
 
43
- **`_voltro_data_imports.staged`.** The pre-upload preflight says what the instance WILL do; the run's own line says what it did and that line is printed inside the instance, which is exactly where an operator using `--target api` cannot read it. So "we were told it would stage" and "it staged" were two claims with no way to close the gap between them from outside. The column is `null` for a mode where the question does not arise; `voltro data imports` and `GET /_voltro/admin/imports` both carry it.
35
+ `--assets` was accepted by `backup` and silently ignored, with the only signal a field in the closing JSON reading `NOT included`; `restore` did not accept it at all. A rows-only backup restores a database whose rows reference objects nothing puts back, and the reference and the object are checked at different times, so that state is discovered by a user rather than by the restore.
44
36
 
45
- **A soft-dropped column is named as one.** `<original>__dropped_<stamp>` is what the differ leaves behind when an app stops declaring a column only the database that did the drop has it, so it drifts against every target. The refusal reported it as an ordinary missing column ("the value has nowhere to go"), which points at the TARGET's schema: the one place the fix does not lie. It now says where the column lives and how to reclaim it.
37
+ Three refusals, each for a belief that is otherwise acted on silently: `backup --assets` with no storage provider is refused rather than swallowed; `restore --assets` on a rows-only artefact is refused; a restore WITHOUT `--assets` over an artefact that has them warns and proceeds, because restoring rows without blobs is legitimate and refusing it would push people at `--force`.
46
38
 
47
- ### Fixed
48
-
49
- - **@voltro/data-transfer, @voltro/sql-sqlite, @voltro/database** — A successful `--mode replace --no-atomic` left a marker that refused the next boot.
39
+ The dump itself still has no resume — a vendor artefact is one opaque file with no offset to restart from, and the logical path is what exists for that.
40
+ - **@voltro/cli, @voltro/database, @voltro/data-transfer** — A native `backup` / `restore` now writes to the same `_voltro_data_transfers` record `import` and `export` use, so `voltro data transfers` answers "did last night's backup finish" from the instance that ran it. A native run reports blobs rather than rows — a vendor tool reports no row count we can trust, and printing `0 row(s)` over a dump that worked would be a wrong measurement.
50
41
 
51
- Two defects, one visible symptom, and both were silent by construction.
42
+ `restore` also writes the `_voltro_replace_in_progress` marker before its first destructive statement and clears it after the last write, blobs included, so a killed restore refuses the next boot instead of serving a half-loaded database. `--allow-live` guards from the wrong side — it asks you not to — and this guards from the right one. The marker carries a `kind` so the refusal can say something different for a `replace` (re-import the capture) and a `restore` (finish the restore), and `tables` is nullable because a native restore has no table count to claim.
52
43
 
53
- **The clear was gated on the WRITE's condition.** `!(useLedger && ledger.truncated)` means "an earlier attempt already recorded this destructive run, do not write a second row" — correct on the write. Copied down to the clear its meaning inverts: `ledger.truncated` is set by the emptying step of THIS run, so on `--no-atomic` (the only mode where `useLedger` is true) the clear was skipped by the very run that wrote the marker.
44
+ The sqlite / turso restore is atomic now (temp file + rename): its marker lives in the very file being replaced, so a plain in-place copy would leave a truncated database with nothing left to catch it.
54
45
 
55
- **And a `Date` in a predicate is not bindable on sqlite.** `better-sqlite3` binds numbers, strings, bigints, buffers and null; the row path has coerced Dates since that store was written, and the eager-join compiler carried its own private copy of the fix, but `query` / `updateMany` / `deleteMany` bound the raw value. So every comparison of a timestamp column against a `Date` failed with `Failed to execute statement` — including the marker's clear, which swallows its errors ON PURPOSE (a store with no marker table must not fail an import over a bookkeeping row) and therefore said nothing. The retention sweep compares `lt(column, cutoff)` the same way.
46
+ A target with no `_voltro_data_transfers` table still gets its backup; the closing line says it was not recorded rather than implying it was.
47
+ - **@voltro/cli** — A re-issued migration note for the `voltro data imports` → `voltro data transfers` rename that shipped in 0.49.0.
56
48
 
57
- Together: a fully successful replace left the marker standing, and the next boot REFUSED with "a destructive import did not finish" over a database that was completely fine. The one recovery is a command the operator has no reason to think they need. It stayed invisible because `atomic` defaults to true for `replace`, and because a staged replace writes no marker at all two defaults hiding the one mode that exists for large, interruptible loads.
49
+ Codemods are selected by `from < version <= to`, so the 0.49.0 note fires once, on the jump that crosses 0.49.0, and cannot be corrected for anyone already past it. Its gate searched `.ts` / `.tsx` while the command it is about lives in shell scripts, CI job definitions and runbooks so a project whose only occurrence sat in `.gitlab-ci.yml` crossed 0.49.0 and was told there was nothing to apply.
58
50
 
59
- The coercion is shared now and applied at all three predicate sites, with a guard that fails on a fourth that forgets. A dry run also closes its trace row: a preview that finished instantly used to leave the record open, so a polling caller waited out its whole budget over a run that was long done.
60
- - **@voltro/cli** — `voltro codegen` declared twenty framework tables fewer than a boot.
51
+ The re-issue is filed under 0.50.0, prints unconditionally, and says what to grep for. Redundant for anyone already fixed; the alternative is firing for nobody.
61
52
 
62
- The app half of this was fixed last release and looked like the whole thing. It was not: `codegen` derived the FEATURE MIX from the file list it had just walked, and that list is the entity/relations set — which contains no `*.workflow.tsx`, no `*.agent.ts`, no `*.cron.tsx`. So every feature flag came back false, and the dialect was never passed at all, taking `_voltro_cdc_offsets` with it. Measured on an app with two workflow files: 18 framework tables where the shared assembly produces 33.
53
+ ### Fixed
63
54
 
64
- It calls `assembleFrameworkTables({ root })` now the same entry `voltro db plan/apply` uses, which detects the mix from the root rather than being handed one.
55
+ - **@voltro/cli** — A `manual` codemod's `appliesTo` can now search every text file the project owns — `.sh`, `.yml`, `.json`, `.md`, `Makefile`, the `.js` scripts through a new `ctx.text` on the predicate context, and a codemod may declare `reach: 'beyond-source'` to print its note even when nothing matched.
65
56
 
66
- The parity guard moved with it. Comparing the two WALKS could not see this: both walks were right about files, and the divergence was introduced one layer past them. It compares the assembled SETS now, and runs codegen's own table function on a tree whose only feature signals are a workflow file and an agent file a source assertion that the right function is CALLED cannot see a wrong argument handed to it, and a wrong argument is what this was.
67
- - **@voltro/cli** — `voltro data --help` advertised four subcommands out of nine.
57
+ Both halves close the same gap, and it was in the codemod most in need of a gate. A manual codemod exists BECAUSE its subject could not be transformed, which usually means it is not source at all — and `appliesTo` was reading the ts-morph project, which holds `.ts` / `.tsx` and nothing else. Measured with one identical CLI invocation in four files: the `.ts` one printed the note, the `.sh`, `.yml` and `.md` ones printed nothing, and the run reported `codemods: nothing to apply for this jump`. That is an acquittal from a check that never looked at the file, and an acquittal gets acted on.
68
58
 
69
- It printed `<export|import|backup|restore>` while the command dispatched those four plus `imports`, `inspect`, `unpack`, `clear-replace-marker` and `clear-staging`. A quoted enumeration is read as exhaustivethe same failure `subcommandNames.ts` was written for after a `db` list cost two wrong conclusions and the missing entry here is the one that answers "what is my import doing right now" for somebody who cannot reach the pod.
59
+ `reach: 'beyond-source'` covers what no scan of one repository can reachan inline script in a CI runner's own UI, a CronJob spec in another repo, a wiki runbook. The note prints either way; only its framing changes, and an uncertain one says plainly that we could not look there.
60
+ - **@voltro/data-transfer** — When `mariadb-dump` is absent and the mysql-family fallback runs Oracle's `mysqldump` against a MariaDB server, the failure now names the way out.
70
61
 
71
- `subcommandHelpParity.test.ts` had listed `data` among the commands it could not check, loudly and correctly: there was no name list to check against. There is one now (`DATA_SUBCOMMANDS`), the dispatch is a keyed `Record`, so a subcommand with no name and a name with no handler are both compile errors, and the usage line is generated from the same array. `data` is out of the unchecked list.
62
+ The stderr it prints is the child's own words and is the right first thing to show and on its own it is a dead end: `Unknown table 'COLUMN_STATISTICS' in information_schema (1109)` names a table nobody asked for, in a schema nobody wrote, about a feature nobody enabled. Everything needed to act on it was known where the fallback was DECIDED: which binary we wanted, which one we took, and why the difference matters. That travels with the step now and prints under the stderr, including the counter-move a reader reaches for on their own (`--column-statistics=0` does not exist on `mariadb-dump`, so it fixes the wrong client and breaks the right one).