@zerotal/arch 1.7.4 → 1.8.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 (126) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +2 -0
  4. package/docs/about.md +16 -16
  5. package/docs/admin/actions.md +9 -9
  6. package/docs/admin/auth.md +2 -2
  7. package/docs/admin/dashboard.md +5 -5
  8. package/docs/admin/extending-ui.md +12 -12
  9. package/docs/admin/extending.md +16 -7
  10. package/docs/admin/forms.md +4 -4
  11. package/docs/admin/index.md +2 -2
  12. package/docs/admin/operations.md +9 -9
  13. package/docs/admin/resources.md +3 -3
  14. package/docs/admin/structure.md +6 -6
  15. package/docs/admin/tables.md +8 -8
  16. package/docs/admin/testing.md +4 -4
  17. package/docs/ai.md +118 -11
  18. package/docs/application.md +12 -12
  19. package/docs/arch.md +25 -3
  20. package/docs/assets.md +11 -11
  21. package/docs/audit.md +13 -13
  22. package/docs/authentication.md +39 -39
  23. package/docs/authorization.md +13 -13
  24. package/docs/broadcasting/channels.md +2 -2
  25. package/docs/broadcasting/client.md +2 -2
  26. package/docs/broadcasting/events.md +9 -10
  27. package/docs/broadcasting/index.md +1 -1
  28. package/docs/broadcasting/testing.md +5 -5
  29. package/docs/cache.md +15 -13
  30. package/docs/carbon.md +35 -35
  31. package/docs/changelog.md +268 -1
  32. package/docs/client/index.md +13 -13
  33. package/docs/commands.md +36 -4
  34. package/docs/components.md +116 -115
  35. package/docs/config-system.md +4 -4
  36. package/docs/container.md +27 -27
  37. package/docs/context.md +27 -27
  38. package/docs/contributing.md +28 -0
  39. package/docs/controllers.md +10 -10
  40. package/docs/conventions.md +23 -15
  41. package/docs/cookies.md +3 -3
  42. package/docs/csrf.md +6 -6
  43. package/docs/database.md +22 -17
  44. package/docs/deployment.md +99 -2
  45. package/docs/devtools.md +16 -16
  46. package/docs/email-verification.md +8 -8
  47. package/docs/encryption.md +7 -7
  48. package/docs/errors.md +6 -6
  49. package/docs/events.md +12 -11
  50. package/docs/flow/components.md +32 -32
  51. package/docs/flow/decorators.md +18 -18
  52. package/docs/flow/events.md +16 -16
  53. package/docs/flow/forms.md +17 -17
  54. package/docs/flow/icons.md +5 -5
  55. package/docs/flow/index.md +21 -21
  56. package/docs/flow/layouts.md +99 -35
  57. package/docs/flow/lifecycle.md +16 -16
  58. package/docs/flow/models.md +9 -9
  59. package/docs/flow/pagination.md +8 -8
  60. package/docs/flow/performance.md +7 -7
  61. package/docs/flow/references.md +3 -3
  62. package/docs/flow/routing.md +26 -26
  63. package/docs/flow/testing.md +20 -20
  64. package/docs/getting-started.md +18 -13
  65. package/docs/health.md +17 -8
  66. package/docs/helpers.md +17 -17
  67. package/docs/i18n.md +10 -10
  68. package/docs/inertia/devtools.md +4 -4
  69. package/docs/inertia/index.md +6 -6
  70. package/docs/inertia/props.md +21 -21
  71. package/docs/inertia/rendering.md +9 -9
  72. package/docs/inertia/ssr.md +2 -2
  73. package/docs/lifecycle.md +2 -2
  74. package/docs/lock.md +11 -11
  75. package/docs/logger.md +12 -12
  76. package/docs/media.md +22 -22
  77. package/docs/middleware.md +17 -17
  78. package/docs/migrations.md +13 -13
  79. package/docs/monitor.md +6 -6
  80. package/docs/notifications.md +24 -24
  81. package/docs/orm/casts.md +14 -14
  82. package/docs/orm/factories.md +8 -8
  83. package/docs/orm/index.md +22 -20
  84. package/docs/orm/lifecycle.md +13 -13
  85. package/docs/orm/queries.md +28 -28
  86. package/docs/orm/relationships.md +16 -16
  87. package/docs/orm/serialization.md +11 -11
  88. package/docs/package-development.md +10 -10
  89. package/docs/pagination.md +14 -14
  90. package/docs/password-reset.md +6 -6
  91. package/docs/providers.md +11 -11
  92. package/docs/query-builder.md +18 -18
  93. package/docs/queue.md +14 -12
  94. package/docs/rate-limiting.md +6 -6
  95. package/docs/responses.md +14 -14
  96. package/docs/roles-and-2fa.md +9 -9
  97. package/docs/routing.md +44 -44
  98. package/docs/scaffolding.md +1 -1
  99. package/docs/scheduler.md +10 -10
  100. package/docs/seeding.md +6 -6
  101. package/docs/session.md +16 -14
  102. package/docs/social.md +14 -14
  103. package/docs/storage.md +25 -13
  104. package/docs/structure.md +2 -2
  105. package/docs/support-policy.md +23 -10
  106. package/docs/telemetry.md +12 -12
  107. package/docs/tenancy.md +34 -40
  108. package/docs/testing/browser.md +6 -6
  109. package/docs/testing/console.md +3 -3
  110. package/docs/testing/database.md +10 -10
  111. package/docs/testing/flow-browser.md +6 -6
  112. package/docs/testing/http.md +12 -12
  113. package/docs/testing/index.md +29 -3
  114. package/docs/testing/mocking.md +8 -8
  115. package/docs/upgrade.md +1 -1
  116. package/docs/validator.md +19 -19
  117. package/docs/view.md +14 -14
  118. package/package.json +4 -4
  119. package/src/config.ts +8 -0
  120. package/src/index.ts +25 -1
  121. package/src/install/ArchInstallCommand.ts +46 -3
  122. package/src/install/guidelines.ts +84 -0
  123. package/src/install/shape.ts +177 -0
  124. package/src/install/skills.ts +271 -0
  125. package/src/provider/ArchProvider.ts +148 -0
  126. package/src/tools/searchDocs.ts +7 -0
