@zerotal/arch 1.7.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,536 @@
1
+ ---
2
+ title: Package Development
3
+ description: Build a first-party @zerotal package that auto-discovers, merges config, and passes the linter like a native one.
4
+ ---
5
+
6
+ # Package Development
7
+
8
+ Every first-party feature — ORM, auth, cache, mail, queues — is a package under
9
+ `packages/*`, and they all follow the same shape. That uniformity is what lets the
10
+ framework auto-discover providers, merge config, and wire conventions without any
11
+ per-package glue.
12
+
13
+ This page documents the patterns so your own packages feel like native ones, and
14
+ so the package linter passes. Scaffold a new package and you get the whole skeleton
15
+ for free:
16
+
17
+ ```bash
18
+ # in your project root
19
+ bun zt make:package billing
20
+ ```
21
+
22
+ ## Anatomy of a package
23
+
24
+ A package is a small, conventional directory tree. The cache package is a good
25
+ reference:
26
+
27
+ ```text
28
+ # packages/cache/
29
+ packages/cache/
30
+ ├── package.json
31
+ └── src/
32
+ ├── index.ts # public API barrel
33
+ ├── config.ts # CacheConfig() factory + CacheConfigShape
34
+ ├── errors.ts # typed errors extending ZerotalError
35
+ ├── CacheManager.ts # the implementation
36
+ ├── provider/
37
+ │ └── CacheProvider.ts # the ServiceProvider (must live here)
38
+ ├── facades/
39
+ │ └── Cache.ts # optional static facade
40
+ ├── drivers/ # implementation details
41
+ └── commands/
42
+ └── index.ts # CLI commands barrel
43
+ ```
44
+
45
+ Two locations are enforced by convention: **providers live at `src/provider/`**,
46
+ and **the config factory lives at `src/config.ts`**. The linter flags anything
47
+ else.
48
+
49
+ ## package.json
50
+
51
+ ```json
52
+ // packages/cache/package.json
53
+ {
54
+ "name": "@zerotal/cache",
55
+ "version": "0.0.1",
56
+ "maturity": "stable",
57
+ "type": "module",
58
+ "main": "./src/index.ts",
59
+ "types": "./src/index.ts",
60
+ "exports": {
61
+ ".": "./src/index.ts",
62
+ "./commands": "./src/commands/index.ts"
63
+ },
64
+ "scripts": {
65
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm",
66
+ "test": "bun test",
67
+ "typecheck": "tsc --noEmit"
68
+ },
69
+ "dependencies": {
70
+ "@zerotal/core": "workspace:*"
71
+ }
72
+ }
73
+ ```
74
+
75
+ Required by the linter: `"type": "module"` and an `"exports"` (or `"main"`) entry.
76
+ Depend on other Zerotal packages with `workspace:*`. The `maturity` field
77
+ (`experimental` | `beta` | `stable`) declares the package's compatibility promise —
78
+ see [Support policy](/docs/support-policy#maturity-levels) for what each level
79
+ commits to, and state the same level in the package's README and CHANGELOG so it is
80
+ visible from npm. A package cannot be more mature than what it depends on. Subpath
81
+ exports like `./commands` keep CLI code out of the main bundle until it's needed.
82
+
83
+ ## The public barrel
84
+
85
+ Everything a consumer should import is re-exported from `index.ts`. Keep
86
+ implementation files internal; export the manager, the provider, the config
87
+ factory and its shape type, any facade, and the typed error vocabulary.
88
+
89
+ ```typescript
90
+ // packages/cache/src/index.ts
91
+ export { CacheManager } from "./CacheManager.ts";
92
+ export { CacheProvider } from "./provider/CacheProvider.ts";
93
+ export { Cache } from "./facades/Cache.ts";
94
+
95
+ // Config factory + its shape
96
+ export { CacheConfig } from "./config.ts";
97
+ export type { CacheConfigShape } from "./config.ts";
98
+
99
+ // Typed error vocabulary
100
+ export * from "./errors.ts";
101
+ ```
102
+
103
+ ## The provider
104
+
105
+ The provider is the only thing the application boots. It binds your services into
106
+ the container and registers any commands. It must live at `src/provider/` and
107
+ declare both `static provides` and `static environments`.
108
+
109
+ ```typescript
110
+ // packages/cache/src/provider/CacheProvider.ts
111
+ import { ServiceProvider } from "@zerotal/core";
112
+ import type { AppEnvironment } from "@zerotal/core";
113
+ import type { ConfigManager } from "@zerotal/core/config";
114
+ import { CacheManager } from "../CacheManager.ts";
115
+
116
+ // Make the binding token type-safe everywhere via declaration merging.
117
+ declare module "@zerotal/core" {
118
+ interface ContainerBindings {
119
+ cache: CacheManager;
120
+ }
121
+ }
122
+
123
+ export class CacheProvider extends ServiceProvider {
124
+ static override provides = ["cache"] as const;
125
+ static override environments: AppEnvironment[] = ["web", "console", "test", "repl"];
126
+
127
+ override onRegister(): void {
128
+ this.app.container.singleton("cache", () => {
129
+ const config = this.app.container.makeSync("config") as ConfigManager;
130
+ const driver = config.get<string>("cache.driver", "sqlite");
131
+ return new CacheManager(/* … built from config … */);
132
+ });
133
+ }
134
+
135
+ override async onBooted(): Promise<void> {
136
+ // Pre-resolve so the facade (makeSync) works after boot.
137
+ await this.app.container.make("cache");
138
+
139
+ // Register CLI commands lazily — the import only runs when invoked.
140
+ const runner = this.app.container.tryMake("commands");
141
+ runner?.registerLazy("cache:clear", () =>
142
+ import("../commands/CacheClearCommand.ts").then((m) => m.CacheClearCommand),
143
+ );
144
+ }
145
+ }
146
+ ```
147
+
148
+ The hooks a provider typically uses, in lifecycle order:
149
+
150
+ - `onRegister` — container bindings (usually a lazy singleton).
151
+ - `onBooted` — pre-resolve the singleton so a facade works, and `registerLazy()`
152
+ any CLI commands.
153
+
154
+ Key patterns:
155
+
156
+ - **`declare module "@zerotal/core"`** to add your token to `ContainerBindings`,
157
+ so `container.make("cache")` is typed across the whole codebase.
158
+ - **`static provides`** lists the tokens you bind — required for the array form of
159
+ `app.defer([CacheProvider])` and used by the linter.
160
+ - **`static environments`** filters out the provider in modes it shouldn't run in.
161
+ - **`static dependsOn`** declares other packages' providers yours needs at boot —
162
+ e.g. `static dependsOn = [FlowProvider]`. They're pulled in and booted first
163
+ automatically, so an app installing your package never has to know the ordering.
164
+ See [Declaring dependencies](/docs/providers#declaring-dependencies).
165
+ - **Read config inside the factory**, not at module load, so overrides are applied
166
+ and the binding stays lazy.
167
+ - **`registerLazy()`** for commands keeps cold-start cheap.
168
+
169
+ See [Service Providers](/docs/providers) for every lifecycle hook.
170
+
171
+ ## The config factory
172
+
173
+ Each package exposes an `XConfig()` factory in `src/config.ts`: an `XConfigShape`
174
+ interface, a `defaults` object, and a factory that deep-merges user overrides onto
175
+ the defaults. The factory parameter must be `Partial<XConfigShape>`.
176
+
177
+ **All config factories must merge with [`deepMerge`](/docs/helpers#objects-deepmerge)** —
178
+ `return deepMerge(defaults, options)`. This is the one canonical merge strategy:
179
+ do not hand-roll per-field `??` defaulting or manual nested spreads, and do not
180
+ reach for `Object.assign`/`{ ...defaults, ...options }` (a shallow spread silently
181
+ drops sibling keys inside nested objects). A single `deepMerge` call guarantees a
182
+ user who overrides one deep key keeps every other default, returns a fresh object
183
+ that never aliases the shared `defaults`, and is safe against prototype pollution.
184
+
185
+ ```typescript
186
+ // packages/cache/src/config.ts
187
+ import { deepMerge } from "@zerotal/core";
188
+
189
+ export interface CacheConfigShape {
190
+ /** Which cache driver to use. Default: 'sqlite' */
191
+ driver: "sqlite" | "redis" | "memory";
192
+ /** Key prefix prepended to all cache keys. Default: 'zerotal:' */
193
+ prefix: string;
194
+ /** Default TTL in seconds. Default: 3600 */
195
+ ttl: number;
196
+ sqlite: { path: string };
197
+ }
198
+
199
+ const defaults: CacheConfigShape = {
200
+ driver: "sqlite",
201
+ prefix: "zerotal:",
202
+ ttl: 3600,
203
+ sqlite: { path: ":memory:" },
204
+ };
205
+
206
+ /**
207
+ * @example
208
+ * import { CacheConfig } from '@zerotal/cache';
209
+ * export default CacheConfig({ driver: 'memory', ttl: 600 });
210
+ */
211
+ export function CacheConfig(options: Partial<CacheConfigShape> = {}): CacheConfigShape {
212
+ return deepMerge(defaults, options);
213
+ }
214
+ ```
215
+
216
+ | Field | Required | Default | Description |
217
+ | ------------- | -------- | ------------ | -------------------------------------------- |
218
+ | `driver` | no | `"sqlite"` | Which cache driver backs the manager. |
219
+ | `prefix` | no | `"zerotal:"` | Key prefix prepended to every cache key. |
220
+ | `ttl` | no | `3600` | Default time-to-live in seconds. |
221
+ | `sqlite.path` | no | `":memory:"` | SQLite file path; `:memory:` for in-process. |
222
+
223
+ Register the namespace for **typed config dot-paths** by augmenting `ConfigRegistry`
224
+ (the config analogue of `ContainerBindings`) at the bottom of `config.ts`:
225
+
226
+ ```typescript
227
+ // packages/cache/src/config.ts
228
+ declare module "@zerotal/core" {
229
+ interface ConfigRegistry {
230
+ cache: CacheConfigShape;
231
+ }
232
+ }
233
+ ```
234
+
235
+ That makes `config("cache.ttl")` resolve to `number` (and autocomplete) in any app
236
+ that imports your package. See
237
+ [Configuration → Typed dot-paths](/docs/config-system#typed-dot-paths).
238
+
239
+ Using [`deepMerge`](/docs/helpers#objects-deepmerge) means a user who overrides
240
+ `sqlite.path` keeps the default `driver`, `prefix`, and `ttl` — overrides are
241
+ specific, not wholesale. The app then writes a tiny `config/cache.ts`:
242
+
243
+ ```typescript
244
+ // config/cache.ts
245
+ import { CacheConfig } from "@zerotal/cache";
246
+ export default CacheConfig({ driver: "memory" });
247
+ ```
248
+
249
+ Document every option's default in the shape's JSDoc — that's what surfaces to
250
+ developers and what the docs render.
251
+
252
+ > **Warning** — Arrays replace, they don't merge. If an option is an array, a user
253
+ > setting it replaces your default array outright (no concat or de-dupe). Document
254
+ > that on the field, and prefer a name-keyed nested **object** (e.g. `stores`,
255
+ > `disks`) over an array whenever users should be able to add entries without
256
+ > losing the built-ins. See
257
+ > [Objects — `deepMerge()`](/docs/helpers#objects-deepmerge) for the full rule.
258
+
259
+ ## Typed errors
260
+
261
+ Packages define their own error vocabulary, and every error **extends
262
+ `ZerotalError`** (never the native `Error`). This lets the exception handler render
263
+ them consistently and keeps HTTP status mapping in one place.
264
+
265
+ ```typescript
266
+ // packages/cache/src/errors.ts
267
+ import { ZerotalError } from "@zerotal/core";
268
+
269
+ export class CacheConnectionError extends ZerotalError {
270
+ constructor(driver: string) {
271
+ super(`Cache driver '${driver}' failed to connect.`);
272
+ }
273
+ }
274
+ ```
275
+
276
+ For HTTP-facing errors, extend one of the built-in `HttpError` subclasses
277
+ (`NotFoundError`, `ConflictError`, …) so the right status is returned
278
+ automatically. See [Errors](/docs/errors) for the full hierarchy.
279
+
280
+ ## Facades
281
+
282
+ A facade is a thin static proxy that resolves your binding from the container, so
283
+ consumers can write `Cache.get(key)` instead of `container.make("cache")`. Build
284
+ one with `createFacade()` — it infers its type from the binding token — and export
285
+ it from the barrel.
286
+
287
+ ```typescript
288
+ // packages/cache/src/facades/Cache.ts
289
+ import { createFacade } from "@zerotal/core";
290
+
291
+ export const Cache = createFacade("cache");
292
+ ```
293
+
294
+ ```typescript
295
+ function createFacade<K extends keyof ContainerBindings>(key: K): ContainerBindings[K];
296
+ ```
297
+
298
+ Facades rely on the binding being pre-resolved, which is why providers call
299
+ `await container.make(token)` in `onBooted()`.
300
+
301
+ > **Warning** — Accessing a facade before `Application.boot()` finishes throws
302
+ > `FacadeAccessedBeforeBootError`. Never call one at module scope on import — only
303
+ > inside request handlers, commands, or other post-boot code.
304
+
305
+ ## Macros
306
+
307
+ A macro adds a static method to a core class that your package doesn't own, so
308
+ consumers call `Router.flow(...)` as though it shipped with the router. Register
309
+ it in `onRegister()` — that runs before route files load, so the method exists by
310
+ the time an app calls it:
311
+
312
+ ```typescript
313
+ // packages/flow/src/FlowProvider.ts — inside onRegister():
314
+ Router.macro("flow", flowRoute);
315
+ ```
316
+
317
+ The call is untyped on its own; augment the matching interface so consumers get
318
+ completion and type-checking:
319
+
320
+ ```typescript
321
+ // packages/flow/src/types.ts
322
+ declare module "@zerotal/core" {
323
+ interface RouterMacros {
324
+ flow(path: string, page: typeof Component, middleware?: MiddlewareClass[]): void;
325
+ }
326
+ }
327
+ ```
328
+
329
+ This is the mechanism behind `Router.flow()`, and `Str.macro()` follows the same
330
+ shape for string helpers. Reach for it only when the method genuinely belongs on
331
+ the core class — a plain export from your barrel is simpler and easier to trace.
332
+
333
+ ## Contributing conventions
334
+
335
+ If your package introduces a new `app/*` directory that should auto-register at
336
+ boot (the way `app/models` and `app/policies` do), contribute a **concern
337
+ descriptor** from your provider's `onRegister()`. Core stays unaware of your
338
+ package — discovery is push-based.
339
+
340
+ ```typescript
341
+ // packages/webhooks/src/provider/WebhooksProvider.ts
342
+ import type { ConcernDescriptor } from "@zerotal/core";
343
+
344
+ export const webhooksConcern: ConcernDescriptor = {
345
+ name: "webhooks", // also the config key for path overrides
346
+ order: 70, // lower runs first (models=10, observers=20, …)
347
+ dir: "app/webhooks", // scanned relative to the project root
348
+ register(exports, ctx) {
349
+ for (const exported of Object.values(exports)) {
350
+ // …inspect and register each exported class…
351
+ }
352
+ },
353
+ };
354
+
355
+ // in your provider:
356
+ override onRegister(): void {
357
+ this.app.registerConcern?.(webhooksConcern);
358
+ }
359
+ ```
360
+
361
+ A descriptor with `dir` + `register` scans a directory and processes each module;
362
+ one with `run` fires a single hook after its files load (use it for one-shot setup
363
+ like auto-migration). Users can disable discovery or remap directories via
364
+ `config/app.ts` → `conventions`. See [Conventions](/docs/conventions) for the full
365
+ model.
366
+
367
+ > **Tip** — Optional-chain the call (`this.app.registerConcern?.(...)`) so unit
368
+ > tests that construct the provider with a minimal app stub don't need to implement
369
+ > it.
370
+
371
+ ## Registering a dev process
372
+
373
+ If your package ships a companion process — a worker, a listener, a watcher —
374
+ declare it and `bun zt dev` runs it beside the server in its own tab. Otherwise
375
+ every user of your package has to remember a second terminal, and there is no way
376
+ for you to help them.
377
+
378
+ ```typescript
379
+ // packages/webhooks/src/provider/WebhooksProvider.ts
380
+ import type { DevProcessDefinition } from "@zerotal/core";
381
+
382
+ override devProcesses(): DevProcessDefinition[] {
383
+ return [
384
+ {
385
+ name: "webhooks", // identity, and what --only / --without take
386
+ command: ["stripe", "listen"], // raw argv…
387
+ enabled: () => this._configured(), // resolved once, at startup
388
+ restart: "on-failure", // or "always" / "never"
389
+ },
390
+ ];
391
+ }
392
+ ```
393
+
394
+ `command` takes three forms, and the one you want is usually the first:
395
+
396
+ | Form | Runs |
397
+ | ---------------------- | ----------------------------------------------------------------------- |
398
+ | `"queue:work"` | A `zt` command, through the app's own entrypoint |
399
+ | `["stripe", "listen"]` | Raw argv, for a tool that is not a `zt` command |
400
+ | `() => [...]` | The same, computed at startup from config you can only read once booted |
401
+
402
+ Use `run: async (signal) => …` instead of `command` for work with no separate
403
+ binary; the signal aborts on shutdown and on a restart. Set `after: "server"` for
404
+ a process that talks to the server, so it does not spend its restart budget
405
+ against a closed port before the server has bound.
406
+
407
+ Two things worth knowing:
408
+
409
+ - **`enabled` is resolved once, at startup.** A process cannot flicker in and out
410
+ of the deck while dev mode is running, and one that throws while probing
411
+ contributes nothing rather than failing dev mode for everyone else. Use it to
412
+ keep a tab off screen when it would have nothing to do — the queue worker sits
413
+ out under the `sync` driver for exactly this reason.
414
+ - **Your `name` is not private.** An app can replace your process by registering
415
+ the same name, or drop it with `app.dev.disable`. That is deliberate: they know
416
+ their setup better than you do.
417
+
418
+ See [Dev mode and the deck](/docs/commands#dev-mode-and-the-deck) for what the
419
+ user sees.
420
+
421
+ > **Your provider must be active in `web`.** Dev mode boots the app as `web` to
422
+ > ask providers what to run, so a provider whose `static environments` excludes
423
+ > it is never asked — and contributes nothing, silently.
424
+
425
+ ## Registering a doctor check
426
+
427
+ `bun zt doctor` is what a developer (or an agent) runs to find out whether an app
428
+ is wired correctly. Contribute the checks only your package can make:
429
+
430
+ ```typescript
431
+ // packages/webhooks/src/provider/WebhooksProvider.ts
432
+ import type { DoctorCheck } from "@zerotal/core";
433
+
434
+ override doctorChecks(): DoctorCheck[] {
435
+ return [
436
+ {
437
+ id: "webhooks-secret",
438
+ label: "Webhooks",
439
+ run: () => {
440
+ const secret = this.app.container.makeSync("config").get("webhooks.secret");
441
+ if (secret) return { status: "ok", message: "signing secret configured" };
442
+ return {
443
+ status: "warn",
444
+ message: "no signing secret — every delivery will be rejected unverified.",
445
+ fix: "Set WEBHOOKS_SECRET in .env.",
446
+ };
447
+ },
448
+ },
449
+ ];
450
+ }
451
+ ```
452
+
453
+ Keep findings machine-readable, and put the resolution in `fix`. The intended
454
+ last step of an agent's task is `zt doctor`, and "looks fine to me" is not a
455
+ result it can act on. A check that throws is reported as that check failing,
456
+ never as the doctor failing.
457
+
458
+ `app.registerDoctorCheck()` does the same thing imperatively from `onRegister()`.
459
+ Prefer the method — it keeps your checks next to your other contributions.
460
+
461
+ ## Tests
462
+
463
+ Every package must ship at least one `*.test.ts` file — the linter treats their
464
+ absence as a high-severity violation. Co-locate tests next to the code they cover
465
+ (`CacheManager.test.ts` beside `CacheManager.ts`) and run them with `bun test`.
466
+
467
+ ```typescript
468
+ // packages/cache/src/config.test.ts
469
+ import { test, expect } from "bun:test";
470
+ import { CacheConfig } from "./config.ts";
471
+
472
+ test("CacheConfig deep-merges overrides", () => {
473
+ const cfg = CacheConfig({ sqlite: { path: "./cache.db" } });
474
+ expect(cfg.driver).toBe("sqlite"); // default preserved
475
+ expect(cfg.sqlite.path).toBe("./cache.db"); // override applied
476
+ });
477
+ ```
478
+
479
+ ## The package linter
480
+
481
+ `bun zt lint:packages` audits every package against the conventions on this
482
+ page and fails CI on violations. What it checks:
483
+
484
+ | Rule | Severity | Requirement |
485
+ | ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
486
+ | `provider-location` | high | Providers must live at `src/provider/`. |
487
+ | `provider-provides` | medium | A provider that binds a token must declare `static provides`. |
488
+ | `provider-environments` | medium | Every provider declares `static environments`. |
489
+ | `config-factory` | medium | `src/config.ts` must export an `XConfig(...)` factory. |
490
+ | `config-casing` | high | The factory name must be PascalCase. |
491
+ | `config-partial` | high | Its parameter must be `Partial<…Shape>`. |
492
+ | `config-deepmerge` | medium | The factory must merge with `deepMerge(defaults, options)`. |
493
+ | `error-base` | medium | Error classes extend `ZerotalError`, not `Error` (client-bundle code under `client/` is exempt — it defines its own native-`Error` base). |
494
+ | `tests` | high | The package ships at least one `*.test.ts(x)`. |
495
+ | `package-json` / `esm` / `exports` | varies | Valid `package.json`, `"type": "module"`, and `"exports"`/`"main"`. |
496
+
497
+ Run it before opening a PR:
498
+
499
+ ```bash
500
+ # in your project root
501
+ bun zt lint:packages
502
+ ```
503
+
504
+ ## Build checklist
505
+
506
+ 1. `bun zt make:package <name>` to scaffold the skeleton.
507
+ 2. Implement the service; keep internals out of the barrel.
508
+ 3. Add the provider at `src/provider/`, with `provides` + `environments` and a
509
+ `declare module` augmentation for its token.
510
+ 4. Add `src/config.ts` with an `XConfig()` factory using `deepMerge`, documenting
511
+ each default in JSDoc.
512
+ 5. Make errors extend `ZerotalError`.
513
+ 6. Contribute a concern descriptor if you introduce a new `app/*` directory.
514
+ 7. Export everything public from `src/index.ts`.
515
+ 8. Write tests.
516
+ 9. `bun zt lint:packages` until clean.
517
+
518
+ ## References
519
+
520
+ | Member | Signature | Description |
521
+ | --------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
522
+ | `ServiceProvider` | `abstract class ServiceProvider` | Base class for providers; override lifecycle hooks like `onRegister`. |
523
+ | `static provides` | `static provides: readonly string[]` | Tokens the provider binds; used by `app.defer()` and the linter. |
524
+ | `static environments` | `static environments: AppEnvironment[]` | Environments the provider runs in (`web`, `console`, `test`, `repl`). |
525
+ | `static dependsOn` | `static dependsOn: ProviderClass[]` | Other providers this one needs — pulled in and booted first. |
526
+ | `createFacade` | `createFacade<K extends keyof ContainerBindings>(key: K): ContainerBindings[K]` | Build a lazy static facade over a container binding. |
527
+ | `deepMerge` | `deepMerge<T extends object>(base: T, override: Partial<T>): T` | Canonical deep-merge for config factories. |
528
+ | `ZerotalError` | `class ZerotalError extends Error` | Base for all package error vocabularies. |
529
+ | `registerConcern` | `registerConcern(descriptor: ConcernDescriptor): this` | Register an auto-discovery concern from a provider. |
530
+
531
+ ## Next steps
532
+
533
+ - [Service Providers](/docs/providers) — provider lifecycle in depth.
534
+ - [Service Container](/docs/container) — bindings, singletons, and facades.
535
+ - [Conventions](/docs/conventions) — the auto-discovery system.
536
+ - [Configuration](/docs/config-system) — how config files are loaded and merged.