@warlock.js/core 5.23.2 → 5.25.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 (207) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/bin/warlock.js +0 -0
  3. package/esm/application/application.d.mts +21 -0
  4. package/esm/application/application.d.mts.map +1 -1
  5. package/esm/application/application.mjs +29 -1
  6. package/esm/application/application.mjs.map +1 -1
  7. package/esm/application/boot-signal.d.mts +2 -1
  8. package/esm/application/boot-signal.d.mts.map +1 -1
  9. package/esm/application/boot-signal.mjs.map +1 -1
  10. package/esm/application/index.d.mts +1 -0
  11. package/esm/application/index.mjs +1 -0
  12. package/esm/application/roles.d.mts +72 -0
  13. package/esm/application/roles.d.mts.map +1 -0
  14. package/esm/application/roles.mjs +124 -0
  15. package/esm/application/roles.mjs.map +1 -0
  16. package/esm/cli/commands/add.command.mjs.map +1 -1
  17. package/esm/cli/commands/create-database.command.mjs.map +1 -1
  18. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
  20. package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
  21. package/esm/cli/commands/generate/utils/path-resolver.mjs.map +1 -1
  22. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  23. package/esm/cli/commands/routes.command.mjs.map +1 -1
  24. package/esm/cli/commands/seed.command.mjs.map +1 -1
  25. package/esm/cli/commands/start-production.command.mjs +54 -5
  26. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  27. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  28. package/esm/cli/commands/storage-put.command.mjs.map +1 -1
  29. package/esm/cli/commands/typings-generator.command.mjs +17 -1
  30. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  31. package/esm/cli/commands/update.command.mjs.map +1 -1
  32. package/esm/cli/commands-loader.mjs.map +1 -1
  33. package/esm/config/config-loader.d.mts.map +1 -1
  34. package/esm/config/config-loader.mjs +3 -1
  35. package/esm/config/config-loader.mjs.map +1 -1
  36. package/esm/connectors/connectors-manager.mjs.map +1 -1
  37. package/esm/connectors/database-connector.mjs.map +1 -1
  38. package/esm/connectors/herald-connector.mjs.map +1 -1
  39. package/esm/connectors/http-connector.d.mts.map +1 -1
  40. package/esm/connectors/http-connector.mjs +7 -0
  41. package/esm/connectors/http-connector.mjs.map +1 -1
  42. package/esm/connectors/logger-connector.mjs.map +1 -1
  43. package/esm/connectors/mail-connector.mjs.map +1 -1
  44. package/esm/connectors/types.d.mts +8 -0
  45. package/esm/connectors/types.d.mts.map +1 -1
  46. package/esm/connectors/types.mjs.map +1 -1
  47. package/esm/database/create-database-action.mjs.map +1 -1
  48. package/esm/database/drop-tables-action.mjs.map +1 -1
  49. package/esm/database/migrate-action.mjs +2 -1
  50. package/esm/database/migrate-action.mjs.map +1 -1
  51. package/esm/database/models/database-log/database-log.mjs.map +1 -1
  52. package/esm/database/seed-command-action.mjs +2 -1
  53. package/esm/database/seed-command-action.mjs.map +1 -1
  54. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  55. package/esm/dev-server/development-server.mjs +2 -0
  56. package/esm/dev-server/development-server.mjs.map +1 -1
  57. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  58. package/esm/dev-server/layer-executor.mjs.map +1 -1
  59. package/esm/dev-server/manifest-manager.mjs.map +1 -1
  60. package/esm/dev-server/module-loader.mjs +10 -5
  61. package/esm/dev-server/module-loader.mjs.map +1 -1
  62. package/esm/dev-server/register-installed-devtools.mjs +47 -0
  63. package/esm/dev-server/register-installed-devtools.mjs.map +1 -0
  64. package/esm/dev-server/run-connector-typings.mjs +28 -0
  65. package/esm/dev-server/run-connector-typings.mjs.map +1 -0
  66. package/esm/dev-server/special-file-patterns.mjs +3 -1
  67. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  68. package/esm/dev-server/special-files-collector.d.mts +2 -2
  69. package/esm/dev-server/special-files-collector.d.mts.map +1 -1
  70. package/esm/dev-server/special-files-collector.mjs +7 -4
  71. package/esm/dev-server/special-files-collector.mjs.map +1 -1
  72. package/esm/encryption/password.mjs.map +1 -1
  73. package/esm/generations/add-command.action.mjs +3 -1
  74. package/esm/generations/add-command.action.mjs.map +1 -1
  75. package/esm/generations/features/access.feature.mjs.map +1 -1
  76. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
  77. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
  78. package/esm/generations/features/ai-google.feature.mjs.map +1 -1
  79. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
  80. package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
  81. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
  82. package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
  83. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
  84. package/esm/generations/features/ai.feature.mjs.map +1 -1
  85. package/esm/generations/features/devtools.feature.mjs +23 -0
  86. package/esm/generations/features/devtools.feature.mjs.map +1 -0
  87. package/esm/generations/features/herald.feature.mjs.map +1 -1
  88. package/esm/generations/features/image.feature.mjs.map +1 -1
  89. package/esm/generations/features/index.mjs +2 -0
  90. package/esm/generations/features/index.mjs.map +1 -1
  91. package/esm/generations/features/mail.feature.mjs.map +1 -1
  92. package/esm/generations/features/mongodb.feature.mjs.map +1 -1
  93. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  94. package/esm/generations/features/postgres.feature.mjs.map +1 -1
  95. package/esm/generations/features/react-email.feature.mjs.map +1 -1
  96. package/esm/generations/features/react.feature.mjs.map +1 -1
  97. package/esm/generations/features/redis.feature.mjs.map +1 -1
  98. package/esm/generations/features/s3.feature.mjs.map +1 -1
  99. package/esm/generations/features/scheduler.feature.mjs.map +1 -1
  100. package/esm/generations/features/ses.feature.mjs.map +1 -1
  101. package/esm/generations/features/socket.feature.mjs.map +1 -1
  102. package/esm/generations/features/test.feature.mjs +6 -2
  103. package/esm/generations/features/test.feature.mjs.map +1 -1
  104. package/esm/generations/features/types.mjs.map +1 -1
  105. package/esm/generations/features/web.feature.mjs.map +1 -1
  106. package/esm/http/database/RequestLog.mjs.map +1 -1
  107. package/esm/http/index.d.mts +2 -1
  108. package/esm/http/index.mjs +2 -1
  109. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  110. package/esm/http/middleware/inject-request-context.mjs +9 -1
  111. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  112. package/esm/http/request.d.mts +7 -0
  113. package/esm/http/request.d.mts.map +1 -1
  114. package/esm/http/request.mjs +8 -0
  115. package/esm/http/request.mjs.map +1 -1
  116. package/esm/http/response.d.mts +5 -5
  117. package/esm/http/response.d.mts.map +1 -1
  118. package/esm/http/response.mjs.map +1 -1
  119. package/esm/http/server-rebuilt.d.mts +20 -0
  120. package/esm/http/server-rebuilt.d.mts.map +1 -0
  121. package/esm/http/server-rebuilt.mjs +21 -0
  122. package/esm/http/server-rebuilt.mjs.map +1 -0
  123. package/esm/http/tracing/index.mjs +1 -1
  124. package/esm/http/tracing/tracing-dispatcher.d.mts +13 -6
  125. package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -1
  126. package/esm/http/tracing/tracing-dispatcher.mjs +46 -14
  127. package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -1
  128. package/esm/http/tracing/tracing.type.d.mts +2 -1
  129. package/esm/http/tracing/tracing.type.d.mts.map +1 -1
  130. package/esm/http/types.d.mts +9 -0
  131. package/esm/http/types.d.mts.map +1 -1
  132. package/esm/image/image.mjs.map +1 -1
  133. package/esm/index.d.mts +4 -2
  134. package/esm/index.mjs +4 -2
  135. package/esm/loader/app-module-importer.d.mts +17 -0
  136. package/esm/loader/app-module-importer.d.mts.map +1 -0
  137. package/esm/loader/app-module-importer.mjs +20 -0
  138. package/esm/loader/app-module-importer.mjs.map +1 -0
  139. package/esm/logger/logger.mjs.map +1 -1
  140. package/esm/mail/events.d.mts +7 -0
  141. package/esm/mail/events.d.mts.map +1 -1
  142. package/esm/mail/events.mjs +7 -0
  143. package/esm/mail/events.mjs.map +1 -1
  144. package/esm/mail/send-mail.d.mts.map +1 -1
  145. package/esm/mail/send-mail.mjs +23 -2
  146. package/esm/mail/send-mail.mjs.map +1 -1
  147. package/esm/mail/types.d.mts +5 -1
  148. package/esm/mail/types.d.mts.map +1 -1
  149. package/esm/mail/types.mjs.map +1 -1
  150. package/esm/manifest/manifest-manager.mjs.map +1 -1
  151. package/esm/production/build-contributions.mjs +19 -3
  152. package/esm/production/build-contributions.mjs.map +1 -1
  153. package/esm/production/production-builder.mjs +28 -8
  154. package/esm/production/production-builder.mjs.map +1 -1
  155. package/esm/production/production-supervisor.mjs +6 -0
  156. package/esm/production/production-supervisor.mjs.map +1 -1
  157. package/esm/production/resolve-build-config.d.mts +1 -1
  158. package/esm/production/resolve-build-config.mjs +15 -0
  159. package/esm/production/resolve-build-config.mjs.map +1 -1
  160. package/esm/production/route-registration-child.mjs +14 -2
  161. package/esm/production/route-registration-child.mjs.map +1 -1
  162. package/esm/production/route-registration-snapshot.mjs +9 -2
  163. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  164. package/esm/resource/resource-field-builder.mjs +1 -1
  165. package/esm/resource/resource-field-builder.mjs.map +1 -1
  166. package/esm/resource/resource.d.mts +5 -0
  167. package/esm/resource/resource.d.mts.map +1 -1
  168. package/esm/resource/resource.mjs +17 -6
  169. package/esm/resource/resource.mjs.map +1 -1
  170. package/esm/router/router.d.mts.map +1 -1
  171. package/esm/router/router.mjs +1 -0
  172. package/esm/router/router.mjs.map +1 -1
  173. package/esm/router/types.d.mts +2 -1
  174. package/esm/router/types.d.mts.map +1 -1
  175. package/esm/tests/index.d.mts +3 -2
  176. package/esm/tests/index.mjs +3 -2
  177. package/esm/tests/vitest-setup.d.mts +16 -1
  178. package/esm/tests/vitest-setup.d.mts.map +1 -1
  179. package/esm/tests/vitest-setup.mjs +20 -2
  180. package/esm/tests/vitest-setup.mjs.map +1 -1
  181. package/esm/use-cases/use-case-events.mjs.map +1 -1
  182. package/esm/utils/is-test-file.mjs +15 -0
  183. package/esm/utils/is-test-file.mjs.map +1 -0
  184. package/esm/utils/normalized-path.d.mts +3 -1
  185. package/esm/utils/normalized-path.d.mts.map +1 -1
  186. package/esm/utils/normalized-path.mjs +4 -2
  187. package/esm/utils/normalized-path.mjs.map +1 -1
  188. package/esm/utils/paths.mjs.map +1 -1
  189. package/esm/validation/plugins/database-plugin.mjs.map +1 -1
  190. package/esm/validation/validateAll.mjs +9 -3
  191. package/esm/validation/validateAll.mjs.map +1 -1
  192. package/esm/warlock-config/default-configurations.mjs +1 -0
  193. package/esm/warlock-config/default-configurations.mjs.map +1 -1
  194. package/esm/warlock-config/types.d.mts +7 -0
  195. package/esm/warlock-config/types.d.mts.map +1 -1
  196. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  197. package/llms-full.txt +264 -139
  198. package/package.json +15 -11
  199. package/skills/configure-app/SKILL.md +2 -0
  200. package/skills/define-resource/SKILL.md +4 -0
  201. package/skills/request-tracing/SKILL.md +38 -15
  202. package/skills/run-app/SKILL.md +71 -34
  203. package/skills/send-mail/SKILL.md +55 -25
  204. package/skills/test-http/SKILL.md +19 -8
  205. package/skills/test-service/SKILL.md +61 -40
  206. package/skills/warlock-conventions/SKILL.md +26 -18
  207. package/skills/write-cli-command/SKILL.md +50 -45