package/docs/changelog.md CHANGED
@@ -27,6 +27,247 @@ the section for every version you cross and apply its migration notes, not only
27
27
  majors. [Releases and versioning](/docs/support-policy#releases-and-versioning) explains
28
28
  when that carve-out ends.
29
29
 
30
+ ## 1.8.0 — 2026-08-24
31
+
32
+ The first render mode, the codemod runner 2.0 depends on, and four failures that
33
+ each looked like something other than what they were.
34
+
35
+ ### Added
36
+
37
+ - **`static interactive = false` — the first rung of Flow's render modes.** Every component
38
+ until now was maximally interactive: rendered on the server, dehydrated into a snapshot,
39
+ tracked by the client, reachable over a socket. Right for a counter, wasteful for a nav rail.
40
+ A static component is rendered in full by its parent and nothing else — no `onDehydrate`, no
41
+ snapshot, no `<script type="application/json">`, no entry in the client's registry, and no
42
+ `data-flow-root`, which would freeze it at its first render since its only route to an update
43
+ is the parent re-rendering it. It takes no place in `_childIds` and does not shift its
44
+ interactive siblings' ids, so no sibling remounts and loses its state when a static one
45
+ appears above it. `lazy`, `defer` and `stream` throw rather than being ignored: each waits
46
+ for the client to ask for the real render, and a static child never registers to do the
47
+ asking.
48
+
49
+ Opt-in — nothing existing changes. `this.isInteractive` reports the mode from inside the
50
+ component, and being a new public member it takes that name away from applications; it is on
51
+ the documented reserved list. See [Static children](/docs/flow/layouts#static-children).
52
+
53
+ - **`zt upgrade` — the codemod runner.** The 2.0 ledger's rule is that every entry that can
54
+ have a codemod has one before 2.0 ships, and until now nothing had been built, which made
55
+ the ledger a list of changes nobody could afford to make.
56
+
57
+ **Dry by default**, which is backwards from most tools and deliberate: it rewrites source
58
+ across a whole project, and the first run should be something you can read and disagree with.
59
+ `--write` applies it. Nothing is written until the whole plan is known, so a run that fails
60
+ halfway leaves no half-upgraded tree, and `--dry` exercises the same code path as the real
61
+ thing rather than a parallel one that can drift from it. Codemods see each other's output,
62
+ because two of them touching one file across a version range is ordinary.
63
+
64
+ **What it could not do is the headline.** A codemod that walks past what it does not
65
+ understand is worse than none, since the changes it _did_ make imply the job is finished. So
66
+ every codemod returns two lists and the runner prints the second last and loudest, with file,
67
+ line and a reason. The first codemod covers the deprecated aliases — `BaseModel` → `Model`,
68
+ `routes:types` → `route:types`, `serve --dev` → `dev`. See [Commands](/docs/commands).
69
+
70
+ - **`--clean` on `assets:build` and `inertia:build`.** Pruning is conservative by default:
71
+ chunk-shaped filenames, plus whatever the last build on this machine recorded in `.zerotal/`.
72
+ That cannot recognise output some other naming produced. `--clean` needs no record — the
73
+ output directory belongs to the build, and what the build did not write does not belong in
74
+ it. It refuses `public/` and the project root, where deleting what was not rebuilt takes the
75
+ app's images and favicon with it, which is the one failure here that building again cannot
76
+ undo. Pruning stays the default; only you can say the directory holds nothing else.
77
+
78
+ - **Agent skills, from `@zerotal/arch`.** `AGENTS.md` is short because every prompt it lands
79
+ in pays for its whole length, so it points rather than teaches. That has a cost: an agent
80
+ gets a map and no detail, and the detail is where the expensive mistakes live. A skill is a
81
+ file with a one-line description that costs nothing until an agent decides it is relevant,
82
+ so a procedure can be written out in full. Two ship — one on changing the schema (who owns
83
+ it in your app, the mixin columns nothing declares, and why an unguarded `ALTER TABLE`
84
+ collides during a release's `migrate`) and one on shipping a release (naming your own deploy
85
+ steps, replacing the asset directory rather than merging into it, `trustedProxies` behind a
86
+ proxy, and the pipe that hides a test suite's exit status).
87
+
88
+ Written to `.agents/skills`, plus `.claude/skills` when that agent is detected. To replace
89
+ one this ships, edit it and delete its marker line — a `SKILL.md` without the marker is
90
+ yours and is never rewritten. `ArchConfig({ skills: false })` turns the feature off. Run
91
+ `zt arch:update` to install them.
92
+
93
+ - **`zt doctor` reports agent instructions that no longer describe your project.** Every fact
94
+ in the generated block moves without anyone thinking about the file: add a migrations
95
+ directory, turn `synchronize` off, install a package. It goes on reading as current while
96
+ describing the app you used to have, and guidance that is confidently out of date gets
97
+ followed rather than questioned. Skills rot the same way and are easier to miss, since
98
+ nothing reads one until an agent decides it is relevant — by which point it is being acted
99
+ on. The check regenerates both and compares, and names `zt arch:update` as the fix. A
100
+ warning, not a failure: it misleads a reader, it does not stop the app working.
101
+
102
+ ### Changed
103
+
104
+ - **A Flow page with nothing interactive on it opens no socket.** Every page connected at
105
+ boot, unconditionally — so a marketing page, a docs article or a rendered report held a
106
+ WebSocket per visitor, open on both ends for the life of the visit, to carry nothing. Both
107
+ paths that write to the socket take a `FlowComponent`, so with none registered there was not
108
+ a frame that _could_ be sent. The connection is made when something needs it now: after the
109
+ initial scan, after an SPA navigation, after a patch registers a child. `<Link navigate>`
110
+ fetches over HTTP, so a static page with links stays disconnected. A routed page honours the
111
+ same static, which is the half that matters — a page is a component, and one whose children
112
+ are static but which is interactive itself still connects.
113
+
114
+ - **The `@zerotal/arch` agent block describes how your app is set up, not only what it
115
+ installed.** A package list answers "what is available here", which is not the question that
116
+ decides what an agent should write: the framework's contracts are not uniform across
117
+ projects, and the places they differ are the places where guessing wrong compiles cleanly and
118
+ fails at runtime. `AGENTS.md` now states the four facts that change an instruction — who owns
119
+ the schema, whether route names are typed, whether `exactOptionalPropertyTypes` or
120
+ `noUncheckedIndexedAccess` are on, and whether there are tests to run. Read off disk rather
121
+ than from a booted app, because a project that will not boot is often why the agent surface
122
+ is being installed. `.env` is deliberately not among the files read: this output is committed
123
+ and pasted into prompts, and a detector that reads secrets is one refactor away from emitting
124
+ them. Re-run `zt arch:update` to pick it up.
125
+
126
+ ### Fixed
127
+
128
+ - **No mail could be sent over port 587.** A STARTTLS upgrade hands back a new socket and
129
+ leaves the old one attached, still firing its callbacks — and what that one delivers from
130
+ then on is the undecrypted TLS stream. Both sets of handlers appended to a single reply
131
+ buffer, so handshake records and ciphertext sat in the middle of the server's replies and no
132
+ line in the buffer matched a reply any more: the driver waited out its timeout without ever
133
+ parsing the `250`, and the server logged a connection lost after STARTTLS. Measured, 1,737
134
+ bytes of ciphertext went into the discarded socket's handler while the TLS handler received
135
+ the replies.
136
+
137
+ `close` and `error` were worse than `data`. The plaintext socket ending is a normal part of
138
+ handing over to TLS, and it marked the live connection closed — rejecting whatever was
139
+ waiting on the session that had just replaced it. Each set of callbacks now captures the
140
+ generation it was installed for, and an upgrade bumps it.
141
+
142
+ - **An Inertia `303` redirect left the browser doing nothing at all.** `X-Inertia: true` was
143
+ set inside the 302-to-303 conversion, so it only ever reached a redirect that arrived as a
144
+ 301 or 302 from a non-GET handler. A handler returning the 303 the protocol asks for skipped
145
+ the only line that marked its response — and `redirect(to, 303)` is what
146
+ [Authentication](/docs/authentication) tells people to write, in eight places. The form
147
+ submitted, the row was written, the mail went out, and the fields stayed filled in: a hang
148
+ from both ends, which is the worst shape a failure can take. Marking now happens for every
149
+ redirect status on an Inertia request, with the conversion a separate decision on top of it.
150
+ `307` and `308` are marked but left alone, since preserving the method is the whole reason to
151
+ choose them.
152
+
153
+ - **Answering the busy-port menu killed `serve --dev` on the spot.** The banner printed, then
154
+ `exited with code 1`, and nothing said why. Reading a prompt locks Bun's stdin stream, and
155
+ the lock is deliberately held for the life of the command so a second prompt can still read —
156
+ so the dev deck taking the terminal over threw `ReadableStream is locked`. It died inside the
157
+ alternate screen buffer, and restoring the terminal on the way out erased the error along
158
+ with everything else drawn there, which is why this was reported as "it just exits" rather
159
+ than as the error it was. The prompt hands stdin back where it took it; a deck that still
160
+ cannot have stdin degrades to streaming rather than dying, and a dev-mode failure stops the
161
+ deck before it reports.
162
+
163
+ - **Two builds sharing an output directory deleted each other's files.** Nothing forbids
164
+ `inertia:build` and `assets:build` writing to the same place, and the defaults invite it: one
165
+ writes to `public/assets`, `app.assets.outDir` often names the same directory, and the
166
+ default release pipeline runs them one after the other. The record of what to prune was one
167
+ flat list per directory, so each build read the other's files as its own previous build and
168
+ removed them. The release ended with whichever ran last and nothing reported a problem — the
169
+ build that lost still said "Build complete" on its way out, and the page it served then 404'd
170
+ its own script. The record is keyed by entry point now: a file another build claimed is not
171
+ this one's to remove, while chunks nobody claims are still swept.
172
+
173
+ - **`zt doctor` failed a schema configuration that works.** Sync on plus migrations present
174
+ read as "the schema needs exactly one source of truth", which misses the documented
175
+ arrangement where sync builds the schema from the models so a fresh clone runs without a
176
+ migration step, and `synchronize` is an expression that is false in production, where the
177
+ deploy runs `migrate`. The two never apply in the same environment. It fails in production
178
+ now, where the deploy really does run both, and warns elsewhere. A check that cries wolf
179
+ against a correct configuration is what stops `zt doctor` ever being trusted to gate a
180
+ deploy.
181
+
182
+ ### Documented
183
+
184
+ - **Replace a release directory, do not merge into it.** Chasing 195 orphaned chunks on a
185
+ server showed the build was never the problem — ten releases of a code-split app into one
186
+ directory hold steady at the build's own output, and every one of the reporting app's 68
187
+ chunks is referenced. What accumulates is the _release_: the archive is extracted over the
188
+ running directory, so every file in it is written and every file not in it is left alone, and
189
+ nothing on that machine ever runs a build. They stay publicly fetchable at their
190
+ content-hashed URLs, which is how copy that was taken down went on being served.
191
+ [Deployment](/docs/deployment) now gives the two spellings that replace the directory.
192
+
193
+ ## 1.7.5 — 2026-08-23
194
+
195
+ Two bugs that shipped to every deployed app, a package promoted to `stable`, and
196
+ the gates that would have caught both.
197
+
198
+ ### Changed
199
+
200
+ - **`@zerotal/arch` is `stable`.** Reviewed ahead of its 1.9.0 date. The API follows
201
+ SemVer strictly from here, and that promise covers the **MCP tool contract** — tool
202
+ names, their arguments, and the shape of what they return. That is what an agent
203
+ client is configured against, and nothing type-level can see it: `archTools` has the
204
+ same signature however the tools are named. `mcp-surface.md` records all nine and CI
205
+ diffs it on every change. The protocol revision the server speaks is not covered; it
206
+ follows the protocol.
207
+
208
+ - **INTERNAL — the writers behind `arch:install` are no longer public API.**
209
+ `detectAgents`, `applyMcpConfig`, `applyBlock`, `buildGuidelines` and the rest are
210
+ `@internal`: still exported, still working, no longer promised. Their only caller is
211
+ the install command, and freezing them would have committed the shape of `.mcp.json`
212
+ writing to the rest of the 1.x line on behalf of a caller who never arrived.
213
+
214
+ - **INTERNAL — `api-surface.md` honours `@internal` across every package.** The
215
+ contract has always read "anything importable without an `@internal` marker keeps its
216
+ shape", and the generator did not read the tag — so symbols already marked internal
217
+ were recorded as though promised. 374 entries across 13 packages are omitted now,
218
+ every one verified marked. Nothing changes at runtime or in the types; the file
219
+ listing the promises now lists the promises.
220
+
221
+ - **A modal locks the page behind it.** `<Modal>` and `<Drawer>` trapped focus
222
+ correctly while the page underneath kept scrolling, which on a phone reads as the
223
+ dialog having broken the page.
224
+
225
+ - **Flow marks the active nav link for everyone.** `<Link navigate>` set
226
+ `data-current`, which styles a link, and nothing that announces it. It sets
227
+ `aria-current="page"` alongside now, so a screen reader can tell which of thirty nav
228
+ items is the current page.
229
+
230
+ ### Fixed
231
+
232
+ - **Assets were cache-busted in development and not in production.** `asset()` appended
233
+ `?v=` only when a dev version was set, so every deployed Zerotal app served the
234
+ previous build's JavaScript and CSS to anyone with a warm cache — indefinitely, since
235
+ the URL never changed. The version is now derived from the built files themselves, so
236
+ it is stable across restarts and moves when the files do.
237
+
238
+ - **DevTools mounted on production pages.** The provider is gated on the environment, so
239
+ the endpoints are absent outside development — and the client took that to mean it
240
+ could start anyway, pinning a floating panel to the page whose tabs read
241
+ `Could not read the map — HTTP 404`. It now mounts only when the server half says it
242
+ is there, via a `<meta>` the middleware writes, and makes no request at all on a
243
+ public hostname.
244
+
245
+ - **Browser tests drove an unstyled site.** `Router.static("/", public)` is registered
246
+ only for the `web` environment, and a test app is not one — so every `FlowBrowser`
247
+ suite served pages without their stylesheet. Invisible to assertions that read text;
248
+ fatal for anything measuring layout.
249
+
250
+ ### Documented
251
+
252
+ - **Every TypeScript example in the documentation is compiled against the real
253
+ packages**, on every pull request. 1,593 blocks. The gate found examples importing
254
+ symbols that do not exist (`currentUser`, `Layout` from the wrong package), calling
255
+ methods that were renamed (`Cache.put`), and configuring fields with `env()` where the
256
+ type is a literal union. Blocks deliberately written as fragments say so in their
257
+ fence and are recorded by key, so a new one is a deliberate act rather than a silent
258
+ exemption.
259
+
260
+ - **A break cannot ship without a release note.** `api:surface:check` demands a
261
+ regenerated snapshot when an export changes and then goes quiet, so the changelog was
262
+ defended by remembering — and 1.7.3 shipped the removal of Flow's `this.title(…)` with
263
+ no BREAKING entry. That entry is now in 1.7.3's notes, the support policy counts three
264
+ breaks rather than two, and `breaking:check` reads the snapshot diff so the next one
265
+ cannot pass silently.
266
+
267
+ - **A maturity label falls due.** The review release for a package below `stable` lives
268
+ in its `package.json` as `maturityReview`, and the package-conventions gate fails once
269
+ the version reaches it.
270
+
30
271
  ## 1.7.4 — 2026-08-21
31
272
 
32
273
  A debug panel that was reaching production, a column type MySQL would not index, and
@@ -89,7 +330,33 @@ A debug panel that was reaching production, a column type MySQL would not index,
89
330
 
90
331
  ## 1.7.3 — 2026-08-20
91
332
 
92
- Two fields that accepted input and threw it away, and a CI job that was testing nothing.
333
+ Two fields that accepted input and threw it away, a CI job that was testing nothing, and a
334
+ name given back to applications.
335
+
336
+ ### Changed
337
+
338
+ - **BREAKING — `this.title(…)` is removed from Flow components.** Declare `static title`
339
+ instead, as a string or a function of the component:
340
+
341
+ ```ts fragment
342
+ // Before
343
+ override async mount(): Promise<void> {
344
+ this.title(`Search: ${this.query}`);
345
+ }
346
+
347
+ // After
348
+ static title = (c: SearchPage) => (c.query ? `Search: ${c.query}` : "Search");
349
+ ```
350
+
351
+ The instance method held a name four separate components wanted for their own data — a
352
+ media row, a guide, a review, an issue — for a one-line accessor that belongs on the class.
353
+ The static form is also the better one: it is resolved on the server for every render and
354
+ every patch, so a title that depends on state follows it without an action remembering to
355
+ update it.
356
+
357
+ A call to `this.title(…)` on a component that declares its own `title` field now sets that
358
+ field instead of the document title, which is silent. Search your components for
359
+ `this.title(` before upgrading; every hit is either a migration or was already shadowed.
93
360
 
94
361
  ### Fixed
95
362
 
@@ -10,7 +10,7 @@ looking up an address. `@zerotal/client` is the outbound HTTP client — a thin,
10
10
  expressive wrapper over `fetch` that adds what every real integration ends up
11
11
  needing anyway and that is tedious to get right by hand.
12
12
 
13
- ```ts
13
+ ```ts fragment
14
14
  import { Client } from "@zerotal/client";
15
15
 
16
16
  const charge = await Client.post("https://api.stripe.com/v1/charges", {
@@ -71,7 +71,7 @@ otherwise. A 2xx returns; anything else throws, which [Errors](#errors) covers.
71
71
 
72
72
  ### Query parameters
73
73
 
74
- ```ts
74
+ ```ts fragment
75
75
  await Client.get("https://api.example.com/v1/charges", undefined, {
76
76
  query: { limit: 25, status: "succeeded", created: { gte: 1_700_000_000 } },
77
77
  });
@@ -87,7 +87,7 @@ A plain object or array is JSON-encoded with the matching `Content-Type`. Anythi
87
87
  a raw string — passes straight through, so the runtime sets the header itself
88
88
  (including multipart boundaries):
89
89
 
90
- ```ts
90
+ ```ts fragment
91
91
  // Form-encoded, which several gateways still require
92
92
  await Client.post(
93
93
  "https://api.example.com/v1/charges",
@@ -99,7 +99,7 @@ await Client.post(
99
99
 
100
100
  Per request, merged over the client's defaults:
101
101
 
102
- ```ts
102
+ ```ts fragment
103
103
  await Client.post("https://api.example.com/v1/messages", payload, {
104
104
  headers: { "Idempotency-Key": crypto.randomUUID() },
105
105
  });
@@ -125,7 +125,7 @@ export default ClientConfig({
125
125
  A `token` may also be a function, including an async one, which is how credentials
126
126
  that expire are handled — it is resolved per request:
127
127
 
128
- ```ts
128
+ ```ts fragment
129
129
  export default ClientConfig({
130
130
  token: async () => await currentAccessToken(),
131
131
  });
@@ -136,7 +136,7 @@ Change it at runtime with `setToken(token)`, or pass `null` to clear it. Its typ
136
136
 
137
137
  For an API that wants something other than a bearer token, set the header directly:
138
138
 
139
- ```ts
139
+ ```ts fragment
140
140
  export default ClientConfig({
141
141
  headers: { "X-Api-Key": env("PARTNER_API_KEY", "") },
142
142
  });
@@ -147,7 +147,7 @@ export default ClientConfig({
147
147
  `onUnauthorized` receives the error and a `retry` function, so a token can be
148
148
  refreshed and the original request replayed once:
149
149
 
150
- ```ts
150
+ ```ts fragment
151
151
  export default ClientConfig({
152
152
  onUnauthorized: async (error, retry) => retry({ Authorization: `Bearer ${await refresh()}` }),
153
153
  });
@@ -158,7 +158,7 @@ export default ClientConfig({
158
158
  There is no timeout by default, because the right one depends on the upstream. Set a
159
159
  default and override per request:
160
160
 
161
- ```ts
161
+ ```ts fragment
162
162
  export default ClientConfig({ timeout: 10_000 });
163
163
 
164
164
  // This one is slow and we accept that
@@ -174,7 +174,7 @@ A network blip or a `503` is worth trying again; a `422` never is. `retry` retri
174
174
  idempotent requests on network errors, 5xx and 429 with exponential backoff, and
175
175
  honours a `Retry-After` header when the server sends one:
176
176
 
177
- ```ts
177
+ ```ts fragment
178
178
  export default ClientConfig({ retry: 2 });
179
179
 
180
180
  await Client.post("https://api.example.com/v1/charges", body, { retry: false });
@@ -189,7 +189,7 @@ statuses qualify.
189
189
 
190
190
  A non-2xx throws `ApiClientError`, carrying what you need to decide what happened:
191
191
 
192
- ```ts
192
+ ```ts fragment
193
193
  import { ApiClientError } from "@zerotal/client";
194
194
 
195
195
  try {
@@ -219,7 +219,7 @@ everything queued behind it. A `CircuitBreaker` stops after a threshold of
219
219
  consecutive failures, fails fast for a cooldown, then lets a single request through
220
220
  to test the water:
221
221
 
222
- ```ts
222
+ ```ts fragment
223
223
  export default ClientConfig({
224
224
  circuitBreaker: { threshold: 5, cooldownMs: 30_000 },
225
225
  });
@@ -237,7 +237,7 @@ to the same upstream and should trip together.
237
237
 
238
238
  Upload with `FormData`; download by asking for the body you want:
239
239
 
240
- ```ts
240
+ ```ts fragment
241
241
  const form = new FormData();
242
242
  form.append("file", Bun.file("./invoice.pdf"));
243
243
  await Client.post("https://api.example.com/v1/documents", form);
@@ -255,7 +255,7 @@ const pdf = await Client.get("https://api.example.com/v1/documents/doc_1", undef
255
255
  `onResponse` runs after every 2xx with the `ResponseContext`. Both take one function
256
256
  or an array:
257
257
 
258
- ```ts
258
+ ```ts fragment
259
259
  export default ClientConfig({
260
260
  onRequest: (config) => {
261
261
  config.headers["X-Request-Id"] = crypto.randomUUID();
package/docs/commands.md CHANGED
@@ -72,7 +72,7 @@ signature's first token is the name; `{arg}` is required, `{arg?}` optional,
72
72
  `{arg=default}` has a default, `{--flag}` is a boolean flag, and `{--flag=}` /
73
73
  `{--flag=default}` is a string flag.
74
74
 
75
- ```typescript
75
+ ```typescript fragment
76
76
  // in a service provider or bootstrap script
77
77
  const runner = app.container.tryMake("commands");
78
78
 
@@ -110,7 +110,7 @@ so it wins a name collision. The directory is configurable via
110
110
 
111
111
  To register a folder from somewhere else, call `discover()` yourself:
112
112
 
113
- ```typescript
113
+ ```typescript fragment
114
114
  // in a service provider or bootstrap script
115
115
  await runner.discover("./vendor/acme/commands");
116
116
  ```
@@ -124,7 +124,7 @@ returns the list of registered names.
124
124
  The `Command` base class provides coloured output helpers and interactive
125
125
  prompts. The prompts read from stdin and only work on a real TTY:
126
126
 
127
- ```typescript
127
+ ```typescript fragment
128
128
  // inside a command's run()
129
129
  this.info("Success"); // green
130
130
  this.warn("Heads up"); // yellow
@@ -291,7 +291,7 @@ Dev processes
291
291
  Your app has the last word. `app.dev.disable` removes a process by name, and
292
292
  registering the same name again replaces it rather than adding a second tab:
293
293
 
294
- ```ts
294
+ ```ts fragment
295
295
  // config/app.ts
296
296
  export default AppConfig({
297
297
  dev: {
@@ -315,6 +315,38 @@ Packages register their own — see
315
315
  | `bun zt doctor` | Check the app for silent misconfigurations |
316
316
  | `bun zt key:generate` | Generate a new `APP_KEY` and write it to `.env` |
317
317
  | `bun zt lint:packages` | Check every workspace package against convention rules |
318
+ | `bun zt upgrade` | Apply the codemods for a version upgrade |
319
+
320
+ ### Upgrading between versions
321
+
322
+ `bun zt upgrade --to <version>` applies the codemods a version gap calls for —
323
+ mechanical rewrites the framework can make on your behalf when an API changes.
324
+
325
+ ```bash
326
+ bun zt upgrade --to 2.0.0 # print the plan, change nothing
327
+ bun zt upgrade --to 2.0.0 --write # apply it
328
+ ```
329
+
330
+ **It writes nothing unless you ask.** That is the opposite of most tools and
331
+ deliberate: it rewrites source across your whole project, so the first run should
332
+ be something you can read and disagree with. `--from` defaults to the `zerotal`
333
+ version in your `package.json`.
334
+
335
+ The half worth reading is the last one. A codemod that quietly walks past
336
+ something it does not understand is worse than none, because the changes it _did_
337
+ make suggest the job is finished — so anything it recognised and deliberately did
338
+ not touch is listed with a file, a line and a reason:
339
+
340
+ ```text
341
+ 1 place(s) need a decision this cannot make for you:
342
+ app/models/Post.ts:9 export function all<T extends BaseModel>(rows: T[]): T[] {
343
+ `BaseModel` in a type position. It resolves to the same class as `Model`, so
344
+ this compiles either way — renaming it is a readability call, not a
345
+ correctness one.
346
+ ```
347
+
348
+ Run it again after applying: a second run should report no changes. Codemods are
349
+ idempotent, and that is the cheapest way to confirm one did what it said.
318
350
 
319
351
  `doctor` runs every static sanity check against the booted app and prints each
320
352
  finding with its fix: APP_KEY strength, `database.synchronize` colliding with