@voltro/cli 0.47.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/{apiBuild-FpHCWiv6.js → apiBuild-QmYHaCF4.js} +1 -1
  3. package/dist/apiBuild-dZbHdqMg.js +2 -0
  4. package/dist/bin.js +1 -1
  5. package/dist/{build-zVygXxGB.js → build-Bi4O_g7T.js} +6 -6
  6. package/dist/{checkCommand-D0dAKciE.js → checkCommand-D8vjQXQt.js} +3 -3
  7. package/dist/{checkCommand-B2Fw73lu.js → checkCommand-udbQpphy.js} +1 -1
  8. package/dist/{codegenCommand-BTtsnRus.js → codegenCommand-DApSH7hK.js} +39 -46
  9. package/dist/{codemodRunner-DSZjgyCL.js → codemodRunner-De1_316I.js} +562 -492
  10. package/dist/{commands-BrFKxnYW.js → commands-a5PxCaIg.js} +68 -60
  11. package/dist/{dashboardCommand-D9SjDc8E.js → dashboardCommand-DaoUUqJS.js} +1 -1
  12. package/dist/dataCommand-CJv_OMoS.js +1156 -0
  13. package/dist/dataProfile-Du3ztYxv.js +51 -0
  14. package/dist/{dbCommand-jbrmzwW_.js → dbCommand-Bf7Cy0mn.js} +5 -5
  15. package/dist/dbCommand-Bnl7MSGC.js +2 -0
  16. package/dist/declaredSchemaGates-BlcwObhb.js +25 -0
  17. package/dist/{dev-RjB7eE_B.js → dev-C31Nvlbv.js} +31 -30
  18. package/dist/{dev-CaIfZnw5.js → dev-DFVuzOj7.js} +1 -1
  19. package/dist/{doctorCommand-D7X29cw6.js → doctorCommand-B3uraeAe.js} +32 -14
  20. package/dist/doctorCommand-CPL6EhrW.js +2 -0
  21. package/dist/doctorDecidedSchema-BlbWgCD_.js +23 -0
  22. package/dist/{dormancyCommand-BGOP27fz.js → dormancyCommand-QUXQ9yoz.js} +1 -1
  23. package/dist/{embeddingsCommand-D72E22bw.js → embeddingsCommand-Zw1-rYSF.js} +1 -1
  24. package/dist/{envCommand-BHSxz6wC.js → envCommand-DtaJ4xEf.js} +1 -1
  25. package/dist/{evolveCommand-C2ofGGlz.js → evolveCommand-JDD3hE1i.js} +2 -2
  26. package/dist/{frameworkTableAssembly-BvZ0Lil_.js → frameworkTableAssembly-BwJVEKLr.js} +96 -119
  27. package/dist/frameworkTableAssembly-CVDB2hCq.js +2 -0
  28. package/dist/index.js +1 -1
  29. package/dist/{infoCommand-CuNl9cbh.js → infoCommand-COsUC1DB.js} +1 -1
  30. package/dist/{migrate-odCPGHPq.js → migrate-D3MK9BpK.js} +4 -4
  31. package/dist/{runtimeTrace-C_OKr0jY.js → runtimeTrace-Di6fcndp.js} +1 -1
  32. package/dist/{sdkgen-D9trW7sl.js → sdkgen-CIi5gM17.js} +1 -1
  33. package/dist/serveCommand-CzSDngKm.js +2 -0
  34. package/dist/serveCommand-N3udT49p.js +2362 -0
  35. package/dist/serveEntry.js +5 -5
  36. package/dist/{subcommandNames-CKG5Dz3a.js → subcommandNames-DpYs3DXr.js} +12 -2
  37. package/dist/{updateCommand-BlyXavoG.js → updateCommand-2BfbeMvp.js} +1 -1
  38. package/dist/updateCommand-CtYm7aKH.js +2 -0
  39. package/dist/{webhooksCommand-BqcWcbuW.js → webhooksCommand-CwX9IzvJ.js} +1 -1
  40. package/package.json +29 -17
  41. package/templates/AGENTS.md +1 -1
  42. package/templates/agent-docs/_index.md +1 -1
  43. package/templates/agent-docs/cli.md +117 -4
  44. package/templates/agent-docs/database/migrations.md +19 -5
  45. package/templates/agent-docs/whats-new.md +51 -14
  46. package/templates/apps/api-ai/package.json +7 -7
  47. package/templates/apps/api-auth/package.json +8 -8
  48. package/templates/apps/api-backend/package.json +7 -7
  49. package/templates/apps/api-backend-deactivation/package.json +7 -7
  50. package/templates/apps/api-backend-mail/package.json +8 -8
  51. package/templates/apps/api-backend-mariadb/package.json +9 -9
  52. package/templates/apps/api-backend-sqlite/package.json +8 -8
  53. package/templates/apps/api-backend-storage/package.json +8 -8
  54. package/templates/apps/api-cms/package.json +10 -10
  55. package/templates/apps/api-collab/package.json +8 -8
  56. package/templates/apps/api-data-advanced/package.json +8 -8
  57. package/templates/apps/api-durable/package.json +8 -8
  58. package/templates/apps/api-feature-flags/package.json +9 -9
  59. package/templates/apps/api-governance/package.json +8 -8
  60. package/templates/apps/api-kv/package.json +8 -8
  61. package/templates/apps/api-moderation/package.json +8 -8
  62. package/templates/apps/api-observability/package.json +8 -8
  63. package/templates/apps/api-ratelimit/package.json +8 -8
  64. package/templates/apps/api-rbac/package.json +8 -8
  65. package/templates/apps/api-rest/package.json +7 -7
  66. package/templates/apps/api-saas/package.json +11 -11
  67. package/templates/apps/api-saas-starter/package.json +10 -10
  68. package/templates/apps/api-search/package.json +8 -8
  69. package/templates/apps/api-status/package.json +8 -8
  70. package/templates/apps/api-versioning/package.json +8 -8
  71. package/templates/apps/api-webhooks/package.json +9 -9
  72. package/templates/apps/changelog/package.json +6 -6
  73. package/templates/apps/edge-functions/package.json +2 -2
  74. package/templates/apps/frontend-admin/package.json +8 -8
  75. package/templates/apps/frontend-app/package.json +9 -9
  76. package/templates/apps/frontend-auth/package.json +8 -8
  77. package/templates/apps/frontend-blank/package.json +7 -7
  78. package/templates/apps/frontend-cms/package.json +9 -9
  79. package/templates/apps/frontend-collab/package.json +10 -10
  80. package/templates/apps/frontend-contact/package.json +7 -7
  81. package/templates/apps/frontend-dashboard/package.json +7 -7
  82. package/templates/apps/frontend-docs/package.json +7 -7
  83. package/templates/apps/frontend-i18n/package.json +6 -6
  84. package/templates/apps/frontend-landing/package.json +7 -7
  85. package/templates/apps/frontend-portal/package.json +8 -8
  86. package/templates/apps/frontend-saas/package.json +8 -8
  87. package/templates/apps/frontend-spa/package.json +7 -7
  88. package/templates/apps/frontend-ssr/package.json +7 -7
  89. package/templates/apps/frontend-ssr-api/package.json +8 -8
  90. package/templates/apps/frontend-static-blog/package.json +6 -6
  91. package/templates/apps/frontend-status/package.json +8 -8
  92. package/templates/apps/mobile-app/package.json +4 -4
  93. package/dist/apiBuild-BsooZpw2.js +0 -2
  94. package/dist/dataCommand-Dl11q14e.js +0 -975
  95. package/dist/dataProfile-Cm0YVKSy.js +0 -18
  96. package/dist/dbCommand-BUD4Fd8v.js +0 -2
  97. package/dist/doctorCommand-5zv6PN-G.js +0 -2
  98. package/dist/frameworkTableAssembly-DDKtwsy2.js +0 -2
  99. package/dist/serveCommand-BU0wfV6V.js +0 -1931
  100. package/dist/serveCommand-Bwrn_9Fn.js +0 -2
  101. package/dist/updateCommand-CT5AvVg7.js +0 -2