package/llms-full.txt CHANGED
@@ -618,8 +618,8 @@ export class UsersRestful extends Restful<User> implements RouteResource {
618
618
  public validation = {
619
619
  create: {
620
620
  schema: v.object({
621
- name: v.string().required().min(2),
622
- email: v.email().required(),
621
+ name: v.string().min(2),
622
+ email: v.email(),
623
623
  }),
624
624
  },
625
625
  update: {
@@ -3148,6 +3148,7 @@ type TracingContext = {
3148
3148
  type TracingPhaseInfo = {
3149
3149
  name: string;
3150
3150
  durationMs: number;
3151
+ startedAt?: number; // epoch milliseconds; supplied when the phase started
3151
3152
  attrs?: Record<string, unknown>;
3152
3153
  };
3153
3154
 
@@ -3166,29 +3167,52 @@ type TracingHooks = {
3166
3167
 
3167
3168
  Every verb is optional — a hook that only wants phase spans need not implement
3168
3169
  `onRequestStart`/`onRequestEnd`. Register as many hooks as you like via
3169
- `hooks: TracingHooks[]`; each fires independently.
3170
+ `hooks: TracingHooks[]`; each fires independently. Use `registerTracingHooks()`
3171
+ when the observer is installed by application code rather than static config:
3172
+
3173
+ ```ts
3174
+ import { registerTracingHooks, type TracingHooks } from "@warlock.js/core";
3175
+
3176
+ const hooks: TracingHooks = {
3177
+ onPhase(ctx, phase) {
3178
+ console.log(ctx.traceId, phase.name, phase.startedAt);
3179
+ },
3180
+ };
3181
+
3182
+ const unregister = registerTracingHooks(hooks);
3183
+ // Later, for example during plugin teardown:
3184
+ unregister();
3185
+ ```
3186
+
3187
+ `registerTracingHooks(hooks): () => void` enables tracing immediately. Runtime
3188
+ hooks run after configured hooks; calling the returned unregister function more
3189
+ than once is safe. When a phase did not provide `startedAt`, the dispatcher
3190
+ derives it from its duration before delivering the callback.
3170
3191
 
3171
3192
  ## Phase names
3172
3193
 
3173
3194
  `core` wires five phases into the request lifecycle, in this order, for every
3174
3195
  HTTP request:
3175
3196
 
3176
- | Phase | Fires around |
3177
- | --- | --- |
3178
- | `route.match` | Resolving the incoming path/method to a registered route |
3179
- | `middleware` | Each middleware in the route's chain — one `onPhase` call per middleware, with `attrs: { name, index }` |
3180
- | `validation` | The route's input validation (`v.object(...)` / RESTful resource validation) |
3181
- | `handler` | The route handler itself |
3197
+ | Phase | Fires around |
3198
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
3199
+ | `route.match` | Resolving the incoming path/method to a registered route |
3200
+ | `middleware` | Each middleware in the route's chain — one `onPhase` call per middleware, with `attrs: { name, index }` |
3201
+ | `validation` | The route's input validation (`v.object(...)` / RESTful resource validation) |
3202
+ | `handler` | The route handler itself |
3182
3203
  | `response.write` | The overall request span, closed once the response has settled (success or thrown error) — this is also where `onRequestEnd` fires |
3183
3204
 
3184
3205
  `@warlock.js/web` page requests report through this same `onPhase` surface
3185
3206
  instead of adding a separate hook API. They add three phases:
3186
3207
 
3187
- | Phase | Fires around |
3188
- | --- | --- |
3189
- | `loader` | Each loader level, once per app/layout/page, with `attrs: { level, layoutPath? }` |
3190
- | `render.shell` | Time from render start until React's shell is ready to stream |
3191
- | `stream.end` | The whole streamed response, including every `defer()` value settling |
3208
+ | Phase | Fires around |
3209
+ | ----------------- | --------------------------------------------------------------------------------------------------- |
3210
+ | `loader` | Each loader level, once per app/layout/page, with `attrs: { level, layoutPath? }` |
3211
+ | `render.shell` | Time from render start until React's shell is ready to stream |
3212
+ | `stream.end` | The whole streamed response, including every `defer()` value settling |
3213
+ | `page.middleware` | Page middleware chain; `attrs: { count, outcome }`, where outcome is `next`, `response`, or `error` |
3214
+ | `page.cache` | Page-cache lookup; `attrs: { status }`, where status is `hit`, `miss`, or `bypass` |
3215
+ | `defer.settle` | A deferred key settling; `attrs: { key, status }`, where status is `fulfilled` or `rejected` |
3192
3216
 
3193
3217
  ## Trace id derivation
3194
3218
 
@@ -3282,9 +3306,8 @@ export default {
3282
3306
  - **No OTel dependency, and none planned for `core`.** An OTel (or other
3283
3307
  vendor) bridge is a separate, optional package that subscribes to these
3284
3308
  hooks — never add `@opentelemetry/api` to `core` itself.
3285
- - **`web`'s phases aren't live yet.** Don't register a hook expecting
3286
- `loader`/`render.shell`/`stream.end` calls today; only the five `core`
3287
- phases fire in this release.
3309
+ - **Web phases share the same hook.** Do not create a second page-tracing
3310
+ integration: subscribe with `TracingHooks.onPhase`.
3288
3311
  - **`route` is `undefined` until routing has matched.** There's no path where
3289
3312
  a hook fires before that, but code branching on `ctx.route` for an
3290
3313
  early-failing request (e.g. a 404 with no match) must handle `undefined`.
@@ -3620,11 +3643,11 @@ Boots the framework in dev mode: file watcher, HMR-style module reload, on-disk
3620
3643
 
3621
3644
  ### Flags
3622
3645
 
3623
- | Flag | Default | Purpose |
3624
- | --------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
3646
+ | Flag | Default | Purpose |
3647
+ | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
3625
3648
  | `--fresh, -f` | off | Delete `.warlock/manifest.json` before start — forces a full re-parse from disk. Use after odd boot states. |
3626
- | `--skip-typings, -st` | off | Skip background type generation **for this run**. Overrides `devServer.generateTypings` config. |
3627
- | `--skip-health, -sh` | off | Skip file health checkers **for this run**. Overrides `devServer.healthCheckers` config. |
3649
+ | `--skip-typings, -st` | off | Skip background type generation **for this run**. Overrides `devServer.generateTypings` config. |
3650
+ | `--skip-health, -sh` | off | Skip file health checkers **for this run**. Overrides `devServer.healthCheckers` config. |
3628
3651
 
3629
3652
  When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.*` value applies. When passed, the flag wins.
3630
3653
 
@@ -3632,14 +3655,14 @@ When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.
3632
3655
 
3633
3656
  Once the server is ready, `warlock dev` listens for single keypresses:
3634
3657
 
3635
- | Key | Does |
3636
- | -------- | ---------------------------------------------------------------------------- |
3637
- | `r` | Restart the server on a fresh process. |
3638
- | `c` | Clear the console. |
3639
- | `q` | Graceful shutdown, exit `0` — the same path as `Ctrl+C`. |
3640
- | `h` | Print the shortcuts that are armed right now. |
3658
+ | Key | Does |
3659
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3660
+ | `r` | Restart the server on a fresh process. |
3661
+ | `c` | Clear the console. |
3662
+ | `q` | Graceful shutdown, exit `0` — the same path as `Ctrl+C`. |
3663
+ | `h` | Print the shortcuts that are armed right now. |
3641
3664
  | `u` | **Only while an update notice is showing** — update every `@warlock.js/*` package, install, and restart. See [`update-packages/SKILL.md`](../update-packages/SKILL.md). |
3642
- | `Ctrl+C` | Graceful shutdown, unchanged. |
3665
+ | `Ctrl+C` | Graceful shutdown, unchanged. |
3643
3666
 
3644
3667
  A "restart" is a fresh process (`r` and `u` both use it) — see [The supervisor](#the-supervisor) below for how that works.
3645
3668
 
@@ -3659,7 +3682,7 @@ shell
3659
3682
 
3660
3683
  The supervisor spawns the worker with `stdio: "inherit"` and mirrors its exit code. To restart, the worker shuts down cleanly — freeing the http port — and exits `75`; the supervisor spawns a replacement. **The tree never gets deeper**, however many restarts happen.
3661
3684
 
3662
- The supervisor is chosen in the `dev` command's `preAction`, which runs *before* the preloaders — deliberately, so the supervisor never loads config or starts connectors. A supervisor holding a second database connection open for the session would be a real bug, not a cosmetic one.
3685
+ The supervisor is chosen in the `dev` command's `preAction`, which runs _before_ the preloaders — deliberately, so the supervisor never loads config or starts connectors. A supervisor holding a second database connection open for the session would be a real bug, not a cosmetic one.
3663
3686
 
3664
3687
  Signals: `SIGINT` reaches the worker directly (same process group), so the supervisor ignores it and only notes that the next exit is final — acting on it would exit the parent while the worker was still draining. `SIGTERM` / `SIGHUP` are forwarded explicitly, since they don't propagate to the group on Windows. After any worker exits, the supervisor puts stdin back out of raw mode in case the worker was killed before it could.
3665
3688
 
@@ -3673,13 +3696,13 @@ A worker that dies **after running healthily for at least 5s** — OOM, a native
3673
3696
  14:31:02 Development server was killed by SIGSEGV — restarting.
3674
3697
  ```
3675
3698
 
3676
- A worker that dies *sooner* than that failed to **boot** — a broken config, a port already taken — and it has already printed why. Restarting there would just reprint the same error and bury it, so the supervisor mirrors the exit code and stops.
3699
+ A worker that dies _sooner_ than that failed to **boot** — a broken config, a port already taken — and it has already printed why. Restarting there would just reprint the same error and bury it, so the supervisor mirrors the exit code and stops.
3677
3700
 
3678
- A busy port is now caught *before* the bind attempt: the HTTP connector calls `assertPortIsAvailable(port, host)` immediately before `listen()`, so the failure is `Port <port> is already in use on <host>. Stop the dev server (or whatever else is listening on port <port>) and run again, or start on a free port…` — not a raw `EADDRINUSE` surfacing from inside Fastify. Same connector, same preflight, for `warlock start`.
3701
+ A busy port is now caught _before_ the bind attempt: the HTTP connector calls `assertPortIsAvailable(port, host)` immediately before `listen()`, so the failure is `Port <port> is already in use on <host>. Stop the dev server (or whatever else is listening on port <port>) and run again, or start on a free port…` — not a raw `EADDRINUSE` surfacing from inside Fastify. Same connector, same preflight, for `warlock start`.
3679
3702
 
3680
3703
  Flapping is capped: more than 3 crashes inside 60s and the supervisor gives up rather than restarting behind your back.
3681
3704
 
3682
- An explicit restart (`r`, `u`, a config change) is a *request*, not a crash, so the uptime rule never swallows it.
3705
+ An explicit restart (`r`, `u`, a config change) is a _request_, not a crash, so the uptime rule never swallows it.
3683
3706
 
3684
3707
  ### Restart on config change
3685
3708
 
@@ -3695,7 +3718,7 @@ Set `devServer.restartOnConfigChange: false` for the previous behaviour (a warni
3695
3718
 
3696
3719
  On first boot `warlock dev` bundles its ESM loader hook and writes it to **your project's** `.warlock/loader-hook.mjs` — the hook runs in a fresh Node worker thread with no TypeScript loader of its own, so it has to be plain, pre-bundled ESM.
3697
3720
 
3698
- Because that file lives in your directory rather than core's, every npm import inside it is rewritten at generation time to an **absolute path resolved from core's own install**. A bare `import "esbuild"` there would resolve from *your* `node_modules`, and `esbuild` / `get-tsconfig` are core's dependencies, not yours.
3721
+ Because that file lives in your directory rather than core's, every npm import inside it is rewritten at generation time to an **absolute path resolved from core's own install**. A bare `import "esbuild"` there would resolve from _your_ `node_modules`, and `esbuild` / `get-tsconfig` are core's dependencies, not yours.
3699
3722
 
3700
3723
  :::note[Fixed in 4.9.2 — pnpm users]
3701
3724
  Before 4.9.2 those imports were left bare. npm and yarn hoist every transitive dependency into one flat tree, so they resolved by accident; pnpm's strict layout does not, and the dev server failed with `ERR_MODULE_NOT_FOUND: Cannot find package 'esbuild'`. The workaround was declaring `esbuild` and `get-tsconfig` in your own `package.json` — no longer needed, and you can drop them.
@@ -3757,12 +3780,12 @@ esbuild bundle of the app down to a single JS file in `dist/`. No flags — ever
3757
3780
  ```ts title="warlock.config.ts"
3758
3781
  export default defineConfig({
3759
3782
  build: {
3760
- outdir: "dist", // default — relative or absolute
3761
- outFile: "app.js", // default — bundle filename
3762
- minify: true, // default — esbuild minify
3763
- sourcemap: true, // default — true | false | "inline" | "linked"
3764
- singleBundle: false, // default — one runnable file, deps inlined
3765
- esmShim: true, // default — require/__filename/__dirname for bundled CJS
3783
+ outdir: "dist", // default — relative or absolute
3784
+ outFile: "app.js", // default — bundle filename
3785
+ minify: true, // default — esbuild minify
3786
+ sourcemap: true, // default — true | false | "inline" | "linked"
3787
+ singleBundle: false, // default — one runnable file, deps inlined
3788
+ esmShim: true, // default — require/__filename/__dirname for bundled CJS
3766
3789
  },
3767
3790
  });
3768
3791
  ```
@@ -3774,7 +3797,7 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
3774
3797
  - **`minify: false`** — flip to debug a production-only bug. Larger bundle, readable stack traces.
3775
3798
  - **`sourcemap: "inline"`** — embed the source map in the bundle. Useful when your error reporter only captures the bundle and can't fetch a `.map` sidecar.
3776
3799
  - **`sourcemap: false`** — skip source maps entirely. Smaller artifact, but stack traces in production logs lose their file:line precision (and `warlock start` will not enable `--enable-source-maps` since there's nothing to map).
3777
- - **`singleBundle: true`** — inline dependencies so `node dist/app.js` runs with no `node_modules` and no launcher. Sets `packages: "bundle"` + `splitting: false` as **defaults you can override**. ⚠ Native `.node` addons are still emitted alongside — "single bundle" is one JS file *plus* any native addons. Do NOT reach for it as the default: keeping deps external is right when you deploy the folder.
3800
+ - **`singleBundle: true`** — inline dependencies so `node dist/app.js` runs with no `node_modules` and no launcher. Sets `packages: "bundle"` + `splitting: false` as **defaults you can override**. ⚠ Native `.node` addons are still emitted alongside — "single bundle" is one JS file _plus_ any native addons. Do NOT reach for it as the default: keeping deps external is right when you deploy the folder.
3778
3801
  - **`esmShim: false`** — only if you are certain nothing in the graph is CommonJS. Leave it on.
3779
3802
 
3780
3803
  ⚠ **The trap this replaced.** Setting `packages: "bundle"` by hand used to produce a clean build whose process died on start with `Error: Dynamic require of "node:assert" is not supported`. The output is ESM; bundled CJS deps call `require(...)` and read `__dirname`, and neither exists in an ES module, so the bundler substitutes a throwing stub. **A green `warlock build` was not evidence the bundle ran.** `esmShim` now injects `createRequire(import.meta.url)` and friends automatically for any ESM build, so both `singleBundle` and a hand-written `packages: "bundle"` work. An existing hand-written `banner` is preserved — the shim is prepended, not substituted.
@@ -3833,6 +3856,41 @@ npx warlock start --inspect # → spawns node --enable-source-maps
3833
3856
  npx warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
3834
3857
  ```
3835
3858
 
3859
+ ### Deploy one build in parts
3860
+
3861
+ One production build can run as API, page, and background-worker processes.
3862
+ Omit `--role` to run every role (the existing default); combine roles with a
3863
+ comma-separated list:
3864
+
3865
+ ```bash
3866
+ npx warlock start --role=api
3867
+ npx warlock start --role=web --sites=admin,storefront
3868
+ npx warlock start --role=worker
3869
+ npx warlock start --role=web,api
3870
+ ```
3871
+
3872
+ `--sites=a,b` is valid only with the `web` role and installs the listed site
3873
+ keys' pages (shared non-site pages remain installed). Unknown roles, an empty
3874
+ list, `--sites` without `web`, and unknown site keys fail boot rather than
3875
+ silently serving the wrong traffic.
3876
+
3877
+ An API or web role binds HTTP. A worker-only process binds no HTTP port; it
3878
+ loads worker work, starts queue workers and the scheduler there, and its
3879
+ readiness banner reports `worker ready`. `warlock dev` always runs every role.
3880
+
3881
+ Inside application code, inspect the roles resolved at bootstrap:
3882
+
3883
+ ```ts
3884
+ import { Application } from "@warlock.js/core";
3885
+
3886
+ if (Application.hasRole("worker")) {
3887
+ // worker-only integration
3888
+ }
3889
+
3890
+ Application.roles; // ReadonlySet<"api" | "web" | "worker">
3891
+ Application.sites; // ReadonlySet<string> | undefined (undefined = all sites)
3892
+ ```
3893
+
3836
3894
  Everything you pass after `start` is forwarded to the spawned Node process. Use this to attach a debugger (`--inspect`), tune memory (`--max-old-space-size`), or pass any other Node flag without editing the command.
3837
3895
 
3838
3896
  ### Source maps
@@ -3875,9 +3933,9 @@ Nothing else is needed for pnpm. Warlock never requires an app to declare a pack
3875
3933
 
3876
3934
  **A success line on stdout means the app is serving requests.** That is a contract, not a convention, and you can build a CI gate or a health probe on it.
3877
3935
 
3878
- | Stream | Carries |
3879
- | ---------- | -------------------------------------------------------------- |
3880
- | **stdout** | the started banner, and start failures. Nothing else. |
3936
+ | Stream | Carries |
3937
+ | ---------- | -------------------------------------------------------------------------------------- |
3938
+ | **stdout** | the started banner, and start failures. Nothing else. |
3881
3939
  | **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
3882
3940
 
3883
3941
  The started banner prints **only** when the running application reports a completed boot — not when the command starts, not when the child is spawned. A child that dies before reporting is a failed start: the failure is written to **both** streams (stderr for humans and log collectors, stdout so a supervisor greping for the banner finds a failure rather than silence), and `warlock start` exits non-zero **even when the child itself exited `0`**.
@@ -3913,7 +3971,7 @@ else is listening on port 3000) and run again, or start on a free port — e.g.
3913
3971
  startHttpTestServer({ port: 3001 }).
3914
3972
  ```
3915
3973
 
3916
- **The process exits `78` (`EX_CONFIG`), and `warlock dev`'s supervisor treats that as terminal — it prints once and stops rather than restarting.** That code is the counterpart of `75`, which a worker uses to ask for a fresh process. The distinction is the point: a code error is worth restarting, and a *startup precondition* — a port held by something else, a port value that is not a port, a boot validator that rejected — cannot change because we tried again six seconds later. Retrying one only reprints the diagnostic and then scrolls it away.
3974
+ **The process exits `78` (`EX_CONFIG`), and `warlock dev`'s supervisor treats that as terminal — it prints once and stops rather than restarting.** That code is the counterpart of `75`, which a worker uses to ask for a fresh process. The distinction is the point: a code error is worth restarting, and a _startup precondition_ — a port held by something else, a port value that is not a port, a boot validator that rejected — cannot change because we tried again six seconds later. Retrying one only reprints the diagnostic and then scrolls it away.
3917
3975
 
3918
3976
  `EACCES` on the port is treated the same way as `EADDRINUSE` (a privileged port you may not bind is also "not available"); anything else the probe throws is rethrown untouched.
3919
3977
 
@@ -3928,7 +3986,7 @@ Configured http.port "03999" normalised to 3999.
3928
3986
  `warlock start` spawns the bundle with an IPC channel and sets `WARLOCK_BOOT_SIGNAL=1` on it. `Application.markBooted()` — which the production entry calls after the late-phase connectors (http, socket) are up — sends one versioned message and closes the channel:
3929
3987
 
3930
3988
  ```ts
3931
- { type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port? }
3989
+ { type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port?, roles }
3932
3990
  ```
3933
3991
 
3934
3992
  Three consequences worth knowing:
@@ -3943,11 +4001,11 @@ If you need the same fact inside the app, use `Application.onceBooted()` / `Appl
3943
4001
 
3944
4002
  `Application.environment` and `Application.runtimeStrategy` are separate axes:
3945
4003
 
3946
- | Mode | `environment` | `runtimeStrategy` | How |
3947
- | ---------------- | --------------- | ----------------- | ----------------------------------------- |
3948
- | `warlock dev` | `development`* | `development` | preload force-sets `runtimeStrategy` |
3949
- | `warlock build` | n/a (no app boots) | n/a | only loads warlock.config.ts |
3950
- | `warlock start` | `production`* | `production`* | usually set via `NODE_ENV` in the env |
4004
+ | Mode | `environment` | `runtimeStrategy` | How |
4005
+ | --------------- | ------------------ | ----------------- | ------------------------------------- |
4006
+ | `warlock dev` | `development`* | `development` | preload force-sets `runtimeStrategy` |
4007
+ | `warlock build` | n/a (no app boots) | n/a | only loads warlock.config.ts |
4008
+ | `warlock start` | `production`* | `production`* | usually set via `NODE_ENV` in the env |
3951
4009
 
3952
4010
  `*` — `environment` follows `NODE_ENV`. The dev command doesn't force it, but the default in most projects is `development`. `start` doesn't force it either; deployments set `NODE_ENV=production` themselves.
3953
4011
 
@@ -4006,7 +4064,7 @@ export default defineConfig({
4006
4064
 
4007
4065
  CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
4008
4066
 
4009
- **This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated *before* any `.env` file was read, so every `env()` call in it returned its default — silently, under every command. If you copied this recipe earlier and concluded that `BUILD_OUT` was ignored, it was.
4067
+ **This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated _before_ any `.env` file was read, so every `env()` call in it returned its default — silently, under every command. If you copied this recipe earlier and concluded that `BUILD_OUT` was ignored, it was.
4010
4068
 
4011
4069
  **Which file the value comes from is decided by `NODE_ENV`, and no Warlock command sets it.** `env()` reads `.env.<NODE_ENV>` when that file exists and falls back to plain `.env`. So:
4012
4070
 
@@ -4086,10 +4144,7 @@ Two APIs over the same engine. The fluent `Mail` builder reads top-to-bottom in
4086
4144
  ```ts
4087
4145
  import { Mail } from "@warlock.js/core";
4088
4146
 
4089
- await Mail.to("user@example.com")
4090
- .subject("Welcome!")
4091
- .text("Thanks for joining.")
4092
- .send();
4147
+ await Mail.to("user@example.com").subject("Welcome!").text("Thanks for joining.").send();
4093
4148
  ```
4094
4149
 
4095
4150
  Or:
@@ -4133,10 +4188,25 @@ For named mailers (multi-provider apps), export a `MailersConfig` instead:
4133
4188
 
4134
4189
  ```ts
4135
4190
  const config: MailersConfig = {
4136
- default: { host: "smtp.sendgrid.net", port: 587, username: "apikey", password: env("SENDGRID_KEY") },
4191
+ default: {
4192
+ host: "smtp.sendgrid.net",
4193
+ port: 587,
4194
+ username: "apikey",
4195
+ password: env("SENDGRID_KEY"),
4196
+ },
4137
4197
  mailers: {
4138
- marketing: { host: "smtp.mailchimp.com", port: 587, username: env("MC_USER"), password: env("MC_PASS") },
4139
- transactional: { host: "smtp.postmark.com", port: 587, username: env("PM_USER"), password: env("PM_PASS") },
4198
+ marketing: {
4199
+ host: "smtp.mailchimp.com",
4200
+ port: 587,
4201
+ username: env("MC_USER"),
4202
+ password: env("MC_PASS"),
4203
+ },
4204
+ transactional: {
4205
+ host: "smtp.postmark.com",
4206
+ port: 587,
4207
+ username: env("PM_USER"),
4208
+ password: env("PM_PASS"),
4209
+ },
4140
4210
  },
4141
4211
  };
4142
4212
  ```
@@ -4168,9 +4238,9 @@ The mail pipeline switches behavior on a global mode:
4168
4238
  ```ts
4169
4239
  import { setMailMode } from "@warlock.js/core";
4170
4240
 
4171
- setMailMode("production"); // actually send (default)
4172
- setMailMode("development"); // log subject + recipient, no send
4173
- setMailMode("test"); // capture to test mailbox, no send
4241
+ setMailMode("production"); // actually send (default)
4242
+ setMailMode("development"); // log subject + recipient, no send
4243
+ setMailMode("test"); // capture to test mailbox, no send
4174
4244
  ```
4175
4245
 
4176
4246
  Production sends. Development logs and returns a fake-success `MailResult`. Test captures every call into an in-memory mailbox (cleared between tests) and returns success — handy for asserting "the welcome email got sent" without hitting SMTP.
@@ -4183,7 +4253,7 @@ Production sends. Development logs and returns a fake-success `MailResult`. Test
4183
4253
 
4184
4254
  ```ts
4185
4255
  await Mail.to("a@example.com")
4186
- .to(["a@example.com", "b@example.com"]) // override
4256
+ .to(["a@example.com", "b@example.com"]) // override
4187
4257
  .cc("manager@example.com")
4188
4258
  .bcc("audit@example.com")
4189
4259
  .replyTo("support@example.com")
@@ -4224,15 +4294,15 @@ await Mail.to("u@e.com")
4224
4294
 
4225
4295
  ### Other knobs
4226
4296
 
4227
- | Method | Purpose |
4228
- | ------------------------------------- | --------------------------------------------- |
4229
- | `.priority("high" \| "normal" \| "low")` | priority header |
4230
- | `.headers({ "X-Foo": "bar" })` | replace custom headers |
4231
- | `.header("X-Foo", "bar")` | add one header |
4232
- | `.tags(["welcome"])` / `.tag("transactional")` | categorization tags |
4233
- | `.correlationId("req-123")` | tracking id (logged with sends) |
4234
- | `.config(MailConfigurations)` | one-off override of the global config |
4235
- | `.mailer("marketing")` | route via a named mailer from config |
4297
+ | Method | Purpose |
4298
+ | ---------------------------------------------- | ------------------------------------- |
4299
+ | `.priority("high" \| "normal" \| "low")` | priority header |
4300
+ | `.headers({ "X-Foo": "bar" })` | replace custom headers |
4301
+ | `.header("X-Foo", "bar")` | add one header |
4302
+ | `.tags(["welcome"])` / `.tag("transactional")` | categorization tags |
4303
+ | `.correlationId("req-123")` | tracking id (logged with sends) |
4304
+ | `.config(MailConfigurations)` | one-off override of the global config |
4305
+ | `.mailer("marketing")` | route via a named mailer from config |
4236
4306
 
4237
4307
  ### Per-mail event handlers
4238
4308
 
@@ -4245,9 +4315,9 @@ await Mail.to("u@e.com")
4245
4315
  .beforeSending((mail) => {
4246
4316
  // mutate `mail` or return false to cancel
4247
4317
  })
4248
- .onSent((mail, result, error) => {}) // always fires after attempt
4249
- .onSuccess((mail, result) => {}) // only on success
4250
- .onError((mail, error) => {}) // only on failure
4318
+ .onSent((mail, result, error) => {}) // always fires after attempt
4319
+ .onSuccess((mail, result) => {}) // only on success
4320
+ .onError((mail, error) => {}) // only on failure
4251
4321
  .send();
4252
4322
  ```
4253
4323
 
@@ -4317,6 +4387,23 @@ mailEvents.onBeforeSending((mail) => {
4317
4387
 
4318
4388
  Global hooks fire for **every** mail. Per-mail handlers from the builder / `sendMail` fire only for that one send.
4319
4389
 
4390
+ ### Observe captured development and test mail
4391
+
4392
+ Development and test sends create a `CapturedMail` and synchronously emit the
4393
+ global `mail.captured` event. Subscribe with `mailEvents.onCaptured`:
4394
+
4395
+ ```ts
4396
+ import { mailEvents, type CapturedMail } from "@warlock.js/core";
4397
+
4398
+ mailEvents.onCaptured((mail: CapturedMail) => {
4399
+ console.log(mail.id, mail.options.subject);
4400
+ });
4401
+ ```
4402
+
4403
+ `CapturedMail.id` is the mail's generated or supplied id. This event is for
4404
+ development/test tooling; production sends do not emit it. Listener failures
4405
+ are logged and never interrupt mail delivery.
4406
+
4320
4407
  For correlated tracking of a specific mail, generate an id and subscribe by id:
4321
4408
 
4322
4409
  ```ts
@@ -4370,10 +4457,11 @@ Captured mail shape:
4370
4457
 
4371
4458
  ```ts
4372
4459
  type CapturedMail = {
4373
- options: MailOptions; // original payload
4460
+ id: string;
4461
+ options: MailOptions; // original payload
4374
4462
  normalized: NormalizedMail; // post-normalization (arrays, resolved from)
4375
4463
  timestamp: Date;
4376
- result?: MailResult; // in test mode, always success
4464
+ result?: MailResult; // in test mode, always success
4377
4465
  error?: MailError;
4378
4466
  };
4379
4467
  ```
@@ -5310,6 +5398,17 @@ Both files (and this config, with `lowerStage3Decorators()` first so decorated m
5310
5398
 
5311
5399
  **HTTP is the exception and stays in `globalSetup`**, which genuinely does run once in the main vitest process and owns a real port. **That split is the point:** one server for the whole run, one framework per test file.
5312
5400
 
5401
+ ### Keep Vitest setup out of development
5402
+
5403
+ `warlock dev` (and `warlock build`) load your app's `vite.config.ts`. Anything you put there for tests runs in development too. Guard vitest-only setup:
5404
+
5405
+ ```ts
5406
+ // vite.config.ts
5407
+ if (process.env.VITEST) {
5408
+ Object.assign(process.env, testEnv); // test database, test keys...
5409
+ }
5410
+ ```
5411
+
5313
5412
  ## HTTP request helpers
5314
5413
 
5315
5414
  Everything is built on native `fetch` — no extra dependency, no special wire format.
@@ -5662,6 +5761,17 @@ export default defineConfig({
5662
5761
 
5663
5762
  The `mongezVite()` plugin handles TypeScript path resolution and the framework's module shape. Without it, your imports break the moment vitest tries to load a Warlock module. `lowerStage3Decorators()` goes **first** — it lets decorated Cascade models (`@RegisterModel`, …) load under Vitest 4 / Vite 8; see [`@warlock.js/core/lower-stage3-decorators/SKILL.md`](@warlock.js/core/lower-stage3-decorators/SKILL.md).
5664
5763
 
5764
+ ### Keep Vitest setup out of development
5765
+
5766
+ `warlock dev` (and `warlock build`) load your app's `vite.config.ts`. Anything you put there for tests runs in development too. Guard vitest-only setup:
5767
+
5768
+ ```ts
5769
+ // vite.config.ts
5770
+ if (process.env.VITEST) {
5771
+ Object.assign(process.env, testEnv); // test database, test keys...
5772
+ }
5773
+ ```
5774
+
5665
5775
  Tests live colocated with the module: `src/app/<module>/tests/*.test.ts`. The `include` pattern picks them up.
5666
5776
 
5667
5777
  ## Patterns
@@ -8417,6 +8527,9 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
8417
8527
  2. **Several files are auto-loaded — never `import` them.** The dev-server file watcher and the production builder categorize these as "special" and load them on boot.
8418
8528
  - `src/app/<module>/routes.ts` — route declarations
8419
8529
  - `src/app/<module>/main.ts` — per-module one-time setup
8530
+ - `src/app/<module>/worker.ts` — worker-only setup (queue job definitions,
8531
+ `scheduler.addJob(...)`, `scheduler.start()`); loaded only for the
8532
+ `worker` role, and always loaded by `warlock dev`
8420
8533
  - `src/app/main.ts` — **project-level** one-time application setup (global hooks and other app initialization; connector instances belong in `warlock.config.ts > connectors`)
8421
8534
  - `src/app/<module>/events/*.ts(x)` — **any** `.ts(x)` file inside the `events/` folder (the `*.event.ts` suffix is convention, not a framework requirement)
8422
8535
  - `src/app/<module>/utils/locales.ts` — module translations via `groupedTranslations(...)`
@@ -8437,7 +8550,7 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
8437
8550
  ```ts title="src/app/<module>/schema/create-<thing>.schema.ts"
8438
8551
  import { v, type Infer } from "@warlock.js/seal";
8439
8552
 
8440
- export const createThingSchema = v.object({ /* … */ });
8553
+ export const createThingSchema = v.object({/* … */});
8441
8554
  export type CreateThingSchema = Infer<typeof createThingSchema>;
8442
8555
  ```
8443
8556
 
@@ -8446,13 +8559,17 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
8446
8559
  import { type CreateThingSchema, createThingSchema } from "../schema/create-thing.schema";
8447
8560
  import { createThingService } from "../services/create-thing.service";
8448
8561
 
8449
- export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({ request, response }) => {
8562
+ export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
8563
+ request,
8564
+ response,
8565
+ }) => {
8450
8566
  const thing = await createThingService(request.validated());
8451
8567
  return response.success({ thing });
8452
8568
  };
8453
8569
 
8454
8570
  createThingController.validation = { schema: createThingSchema };
8455
8571
  ```
8572
+
8456
8573
  8. **Model getters beat `.get<T>("field")`.** Add a typed getter on the model rather than scattering `.get<string>("name")` casts across call sites.
8457
8574
 
8458
8575
  ## Module layout (the standard subfolders)
@@ -8471,26 +8588,27 @@ src/app/<module>/
8471
8588
  seeds/ seed data for `warlock seed`
8472
8589
  routes.ts ← auto-loaded
8473
8590
  main.ts ← auto-loaded once, one-time setup
8591
+ worker.ts ← auto-loaded for the worker role only
8474
8592
  ```
8475
8593
 
8476
8594
  Some older modules still have a `validation/` folder instead of `schema/` — historical drift. The framework treats them as plain folders either way; the generator emits to `schema/`.
8477
8595
 
8478
8596
  ## File naming (project-wide)
8479
8597
 
8480
- | Suffix | Role | Example |
8481
- | ------------------- | ------------------------------------- | ----------------------------------- |
8482
- | `.controller.ts` | HTTP handler | `create-product.controller.ts` |
8483
- | `.service.ts` | Stateless business logic | `create-product.service.ts` |
8484
- | `.usecase.ts` | `useCase()` pipeline | `login.usecase.ts` |
8485
- | `.model.ts` | Cascade model class | `product.model.ts` |
8486
- | `.repository.ts` | `RepositoryManager` subclass | `products.repository.ts` |
8487
- | `.resource.ts` | `Resource` subclass | `product.resource.ts` |
8488
- | `.schema.ts` | seal validation schema + its inferred `<Name>Schema` type from the same file | `create-product.schema.ts` |
8489
- | `.type.ts` | data shape (TypeScript `type`) | `cart-state.type.ts` |
8490
- | `.contract.ts` | interface (TypeScript `interface`) | `model.contract.ts` |
8491
- | `.event.ts` | event listener registrations (any `.ts(x)` inside `events/` works; the suffix is convention) | `audit.event.ts` |
8492
- | `.migration.ts` | Cascade migration | `2026_05_22_120000_product.migration.ts` |
8493
- | `.seed.ts` | seed data | `products.seed.ts` |
8598
+ | Suffix | Role | Example |
8599
+ | ---------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------- |
8600
+ | `.controller.ts` | HTTP handler | `create-product.controller.ts` |
8601
+ | `.service.ts` | Stateless business logic | `create-product.service.ts` |
8602
+ | `.usecase.ts` | `useCase()` pipeline | `login.usecase.ts` |
8603
+ | `.model.ts` | Cascade model class | `product.model.ts` |
8604
+ | `.repository.ts` | `RepositoryManager` subclass | `products.repository.ts` |
8605
+ | `.resource.ts` | `Resource` subclass | `product.resource.ts` |
8606
+ | `.schema.ts` | seal validation schema + its inferred `<Name>Schema` type from the same file | `create-product.schema.ts` |
8607
+ | `.type.ts` | data shape (TypeScript `type`) | `cart-state.type.ts` |
8608
+ | `.contract.ts` | interface (TypeScript `interface`) | `model.contract.ts` |
8609
+ | `.event.ts` | event listener registrations (any `.ts(x)` inside `events/` works; the suffix is convention) | `audit.event.ts` |
8610
+ | `.migration.ts` | Cascade migration | `2026_05_22_120000_product.migration.ts` |
8611
+ | `.seed.ts` | seed data | `products.seed.ts` |
8494
8612
 
8495
8613
  Filenames are kebab-case. Class names are PascalCase. Function names are camelCase. Table/collection names are snake_case (plural).
8496
8614
 
@@ -8501,8 +8619,8 @@ The scaffolded `tsconfig.json` defines:
8501
8619
  ```jsonc
8502
8620
  {
8503
8621
  "paths": {
8504
- "app/*": ["./src/app/*"]
8505
- }
8622
+ "app/*": ["./src/app/*"],
8623
+ },
8506
8624
  }
8507
8625
  ```
8508
8626
 
@@ -9147,16 +9265,16 @@ Run it: `npx warlock users.promote --email=hasan@example.com` (or `npx warlock u
9147
9265
 
9148
9266
  ## `CLICommandOptions` — the factory input
9149
9267
 
9150
- | Field | Type | Required | Notes |
9151
- | ------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
9152
- | `name` | `string` | yes | Dot notation OK (`db.seed`, `jwt.generate`). May include positional placeholders (`name <arg>`). |
9153
- | `description` | `string` | | Shown in `warlock --help` and `warlock <cmd> --help`. |
9154
- | `alias` | `string` | | Short name (`m` for `migrate`). |
9155
- | `action` | `(data) => void \| Promise` | yes | Runs after preloaders. `data` is `{ args, options }`. |
9156
- | `preAction` | `(data) => void \| Promise` | | Runs **before** preloaders — banner, input validation. |
9157
- | `preload` | `CLICommandPreload` | | What to load before `action` runs. See below. |
9158
- | `persistent` | `boolean` | | `true` for long-running commands (dev server). Skips the auto-exit. |
9159
- | `options` | `CLICommandOption[]` | | Flag definitions. See below. |
9268
+ | Field | Type | Required | Notes |
9269
+ | ------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
9270
+ | `name` | `string` | yes | Dot notation OK (`db.seed`, `jwt.generate`). May include positional placeholders (`name <arg>`). |
9271
+ | `description` | `string` | | Shown in `warlock --help` and `warlock <cmd> --help`. |
9272
+ | `alias` | `string` | | Short name (`m` for `migrate`). |
9273
+ | `action` | `(data) => void \| Promise` | yes | Runs after preloaders. `data` is `{ args, options }`. |
9274
+ | `preAction` | `(data) => void \| Promise` | | Runs **before** preloaders — banner, input validation. |
9275
+ | `preload` | `CLICommandPreload` | | What to load before `action` runs. See below. |
9276
+ | `persistent` | `boolean` | | `true` for long-running commands (dev server). Skips the auto-exit. |
9277
+ | `options` | `CLICommandOption[]` | | Flag definitions. See below. |
9160
9278
 
9161
9279
  ## Options — flag shape
9162
9280
 
@@ -9258,29 +9376,29 @@ The framework ships a fixed set of commands you call but don't author. Knowing t
9258
9376
 
9259
9377
  ### Database + migrations
9260
9378
 
9261
- | Command | Flags / args | Preloads |
9262
- | -------- | ----------------------------------------------- | ------------------------------ |
9263
- | `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
9264
- | `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
9265
- | `warlock create-database <name>` | bare positional `<name>` | database |
9266
- | `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
9267
- | `warlock db.indexes` | builds DB indexes for every registered model | database |
9379
+ | Command | Flags / args | Preloads |
9380
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
9381
+ | `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
9382
+ | `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
9383
+ | `warlock create-database <name>` | bare positional `<name>` | database |
9384
+ | `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
9385
+ | `warlock db.indexes` | builds DB indexes for every registered model | database |
9268
9386
 
9269
9387
  **Asking what will run next.** `warlock migrate --list` prints executed migrations and then the pending ones **in execution order**. Do not derive the pending set by differencing `--all` against `--list`: `--all` globs `src/app` only, while `--list` reads the migrations table, which also holds migrations that packages register through `database.migrations` (`@warlock.js/auth` contributes two). The difference under-counts pending, in the direction that says "safe to proceed".
9270
9388
 
9271
9389
  `--list` is a report and always exits `0`. `--pending` is a gate, and its exit code is its whole API:
9272
9390
 
9273
- | Exit | Meaning |
9274
- | ---- | ------- |
9391
+ | Exit | Meaning |
9392
+ | ---- | ------------------------- |
9275
9393
  | `0` | computed, nothing pending |
9276
- | `1` | computed, N pending |
9394
+ | `1` | computed, N pending |
9277
9395
  | `2` | **could not be computed** |
9278
9396
 
9279
9397
  ```bash
9280
9398
  warlock migrate --pending && ./deploy.sh
9281
9399
  ```
9282
9400
 
9283
- `2` is separate from `1` on purpose — a script must be able to tell a backlog from an unknown, because one means *run them* and the other means *stop*. When the migration files cannot be loaded, both commands print `Pending: unavailable — <reason>` beneath a complete executed listing rather than reporting `0`.
9401
+ `2` is separate from `1` on purpose — a script must be able to tell a backlog from an unknown, because one means _run them_ and the other means _stop_. When the migration files cannot be loaded, both commands print `Pending: unavailable — <reason>` beneath a complete executed listing rather than reporting `0`.
9284
9402
 
9285
9403
  **If you are writing a command that reports on pending migrations:** register migrations first. `listPendingMigrations()` filters the runner's registry, so a caller that has not loaded anything gets `[]` — which reads as "nothing pending" and is not the same claim.
9286
9404
 
@@ -9306,36 +9424,41 @@ warlock generate (alias: g) — interactive picker
9306
9424
 
9307
9425
  `warlock add <features...>` installs framework-adjacent features through a curated registry — bundles npm dependencies + scaffold files + tsconfig patches + package.json scripts in one call. Featured names (from `add-command.action.ts`):
9308
9426
 
9309
- | Feature | Installs |
9310
- | ------------- | --------------------------------------------------------------------------------------------------- |
9311
- | `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
9312
- | `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
9313
- | `react` | `react` + `react-dom` + types |
9427
+ | Feature | Installs |
9428
+ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9429
+ | `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
9430
+ | `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
9431
+ | `react` | `react` + `react-dom` + types |
9314
9432
  | `bull-board` | `@bull-board/api` + `@bull-board/fastify`; writes a `dashboard` block to `src/config/queue.ts`. `requires: ["queue"]` — adds the `queue` feature first automatically when it's missing |
9315
- | `image` | `sharp` (for the `Image` class) |
9316
- | `mail` | `nodemailer` + types |
9317
- | `ses` | `@aws-sdk/client-sesv2` |
9318
- | `mongodb` | `mongodb` driver |
9319
- | `postgres` | `pg` |
9320
- | `mysql` | `mysql2` |
9321
- | `redis` | `redis` |
9322
- | `s3` | `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner` |
9323
- | `scheduler` | `@warlock.js/scheduler` |
9324
- | `swagger` | `@warlock.js/swagger` |
9325
- | `postman` | `@warlock.js/postman` |
9326
- | `herald` | `@warlock.js/herald` + `amqplib` + types; ejects `src/config/communicator.ts` |
9327
- | `test` | `vitest` + `@mongez/vite` + coverage; drops `test-global-setup.ts` / `test-setup.ts` / `vite.config.ts` |
9433
+ | `image` | `sharp` (for the `Image` class) |
9434
+ | `mail` | `nodemailer` + types |
9435
+ | `ses` | `@aws-sdk/client-sesv2` |
9436
+ | `mongodb` | `mongodb` driver |
9437
+ | `postgres` | `pg` |
9438
+ | `mysql` | `mysql2` |
9439
+ | `redis` | `redis` |
9440
+ | `s3` | `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner` |
9441
+ | `scheduler` | `@warlock.js/scheduler` |
9442
+ | `swagger` | `@warlock.js/swagger` |
9443
+ | `postman` | `@warlock.js/postman` |
9444
+ | `herald` | `@warlock.js/herald` + `amqplib` + types; ejects `src/config/communicator.ts` |
9445
+ | `test` | `vitest` + `@mongez/vite` + coverage; drops `test-global-setup.ts` / `test-setup.ts` / `vite.config.ts` |
9446
+ | `devtools` | `@warlock.js/devtools` as a dev dependency; `warlock dev` auto-loads its development dashboard at `/__warlock` |
9328
9447
 
9329
9448
  Run `warlock add --list` to see what's currently registered. Pass `--packageManager <yarn\|pnpm\|npm>` to override auto-detection (defaults to whichever lockfile is present).
9330
9449
 
9450
+ `warlock add devtools` never adds anything to `warlock.config.ts`. The package
9451
+ is loaded automatically by `warlock dev` when installed, and is omitted from a
9452
+ production install with dev dependencies excluded.
9453
+
9331
9454
  The Web starter keeps the page's universal non-component setup in `src/web/index.register.ts`, while `src/web/index.page.tsx` re-exports `register`. Warlock still discovers and runs the hook from the page namespace in both SSR and the browser, but editing the page component remains inside React's Fast Refresh boundary. Do not move the `register()` declaration inline beside the component: React Refresh treats that non-component export as incompatible and reloads the page, losing component state.
9332
9455
 
9333
9456
  ### Misc
9334
9457
 
9335
- | Command | Purpose |
9336
- | -------- | ----------------------------------------------------------- |
9337
- | `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk |
9338
- | `warlock jwt.generate` | Generate a JWT secret in `.env` (from `@warlock.js/auth`) |
9458
+ | Command | Purpose |
9459
+ | ----------------------------------------------- | --------------------------------------------------------- |
9460
+ | `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk |
9461
+ | `warlock jwt.generate` | Generate a JWT secret in `.env` (from `@warlock.js/auth`) |
9339
9462
 
9340
9463
  ## Common patterns
9341
9464
 
@@ -9397,7 +9520,7 @@ export default command({
9397
9520
  });
9398
9521
  ```
9399
9522
 
9400
- `preAction` runs *before* preloaders — cheap way to bail out without spinning up the database.
9523
+ `preAction` runs _before_ preloaders — cheap way to bail out without spinning up the database.
9401
9524
 
9402
9525
  ## Gotchas
9403
9526
 
@@ -9405,7 +9528,7 @@ export default command({
9405
9528
  - **Don't put logic at module top level.** The file gets imported during the commands scan (`warlock --warm-cache`). Anything outside `action` runs at scan time, possibly before any config or env is loaded.
9406
9529
  - **Required options block execution.** If `required: true` and the user omits the flag, the framework prints `Missing required options:` and exits `1` before `action` runs.
9407
9530
  - **Connectors are not free.** `connectors: true` boots the database, cache, storage, etc. For a print-version-and-exit command, leave `preload` undefined.
9408
- - **`name` field with positional slots** (`name: "storage.put <localPath>"`) — the *registered* name is still `storage.put` (the first whitespace-separated token), so look up + alias work normally. Slots are only documentation/help-output.
9531
+ - **`name` field with positional slots** (`name: "storage.put <localPath>"`) — the _registered_ name is still `storage.put` (the first whitespace-separated token), so look up + alias work normally. Slots are only documentation/help-output.
9409
9532
  - **Aliases must be unique across plugin + framework + project.** The first registration wins; later collisions silently overwrite the map entry.
9410
9533
 
9411
9534
  ## See also
@@ -10302,6 +10425,8 @@ Per-invocation callbacks fire first, then use-case-level, then globals. The retu
10302
10425
 
10303
10426
  Don't reach for `useCase` for a 5-line service — the ceremony outweighs the benefit.
10304
10427
 
10428
+ For the plain-function flavour (`warlock generate.use-case <module>/<verb-noun>`) shared by a page action and a controller, see the guide [One use-case, two adapters](https://warlock.js.org/v/latest/core/guides/one-use-case-two-adapters/).
10429
+
10305
10430
  ## Gotchas
10306
10431
 
10307
10432
  - **`name` must be unique.** The registry de-dupes by name; a duplicate in dev logs a warning and one wins. Use namespaced names: `"orders.placeOrder"`.