@voltro/cli 0.16.0 → 0.17.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 (135) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/{apiBuild-BJtiqoOu.js → apiBuild-ChdlLqGv.js} +2 -2
  3. package/dist/apiBuild-D5WB119b.js +2 -0
  4. package/dist/bin.js +3 -3
  5. package/dist/{commands-8A5RU3MW.js → commands-DYQbv-DG.js} +2328 -2292
  6. package/dist/dbCommand-C3R5LrBZ.js +2 -0
  7. package/dist/{dbCommand-OPMtO0sX.js → dbCommand-DJSxnjPt.js} +2 -2
  8. package/dist/{dev-HdG7JdSk.js → dev-BCmoJfBm.js} +1 -1
  9. package/dist/{dev-DGjEvvkV.js → dev-DB7pbLob.js} +89 -89
  10. package/dist/{fileConventions-Cu0tM-Bl.js → fileConventions-CBNPZVOV.js} +1 -1
  11. package/dist/frameworkTableAssembly-DRJrAVr1.js +2 -0
  12. package/dist/{frameworkTableAssembly-BA2bAmYN.js → frameworkTableAssembly-sg41s14F.js} +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/{inspectMetrics-CYDSO3za.js → inspectMetrics-DWh56Qas.js} +205 -205
  15. package/dist/{serveCommand-C3lq3mFd.js → serveCommand-DO6qr5Ok.js} +5 -5
  16. package/dist/serveEntry.js +2 -2
  17. package/dist/{start-BtgatJN3.js → start-ksY0wMZG.js} +379 -379
  18. package/dist/startEntry.js +2 -2
  19. package/package.json +17 -17
  20. package/templates/AGENTS.core.md +24 -9
  21. package/templates/AGENTS.md +25 -10
  22. package/templates/agent-docs/_index.md +1 -1
  23. package/templates/agent-docs/cli.md +2 -2
  24. package/templates/agent-docs/database/migrations.md +3 -3
  25. package/templates/agent-docs/internationalization.md +3 -3
  26. package/templates/agent-docs/introduction.md +2 -2
  27. package/templates/agent-docs/reference.md +4 -4
  28. package/templates/agent-docs/routing.md +40 -40
  29. package/templates/agent-docs/templates/appshells.md +24 -24
  30. package/templates/agent-docs/whats-new.md +31 -95
  31. package/templates/apps/api-ai/package.json +7 -7
  32. package/templates/apps/api-auth/package.json +8 -8
  33. package/templates/apps/api-backend/package.json +7 -7
  34. package/templates/apps/api-backend-deactivation/package.json +7 -7
  35. package/templates/apps/api-backend-mail/package.json +8 -8
  36. package/templates/apps/api-backend-mariadb/package.json +9 -9
  37. package/templates/apps/api-backend-storage/package.json +8 -8
  38. package/templates/apps/api-data-advanced/package.json +8 -8
  39. package/templates/apps/api-durable/package.json +8 -8
  40. package/templates/apps/api-feature-flags/package.json +9 -9
  41. package/templates/apps/api-governance/package.json +8 -8
  42. package/templates/apps/api-kv/package.json +8 -8
  43. package/templates/apps/api-moderation/package.json +8 -8
  44. package/templates/apps/api-observability/package.json +8 -8
  45. package/templates/apps/api-ratelimit/package.json +8 -8
  46. package/templates/apps/api-rbac/package.json +8 -8
  47. package/templates/apps/api-rest/package.json +7 -7
  48. package/templates/apps/api-saas/package.json +11 -11
  49. package/templates/apps/api-search/package.json +8 -8
  50. package/templates/apps/api-versioning/package.json +8 -8
  51. package/templates/apps/api-webhooks/package.json +9 -9
  52. package/templates/apps/changelog/package.json +6 -6
  53. package/templates/apps/changelog/src/pages/[locale]/{[slug].page.tsx → [slug]/page.tsx} +4 -4
  54. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +2 -2
  55. package/templates/apps/changelog/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  56. package/templates/apps/changelog/src/pages/{[slug].page.test.tsx → [slug]/page.test.tsx} +1 -1
  57. package/templates/apps/changelog/src/pages/{[slug].page.tsx → [slug]/page.tsx} +2 -2
  58. package/templates/apps/changelog/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  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-admin/src/pages/(marketing)/{login.page.test.tsx → login/page.test.tsx} +2 -2
  62. package/templates/apps/frontend-admin/src/pages/(marketing)/{login.page.tsx → login/page.tsx} +2 -2
  63. package/templates/apps/frontend-admin/src/pages/(marketing)/{index.page.test.tsx → page.test.tsx} +1 -1
  64. package/templates/apps/frontend-admin/src/pages/admin/{[entity].page.tsx → [entity]/page.tsx} +1 -1
  65. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +1 -1
  66. package/templates/apps/frontend-admin/src/pages/admin/{index.page.test.tsx → page.test.tsx} +1 -1
  67. package/templates/apps/frontend-app/package.json +8 -8
  68. package/templates/apps/frontend-app/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  69. package/templates/apps/frontend-app/src/pages/{schema-ui.page.test.tsx → schema-ui/page.test.tsx} +2 -2
  70. package/templates/apps/frontend-app/src/pages/{schema-ui.page.tsx → schema-ui/page.tsx} +1 -1
  71. package/templates/apps/frontend-blank/package.json +7 -7
  72. package/templates/apps/frontend-blank/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  73. package/templates/apps/frontend-contact/package.json +7 -7
  74. package/templates/apps/frontend-contact/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  75. package/templates/apps/frontend-contact/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  76. package/templates/apps/frontend-dashboard/package.json +7 -7
  77. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.page.test.tsx → login/page.test.tsx} +2 -2
  78. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.page.tsx → login/page.tsx} +2 -2
  79. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.page.test.tsx → page.test.tsx} +1 -1
  80. package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.page.test.tsx → page.test.tsx} +1 -1
  81. package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.page.test.tsx → settings/page.test.tsx} +2 -2
  82. package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.page.tsx → settings/page.tsx} +2 -2
  83. package/templates/apps/frontend-docs/package.json +7 -7
  84. package/templates/apps/frontend-docs/src/pages/[locale]/docs/{[...slug].page.tsx → [...slug]/page.tsx} +4 -4
  85. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +2 -2
  86. package/templates/apps/frontend-docs/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  87. package/templates/apps/frontend-docs/src/pages/docs/{[...slug].page.test.tsx → [...slug]/page.test.tsx} +3 -3
  88. package/templates/apps/frontend-docs/src/pages/docs/{[...slug].page.tsx → [...slug]/page.tsx} +1 -1
  89. package/templates/apps/frontend-docs/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  90. package/templates/apps/frontend-i18n/package.json +6 -6
  91. package/templates/apps/frontend-i18n/src/pages/[locale]/{about.page.tsx → about/page.tsx} +3 -3
  92. package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +2 -2
  93. package/templates/apps/frontend-i18n/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  94. package/templates/apps/frontend-i18n/src/pages/{about.page.test.tsx → about/page.test.tsx} +3 -3
  95. package/templates/apps/frontend-i18n/src/pages/{about.page.tsx → about/page.tsx} +1 -1
  96. package/templates/apps/frontend-i18n/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  97. package/templates/apps/frontend-landing/package.json +7 -7
  98. package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +1 -1
  99. package/templates/apps/frontend-landing/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  100. package/templates/apps/frontend-landing/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  101. package/templates/apps/frontend-spa/package.json +7 -7
  102. package/templates/apps/frontend-spa/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  103. package/templates/apps/frontend-ssr/package.json +7 -7
  104. package/templates/apps/frontend-ssr/src/pages/{feed.page.test.tsx → feed/page.test.tsx} +2 -2
  105. package/templates/apps/frontend-ssr/src/pages/{feed.page.tsx → feed/page.tsx} +1 -1
  106. package/templates/apps/frontend-ssr/src/pages/{feed-swr.page.test.tsx → feed-swr/page.test.tsx} +2 -2
  107. package/templates/apps/frontend-ssr/src/pages/{feed-swr.page.tsx → feed-swr/page.tsx} +1 -1
  108. package/templates/apps/frontend-ssr/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  109. package/templates/apps/frontend-ssr-api/package.json +8 -8
  110. package/templates/apps/frontend-ssr-api/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  111. package/templates/apps/frontend-static-blog/package.json +6 -6
  112. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/{[slug].page.tsx → [slug]/page.tsx} +4 -4
  113. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +2 -2
  114. package/templates/apps/frontend-static-blog/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  115. package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].page.test.tsx → [slug]/page.test.tsx} +3 -3
  116. package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].page.tsx → [slug]/page.tsx} +3 -3
  117. package/templates/apps/frontend-static-blog/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  118. package/dist/apiBuild-BOczx9-m.js +0 -2
  119. package/dist/dbCommand-DNQ6l-su.js +0 -2
  120. package/dist/frameworkTableAssembly-D1zJSgOU.js +0 -2
  121. /package/templates/apps/changelog/src/pages/{index.page.tsx → page.tsx} +0 -0
  122. /package/templates/apps/frontend-admin/src/pages/(marketing)/{index.page.tsx → page.tsx} +0 -0
  123. /package/templates/apps/frontend-admin/src/pages/admin/{index.page.tsx → page.tsx} +0 -0
  124. /package/templates/apps/frontend-app/src/pages/{index.page.tsx → page.tsx} +0 -0
  125. /package/templates/apps/frontend-blank/src/pages/{index.page.tsx → page.tsx} +0 -0
  126. /package/templates/apps/frontend-contact/src/pages/{index.page.tsx → page.tsx} +0 -0
  127. /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.page.tsx → page.tsx} +0 -0
  128. /package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.page.tsx → page.tsx} +0 -0
  129. /package/templates/apps/frontend-docs/src/pages/{index.page.tsx → page.tsx} +0 -0
  130. /package/templates/apps/frontend-i18n/src/pages/{index.page.tsx → page.tsx} +0 -0
  131. /package/templates/apps/frontend-landing/src/pages/{index.page.tsx → page.tsx} +0 -0
  132. /package/templates/apps/frontend-spa/src/pages/{index.page.tsx → page.tsx} +0 -0
  133. /package/templates/apps/frontend-ssr/src/pages/{index.page.tsx → page.tsx} +0 -0
  134. /package/templates/apps/frontend-ssr-api/src/pages/{index.page.tsx → page.tsx} +0 -0
  135. /package/templates/apps/frontend-static-blog/src/pages/{index.page.tsx → page.tsx} +0 -0