@@ -1,5 +1,5 @@
1
- import { t as e } from "./loadEnv-D9nEOClM.js";
2
- import { r as t } from "./appModuleLoader-C9r9mxZt.js";
3
- import { i as n } from "./dialectDriver-czCHYpeH.js";
4
- import { t as r } from "./serveCommand-BU0wfV6V.js";
5
- export { e as loadDotEnv, t as registerAppModules, n as registerDriver, r as runServe };
1
+ import { t as e } from "./serveCommand-N3udT49p.js";
2
+ import { t } from "./loadEnv-D9nEOClM.js";
3
+ import { r as n } from "./appModuleLoader-C9r9mxZt.js";
4
+ import { i as r } from "./dialectDriver-czCHYpeH.js";
5
+ export { t as loadDotEnv, n as registerAppModules, r as registerDriver, e as runServe };
@@ -19,9 +19,19 @@ var e = [
19
19
  "scan-credentials",
20
20
  "encrypt-column"
21
21
  ], t = [
22
+ "export",
23
+ "import",
24
+ "transfers",
25
+ "inspect",
26
+ "unpack",
27
+ "backup",
28
+ "restore",
29
+ "clear-replace-marker",
30
+ "clear-staging"
31
+ ], n = [
22
32
  "scope",
23
33
  "export",
24
34
  "erase"
25
- ], n = (e) => `<${e.join("|")}>`;
35
+ ], r = (e) => `<${e.join("|")}>`;
26
36
  //#endregion
27
- export { t as n, n as r, e as t };
37
+ export { r as i, e as n, n as r, t };
@@ -1,4 +1,4 @@
1
- import { n as e, r as t, t as n } from "./codemodRunner-DSZjgyCL.js";
1
+ import { n as e, r as t, t as n } from "./codemodRunner-De1_316I.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";
@@ -0,0 +1,2 @@
1
+ import { f as e, p as t } from "./updateCommand-2BfbeMvp.js";
2
+ export { e as runApplyCodemods, t as runUpdateCommand };
@@ -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-CaIfZnw5.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
225
+ let { walk: t, loadDiscovered: n } = await import("./dev-DFVuzOj7.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.47.0",
3
+ "version": "0.49.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",
@@ -652,6 +652,18 @@
652
652
  "title": "an aggregate’s `incremental.source` is typechecked against the app’s tables",
653
653
  "kind": "manual"
654
654
  },
655
+ {
656
+ "version": "0.49.0",
657
+ "id": "0.49.0/01_data-imports-to-transfers",
658
+ "title": "`voltro data imports` → `voltro data transfers`",
659
+ "kind": "manual"
660
+ },
661
+ {
662
+ "version": "0.49.0",
663
+ "id": "0.49.0/02_import-run-symbols",
664
+ "title": "`ImportRun` / `describeImportRun` / `IMPORT_RUNS_TABLE` → their `…TransferRun` names",
665
+ "kind": "transform"
666
+ },
655
667
  {
656
668
  "version": "0.5.0",
657
669
  "id": "0.5.0/01_one-terminal-exactly-one",
@@ -739,22 +751,22 @@
739
751
  "@effect/platform-node": "^0.108.0",
740
752
  "@effect/sql": "^0.52.0",
741
753
  "@effect/workflow": "^0.19.0",
742
- "@voltro/ai": "0.47.0",
743
- "@voltro/cache": "0.47.0",
744
- "@voltro/data-transfer": "0.47.0",
745
- "@voltro/database": "0.47.0",
746
- "@voltro/env": "0.47.0",
747
- "@voltro/kv": "0.47.0",
748
- "@voltro/logger": "0.47.0",
749
- "@voltro/plugin-auth": "0.47.0",
750
- "@voltro/plugin-broadcast": "0.47.0",
751
- "@voltro/plugin-mail": "0.47.0",
752
- "@voltro/plugin-storage": "0.47.0",
753
- "@voltro/plugin-webhooks": "0.47.0",
754
- "@voltro/protocol": "0.47.0",
755
- "@voltro/runtime": "0.47.0",
756
- "@voltro/serverless": "0.47.0",
757
- "@voltro/workflow": "0.47.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",
758
770
  "chokidar": "^5.0.0",
759
771
  "ioredis": "^5.11.1",
760
772
  "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.47.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.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. |
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.47.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.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. |
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. |
@@ -2613,11 +2613,13 @@ Rows are written as newline-delimited JSON — streamable, resumable by line, an
2613
2613
  | *(none)* | Every table the app has DATA in (the default) — see the note below on the handful that describe a deployment rather than filling it. |
2614
2614
  | `--tenant <id>` | Every `tenant()`-scoped table filtered to that tenant, **plus** the FK closure in BOTH directions: (1) the transitive **FK-parent** closure of those rows — closure-pulled shared tables (a global `users` / reference table) are **row-subset to the ids the tenant's rows actually reference**, never exported in full; and (2) the **child** closure — rows that *reference* the tenant's rows (the comments on the tenant's projects) come along too, each scoped to the ids that actually point into the tenant set. The child walk is anchored on the `tenant()` tables, so a row that references **only a shared parent** (a global `users` another tenant also references) is **not** pulled — that would be a cross-tenant leak. A `--tenant` bundle therefore carries the tenant's parents AND children and **no other tenant's rows** — that's what makes it safe as a GDPR / per-tenant takeout. |
2615
2615
  | `--tables a,b` | An explicit set (you own referential integrity; the importer's deferred-FK resolution covers load-order dangles, see below). |
2616
- | `--exclude a,b` | Everything EXCEPT these, expanded against the live table list. Direct target only. Cannot be combined with `--tables` or `--tenant`; a name that does not exist is refused, because an exclusion that excludes nothing leaves the run looking like it worked. |
2616
+ | `--exclude a,b` | Everything EXCEPT these. The scope stays `all` and records what was left out — so the deployment-describing tables are still filtered, and `replace` still accepts the bundle. Works on both targets (the instance resolves it against its own live list). Cannot be combined with `--tables` or `--tenant`; a name that does not exist is refused, because an exclusion that excludes nothing leaves the run looking like it worked. |
2617
2617
 
2618
2618
  #### What `all` deliberately leaves out
2619
2619
 
2620
- A few framework tables hold rows **about a deployment** rather than an app's data — its migration ledger, its file-migration and seed records, its CDC offsets, its schedule claims and wakeups, its workflow watermarks / pending starts / admissions / pauses. They are dropped from `all`, skipped on import, and never emptied by a `replace`, and the run says which ones and why.
2620
+ A number of framework tables hold rows **about a deployment** rather than an app's data. The test is: would a row from elsewhere make this target *act* send, run, admit, refuse, skip a delivery — or assert something untrue about its own history? That covers its migration ledger, file-migration and seed records, CDC offsets and change log, schedule claims, wakeups and firing history, workflow watermarks / pending starts / admissions / pauses / delivered events, its outbox and delivery attempts, its idempotency keys, its storage grants, its spend and usage accounting, and its own traces and undo log. They are dropped from `all`, skipped on import, and never emptied by a `replace`, and the run says which ones and why.
2621
+
2622
+ Every framework table is classified one way or the other, and a new one fails the build until somebody decides — the classification used to live in two places, the module and a hand-kept copy in its test, and the two disagreed about exactly the tables that later caused trouble.
2621
2623
 
2622
2624
  The reason is worth one paragraph, because it cost a real environment ninety minutes. A `scope: all` bundle carried `_voltro_migration_plans`, `replace` wrote it, and the target's next boot refused:
2623
2625
 
@@ -2625,7 +2627,7 @@ The reason is worth one paragraph, because it cost a real environment ninety min
2625
2627
  auto-migrate: SCHEMA FINGERPRINT MISMATCH — declared=6e2c61081a9ed80c live=28af9a54414f22f1
2626
2628
  ```
2627
2629
 
2628
- The refusal was right. That fingerprint is computed over the **declared table set**, which legitimately differs per environment — `NODE_ENV=production` declares `_voltro_traces` and `_voltro_undo_log`, a `development` run does not. So the imported row was not out of date, it was **foreign**: it stated a schema decision made somewhere else. Two of the others would have made the target *act* — a pending workflow start runs a workflow somebody queued elsewhere, a pause silently stops one here.
2630
+ The refusal was right. That fingerprint is computed over the **declared table set**, so the imported row was not out of date, it was **foreign**: it stated a schema decision made somewhere else. (The declared set could also differ per environment then, because `NODE_ENV` decided two of the tables. It no longer does — but that removes one *way* for two deployments to differ, not the reason a foreign ledger row is wrong.) Two of the others would have made the target *act* — a pending workflow start runs a workflow somebody queued elsewhere, a pause silently stops one here.
2629
2631
 
2630
2632
  `all` is the only scope filtered. **Name one of these in `--tables` and you get it** — an explicit name is an expectation, and this command refuses to drop those silently.
2631
2633
 
@@ -2883,7 +2885,7 @@ recorder is for. Every suspended run says so, and the suspension is scoped to th
2883
2885
  run rather than the process, so requests served alongside it keep recording.
2884
2886
 
2885
2887
  Instead of per-row history, the operation records **itself**. One row in
2886
- `_voltro_data_imports` per run: the mode, the transport, the bundle, the source
2888
+ `_voltro_data_transfers` per run — in either direction: the mode, the transport, the bundle, the source
2887
2889
  deployment's schema fingerprint, the counts — and the failure, for the run you
2888
2890
  are usually looking for. A trail that only records successes goes quiet exactly
2889
2891
  when it is needed.
@@ -2893,6 +2895,117 @@ interlock and a run which cannot write it must not proceed, while this is
2893
2895
  history. A target whose schema is not migrated yet still imports, and says the
2894
2896
  trace could not be written.
2895
2897
 
2898
+ ### Every request fits a budget
2899
+
2900
+ Some environments cap a single request: a job runner that kills a client after
2901
+ ten minutes, an ingress with a 30-second ceiling, a CI step with a deadline. A
2902
+ whole transfer may still take an hour — it just has to do so as a series of short
2903
+ requests, each individually abandonable and individually retryable.
2904
+
2905
+ The rule the endpoints follow:
2906
+
2907
+ > **A request that carries bytes never runs a transfer. A request that starts a
2908
+ > transfer never carries bytes.**
2909
+
2910
+ ```bash
2911
+ # Nothing here may take longer than 30 seconds per request.
2912
+ voltro data import ./bundle --target api --api-url https://app.example \
2913
+ --mode replace --max-request-seconds 30
2914
+
2915
+ # Or: start it and walk away. The outcome is NOT known when this returns.
2916
+ voltro data import ./bundle --target api --api-url https://app.example --detach
2917
+ ```
2918
+
2919
+ Both staging areas — the uploaded bundle on its way in, the produced one on its
2920
+ way out — sit in the system temp directory by default and move with
2921
+ `VOLTRO_IMPORT_UPLOAD_DIR` / `VOLTRO_EXPORT_ARTIFACT_DIR`. Worth setting on a
2922
+ container: a bundle is the size your database is, and a default `/tmp` is
2923
+ frequently a small tmpfs.
2924
+
2925
+ `--max-request-seconds` is DECLARED rather than probed. The thing that kills a
2926
+ request is a policy on your side, and only you know it — in the one environment
2927
+ this was measured against, the ingress would happily hold a connection for an
2928
+ hour and the caller's own toolchain killed the client at ten minutes.
2929
+
2930
+ **An import is three steps.** The bundle goes up (in 16 MiB chunks when it is
2931
+ big, resumable, one plain request when it is small); `import/start` begins the
2932
+ run and answers as soon as the run is recorded; then the client watches the
2933
+ record. `start` is idempotent — retrying it under a budget returns the run that
2934
+ is already going rather than beginning a second destructive one.
2935
+
2936
+ **An export is three steps too.** Ask, wait for the record, then fetch the bytes
2937
+ in ranges from `GET /_voltro/admin/export/download`. The archive used to arrive
2938
+ in the response body, which made the request last as long as reading your whole
2939
+ database.
2940
+
2941
+ **`--detach` returns once the run has started** and says so plainly: exit `0`
2942
+ there means "it started", not "it worked". Attached, the exit code comes from the
2943
+ run's record — `0` finished, `1` failed, `2` still going when you stopped
2944
+ watching. Ctrl-C loses the watching and never the run.
2945
+
2946
+ ### Watching a transfer you cannot see
2947
+
2948
+ Over `--target api` the run happens INSIDE the instance. Everything it decides
2949
+ — whether it stages, whether recorders are suspended, how far it has got — is
2950
+ printed in the pod's log, and someone reaching for `--target api` is by
2951
+ construction someone who cannot reach the database directly and usually cannot
2952
+ read that log either. The run is also deliberately decoupled from the caller:
2953
+ killing the client does not stop it, which is what keeps a dead client from
2954
+ leaving a half-emptied target. Both properties are right, and together they used
2955
+ to mean an operator could neither see the run nor learn how it ended.
2956
+
2957
+ Two things close that, and neither is a streamed response on the upload
2958
+ connection. That connection is the thing an operator is most likely to lose: a
2959
+ client killed by a job timeout while the import runs on inside the instance is
2960
+ the ordinary case, and a stream would go quiet at exactly the moment somebody
2961
+ needs to know what happened. A poll can be run from a different machine than
2962
+ the one that started the import.
2963
+
2964
+ **Ask before you upload.** A `replace` over the api asks the instance what it is
2965
+ going to do, before a byte of the bundle goes up. The instance answers from the
2966
+ same function the run itself calls, so the answer cannot drift from the run:
2967
+
2968
+ ```
2969
+ $ voltro data import ./bundle --target api --api-url https://app.example --mode replace
2970
+ api replace: the instance WILL stage — 104 table(s) load into copies and the
2971
+ target keeps its rows until one short swap at the end. Interrupting the load leaves the target intact.
2972
+ importing — 41200 row(s) across 18 table(s) (staged; the target still holds its own rows)
2973
+ done — 228866 row(s) across 104 table(s)
2974
+ ```
2975
+
2976
+ With `--bundle-key` the client never holds the bundle, so it sends the key and
2977
+ the instance reads the table list out of the archive itself — the caller about to
2978
+ have an instance empty its own database is the last one who should be told to
2979
+ check a log they cannot read.
2980
+
2981
+ If it will not stage, the line says so and names the reason — a bundle table the
2982
+ target does not have, a foreign-key cycle, a dialect this build cannot stage on.
2983
+ That is the difference between "interrupting this is safe" and "interrupting
2984
+ this empties the target", which is exactly the decision an operator is making
2985
+ while they watch.
2986
+
2987
+ **Read the run from anywhere.** `_voltro_data_transfers` carries one row per run
2988
+ in EITHER direction: opened before the first table, advanced every couple of
2989
+ seconds as tables land, closed with the outcome — so polling it IS the progress
2990
+ feed:
2991
+
2992
+ ```
2993
+ $ voltro data transfers --target api --api-url https://app.example
2994
+ 1 import(s) IN FLIGHT — re-run this to watch the counters move
2995
+ 2026-08-23T09:04:01.000Z · replace — started and never reported finishing · via api · staged (target untouched until the swap) · from /tmp/b
2996
+ 2026-08-23T08:00:00.000Z · export all — 228866 row(s) across 104 table(s) · via api · from backups/nightly.vbundle
2997
+ ```
2998
+
2999
+ Same data over `GET /_voltro/admin/imports?limit=20`, behind the same
3000
+ data-transfer secret (the row names bundles and schema fingerprints). It works
3001
+ from a different machine than the one that started the import, and through
3002
+ anything that forwards a GET.
3003
+
3004
+ The `staged` part is a separate claim from the preflight's, and both are needed.
3005
+ The preflight says what the instance WILL do; this says what it did. Without it
3006
+ the two could only be closed by reading the pod's log, which is the one place a
3007
+ `--target api` caller cannot reach.
3008
+
2896
3009
  ### An interrupted `replace` cannot be silent
2897
3010
 
2898
3011
  The capture only helps if somebody knows to reach for it. A half-replaced database is indistinguishable from an empty one **from the inside** — every table exists, every constraint holds, every query returns nothing without erroring — so a run that emptied a target and disappeared can be served over for hours before anyone asks the right question.
@@ -3179,14 +3179,28 @@ flag flipped alone:
3179
3179
 
3180
3180
  **From 0.35.0 none of them does.** `_voltro_cdc_offsets` follows the DIALECT, so
3181
3181
  a mariadb/mssql app declares it either way; the other two are declared in
3182
- `app.config.ts` and the env vars only choose what a process captures:
3182
+ `app.config.ts` and the env vars only choose what a process captures.
3183
+
3184
+ **And from 0.48.0 `NODE_ENV` does not either.** Until then, `_voltro_traces` and
3185
+ `_voltro_undo_log` defaulted to on outside production — safe for the
3186
+ fingerprint, which only ever compares processes inside ONE deployment, and
3187
+ unsafe for the reader that spans two. `voltro data` is that reader: a bundle
3188
+ exported from a development database carries the tables that database has, and
3189
+ a staged (non-destructive) `replace` needs every bundle table to exist in the
3190
+ target. One source tree therefore produced a bundle a production target could
3191
+ not stage, and the run fell back to truncating it — with nothing red anywhere.
3192
+
3193
+ Both are declared in every environment now. An unused declared table is an empty
3194
+ table; a schema that differs per environment is a class of failure. Declare them
3195
+ only to leave one OUT:
3183
3196
 
3184
3197
  ```ts
3185
3198
  export default {
3186
- // Both default to on outside production. Declare them when you want the
3187
- // table in a production schema `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`
3188
- // alone is no longer enough, because your migrate job does not carry them.
3189
- schema: { undo: true, traces: true },
3199
+ // Declared in EVERY environment by default. Set `false` to keep a table out —
3200
+ // and then set it in every environment, or two deployments of one source tree
3201
+ // declare different schemas again. `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`
3202
+ // still decide what a process WRITES; they no longer decide what exists.
3203
+ schema: { undo: false, traces: false },
3190
3204
  }
3191
3205
  ```
3192
3206
 
@@ -1,4 +1,4 @@
1
- # What's new in 0.47.0
1
+ # What's new in 0.49.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
@@ -7,28 +7,65 @@ workaround for something that shipped two versions ago.
7
7
 
8
8
  BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
- ### Changed
10
+ ### ⚠ BREAKING
11
11
 
12
- - **@voltro/database, @voltro/data-transfer, @voltro/cli, @voltro/plugin-storage** — A `--mode replace` no longer writes per-row history, and three things that were reported alongside it.
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.
13
13
 
14
- **Write recorders are suspended for a `replace`.** A replace SETS a state; it does not change rows, so a per-row history entry describes something that did not happen. A deployment measured what that costs: they run `versioningPlugin({ timing: 'in-transaction' })` on 70 of 80 tables, so one import wrote 242 950 history rows and doubled the write load of the most expensive run they make and those rows were the source of refused writes they spent three rounds diagnosing.
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.
15
15
 
16
- The deciding argument is not the cost. **The import path had already decided this, and the recorder was the one layer that did not hear.** These routes write through the RAW store on purpose no tenant scoping, no row filter, no `audit()` stamping. A recorder fired anyway because it hangs one level below the wrapper. Suspending it makes the layers agree.
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.
17
17
 
18
- Suspended for `replace` only: an `upsert` or an `append` CHANGES existing state, which is exactly what a recorder is for. Scoped per execution context rather than by a switch, because an import runs while the app serves requests and a process-global flag would silently drop recording for everything concurrent with it. And every such run SAYS what it suspended dropping history quietly would be the same defect in a nicer costume.
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.
19
19
 
20
- This also removes the reason a staged `replace` used to fall back. A recorder is keyed by table name, so a staged write found none and the recorder never ran; rather than record inconsistently, the run took the slower path. For the deployment above that meant the staged path could never activate permanently, on every environment.
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.
21
21
 
22
- **`voltro codegen` wrote a truncated table declaration.** It discovered tables with the walk that collects the rpc GROUP's inputs descriptors, workflows, events, and deliberately no `*.entity.ts`. So the generated `voltro-tables.generated.d.ts` listed the framework's tables plus `actors`, which the framework injects. Measured downstream: 37 names where a `voltro dev` boot writes 117, and 299 `TS2322` errors from every `source:` naming one of their own tables. The way in was our own message — `voltro test` refuses a stale rpc group and tells you to run `voltro codegen`. Table discovery has its own walk now, and a test compares the two walks' RESULTS on a real tree rather than trusting they mean the same thing.
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
23
 
24
- **The eager-relation doctor rule missed the state that is most wrong.** It resolved relations only against the tables a query already DECLARED, on the reasoning that the base is virtually always in `source:`. Measured: a query reading `projects` while declaring only `projectTeams` produced no finding at all, while the same query with the base added produced two. The base is a fact about the executor, so it is read from the executor now.
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
25
 
26
- **A rollback capture says when its path shares the root filesystem.** The 409 answered "is storage configured", which is not the question a `filesystem` provider pointed at a container directory with no volume behind it passes it and dies with the pod. What a process CAN observe is that a mounted volume is a different filesystem: `pathDurability` compares device ids, and a capture landing on the same device as `/` says so. Three-valued on purpose, and explicitly not an alarm on a development machine, where everything is one device and nothing is a pod.
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
28
 
28
- **And the import records ITSELF, once.** Dropping per-row history left an operator asking "was this data imported, and when" with nothing to read, and trading too much for none is not obviously the better trade. `_voltro_data_imports` carries one row per RUN: the mode, the transport, the bundle, the SOURCE deployment's schema fingerprint, the counts, and — for the run an operator is actually looking for — the failure. It is written for a failed import as well as a finished one; a trail that only records successes goes quiet exactly when it is needed.
29
+ `@voltro/database` (and `voltro/database`) renamed four public exports along with the table behind them:
29
30
 
30
- Best effort, unlike the interrupted-replace marker, and the difference is deliberate: the marker is a safety interlock and a run that cannot write it must not proceed, while history is valuable and not load-bearing. A target that has not been migrated yet still imports, and says the trace could not be written.
31
+ | was | is | |---|---| | `ImportRun` | `DataTransferRun` | | `describeImportRun` | `describeTransferRun` | | `IMPORT_RUNS_TABLE` | `DATA_TRANSFERS_TABLE` | | `_voltroImportRunsTable` | `_voltroDataTransfersTable` |
31
32
 
32
- Two things it learned from being wired. The table is `.nonReactive()` nobody subscribes to "an import happened", and a reactive bookkeeping write showed up in every suite that counts the LOAD's writes. And it is written only when the target actually DECLARES the table: the in-memory store accepts a write to any name, so an absent table cannot be detected by the write failing, and inventing the row would put a framework write into every embedder's counts.
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.
33
34
 
34
- **Measured at the reported magnitude.** 114 tables, 243 048 rows, real MariaDB, staged replace: the whole run takes 56.5s and the DESTRUCTIVE TRANSACTION takes **1.13s**. The window in which a dead process can leave a half-replaced target is the second number; before this it was the first.
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.
38
+
39
+ ### Added
40
+
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.
42
+
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.
44
+
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.
46
+
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.
50
+
51
+ Two defects, one visible symptom, and both were silent by construction.
52
+
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.
54
+
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.
56
+
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.
58
+
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.
61
+
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.
63
+
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.
65
+
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.
68
+
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 exhaustive — the 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.
70
+
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.
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.97.0",
14
14
  "@effect/rpc": "^0.76.0",
15
- "@voltro/ai": "0.47.0",
16
- "@voltro/cli": "0.47.0",
17
- "@voltro/database": "0.47.0",
18
- "@voltro/env": "0.47.0",
19
- "@voltro/protocol": "0.47.0",
20
- "@voltro/runtime": "0.47.0",
15
+ "@voltro/ai": "0.49.0",
16
+ "@voltro/cli": "0.49.0",
17
+ "@voltro/database": "0.49.0",
18
+ "@voltro/env": "0.49.0",
19
+ "@voltro/protocol": "0.49.0",
20
+ "@voltro/runtime": "0.49.0",
21
21
  "effect": "^3.22.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.47.0",
24
+ "@voltro/testing": "0.49.0",
25
25
  "typescript": "^6.0.3",
26
26
  "@vitest/coverage-v8": "^4.1.10",
27
27
  "vitest": "^4.1.10"
@@ -13,17 +13,17 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.47.0",
17
- "@voltro/database": "0.47.0",
18
- "@voltro/env": "0.47.0",
19
- "@voltro/plugin-auth": "0.47.0",
20
- "@voltro/protocol": "0.47.0",
21
- "@voltro/runtime": "0.47.0",
22
- "@voltro/sql-postgres": "0.47.0",
16
+ "@voltro/cli": "0.49.0",
17
+ "@voltro/database": "0.49.0",
18
+ "@voltro/env": "0.49.0",
19
+ "@voltro/plugin-auth": "0.49.0",
20
+ "@voltro/protocol": "0.49.0",
21
+ "@voltro/runtime": "0.49.0",
22
+ "@voltro/sql-postgres": "0.49.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.47.0",
26
+ "@voltro/testing": "0.49.0",
27
27
  "typescript": "^6.0.3",
28
28
  "@vitest/coverage-v8": "^4.1.10",
29
29
  "vitest": "^4.1.10"
@@ -16,16 +16,16 @@
16
16
  "dependencies": {
17
17
  "@effect/platform": "^0.97.0",
18
18
  "@effect/rpc": "^0.76.0",
19
- "@voltro/cli": "0.47.0",
20
- "@voltro/database": "0.47.0",
21
- "@voltro/env": "0.47.0",
22
- "@voltro/plugin-multitenancy": "0.47.0",
23
- "@voltro/protocol": "0.47.0",
24
- "@voltro/runtime": "0.47.0",
19
+ "@voltro/cli": "0.49.0",
20
+ "@voltro/database": "0.49.0",
21
+ "@voltro/env": "0.49.0",
22
+ "@voltro/plugin-multitenancy": "0.49.0",
23
+ "@voltro/protocol": "0.49.0",
24
+ "@voltro/runtime": "0.49.0",
25
25
  "effect": "^3.22.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@voltro/testing": "0.47.0",
28
+ "@voltro/testing": "0.49.0",
29
29
  "typescript": "^6.0.3",
30
30
  "@vitest/coverage-v8": "^4.1.10",
31
31
  "vitest": "^4.1.10"
@@ -13,16 +13,16 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.47.0",
17
- "@voltro/database": "0.47.0",
18
- "@voltro/env": "0.47.0",
19
- "@voltro/plugin-deactivation": "0.47.0",
20
- "@voltro/protocol": "0.47.0",
21
- "@voltro/runtime": "0.47.0",
16
+ "@voltro/cli": "0.49.0",
17
+ "@voltro/database": "0.49.0",
18
+ "@voltro/env": "0.49.0",
19
+ "@voltro/plugin-deactivation": "0.49.0",
20
+ "@voltro/protocol": "0.49.0",
21
+ "@voltro/runtime": "0.49.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.47.0",
25
+ "@voltro/testing": "0.49.0",
26
26
  "typescript": "^6.0.3",
27
27
  "@vitest/coverage-v8": "^4.1.10",
28
28
  "vitest": "^4.1.10"
@@ -13,18 +13,18 @@
13
13
  "dependencies": {
14
14
  "@react-email/components": "^1.0.12",
15
15
  "@react-email/render": "^1.4.0",
16
- "@voltro/cli": "0.47.0",
17
- "@voltro/database": "0.47.0",
18
- "@voltro/env": "0.47.0",
19
- "@voltro/plugin-mail": "0.47.0",
20
- "@voltro/plugin-multitenancy": "0.47.0",
21
- "@voltro/protocol": "0.47.0",
22
- "@voltro/runtime": "0.47.0",
16
+ "@voltro/cli": "0.49.0",
17
+ "@voltro/database": "0.49.0",
18
+ "@voltro/env": "0.49.0",
19
+ "@voltro/plugin-mail": "0.49.0",
20
+ "@voltro/plugin-multitenancy": "0.49.0",
21
+ "@voltro/protocol": "0.49.0",
22
+ "@voltro/runtime": "0.49.0",
23
23
  "effect": "^3.22.0",
24
24
  "react": "^19.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.47.0",
27
+ "@voltro/testing": "0.49.0",
28
28
  "typescript": "^6.0.3",
29
29
  "@vitest/coverage-v8": "^4.1.10",
30
30
  "vitest": "^4.1.10"