@voltro/cli 0.2.2 → 0.4.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 (77) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/THIRD-PARTY-NOTICES.md +234 -1
  3. package/bin/voltro.mjs +71 -1
  4. package/dist/apiBuild-CvtQeBMs.js +190 -0
  5. package/dist/apiBuild-DQBNqNZ8.js +2 -0
  6. package/dist/bin.js +2 -2
  7. package/dist/{commands-DQy4812j.js → commands-DhyBIs1O.js} +2381 -1768
  8. package/dist/{dev--jHe1vcu.js → dev-CQxbrpDz.js} +1677 -1626
  9. package/dist/dev-DYjGqPGD.js +2 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +1 -1
  12. package/dist/serveCommand-BZzUJIyo.js +1077 -0
  13. package/dist/serveEntry.js +2 -2
  14. package/package.json +22 -19
  15. package/templates/AGENTS.core.md +61 -0
  16. package/templates/AGENTS.md +61 -0
  17. package/templates/agent-docs/_manifest.json +4 -4
  18. package/templates/agent-docs/ai.md +28 -0
  19. package/templates/agent-docs/authentication.md +5 -2
  20. package/templates/agent-docs/cli.md +98 -0
  21. package/templates/agent-docs/data.md +193 -3
  22. package/templates/agent-docs/database/advancedqueries.md +29 -0
  23. package/templates/agent-docs/database/querying.md +2 -0
  24. package/templates/agent-docs/deployment.md +3 -1
  25. package/templates/agent-docs/internationalization.md +237 -2
  26. package/templates/agent-docs/observability.md +9 -0
  27. package/templates/agent-docs/plugins.md +2 -2
  28. package/templates/agent-docs/reference.md +76 -0
  29. package/templates/agent-docs/schema-driven-ui.md +551 -2
  30. package/templates/agent-docs/templates/apibackends.md +10 -8
  31. package/templates/agent-docs/templates/overview.md +1 -1
  32. package/templates/agent-docs/testing.md +177 -3
  33. package/templates/apps/api-ai/package.json +7 -7
  34. package/templates/apps/api-auth/package.json +8 -8
  35. package/templates/apps/api-backend/package.json +7 -7
  36. package/templates/apps/api-backend-deactivation/package.json +7 -7
  37. package/templates/apps/api-backend-mail/package.json +8 -8
  38. package/templates/apps/api-backend-mariadb/package.json +9 -9
  39. package/templates/apps/api-backend-storage/package.json +8 -8
  40. package/templates/apps/api-data-advanced/package.json +8 -8
  41. package/templates/apps/api-durable/package.json +8 -8
  42. package/templates/apps/api-feature-flags/package.json +9 -9
  43. package/templates/apps/api-governance/package.json +8 -8
  44. package/templates/apps/api-kv/package.json +8 -8
  45. package/templates/apps/api-moderation/package.json +8 -8
  46. package/templates/apps/api-observability/package.json +8 -8
  47. package/templates/apps/api-ratelimit/package.json +8 -8
  48. package/templates/apps/api-rbac/README.md +9 -4
  49. package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +1 -1
  50. package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +6 -6
  51. package/templates/apps/api-rbac/package.json +8 -8
  52. package/templates/apps/api-rbac/tests/notes.create.test.ts +8 -8
  53. package/templates/apps/api-rest/package.json +7 -7
  54. package/templates/apps/api-saas/package.json +11 -11
  55. package/templates/apps/api-search/package.json +8 -8
  56. package/templates/apps/api-versioning/package.json +8 -8
  57. package/templates/apps/api-webhooks/package.json +8 -8
  58. package/templates/apps/changelog/package.json +6 -6
  59. package/templates/apps/edge-functions/package.json +2 -2
  60. package/templates/apps/frontend-admin/package.json +8 -8
  61. package/templates/apps/frontend-app/package.json +8 -8
  62. package/templates/apps/frontend-blank/package.json +7 -7
  63. package/templates/apps/frontend-contact/package.json +7 -7
  64. package/templates/apps/frontend-dashboard/package.json +7 -7
  65. package/templates/apps/frontend-docs/package.json +7 -7
  66. package/templates/apps/frontend-i18n/package.json +6 -6
  67. package/templates/apps/frontend-landing/package.json +7 -7
  68. package/templates/apps/frontend-spa/package.json +7 -7
  69. package/templates/apps/frontend-ssr/package.json +7 -7
  70. package/templates/apps/frontend-ssr-api/package.json +8 -8
  71. package/templates/apps/frontend-static-blog/package.json +6 -6
  72. package/templates/baselines/compose/docker/api.Dockerfile +10 -5
  73. package/templates/baselines/compose-mariadb/docker/api.Dockerfile +10 -5
  74. package/dist/apiBuild-OpZROja5.js +0 -2
  75. package/dist/apiBuild-o70rjpVJ.js +0 -184
  76. package/dist/dev-BKkZglQV.js +0 -2
  77. package/dist/serveCommand-93rRdEp0.js +0 -1077
@@ -1,4 +1,4 @@
1
- import { At as e, nt as t } from "./dev--jHe1vcu.js";
1
+ import { jt as e, rt as t } from "./dev-CQxbrpDz.js";
2
2
  import { a as n } from "./startupRunner-DhlX9nqd.js";
3
- import { t as r } from "./serveCommand-93rRdEp0.js";
3
+ import { t as r } from "./serveCommand-BZzUJIyo.js";
4
4
  export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.2.2",
3
+ "version": "0.4.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",
@@ -52,27 +52,30 @@
52
52
  "@effect/platform-node": "^0.107.0",
53
53
  "@effect/sql": "^0.51.1",
54
54
  "@effect/workflow": "^0.18.2",
55
+ "@voltro/ai": "0.4.0",
56
+ "@voltro/cache": "0.4.0",
57
+ "@voltro/data-transfer": "0.4.0",
58
+ "@voltro/database": "0.4.0",
59
+ "@voltro/env": "0.4.0",
60
+ "@voltro/kv": "0.4.0",
61
+ "@voltro/logger": "0.4.0",
62
+ "@voltro/plugin-auth": "0.4.0",
63
+ "@voltro/plugin-broadcast": "0.4.0",
64
+ "@voltro/plugin-mail": "0.4.0",
65
+ "@voltro/plugin-storage": "0.4.0",
66
+ "@voltro/plugin-webhooks": "0.4.0",
67
+ "@voltro/protocol": "0.4.0",
68
+ "@voltro/runtime": "0.4.0",
69
+ "@voltro/serverless": "0.4.0",
70
+ "@voltro/workflow": "0.4.0",
71
+ "chokidar": "^5.0.0",
72
+ "ioredis": "^5.11.1",
73
+ "ts-morph": "^28.0.0"
74
+ },
75
+ "optionalDependencies": {
55
76
  "@tailwindcss/vite": "^4.3.2",
56
77
  "@vitejs/plugin-react": "^6.0.3",
57
- "@voltro/ai": "0.2.2",
58
- "@voltro/cache": "0.2.2",
59
- "@voltro/data-transfer": "0.2.2",
60
- "@voltro/database": "0.2.2",
61
- "@voltro/env": "0.2.2",
62
- "@voltro/kv": "0.2.2",
63
- "@voltro/logger": "0.2.2",
64
- "@voltro/plugin-auth": "0.2.2",
65
- "@voltro/plugin-broadcast": "0.2.2",
66
- "@voltro/plugin-mail": "0.2.2",
67
- "@voltro/plugin-storage": "0.2.2",
68
- "@voltro/plugin-webhooks": "0.2.2",
69
- "@voltro/protocol": "0.2.2",
70
- "@voltro/runtime": "0.2.2",
71
- "@voltro/serverless": "0.2.2",
72
- "@voltro/workflow": "0.2.2",
73
- "chokidar": "^5.0.0",
74
78
  "esbuild": "^0.28.0",
75
- "ioredis": "^5.11.1",
76
79
  "tsx": "^4.23.0",
77
80
  "vite": "^8.1.4"
78
81
  },
@@ -61,6 +61,53 @@ Ask in order; stop at the first "yes":
61
61
  Deep dive: **`data.md`** (queries/mutations/actions/streams/errors),
62
62
  **`workflows.md`**, **`scheduling.md`**.
63
63
 
64
+ ### Pick the CLIENT primitive (decision rubric)
65
+
66
+ `useSubscription` / `useMutation` / `useAction` are the transport — they are NOT
67
+ the whole client surface. The framework ships a **headless primitive for every
68
+ common UI job**, each derived from the descriptor Schema you already wrote. Reach
69
+ for one BEFORE hand-rolling; ask in order:
70
+
71
+ 1. **A form / any typed write UI?** → **`useFormBinding`** — fields + validation
72
+ derived from the mutation's input Schema, and a server
73
+ `ValidationError({ field })` routes itself to that field. Not per-field `useState`.
74
+ 2. **A table / list with sort, filter, pagination?** → **`useDataTable`** (columns
75
+ from the query's output Schema, live rows). **`useQueryFilters`** builds filter
76
+ controls from the query's INPUT Schema; **`useQueryField`** is the query-bound picker.
77
+ 3. **A file upload?** → **`useUpload`** (progress, cancel, provider-agnostic).
78
+ Not `FileReader` → base64 → action.
79
+ 4. **Gating UI on permission?** → **`useCan`** / **`useCanAny`** (scopes, from
80
+ `@voltro/client`, fed by `<PermissionProvider scopes>`) or
81
+ **`useResourceCan`** / **`useResourceCans`** (per-resource ReBAC, reactive).
82
+ These live in the client, not behind a plugin — scopes are a framework
83
+ concept, so gating a button never requires installing rbac.
84
+ 5. **A value derived from one or more subscriptions?** → **`useDerived`**
85
+ (dependency-tracked, referentially stable). Not a hand-maintained `useMemo` dep array.
86
+ 6. **A very large list?** → **`useWindowedSubscription`** (subscribe to the visible
87
+ window only, not a million rows).
88
+ 7. **Debounced input?** → **`useDebounced`**; **`useAsyncValidation`** for checks
89
+ that need the DB (uniqueness, cross-row) — live, over a query binding.
90
+ 8. **A loading placeholder?** → **`useFormSkeleton`** / **`useTableSkeleton`** —
91
+ shaped like the REAL data, from the same Schema.
92
+ 9. **Offline writes · undo · dry-run · "why is this value here?"** → **`useOutbox`**,
93
+ **`useUndoLog`**, **`usePreview`**, **`useProvenance`**.
94
+ 10. **Global rpc-error handling?** → **`useOnRpcError`** + **`reportClientError`**.
95
+
96
+ | You're about to write | Reach for instead |
97
+ |---|---|
98
+ | per-field `useState` + `isSubmitting` + `resetForm()` | `useFormBinding` |
99
+ | shadcn `<Table>` + per-table sort/filter state | `useDataTable` |
100
+ | `FileReader` → base64 → action → `storage.put` | `useUpload` |
101
+ | `useMemo` fanning in 3+ subscriptions | `useDerived` |
102
+ | `setTimeout` debounce inside `useEffect` | `useDebounced` |
103
+ | `data === undefined ? <Skeleton/> : …` | `useFormSkeleton` / `useTableSkeleton` |
104
+ | a hand-kept page-scope check | `useCan` / `useResourceCan` |
105
+ | a hand-rolled presence roster / notification inbox / feature flag | `@voltro/plugin-presence` / `-notifications` / `-flags` |
106
+
107
+ Deep dive: **`schema-driven-ui.md`** (forms, tables, pickers, filters, skeletons),
108
+ **`reference.md`** (the complete hook list). If you are hand-writing more than ~20
109
+ lines of generic UI plumbing, stop and check those two first.
110
+
64
111
  ## Project layout
65
112
 
66
113
  One OR MORE **projects** live under `apps/`. A project (`project.json`) is the
@@ -220,8 +267,22 @@ primitive → just save; the supervised dev loop respawns and regenerates.
220
267
  But DO call `assertOwnTenant(input.tenantId, ctx.request.subject)` (from
221
268
  `@voltro/plugin-multitenancy/guard`) in custom mutations that write raw rows,
222
269
  and declare `error: TenantMismatch` — subscriptions are auto-scoped, writes are not.
270
+ - **Gate authorization declaratively with `guards:`.** Add
271
+ `guards: [{ scope: 'notes:write' }]` to `defineMutation`/`defineQuery`/`defineAction`
272
+ — the framework enforces it BEFORE the executor (before the txn opens), fails
273
+ with a typed `ScopeError` (auto-merged into the wire error union), and checks the
274
+ caller's EFFECTIVE scopes (raw ∪ rbac roles). Guards are browser-safe DATA (scope
275
+ strings + a pure `resource: (input) => id` extractor — never a server fn). Use the
276
+ in-handler `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` (or rbac's
277
+ `permission()`) only for checks that need LOADED data (row ownership).
223
278
  - **Don't store secrets in the schema or in `Subject`.** Declare env via
224
279
  `defineEnv` (`configuration.md`); carry only ids in `Subject`.
280
+ - **In production, `voltro build` BEFORE `voltro serve`.** A production
281
+ (`NODE_ENV=production`) serve REQUIRES the precompiled serve bundle and fails
282
+ loud if it's missing — production never transpiles on demand. The generated
283
+ Dockerfiles already do `voltro build` then `voltro serve`; if you write your
284
+ own prod start, build first. (`voltro dev` + a non-prod local `serve` still use
285
+ tsx.) Depth: the deployment topic.
225
286
  - **Don't copy prod data down unmasked.** `voltro data export` (and
226
287
  `--target api`) reads REAL rows — PII included. Copying prod → dev/stage MUST
227
288
  go through a masking profile (`--profile`; classify columns `.sensitive()` /
@@ -61,6 +61,53 @@ Ask in order; stop at the first "yes":
61
61
  Deep dive: **`data.md`** (queries/mutations/actions/streams/errors),
62
62
  **`workflows.md`**, **`scheduling.md`**.
63
63
 
64
+ ### Pick the CLIENT primitive (decision rubric)
65
+
66
+ `useSubscription` / `useMutation` / `useAction` are the transport — they are NOT
67
+ the whole client surface. The framework ships a **headless primitive for every
68
+ common UI job**, each derived from the descriptor Schema you already wrote. Reach
69
+ for one BEFORE hand-rolling; ask in order:
70
+
71
+ 1. **A form / any typed write UI?** → **`useFormBinding`** — fields + validation
72
+ derived from the mutation's input Schema, and a server
73
+ `ValidationError({ field })` routes itself to that field. Not per-field `useState`.
74
+ 2. **A table / list with sort, filter, pagination?** → **`useDataTable`** (columns
75
+ from the query's output Schema, live rows). **`useQueryFilters`** builds filter
76
+ controls from the query's INPUT Schema; **`useQueryField`** is the query-bound picker.
77
+ 3. **A file upload?** → **`useUpload`** (progress, cancel, provider-agnostic).
78
+ Not `FileReader` → base64 → action.
79
+ 4. **Gating UI on permission?** → **`useCan`** / **`useCanAny`** (scopes, from
80
+ `@voltro/client`, fed by `<PermissionProvider scopes>`) or
81
+ **`useResourceCan`** / **`useResourceCans`** (per-resource ReBAC, reactive).
82
+ These live in the client, not behind a plugin — scopes are a framework
83
+ concept, so gating a button never requires installing rbac.
84
+ 5. **A value derived from one or more subscriptions?** → **`useDerived`**
85
+ (dependency-tracked, referentially stable). Not a hand-maintained `useMemo` dep array.
86
+ 6. **A very large list?** → **`useWindowedSubscription`** (subscribe to the visible
87
+ window only, not a million rows).
88
+ 7. **Debounced input?** → **`useDebounced`**; **`useAsyncValidation`** for checks
89
+ that need the DB (uniqueness, cross-row) — live, over a query binding.
90
+ 8. **A loading placeholder?** → **`useFormSkeleton`** / **`useTableSkeleton`** —
91
+ shaped like the REAL data, from the same Schema.
92
+ 9. **Offline writes · undo · dry-run · "why is this value here?"** → **`useOutbox`**,
93
+ **`useUndoLog`**, **`usePreview`**, **`useProvenance`**.
94
+ 10. **Global rpc-error handling?** → **`useOnRpcError`** + **`reportClientError`**.
95
+
96
+ | You're about to write | Reach for instead |
97
+ |---|---|
98
+ | per-field `useState` + `isSubmitting` + `resetForm()` | `useFormBinding` |
99
+ | shadcn `<Table>` + per-table sort/filter state | `useDataTable` |
100
+ | `FileReader` → base64 → action → `storage.put` | `useUpload` |
101
+ | `useMemo` fanning in 3+ subscriptions | `useDerived` |
102
+ | `setTimeout` debounce inside `useEffect` | `useDebounced` |
103
+ | `data === undefined ? <Skeleton/> : …` | `useFormSkeleton` / `useTableSkeleton` |
104
+ | a hand-kept page-scope check | `useCan` / `useResourceCan` |
105
+ | a hand-rolled presence roster / notification inbox / feature flag | `@voltro/plugin-presence` / `-notifications` / `-flags` |
106
+
107
+ Deep dive: **`schema-driven-ui.md`** (forms, tables, pickers, filters, skeletons),
108
+ **`reference.md`** (the complete hook list). If you are hand-writing more than ~20
109
+ lines of generic UI plumbing, stop and check those two first.
110
+
64
111
  ## Project layout
65
112
 
66
113
  One OR MORE **projects** live under `apps/`. A project (`project.json`) is the
@@ -220,8 +267,22 @@ primitive → just save; the supervised dev loop respawns and regenerates.
220
267
  But DO call `assertOwnTenant(input.tenantId, ctx.request.subject)` (from
221
268
  `@voltro/plugin-multitenancy/guard`) in custom mutations that write raw rows,
222
269
  and declare `error: TenantMismatch` — subscriptions are auto-scoped, writes are not.
270
+ - **Gate authorization declaratively with `guards:`.** Add
271
+ `guards: [{ scope: 'notes:write' }]` to `defineMutation`/`defineQuery`/`defineAction`
272
+ — the framework enforces it BEFORE the executor (before the txn opens), fails
273
+ with a typed `ScopeError` (auto-merged into the wire error union), and checks the
274
+ caller's EFFECTIVE scopes (raw ∪ rbac roles). Guards are browser-safe DATA (scope
275
+ strings + a pure `resource: (input) => id` extractor — never a server fn). Use the
276
+ in-handler `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` (or rbac's
277
+ `permission()`) only for checks that need LOADED data (row ownership).
223
278
  - **Don't store secrets in the schema or in `Subject`.** Declare env via
224
279
  `defineEnv` (`configuration.md`); carry only ids in `Subject`.
280
+ - **In production, `voltro build` BEFORE `voltro serve`.** A production
281
+ (`NODE_ENV=production`) serve REQUIRES the precompiled serve bundle and fails
282
+ loud if it's missing — production never transpiles on demand. The generated
283
+ Dockerfiles already do `voltro build` then `voltro serve`; if you write your
284
+ own prod start, build first. (`voltro dev` + a non-prod local `serve` still use
285
+ tsx.) Depth: the deployment topic.
225
286
  - **Don't copy prod data down unmasked.** `voltro data export` (and
226
287
  `--target api`) reads REAL rows — PII included. Copying prod → dev/stage MUST
227
288
  go through a masking profile (`--profile`; classify columns `.sensitive()` /
@@ -35,7 +35,7 @@
35
35
  "group": null,
36
36
  "description": "The voltro CLI — every command, grouped by purpose, with the flags that actually matter.",
37
37
  "path": "agent-docs/cli.md",
38
- "files": 8
38
+ "files": 9
39
39
  },
40
40
  {
41
41
  "id": "configuration",
@@ -170,7 +170,7 @@
170
170
  "group": null,
171
171
  "description": "Voltro's i18n layer (@voltro/i18n) — an opinionated wrap over react-intl, auto-wired from a single app.config.ts field, with cookie + Accept-Language locale resolution.",
172
172
  "path": "agent-docs/internationalization.md",
173
- "files": 3
173
+ "files": 4
174
174
  },
175
175
  {
176
176
  "id": "introduction",
@@ -242,7 +242,7 @@
242
242
  "group": null,
243
243
  "description": "Project the typed descriptor graph into UI — forms, tables, pickers, and reactive components, all bound to a descriptor with near-zero glue.",
244
244
  "path": "agent-docs/schema-driven-ui.md",
245
- "files": 13
245
+ "files": 23
246
246
  },
247
247
  {
248
248
  "id": "security",
@@ -305,7 +305,7 @@
305
305
  "group": null,
306
306
  "description": "Voltro's test story — the @voltro/testing package, the test pyramid (unit handlers/tools → workflow runner → e2e), and the voltro test / voltro e2e CLI commands.",
307
307
  "path": "agent-docs/testing.md",
308
- "files": 4
308
+ "files": 5
309
309
  },
310
310
  {
311
311
  "id": "workflows",
@@ -1019,6 +1019,34 @@ It returns a `Stream<SeqEvent>` — each element is `{ seq, event }`. The descri
1019
1019
 
1020
1020
  On the client, `useResumableAgentStream('app', 'support.run')` does the rest: it unwraps each `SeqEvent` (so `.events` are the plain inner events), tracks the highest `seq`, and on a transport drop BEFORE the run's terminal event it auto-reconnects with `fromSeq` = the last seq it rendered (exponential backoff; the no-progress cap resets whenever a reconnect delivers a new event, so a long flaky stream survives any number of well-spaced drops). The server replays past the cursor, then continues — one seamless stream.
1021
1021
 
1022
+ The hook's own surface is small:
1023
+
1024
+ ```tsx
1025
+ import { useResumableAgentStream } from '@voltro/client'
1026
+ import type { AgentEvent } from '@voltro/ai/events'
1027
+
1028
+ const run = useResumableAgentStream<AgentEvent>('app', 'support.run')
1029
+
1030
+ // `input` MUST carry the resume key — `fromSeq` is injected by the hook.
1031
+ run.start({ streamId, message: prompt })
1032
+ run.cancel()
1033
+ ```
1034
+
1035
+ | Field | Meaning |
1036
+ |---|---|
1037
+ | `events` | The unwrapped inner events so far, in order and deduped by `seq`. |
1038
+ | `status` | `'idle'`, `'streaming'`, `'reconnecting'`, `'done'`, or `'error'`. |
1039
+ | `reconnects` | How many times the transport dropped and auto-reconnected this run. |
1040
+ | `error` | Set when `status === 'error'`. |
1041
+ | `start(input?)` | Begins a run, clearing previous events. |
1042
+ | `cancel()` | Stops the run and any pending reconnect. |
1043
+
1044
+ A third options argument tunes the reconnect policy: `maxReconnects` (default
1045
+ `6`), `backoffMs` (`400`), `maxBackoffMs` (`8000`), and `isTerminal` — which
1046
+ defaults to treating an `AgentEvent`-shaped `{ _tag: 'done' | 'error' }` as the
1047
+ end of the run. Override `isTerminal` when your element type signals completion
1048
+ some other way, or the hook will keep trying to resume a finished stream.
1049
+
1022
1050
  For multi-node deployments use `dataStoreResumableStreamStore(ctx.store)` — it persists to the framework's own database (`streamEventsTable` + `streamStateTable`, register them in your `database/index.ts`) and elects exactly ONE producer per `streamId` via an atomic claim, so only one node runs the model while every node's consumers tail the shared log. Sweep finished streams with `gcResumableStreams(store, { olderThan })`.
1023
1051
 
1024
1052
  For the fastest path, `redisResumableStreamStore(redis, { ttlSeconds })` backs the log with a Redis LIST (`RPUSH`/`LRANGE`) plus a `SET … NX` producer claim — TTL evicts finished/abandoned streams without a sweep. `@voltro/ai` takes no Redis dependency; you inject a tiny `ResumableRedis` client (five methods: `setNx` / `rpush` / `lrange` / `set` / `exists`) adapting ioredis / node-redis. All three backends satisfy the same `ResumableStreamStore` interface, so they swap without touching the producer/consumer code.
@@ -403,7 +403,7 @@ if (!canTrigger) throw new Unauthorised({})
403
403
  ```ts
404
404
  import { permission } from '@voltro/plugin-rbac'
405
405
 
406
- yield* permission(ctx, 'admin:full') // Effect<void, Forbidden>
406
+ yield* permission(ctx, 'admin:full') // Effect<void, ScopeError>
407
407
  ```
408
408
 
409
409
  See [the RBAC plugin](/docs/plugins/rbac) for the full model.
@@ -1778,9 +1778,12 @@ the headline that builds on it.)
1778
1778
  import { useResourceCan } from '@voltro/client'
1779
1779
 
1780
1780
  const canEdit = useResourceCan('app', 'todos.can', { action: 'edit', resourceType: 'todo', resourceId: id })
1781
- // canEdit.allowed: boolean | undefined (undefined until resolved); reactive.
1781
+ // canEdit.allowed: boolean (false until the first verdict); canEdit.pending: boolean.
1782
1782
  ```
1783
1783
 
1784
+ Full API — including the batch `useResourceCans` for per-row gating — in
1785
+ [usePermissions](/docs/ui/client-utilities/use-permissions).
1786
+
1784
1787
  ## Capability map
1785
1788
 
1786
1789
  `rebacPolicyGraph()` returns every resource type, its actions, the relations each
@@ -25,6 +25,7 @@ The dispatcher routes `voltro <command> [args]` to the matching subcommand and p
25
25
  | [Build & run](/docs/cli/build-and-start) | `build`, `start`, `serve` |
26
26
  | Deploy | `deploy` (`plan` — auto-detect the target tier per app + function), [`serverless`](/docs/deployment/serverless-functions) (`list` / `dev` / `serve` / `build` / `deploy`), [`static`](/docs/deployment/static-sites) (`hosts` / `deploy`) |
27
27
  | [Database](/docs/cli/migrate) | `migrate`, `db` (`plan` / `apply` / `plans` / `drift` / `squash` / `restore-snapshot` / `migrate` / `rollback` / `status` / `seed`) |
28
+ | [Update](/docs/cli/update) | `update` (`--to` / `--dry-run` / `--force` / `--exact`) — bump every `@voltro/*`, install, run the codemods that adapt your source to the new version |
28
29
  | [Data transfer](/docs/cli/data) | `data` (`export` / `import` / `unpack` / `inspect` / `backup` / `restore`) — directory + single-file `.vbundle` bundles, streaming assets, masking, at-rest encryption |
29
30
  | Ops / infra | `cache` (`status` / `flush` / `invalidate`), `add` (`redis`), `baseline` (`list` / `status` / `set`), `schedule-manifest`, [`storage`](/docs/plugins/storage) (`doctor` / `cors`) |
30
31
  | AI / data | `embeddings backfill <table> --text <field> --vector <field>` — (re)embed rows the `vectorEmbedding()` mixin missed (pre-existing rows / a model change); `--dry-run` to preview |
@@ -149,6 +150,7 @@ The HTTP surface is reachable directly too — e.g. `curl -s localhost:4000/_vol
149
150
  - [Dev](/docs/cli/dev) — what happens during `voltro dev`
150
151
  - [Build & start](/docs/cli/build-and-start) — production paths
151
152
  - [Migrate](/docs/cli/migrate) — schema changes end-to-end
153
+ - [Update](/docs/cli/update) — upgrade the framework + run codemods
152
154
  - [Inspect & test](/docs/cli/inspect) — debugging + harness
153
155
 
154
156
 
@@ -720,6 +722,23 @@ For WebSocket connections to land on the same backend (required for in-process s
720
722
  - Reverse proxy: `lb_policy ip_hash` (Caddy) / `ip_hash` (nginx).
721
723
  - Or use `@voltro/plugin-cluster` to share subscription state across instances → any-load-balancer-works.
722
724
 
725
+ ## `voltro doctor` — preflight a production serve
726
+
727
+ Production `voltro serve` for an **API** app boots ONLY from the precompiled serve
728
+ bundle and is **fatal if it's missing** — a hand-rolled Dockerfile that runs
729
+ `voltro serve` without a prior `voltro build` breaks at deploy. `voltro doctor`
730
+ (or `voltro serve --preflight`) catches that at BUILD time instead of cold-start:
731
+
732
+ ```bash
733
+ voltro doctor . # check the serve bundle exists; print the fix if not
734
+ voltro serve --preflight . # same check, then exit — never boots
735
+ ```
736
+
737
+ It exits **1** when the serve bundle is missing on an API app (so it fails a CI /
738
+ Docker step) and prints the exact remedy: add a `voltro build .` step before
739
+ `voltro serve .`. Drop it into your image build right after `voltro build` to
740
+ guarantee the artefact is present before the image ships.
741
+
723
742
  ## Anti-patterns
724
743
 
725
744
  - **Running `voltro start` against a directory without `dist/`.** It exits 1 with a clear `no built dist found — run voltro build first` (checked against `.framework/dist/index.html` before any heavy work). Run `voltro build` first.
@@ -1648,3 +1667,82 @@ The manifest is read through a TTL-cached source (~10 seconds): a procedure you
1648
1667
  ## Protocol scope
1649
1668
 
1650
1669
  MCP over JSON-RPC 2.0. `initialize` negotiates the protocol revision (`2025-06-18`, `2025-03-26`, `2024-11-05`) and advertises the `tools`, `resources`, and `prompts` capabilities; methods are `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`. The stdio bin frames this as newline-delimited JSON-RPC; the HTTP bin serves it over Streamable HTTP. Both transports route to the same pure protocol core (`handleMcpRequest`, `callTool`, `listResources`/`readResource`, `listPrompts`/`getPrompt`, `routeHttp`), all exported from `@voltro/mcp`.
1670
+
1671
+
1672
+
1673
+ ---
1674
+
1675
+ <!-- source: en/cli/update.md -->
1676
+ ## Update
1677
+
1678
+ _voltro update — bump the framework to the latest version and run the codemods that adapt your source to any changed APIs._
1679
+
1680
+ `voltro update` upgrades an app to the latest framework release. It does three things in order:
1681
+
1682
+ 1. **Bump** every `@voltro/*` dependency in `package.json` to the target version.
1683
+ 2. **Install** with your package manager (detected from the lockfile — pnpm / npm / yarn / bun).
1684
+ 3. **Run the codemods** shipped with the target version — automatic source rewrites for any breaking API change, plus printed manual steps for anything that can't be automated.
1685
+
1686
+ ```bash
1687
+ voltro update # bump to the latest published version, install, run codemods
1688
+ voltro update --to 0.4.0 # pin an explicit target version
1689
+ voltro update --dry-run # preview the bump + which codemods would run — writes nothing
1690
+ voltro update --force # allow a dirty working tree (not recommended)
1691
+ voltro update --exact # pin exact versions (drop the ^ / ~ range prefix)
1692
+
1693
+ # Recover the codemods after a MANUAL version bump (no bump, no install):
1694
+ voltro update --codemods-only --from 0.3.0 # re-run codemods 0.3.0 → installed
1695
+ voltro update --codemods-only --from 0.3.0 --to 0.4.0 # explicit delta
1696
+ ```
1697
+
1698
+ ## Already bumped by hand? Recover the codemods
1699
+
1700
+ If you bump `@voltro/*` versions in `package.json` yourself and install first, a
1701
+ plain `voltro update` sees the installed version already equals the target and
1702
+ reports **"already on X — nothing to do"** — skipping the codemods AND the
1703
+ printed manual steps for the delta you actually crossed. To re-apply them without
1704
+ touching `package.json` again:
1705
+
1706
+ ```bash
1707
+ voltro update --codemods-only --from <version-you-came-from>
1708
+ ```
1709
+
1710
+ `--codemods-only` (alias `--run-codemods`) runs the codemods + manual notes for
1711
+ `[from, to]` against the already-installed tree — no version bump, no install.
1712
+ `--to` defaults to the installed version; pass it to pin an explicit delta.
1713
+ `--from` also works on a normal `voltro update` to override the auto-detected
1714
+ source version.
1715
+
1716
+ ## The clean-tree guard
1717
+
1718
+ Codemods **rewrite your source**, so you need a clean diff to review afterwards. `voltro update` refuses to run on a dirty git working tree — commit or stash first. Use `--dry-run` to preview without touching anything, or `--force` to override the guard (you accept a mixed diff).
1719
+
1720
+ ## What gets bumped
1721
+
1722
+ Every `@voltro/*` entry in `dependencies` and `devDependencies`, with the range style preserved (`^0.3.0` stays caret, `~0.3.0` stays tilde) unless you pass `--exact`. Non-registry specs (`workspace:*`, `catalog:`, `link:`, …) are left untouched — they're already resolved by your monorepo or catalog.
1723
+
1724
+ ## Codemods
1725
+
1726
+ Each breaking public-API change in a release ships a **codemod**. When you update across that release, `voltro update` applies it:
1727
+
1728
+ - A **transform codemod** rewrites your source automatically — renamed imports, moved modules, changed component props, restructured call signatures. The rewrite is scoped to files that actually import the affected symbol.
1729
+ - A **manual codemod** prints written steps during the update, only when your app is affected — for changes that can't be mechanically transformed (a behavior change, a descriptor/executor restructure). Where the affected sites can be found but the fix needs your judgment, a codemod inserts `// TODO(voltro-migration): …` markers so you can locate every spot.
1730
+
1731
+ Codemods that span multiple versions run in order (e.g. upgrading `0.2.0 → 0.4.0` runs the `0.3.0` and `0.4.0` codemods in sequence). Review the resulting diff before committing.
1732
+
1733
+ ## The database is separate
1734
+
1735
+ `voltro update` does **not** touch your database. Framework-owned `_voltro_*` tables (workflow runs, schedules, …) are reconciled by the declarative differ, not by codemods: when a release changes one of those tables, your next `voltro db apply` (or `voltro dev` boot, which auto-applies) picks up the change. After an update:
1736
+
1737
+ ```bash
1738
+ voltro update
1739
+ voltro db apply # reconcile any changed framework tables — NOT voltro db migrate
1740
+ # then run your typecheck to confirm your code compiles against the new API
1741
+ ```
1742
+
1743
+ Use `voltro db apply` (the declarative diff), not `voltro db migrate` (the imperative file-runner) — only the former reconciles framework tables.
1744
+
1745
+ ## Where to read next
1746
+
1747
+ - [Migrate](/docs/cli/migrate) — schema changes end-to-end
1748
+ - [Build & start](/docs/cli/build-and-start) — production paths