@@ -39,7 +39,7 @@ apps/acme/web/ # dir named by the app, not the template
39
39
 
40
40
  ## The page
41
41
 
42
- `src/pages/index.page.tsx` is plain JSX with the two static-render exports:
42
+ `src/pages/page.tsx` is plain JSX with the two static-render exports:
43
43
 
44
44
  ```tsx
45
45
  export const renderMode = 'static' as const // pre-render at build time
@@ -59,7 +59,7 @@ export default function Index() {
59
59
  </section>
60
60
  <section>
61
61
  <h2>Ready to start?</h2>
62
- <p>Edit <code>src/pages/index.page.tsx</code> to make it yours.</p>
62
+ <p>Edit <code>src/pages/page.tsx</code> to make it yours.</p>
63
63
  </section>
64
64
  </main>
65
65
  )
@@ -80,7 +80,7 @@ export default island(SignupForm, { name: 'SignupForm', hydrate: 'visible' })
80
80
  ```
81
81
 
82
82
  ```tsx
83
- // src/pages/index.page.tsx
83
+ // src/pages/page.tsx
84
84
  export const interactive = 'islands' as const // was 'none'
85
85
  import SignupForm from '../components/SignupForm.island'
86
86
  // … render <SignupForm /> somewhere in the page …
@@ -185,7 +185,7 @@ If your final product is going to be a:
185
185
 
186
186
  _A documentation site with a catch-all docs router and build-time static pre-rendering of every page via getStaticPaths._
187
187
 
188
- A documentation-site starter. `src/pages/docs/[...slug].page.tsx` is a catch-all router; `getStaticPaths` enumerates every doc URL at build time so `voltro build` pre-renders them all into a static site you can deploy anywhere. Template id: **`frontend-docs`**.
188
+ A documentation-site starter. `src/pages/docs/[...slug]/page.tsx` is a catch-all router; `getStaticPaths` enumerates every doc URL at build time so `voltro build` pre-renders them all into a static site you can deploy anywhere. Template id: **`frontend-docs`**.
189
189
 
190
190
  The starter ships a tiny in-file `DOCS` array as stub content. Replace it with a real loader (a markdown reader, a CMS query, a database fetch) — the page component's contract stays the same.
191
191
 
@@ -215,7 +215,7 @@ apps/acme/docs/ # dir named by the app, not the template
215
215
 
216
216
  ## The catch-all renderer
217
217
 
218
- `src/pages/docs/[...slug].page.tsx` is the whole engine. It declares:
218
+ `src/pages/docs/[...slug]/page.tsx` is the whole engine. It declares:
219
219
 
220
220
  ```tsx
221
221
  export const renderMode = 'static' as const // pre-render at build
@@ -418,7 +418,7 @@ No rpc client, no loader, no `apis:` entry, no `@voltro/ui-shadcn` dependency. T
418
418
  The page exports `renderMode = 'spa'`, which tells `voltro build` not to pre-render it: the client renders it on load. Use this when the whole page is interactive AND its state lives in the browser — an SSR'd first paint would just be discarded on hydration, so rendering it on the server buys nothing.
419
419
 
420
420
  ```tsx
421
- // src/pages/index.page.tsx — a pure client-side SPA (a bill splitter)
421
+ // src/pages/page.tsx — a pure client-side SPA (a bill splitter)
422
422
  import type { ReactNode } from 'react'
423
423
  import { useEffect, useState } from 'react'
424
424
 
@@ -603,7 +603,7 @@ The full reference for these modes lives in the [render modes guide](/docs/routi
603
603
  `index.tsx` exports `renderMode = 'ssr'`. Its loader runs SERVER-SIDE on EVERY request under `voltro start` — the timestamp and nonce change on each refresh, and the loader can read the incoming request to personalise the HTML before it's sent. There is no caching: the response carries `x-voltro-rendered-by: ssr`.
604
604
 
605
605
  ```tsx
606
- // src/pages/index.page.tsx
606
+ // src/pages/page.tsx
607
607
  import type { ReactNode } from 'react'
608
608
  import { useLoaderData, useServerRequest, type LoaderFn, type PageMeta } from '@voltro/web'
609
609
 
@@ -646,7 +646,7 @@ Use SSR for anything that varies per request: a logged-in dashboard, a page that
646
646
  `feed.tsx` takes the SAME render path as SSR, but `voltro start` CACHES the HTML and serves the cached copy to every request within the `revalidate` window. After the window expires the next request re-renders and replaces the cache — cache hits return in well under a millisecond. `tenantAware: true` folds the `x-tenant` request header into the cache key, so tenant A's render is never served to tenant B:
647
647
 
648
648
  ```tsx
649
- // src/pages/feed.page.tsx
649
+ // src/pages/feed/page.tsx
650
650
  import type { ReactNode } from 'react'
651
651
  import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
652
652
 
@@ -685,7 +685,7 @@ The loader's `headers` is populated server-side (lowercased keys) — that's how
685
685
  `feed-swr.tsx` is the same ISR page plus a `staleWhileRevalidate` window. When `revalidate` expires, instead of blocking the next visitor on a fresh render, the cache serves the STALE HTML immediately AND kicks off a background re-render. The visitor never waits; the cache catches up out-of-band. Beyond `revalidate + staleWhileRevalidate` the next request blocks on a fresh render, like a cold MISS:
686
686
 
687
687
  ```tsx
688
- // src/pages/feed-swr.page.tsx
688
+ // src/pages/feed-swr/page.tsx
689
689
  import type { ReactNode } from 'react'
690
690
  import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
691
691
 
@@ -878,10 +878,10 @@ export const posts: ReadonlyArray<Post> = [
878
878
 
879
879
  ## SSG dynamic routes — `getStaticPaths` + `loader` + per-post `meta`
880
880
 
881
- A dynamic route like `src/pages/blog/[slug].page.tsx` matches infinitely many URLs. To pre-render it statically the build needs to know WHICH slugs exist — that is what `getStaticPaths` answers, by mapping over the content source. For each enumerated `params` the build runs the `loader` (server-side, at build time) and renders one HTML file. The `meta` export is a FUNCTION of the loader data, so each post gets its own correct `<title>` / `<description>` baked into the HTML — exactly what you want for SEO and social cards.
881
+ A dynamic route like `src/pages/blog/[slug]/page.tsx` matches infinitely many URLs. To pre-render it statically the build needs to know WHICH slugs exist — that is what `getStaticPaths` answers, by mapping over the content source. For each enumerated `params` the build runs the `loader` (server-side, at build time) and renders one HTML file. The `meta` export is a FUNCTION of the loader data, so each post gets its own correct `<title>` / `<description>` baked into the HTML — exactly what you want for SEO and social cards.
882
882
 
883
883
  ```tsx
884
- // src/pages/blog/[slug].page.tsx
884
+ // src/pages/blog/[slug]/page.tsx
885
885
  import type { ReactNode } from 'react'
886
886
  import { useLoaderData, notFound, type LoaderFn, type PageMeta } from '@voltro/web'
887
887
  import { posts, type Post } from '../../content/posts'
@@ -972,7 +972,7 @@ export default island(ReadingProgress, { name: 'ReadingProgress', hydrate: 'load
972
972
  The index at `/` is pure content: a list of posts with no interactivity. It declares `interactive: 'none'`, which makes the framework strip EVERY `<script>` it would otherwise emit — the HTML ships with no JS bundle at all. It reads the same content source the `[slug]` page + `getStaticPaths` use:
973
973
 
974
974
  ```tsx
975
- // src/pages/index.page.tsx
975
+ // src/pages/page.tsx
976
976
  import type { ReactNode } from 'react'
977
977
  import type { PageMeta } from '@voltro/web'
978
978
  import { posts } from '../content/posts'
@@ -1106,10 +1106,10 @@ The serverless function lives next to the web app but is a separate deploy artif
1106
1106
 
1107
1107
  ## The static page + the island form
1108
1108
 
1109
- `src/pages/index.page.tsx` is `renderMode: 'static'` (pre-rendered, servable from a CDN) and `interactive: 'islands'` — so only the `<ContactForm>` island hydrates; the headline + copy ship as inert HTML with no React lifecycle:
1109
+ `src/pages/page.tsx` is `renderMode: 'static'` (pre-rendered, servable from a CDN) and `interactive: 'islands'` — so only the `<ContactForm>` island hydrates; the headline + copy ship as inert HTML with no React lifecycle:
1110
1110
 
1111
1111
  ```tsx
1112
- // src/pages/index.page.tsx
1112
+ // src/pages/page.tsx
1113
1113
  import type { ReactNode } from 'react'
1114
1114
  import type { PageMeta } from '@voltro/web'
1115
1115
  import ContactForm from '../components/ContactForm.island'
@@ -1358,7 +1358,7 @@ apps/demo/web/
1358
1358
  ├── README.md
1359
1359
  └── src/pages/
1360
1360
  ├── layout.tsx # root shell (header + <main>)
1361
- └── index.page.tsx # the reactive page — useSubscription + useMutation
1361
+ └── page.tsx # the reactive page — useSubscription + useMutation
1362
1362
  ```
1363
1363
 
1364
1364
  ## The `apis` wiring
@@ -1390,7 +1390,7 @@ The `package` resolves the api workspace package; its port is auto-discovered fr
1390
1390
  ## The reactive loop
1391
1391
 
1392
1392
  ```tsx
1393
- // src/pages/index.page.tsx
1393
+ // src/pages/page.tsx
1394
1394
  import { useMutation, useSubscription } from '@voltro/client'
1395
1395
 
1396
1396
  const TENANT = 'acme'
@@ -1418,7 +1418,7 @@ The subscription builder also exposes `revision`, `emittedAt`, `error`, and `pen
1418
1418
 
1419
1419
  ## Pairing with a different api
1420
1420
 
1421
- This page is wired to api-backend's `notes` domain. To point it at your own api, change `apis.app.package` in `app.config.ts`, then swap the rpc tags + the row type in `src/pages/index.page.tsx` to match your descriptors. The hook shapes are identical for any api.
1421
+ This page is wired to api-backend's `notes` domain. To point it at your own api, change `apis.app.package` in `app.config.ts`, then swap the rpc tags + the row type in `src/pages/page.tsx` to match your descriptors. The hook shapes are identical for any api.
1422
1422
 
1423
1423
  ## When to use
1424
1424
 
@@ -1533,7 +1533,7 @@ export const withLocalePrefix = (path: string, locale: string): string =>
1533
1533
  Each page has a one-line **mirror** under `src/pages/[locale]/`. The mirror re-exports the bare page verbatim (the component reads its locale from the URL via the layout's provider) and uses `getStaticPaths` to enumerate the *non-default* locales — which is what tells `voltro build` to emit the `/de/...` HTML. Without the mirror, only the default-locale URL is built.
1534
1534
 
1535
1535
  ```tsx
1536
- // src/pages/[locale]/index.page.tsx — the entire file
1536
+ // src/pages/[locale]/page.tsx — the entire file
1537
1537
  import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../lib/locale'
1538
1538
 
1539
1539
  export { default } from '../index'
@@ -1550,7 +1550,7 @@ export const getStaticPaths = async (): Promise<Array<{ params: { locale: string
1550
1550
  The body of each page is localised automatically (the SSG pipeline wraps every variant in the right catalog's provider). The document head is NOT — unless you export `meta` as a **function of `{ locale }`**. The framework drives `locale` from `params.locale` on the `[locale]` route (and `defaultLocale` on the bare path), so each variant bakes the right `<title>` / `<meta description>` into its HTML — exactly what you want for SEO and social cards. A plain `meta` object would leave the head English on `/de`.
1551
1551
 
1552
1552
  ```tsx
1553
- // src/pages/index.page.tsx
1553
+ // src/pages/page.tsx
1554
1554
  import { T, useT, useLocale } from '@voltro/i18n'
1555
1555
  import { getCatalog } from '../lib/locale'
1556
1556
 
@@ -1662,7 +1662,7 @@ The `[locale]` mirrors pick it up automatically — `getStaticPaths` reads `SUPP
1662
1662
 
1663
1663
  ## Anti-patterns
1664
1664
 
1665
- - **Forgetting the `[locale]` mirror for a new page.** Create `src/pages/foo.page.tsx` AND a one-line mirror `src/pages/[locale]/foo.page.tsx`. Without the mirror, only the default-locale URL is emitted — `/de/foo` 404s.
1665
+ - **Forgetting the `[locale]` mirror for a new page.** Create `src/pages/foo/page.tsx` AND a one-line mirror `src/pages/[locale]/foo/page.tsx`. Without the mirror, only the default-locale URL is emitted — `/de/foo` 404s.
1666
1666
  - **Exporting `meta` as a plain object.** The body localises, but the `<title>` / `<meta description>` stay English on `/de`. Export `meta` as a function of `{ locale }` and read the catalog via `getCatalog(locale)`.
1667
1667
  - **Skipping `defineLocale<typeof en>()` for a non-base catalog.** Plain `defineCatalog(...)` compiles but loses the parity check — the one feature that stops translations drifting. Always run non-base locales through `defineLocale<typeof en>()`.
1668
1668
  - **Angle brackets in catalog strings.** react-intl's ICU parser treats `<tag>` as a rich-text element and fails (`FORMAT_ERROR`) if there's no matching close tag. Write "page titles", not "`<title>` tags", in a message.
@@ -1759,7 +1759,7 @@ export default function DashboardLayout({ children }) {
1759
1759
  The gate must run **per request** — it reads the request's cookie and may redirect. A `static` page is pre-rendered once at build time with no per-visitor render, so it can't redirect per-visitor; in fact `voltro build` **rejects** a `RedirectError` thrown during static pre-render. So the authed pages declare `renderMode: 'ssr'`:
1760
1760
 
1761
1761
  ```tsx
1762
- // src/pages/dashboard/index.page.tsx
1762
+ // src/pages/dashboard/page.tsx
1763
1763
  export const renderMode = 'ssr' as const
1764
1764
  export const meta = { title: 'Overview' }
1765
1765
  export default function Overview() { /* … */ }
@@ -1884,7 +1884,7 @@ export default {
1884
1884
  `query` is present ONLY server-side (`ssr` / `isr`). It invokes the api's rpc directly over `POST /rpc`, **forwarding the request's session cookie** so the same Subject + tenant resolve as the WebSocket path. A streaming query is drained to its FIRST snapshot — here, the current notes for this tenant. `meta` reads the result, so the `<title>` reflects real data in the server-rendered HTML:
1885
1885
 
1886
1886
  ```tsx
1887
- // src/pages/index.page.tsx
1887
+ // src/pages/page.tsx
1888
1888
  import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
1889
1889
  import { useSubscription } from '@voltro/client'
1890
1890
 
@@ -1994,7 +1994,7 @@ const entities = manifest ? deriveEntityAdmins(manifest) : []
1994
1994
  ## Per-entity CRUD
1995
1995
 
1996
1996
  ```tsx
1997
- // src/pages/admin/[entity].page.tsx (abridged)
1997
+ // src/pages/admin/[entity]/page.tsx (abridged)
1998
1998
  const canCreate = useCan(spec.createScope) // <table>:create
1999
1999
  {spec.createTag && canCreate ? (
2000
2000
  <AutoForm api="app" mutation={spec.createTag} submitLabel={`Add ${spec.table}`} />
@@ -2041,7 +2041,7 @@ apps/acme/web/
2041
2041
 
2042
2042
  ## Make it yours
2043
2043
 
2044
- Everything under `src/pages/admin/` is yours. Swap the cookie gate (`lib/auth.ts`) + demo scopes (`lib/admin.ts`) for real auth; replace the generic `[entity].page.tsx` with a hand-built page for any entity that needs more than CRUD.
2044
+ Everything under `src/pages/admin/` is yours. Swap the cookie gate (`lib/auth.ts`) + demo scopes (`lib/admin.ts`) for real auth; replace the generic `[entity]/page.tsx` with a hand-built page for any entity that needs more than CRUD.
2045
2045
 
2046
2046
  ## When to use frontend-admin vs. the other web templates
2047
2047
 
@@ -1,4 +1,4 @@
1
- # What's new in 0.15.0
1
+ # What's new in 0.16.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,125 +7,61 @@ 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
- ### ⚠ BREAKING
11
-
12
- - **@voltro/cli** — **A rename now carries the ALIASED importers, not only the relative ones.**
13
-
14
- An app's web build stopped compiling after `voltro update`: 84 relative imports were rewritten correctly, 163 aliased ones across 88 files were not, and `tsc` reported 249 errors on names that no longer existed. Nothing in the codemod's output hinted that a whole class of import had been skipped.
15
-
16
- Two independent halves, and each alone leaves the imports stale:
17
-
18
- - The codemod's ts-morph project was built with **no `baseUrl` and no `paths`**, so `@/components/link` resolved to nothing. It now gets the compiler's own shape — the raw `paths`, not the pre-resolved Vite table, because a `paths` target is relative to `baseUrl` by definition and an absolute one does not resolve. - `SourceFile.move()` rewrites relative specifiers and **nothing else**, which is correct on its own terms: ts-morph cannot know whether the alias mapping or the file is meant to change. So the codemods now rewrite the aliased ones themselves, narrowly — only specifiers that RESOLVED to the moved file, and only the trailing stem, which needs no alias table and so cannot disagree with one. The run reports how many it rewrote.
19
-
20
- **A file an exact `paths` entry names is left alone and reported.** Renaming `link.tsx` while `"@/link": ["src/components/link.tsx"]` points at it leaves the mapping resolving to nothing — and the same path is usually repeated in a vite/vitest alias table no codemod owns. One app hit this and then saw a taxonomy violation reported on a file the codemod had itself created.
21
-
22
- **`page/unsuffixed-in-pages` is now an error, not a warning.** The comment justifying the warning contradicted the scanner it described: that bucket is already narrowed to a default export nothing imports, which is what an unmigrated page looks like and what a co-located component never does. The failure it names is invisible everywhere else — an unmigrated route simply 404s, with a clean `tsc` and a green suite. One app finished a migration with 51 of them. `codemod: none`: the rename it asks for already ships as `0.14.0/03_pages-suffix`, and nothing about a user's SOURCE changes here — what changes is that `voltro check` now fails on a route that does not route.
23
- - **@voltro/protocol, @voltro/client, @voltro/web** — **`errorTag` moves from `@voltro/client` to `@voltro/protocol`.**
24
-
25
- It reads the `_tag` that `toRpc` writes, so it now lives beside `toRpc` — one file owning both ends of that contract. Where it used to live had a cost invisible from inside the framework: an app's shared error handler sat in a package that pulled only `@voltro/i18n`, and reading a tag would have meant depending on the entire client package for seven lines. They declined, and kept parsing message strings with a regex — the exact outcome the helper exists to prevent.
26
-
27
- `@voltro/web` re-exports the client surface, so it loses the symbol too — the same codemod covers an app that imported it from there.
28
-
29
- Not re-exported from `@voltro/client`: two import paths for one helper is how the next reader learns the wrong one. The transform codemod repoints the import, preserving an alias (`errorTag as tagOf`) and the type-only form, and merges into an existing `@voltro/protocol` import rather than adding a second.
30
-
31
- While moving it, its doc comment gained the thing that matters at the call site and was only implied before: **`instanceof` does not hold on the client.** What arrives there was decoded from JSON and never constructed, so match on the tag, not on the class. One team read the old wording as a promise that `instanceof` works and was right to say so.
32
-
33
10
  ### Added
34
11
 
35
- - **@voltro/protocol, @voltro/cli** — **An auth strategy reaches the app's DataStore, on `input.store`.**
36
-
37
- ```ts
38
- const sessionStrategy: AuthStrategy = {
39
- id: 'db-session',
40
- resolve: async ({ headers, store }) => {
41
- if (store === undefined) return { kind: 'skip' } // still booting
42
- const [row] = await store.query(sessions.byToken(headers.authorization))
43
- return row ? { kind: 'matched', subject: toSubject(row) } : { kind: 'skip' }
44
- },
45
- }
46
- ```
47
-
48
- Without it, a DB-backed strategy — a session row, an API-key record, a PAT table — had to open a SECOND connection path beside the framework's, to the same database the request store opens a moment later. One adopter's `auth/db.ts` is 105 lines of exactly that: a second `ManagedRuntime` plus a `MysqlClient`, load-bearing for their session lookup and their ApiKeyStore. Every DB-backed OIDC / SAML / PAT integration rebuilds it, which is what made this a framework gap rather than an app's problem.
49
-
50
- It is the **same value** `auth.resolveScopes` already receives, through the **same lazy getter** — one ref, two consumers, rather than each caller reaching for the store its own way. That is deliberate: `voltro dev` builds the store AFTER the auth chain and `voltro serve` builds it BEFORE, so a value captured at config time would be `undefined` forever in dev and correct in production. The getter is read ONCE per request, not once per strategy.
12
+ - **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/database** — **A plugin HTTP route reaches the app's DataStore, on `req.store`** — and `voltro db apply` finally honours `VOLTRO_DESTRUCTIVE_OK`.
51
13
 
52
- `store` is `undefined` only while the store is still being built, and on an app with no store — a strategy should `skip` rather than throw. It is the BOOT store, not a request-scoped one: strategies resolve before a request store exists.
14
+ ### `PluginHttpRouteRequest.store`
53
15
 
54
- **Not narrowed to a read-only surface**, and the reason is worth stating: the narrower type would be the better guarantee, `DataStore` is the driver SPI, and giving strategies a different type from the one `resolveScopes` gets would put two views of one object in the same file. A strategy that writes during subject resolution is a design mistake; the type system is not going to catch it for you. Read users / sessions / keys, do not run domain writes.
16
+ The same seam as `AuthStrategyInput.store`, one layer over, and the same report produced it. An adopter's `auth/db.ts` has five consumers: two are auth strategies and collapsed onto `input.store` exactly as designed; three are plugin HTTP routes and could not, so the second `ManagedRuntime` + `MysqlClient` stayed for them.
55
17
 
56
- ### Fixed
57
-
58
- - **@voltro/cli** — `voltro dev` terminates when its dev server does, instead of living forever.
59
-
60
- The supervisor never watched its child die. `runChild` was an `Effect.acquireUseRelease` whose `use` was `Effect.never`, so `proc.on('exit')` existed ONLY in the release path — and that path runs when the fiber is interrupted (a restart, a signal), never when the child exits by itself. Two more places assumed the same thing: `runSupervisor` was typed `Effect<never>`, and `dev.ts` returned `new Promise(() => {})` after starting it.
61
-
62
- So a boot that aborted — an unreachable database, a refused migration, a failed env gate — left the child dead and the supervisor waiting for a file change that nobody was there to make. Measured, not inferred: one developer machine carried 15 such `voltro dev` process pairs, `ppid=1`, the oldest 7 days old, every one a boot that had failed against a remote database. They hold a watcher and a terminal-less process each; in CI the same shape keeps a runner busy after the job "finished".
63
-
64
- `use` now awaits the child (`awaitChildExit`) and the supervisor races that against the watch loop, so whichever happens first decides. A restart still does NOT end it — `stopChild` interrupts the fiber, so the deferred is never completed on that path. A child killed by a signal reports `code: null`, which is reported as a failure rather than a clean 0.
65
-
66
- What a self-exit MEANS then depends on whether anyone is watching, because the two failure modes pull in opposite directions:
67
-
68
- - **Interactive** (stdout is a TTY) — a crashed boot is something you are about to fix, so the supervisor says so and keeps watching. The next save restarts it, which is what every other dev server does; stopping would throw away the watcher mid-edit and make you retype the command. - **Non-interactive** — nobody is going to fix anything. `voltro dev` exits with the child's code, so a failed boot is a failed command. This is the case that produced the invisible processes, and the one CI actually waits on.
18
+ Login is the sharpest case and it is not exotic: it MUST write (the session row), it cannot be an rpc mutation because it is what mints the cookie, and it is a documented first-class pattern — `@voltro/plugin-auth` ships `handleSignIn` / `handleSignUp` and the reference consumer mounts them on this surface. Every app that does so needed a store the route contract did not give it.
69
19
 
70
- A CLEAN exit always stops, watched or not. `VOLTRO_DEV_KEEP_ALIVE=1|0` forces the answer for what the TTY check cannot see — a CI runner with a TTY allocated, or a wrapper that pipes output while a human still watches it and cannot keep a clean exit alive, which would turn a deliberate shutdown into a hang.
20
+ It is the BOOT store, through the same lazy getter the auth chain reads one ref, three consumers now and `undefined` while it is still being built, so a route should answer rather than throw. It is **not tenant-scoped**: a route serves raw HTTP with no resolved Subject, so a route reading tenant-owned rows must derive and apply that scope itself. That is the price of the surface being raw, and the reason an rpc procedure stays the better home for anything that can be one.
71
21
 
72
- Pinned against REAL child processes, because the defect was an Effect that never settled — a stubbed `once('exit')` that resolves is exactly what would have passed while the bug shipped.
73
- - **@voltro/database** — **A user-facing message that names an API must have one — now checked in CI.**
22
+ ### `db apply` honours `VOLTRO_DESTRUCTIVE_OK`
74
23
 
75
- The sibling of the claimed-wiring check. That one asserts a doc comment's claimed caller exists; this one asserts a message's claimed API exists. Same failure shape, worse audience: a doc comment is read by somebody browsing, a refusal by somebody already blocked and looking for the sanctioned way out.
24
+ The table-list opt-in shipped on the auto-migrate path only. `voltro db apply` computes its own plan and checked `summary.blocked` directly; its module contained no occurrence of the variable at all. So an app's staging migration Job running the sanctioned `db apply --plan` form had **no route through an intentional, declared, `lossy`-classified table drop**, and the release note's own example was a command that ignored the variable it set.
76
25
 
77
- It exists for a reported bug that nothing could have caught. The drop-table refusal offered, as its FIRST option, *"chain `.dropped()` on it"* tables have no such marker, only columns do. Doc SAMPLES are typechecked; message strings are not, and cannot be. Three of one release's reported defects lived in that blind spot.
26
+ Both forms now route through the same helper as the boot gate: every blocked op must be `lossy`, a named scope unblocks only the tables it names, anything still blocked refuses the whole plan. The applier receives the UNBLOCKED plan passing the still-blocked one would have it refuse a second time, which is the exact bug `unblockLossy` was written for.
78
27
 
79
- Three rules, each with an unambiguous answer, because a noisy gate is skipped and then costs more than it saves:
28
+ Worth naming, because it is a boundary of the message-API gate added last release: the variable exists, is spelled correctly, and IS read by a different command than the one printing it. *"The named API exists"* and *"the named API is reachable from here"* are different claims, and only the first is checkable from a string.
80
29
 
81
- - a `VOLTRO_*` variable a message tells you to **set** must be read somewhere, - a `` `.method()` `` a message tells you to **chain** must be a callable MEMBER of a published type, - a `--flag` in a `voltro …` instruction must be parsed.
82
-
83
- The member rule is the one that took two attempts. The first version asked "does this name exist in the public surface" and the motivating bug **passed it**: `dropped` is exported, as a free `dropped()` you write as a column's value. A dotted claim is a claim about something chainable, so a free function and a `readonly dropped?: boolean` data property are both correctly rejected now.
84
-
85
- **It immediately found a second instance nobody had reported** — the drop-COLUMN refusal also said "chain `.dropped()`", one level down from the reported one, and the real spelling is `<column>: dropped()` as the field's value. Close enough to guess from, which is why it survived.
86
-
87
- Ships with a `--selftest` that runs first in CI, for the reason the changelog gate has one: a check that has quietly stopped detecting anything still prints green, and green is read as evidence.
88
- - **@voltro/database** — **Two migration refusals sent people the wrong way** — the worst place for a bad hint, because whoever reads one is already blocked and looking for the sanctioned way out.
89
-
90
- **The drop-table refusal recommended an API that does not exist.** Its first option was *"add it to your declared set + chain `.dropped()` on it"*. There is no table-level `dropped()` — only the column marker. The recommendation was also the conceptually RIGHT one, which is what made it expensive: the two options that do work are both worse, so a reader picks the one they cannot follow.
91
-
92
- There is now a real per-table answer: **`VOLTRO_DESTRUCTIVE_OK` accepts a table list**, not just `1`. `VOLTRO_DESTRUCTIVE_OK=old_things` acknowledges the data loss for that table and leaves every other lossy op in the plan blocked. `1` still means all of them — which is rarely what somebody means, and was previously the only way to say anything. A user with one intended drop and three other lossy ops had to acknowledge all four or hand-write a `DROP TABLE` migration, the path 0.14.0's own upgrade note warns against.
93
-
94
- The message also states why there is deliberately no table marker: a dropped COLUMN leaves a slot worth documenting in the declaration; a dropped TABLE leaves nothing, so the marker would be a dead entry you must remember to delete.
30
+ ### Fixed
95
31
 
96
- **The drop-column refusal never mentioned `renamedFrom`.** It offered "chain `.dropped()`" or "restore the field" — and followed literally on a rename, the first costs exactly the data the user was trying to keep. When the plan drops AND adds columns on the same table, the message now leads with *"did you rename one?"* and names both sides. The evidence was in the plan the whole time.
32
+ - **@voltro/cli, @voltro/database** **`voltro update --codemods-only` crashed on a file deleted but not staged** a regression introduced by 0.15.0's own git-based enumeration.
97
33
 
98
- Finding that required fixing a second thing: the footer read only the BLOCKED operations, and an `add-column` is `safe`. The counterpart of a rename was never in the list it was looking at.
99
- - **@voltro/cli** — **0.14.0's taxonomy codemod renamed two kinds of file it should not have, and a repo that already upgraded carries the damage with a green build.** Both were found by adopters running it on real projects; both are silent — the rename succeeds, the imports are rewritten, nothing throws.
34
+ `git ls-files --cached` reads the INDEX, and the index still holds a path that is already gone from disk until the deletion is staged. `addSourceFileAtPath` then threw ENOENT, surfacing as a raw ts-morph stack rather than as anything a reader could act on.
100
35
 
101
- **A framework primitive was treated as an undeclared file.** `health.route.tsx` `health.route.component.tsx`, five times in one app. The codemod kept its OWN list of "suffixes that already carry a contract" instead of reading `fileConventions.ts`, and `.route.` was not on it — the exact drift that module exists to prevent, reproduced inside a file that imports from it. The list is gone; the registry answers now, and it gained `ROUTE_PATTERN` plus a `carriesFrameworkConvention()` every consumer shares.
36
+ The state is not exotic it is what an ordinary mid-work tree looks like, and `--dry-run` is documented as allowed on a dirty tree, which is exactly where an unstaged deletion lives. The command was unusable in the state it explicitly permits. Paths that are gone are now filtered out: there is nothing to rewrite and nothing to report.
102
37
 
103
- The root cause underneath was worse than a missing entry: `export default defineRestRoute({...})` resolved to the placeholder name `Default`, which starts with a capital, and was counted as a COMPONENT on that basis. A default export is now only evidence of a component when the exported thing is callable so a convention nobody has registered yet is safe too.
38
+ **A failing DDL statement now travels with the error, not only to stderr.** A soft-drop aborted a boot with a bare `SqlError: Failed to execute statement` no statement, no table, no operation. The applier wrote the detail with `process.stderr.write` inside a `tapError` and re-raised the ORIGINAL error, so the detail was lost whenever the process aborted before the stream flushed, or whenever the caller rendered the error rather than the console. `migrate.ts` already carried it in the error for file-based migrations; the planner-driven applier did not, so **which path failed decided whether you could see what failed.**
39
+ - **@voltro/cli** — **`voltro update --codemods-only` refused in the one state it exists for.**
104
40
 
105
- **A file with no exports was called a type file.** `test-setup.ts` `test-setup.types.ts`, while `vitest.config.ts` still named `./test-setup.ts` as a **string**. Not an import, so nothing rewrote it and nothing failed: that suite would have run without its setup and stayed green. Five more went the same way a registry module, two migration runners, a `.register.ts`, and a code generator whose `export` tokens live inside template strings.
41
+ The clean-tree guard ran before the repair path, so the command whose entire job is finishing an interrupted upgrade refused whenever the tree was dirty. An adopter reported it twice; the second time their tree carried uncommitted work **from the previous upgrade**, so they bumped four manifests by hand and ran the install themselves the outcome `voltro update` exists to prevent, reached through its own guard.
106
42
 
107
- `*.types.ts` promises "zero runtime exports", and that promise only means something for a file that exports TYPES. Zero of everything promises nothing, and renames a module whose whole purpose is being imported for effect — where the filename is often the only reference there is. It now requires at least one exported type.
43
+ `--force` was always available and is the wrong answer: it is documented as "not recommended", so it reads as an escape hatch rather than as the sanctioned route through a state we explicitly support.
108
44
 
109
- **The shipped codemod undoes both**, and can only reach files whose content proves the suffix was wrong: a `.component.` on a name that already carries a framework convention, and a `.types.` on a file that exports nothing at all. It also prints the one thing it cannot fix references by PATH rather than by import (a vitest `setupFiles`, a tsconfig `include`, a Docker `COPY`) were strings on the way out and are strings on the way back.
110
- - **@voltro/cli** — Three ways `voltro update` failed on a real adopter's host, none of which we could have found ourselves — each needs a machine we do not have.
45
+ `--codemods-only` now **warns** instead of refusing, saying the codemod diff will be mixed in with the existing changes and pointing at `--dry-run` / `--only`. The full update still refusesbump, install and rewrite in one pass on top of unrelated changes is a diff nobody can read and its refusal now names the repair path.
111
46
 
112
- **The install could not run here, and the refusal left the tree half-upgraded.** Their install runs in a container against its own store. `voltro update` ran the package manager on the host anyway; pnpm refused (it wanted to remove `node_modules` and had no TTY to ask) and exited — after the version bumps were already written and before any codemod ran. That is the state this command's own documentation calls the worst one to be in, and it was reachable by design.
47
+ ### Internal (no consumer-facing effect)
113
48
 
114
- `--no-install` now writes the bump and stops, saying plainly that the tree is half-upgraded and naming both remaining steps. The install-failed message points at it too. Note what this is not: a compatibility flag. It is a mode for a host where the install is somebody else's job, and it ends by telling you the job is not done.
49
+ - **@voltro/cli** The admin-import rejection cases share ONE booted server, and report their phase timings.
115
50
 
116
- **The codemod scan exhausted a 12 GB heap, and said nothing about why.** The crash was a bare V8 out-of-memory stack. The scan pruned the directories WE know about `node_modules`, `dist`, `.turbo` — which cannot cover a project's own heavy ignored trees (a build cache, a data dump, a virtualenv).
51
+ Two release gates have now failed on that single test line, with two different symptoms and neither an auth defect: first a **400** this server rejects a request whose body framing broke BEFORE routing, so `gate()` never ran and then, after the body was removed, a **120-second timeout**. A bare `Test timed out` cannot say whether the boot, the request or the close is what hung, which is why the second failure taught us nothing the first hadn't.
117
52
 
118
- Inside a git repository the scan now asks git: `git ls-files --cached --others --exclude-standard` is exactly "files this project considers its own", and a codemod rewrites source source that git ignores is not source we may rewrite. It also removes the traversal, so there is nothing left to exhaust memory on. Outside a repo the walk remains, now with a ceiling that REPORTS which directory to exclude instead of dying namelessly.
53
+ This file was standing up SIX real `serveApi` instances, each with its own socket and Effect layer, on a machine already running every other package's suite under `turbo --concurrency=2`. The two rejection cases assert nothing about the store, so they now share one boot, and the assertion carries `boot=…ms absent=…ms wrong=…ms` a future failure names the slow phase instead of just the line number.
119
54
 
120
- **"not a Voltro app" was the wrong conclusion.** Said of a directory containing an `app.config.ts`, it sends the reader looking for the wrong problem. Three web apps in a workspace inherited from another tool had their `@voltro/*` dependencies in an ancestor `package.json` — the apps ARE Voltro apps; only the declaration lives elsewhere. With an `app.config.ts` present the message now says that, and names the two ways forward.
55
+ No product code changed.
56
+ - **@voltro/cli** — The admin-import rejection tests stop uploading an archive they never needed.
121
57
 
122
- **A monorepo may keep ONE root `package.json`** with its apps carrying only an `app.config.ts`. Running `voltro update` inside such an app used to say "no package.json at <dir>" true, and useless. It now recognises the layout, says it is supported, and prints the command with the root already filled in.
123
- - **@voltro/cli** — **`voltro update --only <id>`**, and a summary that stops contradicting itself.
58
+ A release gate failed with `expected 400 to be 401` on `401 without a Bearer token` and did not reproduce in five later runs. It was not an auth defect and not load-flake in the usual sense: measured against a live server on that path, an honest no-auth request answers **401**, a body-less one answers **401**, and one whose `Content-Length` lies — or whose chunked framing ends early — answers **400**, because the HTTP layer rejects broken framing BEFORE routing. `gate()` is the handler's first statement, so when it never runs there is no 401 to give.
124
59
 
125
- `--only` runs just the named codemods (repeatable or comma-separated; ids are what `--dry-run` prints). The ask behind it: two of three codemods were load-bearing for one app without them, 52 type errors and 51 routes that 404 while the third was elective, and the repairing tool refuses on a dirty tree, so there was no way to take the necessary half first.
60
+ Those two tests were POSTing the full packed bundle to assert an authorization property that is decided without reading the body at all. The archive proved nothing and made a multi-KB upload a precondition of an auth assertion. They now send no body, which is both flake-free and the sharper claim; the assertions carry the response body so a future mismatch names the responder instead of printing a bare status.
126
61
 
127
- It is deliberately NOT a `--required` flag over a REQUIRED/OPTIONAL axis on each codemod. "Required" would have to mean *this app does not run without it*, and that is a property of the app: `03_pages-suffix` is unavoidable for a project with pages and irrelevant to an api-only one. Marking it on the codemod would encode a guess as a contract. An unknown id is an error listing the ids that ARE available — "it did nothing" and "you typed it wrong" otherwise look identical.
62
+ No product code changed the endpoint's behaviour is unaltered.
63
+ - **@voltro/cli** — The admin-import auth test reports WHO answered, not just that the number was wrong.
128
64
 
129
- **The summary counted only edited files, and renames vanished from it.** The before-snapshot is keyed by PATH, so a moved file has no entry under its new one: a pass that renamed 238 files and edited 9 importers reported `(9 files)`, understating the change by a factor of 26 in the line a user plans around. Moves are now paired by content and reported separately `(238 renamed, 9 edited)`.
65
+ It failed once under full-gate load with `expected 400 to be 401`, and did not reproduce in four subsequent runs (the file alone, the integration group alone, two full suites, a second full gate). The bare status made the log useless: `gate()` is the first statement in `handleAdminImport` and always 401s an absent token, so a 400 proves the request never reached that handler but nothing in the failure said which handler DID answer.
130
66
 
131
- **A dry run no longer speaks in the past tense.** It printed `✓ <id> <title>`, the same line a real run prints. Now `·` and `[would apply]`.
67
+ The assertion now carries the response body and the target URL, so the next occurrence names the responder instead of costing an afternoon. No product code changed; the endpoint's behaviour is unaltered.
@@ -11,16 +11,16 @@
11
11
  "dependencies": {
12
12
  "@effect/platform": "^0.96.1",
13
13
  "@effect/rpc": "^0.75.1",
14
- "@voltro/ai": "0.16.0",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/protocol": "0.16.0",
19
- "@voltro/runtime": "0.16.0",
14
+ "@voltro/ai": "0.17.0",
15
+ "@voltro/cli": "0.17.0",
16
+ "@voltro/database": "0.17.0",
17
+ "@voltro/env": "0.17.0",
18
+ "@voltro/protocol": "0.17.0",
19
+ "@voltro/runtime": "0.17.0",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.16.0",
23
+ "@voltro/testing": "0.17.0",
24
24
  "typescript": "^5.7.0",
25
25
  "vitest": "^3.0.0"
26
26
  }
@@ -12,17 +12,17 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-auth": "0.16.0",
19
- "@voltro/protocol": "0.16.0",
20
- "@voltro/runtime": "0.16.0",
21
- "@voltro/sql-postgres": "0.16.0",
15
+ "@voltro/cli": "0.17.0",
16
+ "@voltro/database": "0.17.0",
17
+ "@voltro/env": "0.17.0",
18
+ "@voltro/plugin-auth": "0.17.0",
19
+ "@voltro/protocol": "0.17.0",
20
+ "@voltro/runtime": "0.17.0",
21
+ "@voltro/sql-postgres": "0.17.0",
22
22
  "effect": "^3.21.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.16.0",
25
+ "@voltro/testing": "0.17.0",
26
26
  "typescript": "^5.7.0",
27
27
  "vitest": "^3.0.0"
28
28
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-multitenancy": "0.16.0",
19
- "@voltro/protocol": "0.16.0",
20
- "@voltro/runtime": "0.16.0",
15
+ "@voltro/cli": "0.17.0",
16
+ "@voltro/database": "0.17.0",
17
+ "@voltro/env": "0.17.0",
18
+ "@voltro/plugin-multitenancy": "0.17.0",
19
+ "@voltro/protocol": "0.17.0",
20
+ "@voltro/runtime": "0.17.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.16.0",
24
+ "@voltro/testing": "0.17.0",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-deactivation": "0.16.0",
19
- "@voltro/protocol": "0.16.0",
20
- "@voltro/runtime": "0.16.0",
15
+ "@voltro/cli": "0.17.0",
16
+ "@voltro/database": "0.17.0",
17
+ "@voltro/env": "0.17.0",
18
+ "@voltro/plugin-deactivation": "0.17.0",
19
+ "@voltro/protocol": "0.17.0",
20
+ "@voltro/runtime": "0.17.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.16.0",
24
+ "@voltro/testing": "0.17.0",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@react-email/components": "^1.0.12",
14
14
  "@react-email/render": "^1.4.0",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-mail": "0.16.0",
19
- "@voltro/plugin-multitenancy": "0.16.0",
20
- "@voltro/protocol": "0.16.0",
21
- "@voltro/runtime": "0.16.0",
15
+ "@voltro/cli": "0.17.0",
16
+ "@voltro/database": "0.17.0",
17
+ "@voltro/env": "0.17.0",
18
+ "@voltro/plugin-mail": "0.17.0",
19
+ "@voltro/plugin-multitenancy": "0.17.0",
20
+ "@voltro/protocol": "0.17.0",
21
+ "@voltro/runtime": "0.17.0",
22
22
  "effect": "^3.21.2",
23
23
  "react": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.16.0",
26
+ "@voltro/testing": "0.17.0",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-multitenancy": "0.16.0",
19
- "@voltro/plugin-storage": "0.16.0",
20
- "@voltro/protocol": "0.16.0",
21
- "@voltro/runtime": "0.16.0",
22
- "@voltro/sql-mysql": "0.16.0",
15
+ "@voltro/cli": "0.17.0",
16
+ "@voltro/database": "0.17.0",
17
+ "@voltro/env": "0.17.0",
18
+ "@voltro/plugin-multitenancy": "0.17.0",
19
+ "@voltro/plugin-storage": "0.17.0",
20
+ "@voltro/protocol": "0.17.0",
21
+ "@voltro/runtime": "0.17.0",
22
+ "@voltro/sql-mysql": "0.17.0",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.16.0",
26
+ "@voltro/testing": "0.17.0",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }