@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/CHANGELOG.md CHANGED
@@ -6,6 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  > ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
8
8
 
9
+ ## 5.25.0 - 2026-09-28
10
+
11
+ ### Added
12
+
13
+ - `registerTracingHooks()` lets development tools subscribe to HTTP tracing at runtime, and phase tracing now includes each phase's `startedAt` timestamp.
14
+ - `mailEvents.onCaptured()` (`mail.captured`): every mail sent in development or test mode is delivered in full (html, text, headers, attachments) to listeners. Development mode still logs instead of sending. `CapturedMail` gains `id`.
15
+ - **Devtools:** `warlock add devtools` installs `@warlock.js/devtools` as a dev dependency, and `warlock dev` loads it automatically when it is installed. You never name it in `warlock.config.ts`, so production installs, which omit dev dependencies, have nothing to resolve. `warlock add` now pins `@warlock.js/*` dev dependencies to the installed framework version too.
16
+ - **Deploy in parts:** `warlock start --role=api|web|worker` (combinable, e.g. `--role=web,api`) runs one build as separate processes, and `--sites=a,b` limits a `web` process to those sites.
17
+ - Roles are file-based. `routes.ts` is `api`, pages are `web`, and the new per-module `worker.ts` is `worker`. `main.ts` loads for every role.
18
+ - A process with neither `api` nor `web` binds no HTTP port and reports `worker ready`.
19
+ - `Application.roles`, `Application.hasRole()` and `Application.sites` expose the active set. The `warlock:ready` signal now carries `roles`.
20
+ - With no `--role`, every role is on, so existing apps and `warlock dev` behave exactly as before.
21
+ - Unknown roles, an empty list, or `--sites` without `web` fail before the app starts.
22
+
23
+ ### Changed
24
+
25
+ - **Behaviour change:** model-write validation failures now return the opaque internal-error response with status 500 by default, instead of returning a 400 and validation details. Details are logged server-side. Set `http.modelValidationErrorStatus` to choose a different status; a `4xx` status returns `{ errors }` to the client.
26
+
27
+ ### Fixed
28
+
29
+ - In development, a plugin's own parametric route (such as devtools' `/__warlock/api/requests/:id`) keeps its params when an app catch-all route also matches the URL. The dev dispatcher used to overwrite them, so the devtools request detail answered 404.
30
+ - `warlock seed` and `warlock migrate` skip test files (`*.spec.ts`, `*.test.ts`, …) in `seeds/` and `migrations/` folders. A spec kept beside its seeder used to abort the whole seed run.
31
+ - `onHttpServerRebuilt(listener)` fires with the new Fastify instance when the HTTP connector restarts (before it listens), so dev connectors can re-attach instance-bound hooks.
32
+ - `warlock generate.typings` now also writes `.warlock/typings/web-routes.d.ts` (through the new optional `build.typings` connector hook, so core never imports web), so `generate.typings && tsc && warlock build` typechecks typed `href()` calls on a clean checkout. API-only apps are unchanged.
33
+ - `warlock build` route registration now has a configurable timeout and names the module still registering when it expires.
34
+ - `defineResource` output `id` now falls back to `_id` on Mongo records when `id` is absent; an explicit `id` still wins. Collections with native ObjectIds should set `primaryKey = "_id"` on the model so `Model.find(hex)` works.
35
+ - `setupTest({ importModule })` lets tests load app modules (config, routes, events) through Vitest's own module graph, so classes like `User` exist once and `instanceof` works. The generated `src/test-setup.ts` passes `(file) => import(file)`; `setupTest()` under Vitest without it warns once.
36
+ - Default route schema validation now supplies matching declared route params when body and query omit them, without overriding body or query values.
37
+ - `isPortAvailable()` and `assertPortIsAvailable()` are exported from `@warlock.js/core/tests` for HTTP test setup.
38
+
39
+ ## 5.24.0 - 2026-09-27
40
+
41
+ ### Fixed
42
+
43
+ - `request.input(key)` now sees a route param set with `Request.setParam()`. Before, it updated only `request.params`, so in a multi-site app (where the site dispatcher sets each page param) every dynamic segment read as `undefined` through `input()`, `only()` and `all()`. Params keep their precedence over query and body.
44
+
9
45
  ## 5.23.2 - 2026-09-26
10
46
 
11
47
  ### Changed
@@ -77,7 +113,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
77
113
  registration for a `GET` route at the same path. GET-only routes retain
78
114
  Fastify's implicit HEAD behavior.
79
115
 
80
-
81
116
  ### Changed
82
117
 
83
118
  - Lockstep release maintenance and dependency refresh.
package/bin/warlock.js CHANGED
File without changes
@@ -1,4 +1,5 @@
1
1
  import { Environment, RuntimeStrategy } from "../utils/environment.mjs";
2
+ import { AppRole } from "./roles.mjs";
2
3
 
3
4
  //#region ../core/src/application/application.d.ts
4
5
  /**
@@ -247,6 +248,26 @@ declare class Application {
247
248
  * Check if the application is running in test environment
248
249
  */
249
250
  static get isTest(): boolean;
251
+ /**
252
+ * The roles this process serves — `WARLOCK_ROLES`, resolved once at
253
+ * bootstrap. Every role is on when the variable is absent (plain `warlock
254
+ * start`, `warlock dev`, and tests).
255
+ */
256
+ static get roles(): ReadonlySet<AppRole>;
257
+ /**
258
+ * The site keys this process installs pages for — `WARLOCK_SITES`,
259
+ * resolved once at bootstrap. `undefined` means every site.
260
+ */
261
+ static get sites(): ReadonlySet<string> | undefined;
262
+ /**
263
+ * Whether this process serves the given role.
264
+ *
265
+ * @example
266
+ * if (Application.hasRole("worker")) {
267
+ * scheduler.start();
268
+ * }
269
+ */
270
+ static hasRole(role: AppRole): boolean;
250
271
  /**
251
272
  * Get the root path
252
273
  */
@@ -1 +1 @@
1
- {"version":3,"file":"application.d.mts","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"mappings":";;;;;AAiBA;;;;KAAY,WAAA;EACV,WAAA,EAAa,WAAA;EACb,eAAA,EAAiB,eAAe;EAAf;;;AAKH;EAAd,cAAA;AAAA;;;;;;KAQU,YAAA,IAAgB,OAAA,EAAS,WAAA,YAAuB,OAAO;AAAA;AAYnE;;;;AAAgD;AAkBhD;;;;AA9BmE,KAYvD,aAAA,gBAA6B,OAAO;AAoBhD;;;;;AAAA,KAFY,gBAAA,gBAAgC,OAAO;AAAA,cAEtC,WAAA;EAwHyB;;;EAAA,gBApHb,SAAA,EAAS,IAAA;EA2OY;;;EAAA,OAtO9B,eAAA,EAAiB,eAAA;EAoVG;;;EAAA,eA/UnB,MAAA;EAViB;;;EAAA,eAejB,WAAA;EAAA;;;;EAAA,eAMA,aAAA;EA2BA;;;;;EAAA,eApBA,SAAA;EA8CG;;;;EAAA,eAxCH,cAAA;EAmEU;;;;;;EAAA,eA3DV,UAAA;EAiIM;;;;EAAA,eA3HN,YAAA;EA2L6B;;;EAAA,eAtL7B,iBAAA;EAqOU;;;EAAA,WAhOP,OAAA;EAiRA;;;EAAA,OA1QJ,kBAAA,CAAmB,QAAA,EAAU,eAAA;EAwRT;;;;;EAAA,WA/QhB,QAAA;EAkTA;;;;;EAAA,OAzSJ,aAAA,CAAc,IAAA;EAqUA;;;;;;;;;;;;;;EAAA,OAnTd,UAAA,CAAW,QAAA,EAAU,YAAA;;;;;SAerB,UAAA,IAAc,OAAA,CAAQ,WAAA;;;;;;;;;;;;;;;SAwBtB,UAAA,CAAW,OAAA,EAAS,WAAA;;;;;iBA+Bb,eAAA;;;;;;;;;;;;;;;;;;;;;;;;SAkCP,cAAA,CAAe,SAAA,EAAW,aAAA;;;;;;;;;;;;;;;;;;SA8BpB,oBAAA,IAAwB,OAAA;;;;;;aAgC1B,cAAA;;;;;;;;;;;;SAeJ,UAAA,CAAW,QAAA,EAAU,gBAAA;;;;;;;;;;SAmBf,gBAAA,IAAoB,OAAA;;;;;iBAmBnB,mBAAA;;;;aAWH,MAAA;;;;aAOA,WAAA,IAAe,WAAA;;;;SAOnB,cAAA,CAAe,GAAA,EAAK,WAAA;;;;aAOhB,YAAA;;;;aAOA,aAAA;;;;aAOA,MAAA;;;;aAOA,QAAA;;;;aAOA,OAAA;;;;aAOA,OAAA;;;;aAOA,WAAA;;;;aAOA,WAAA;;;;aAOA,UAAA;AAAA"}
1
+ {"version":3,"file":"application.d.mts","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"mappings":";;;;;;AAkBA;;;;KAAY,WAAA;EACV,WAAA,EAAa,WAAA;EACb,eAAA,EAAiB,eAAe;EAAf;;;AAKH;EAAd,cAAA;AAAA;;;;;;KAQU,YAAA,IAAgB,OAAA,EAAS,WAAA,YAAuB,OAAO;AAAA;AAYnE;;;;AAAgD;AAkBhD;;;;AA9BmE,KAYvD,aAAA,gBAA6B,OAAO;AAoBhD;;;;;AAAA,KAFY,gBAAA,gBAAgC,OAAO;AAAA,cAEtC,WAAA;EAwHyB;;;EAAA,gBApHb,SAAA,EAAS,IAAA;EA4OY;;;EAAA,OAvO9B,eAAA,EAAiB,eAAA;EAqVG;;;EAAA,eAhVnB,MAAA;EAkYa;;;EAAA,eA7Xb,WAAA;EAfiB;;;;EAAA,eAqBjB,aAAA;EAAA;;;;;EAAA,eAOA,SAAA;EA8BG;;;;EAAA,eAxBH,cAAA;EAiDD;;;;;;EAAA,eAzCC,UAAA;EA0EqB;;;;EAAA,eApErB,YAAA;EA8JD;;;EAAA,eAzJC,iBAAA;EAuL6B;;;EAAA,WAlL1B,OAAA;EAiOO;;;EAAA,OA1NX,kBAAA,CAAmB,QAAA,EAAU,eAAA;EA2QzB;;;;;EAAA,WAlQA,QAAA;EAuRA;;;;;EAAA,OA9QJ,aAAA,CAAc,IAAA;EA6SV;;;;;;;;;;;;AAsDU;;EAtDV,OA3RJ,UAAA,CAAW,QAAA,EAAU,YAAA;;;;;SAerB,UAAA,IAAc,OAAA,CAAQ,WAAA;;;;;;;;;;;;;;;SAwBtB,UAAA,CAAW,OAAA,EAAS,WAAA;;;;;iBAgCb,eAAA;;;;;;;;;;;;;;;;;;;;;;;;SAkCP,cAAA,CAAe,SAAA,EAAW,aAAA;;;;;;;;;;;;;;;;;;SA8BpB,oBAAA,IAAwB,OAAA;;;;;;aAgC1B,cAAA;;;;;;;;;;;;SAeJ,UAAA,CAAW,QAAA,EAAU,gBAAA;;;;;;;;;;SAmBf,gBAAA,IAAoB,OAAA;;;;;iBAmBnB,mBAAA;;;;aAWH,MAAA;;;;aAOA,WAAA,IAAe,WAAA;;;;SAOnB,cAAA,CAAe,GAAA,EAAK,WAAA;;;;aAOhB,YAAA;;;;aAOA,aAAA;;;;aAOA,MAAA;;;;;;aASA,KAAA,IAAS,WAAA,CAAY,OAAA;;;;;aAQrB,KAAA,IAAS,WAAA;;;;;;;;;SAYb,OAAA,CAAQ,IAAA,EAAM,OAAA;;;;aAOV,QAAA;;;;aAOA,OAAA;;;;aAOA,OAAA;;;;aAOA,WAAA;;;;aAOA,WAAA;;;;aAOA,UAAA;AAAA"}
@@ -2,6 +2,7 @@ import { environment, setEnvironment } from "../utils/environment.mjs";
2
2
  import { appPath, publicPath, rootPath, srcPath, storagePath, uploadsPath } from "../utils/paths.mjs";
3
3
  import { BOOT_SIGNAL_VERSION, sendBootSignal } from "./boot-signal.mjs";
4
4
  import { getFrameworkVersion } from "../utils/framework-vesion.mjs";
5
+ import { getRoles, getSites, hasRole } from "./roles.mjs";
5
6
  import { log } from "@warlock.js/logger";
6
7
 
7
8
  //#region ../core/src/application/application.ts
@@ -114,7 +115,8 @@ var Application = class {
114
115
  environment: context.environment,
115
116
  runtimeStrategy: context.runtimeStrategy,
116
117
  bootDurationMs: context.bootDurationMs,
117
- port: this.servedPort
118
+ port: this.servedPort,
119
+ roles: [...this.roles]
118
120
  });
119
121
  const listeners = this.bootListeners;
120
122
  this.bootListeners = [];
@@ -276,6 +278,32 @@ var Application = class {
276
278
  return this.environment === "test";
277
279
  }
278
280
  /**
281
+ * The roles this process serves — `WARLOCK_ROLES`, resolved once at
282
+ * bootstrap. Every role is on when the variable is absent (plain `warlock
283
+ * start`, `warlock dev`, and tests).
284
+ */
285
+ static get roles() {
286
+ return getRoles();
287
+ }
288
+ /**
289
+ * The site keys this process installs pages for — `WARLOCK_SITES`,
290
+ * resolved once at bootstrap. `undefined` means every site.
291
+ */
292
+ static get sites() {
293
+ return getSites();
294
+ }
295
+ /**
296
+ * Whether this process serves the given role.
297
+ *
298
+ * @example
299
+ * if (Application.hasRole("worker")) {
300
+ * scheduler.start();
301
+ * }
302
+ */
303
+ static hasRole(role) {
304
+ return hasRole(role);
305
+ }
306
+ /**
279
307
  * Get the root path
280
308
  */
281
309
  static get rootPath() {
@@ -1 +1 @@
1
- {"version":3,"file":"application.mjs","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { BOOT_SIGNAL_VERSION, sendBootSignal } from \"./boot-signal\";\nimport {\n environment,\n setEnvironment,\n type Environment,\n type RuntimeStrategy,\n} from \"../utils/environment\";\nimport { getFrameworkVersion } from \"../utils/framework-vesion\";\nimport { appPath, publicPath, rootPath, srcPath, storagePath, uploadsPath } from \"../utils/paths\";\n\n/**\n * Snapshot of the global facts known the moment the application finished\n * booting: every connector (early + late) is active and all app files\n * (locales, events, main, routes) are loaded. Passed to every\n * `Application.onceBooted` listener and resolved from `Application.whenBooted`.\n */\nexport type BootContext = {\n environment: Environment;\n runtimeStrategy: RuntimeStrategy;\n /**\n * Wall-clock milliseconds the boot took. The dev server measures it; the\n * production entry omits it.\n */\n bootDurationMs?: number;\n};\n\n/**\n * A callback registered through `Application.onceBooted`. May be async — its\n * rejection is caught and logged so a single listener can never break boot or\n * the other listeners.\n */\nexport type BootListener = (context: BootContext) => void | Promise<void>;\n\n/**\n * A callback registered through `Application.onValidateBoot`. May be async —\n * unlike {@link BootListener}, its rejection is NOT caught: it aborts boot.\n *\n * Use this for configuration that makes the application WRONG rather than\n * degraded when missing (a signing secret, a required integration key) — the\n * kind of check that must fail closed. Use `onceBooted` for everything else;\n * that contract stays fire-and-forget on purpose (it is the right behaviour\n * for plugins reacting to boot, not for gating it).\n */\nexport type BootValidator = () => void | Promise<void>;\n\n/**\n * `new Error(message, { cause })` is ES2022, and this package still compiles\n * against the ES2020 lib, where `Error` is typed as taking a message only.\n * Node has supported the option since v16, so this is the type layer\n * catching up with the runtime, not a change in what the code does. Same\n * workaround as `image.ts` (see #16).\n */\ntype ErrorWithCauseConstructor = new (message: string, options: { cause: unknown }) => Error;\n\nconst ErrorWithCause = Error as ErrorWithCauseConstructor;\n\n/**\n * A callback registered through `Application.onShutdown`. Runs once, while the\n * connectors (db, cache, http) are still up, so it can release app-owned\n * resources cleanly. May be async — its rejection is caught and logged.\n */\nexport type ShutdownListener = () => void | Promise<void>;\n\nexport class Application {\n /**\n * Project start time regarding the process start time\n */\n public static readonly startedAt = new Date(Date.now() - process.uptime() * 1000);\n\n /**\n * Runtime strategy\n */\n public static runtimeStrategy: RuntimeStrategy;\n\n /**\n * Whether the application has finished booting. Flipped once by `markBooted`.\n */\n private static booted = false;\n\n /**\n * The boot context, set once booted so it can be replayed to late subscribers.\n */\n private static bootContext: BootContext | undefined;\n\n /**\n * Listeners queued before boot completed, drained in registration order by\n * `markBooted`.\n */\n private static bootListeners: BootListener[] = [];\n\n /**\n * Whether `runStartupValidators` has already run. Flipped once, so a\n * validator registered afterwards is refused instead of silently skipped\n * or run too late to matter.\n */\n private static validated = false;\n\n /**\n * Startup validators queued before `runStartupValidators` ran, drained in\n * registration order.\n */\n private static bootValidators: BootValidator[] = [];\n\n /**\n * The http port this process actually bound, once it has. Reported in the\n * readiness signal so a supervisor knows where to send its health check\n * instead of re-deriving it from config it may not be able to read.\n * Undefined for an app with no http connector — a queue worker still boots.\n */\n private static servedPort: number | undefined;\n\n /**\n * Whether the application has begun (or finished) shutting down. Flipped once\n * by `runShutdownHooks`.\n */\n private static shuttingDown = false;\n\n /**\n * Teardown callbacks, run once when shutdown begins.\n */\n private static shutdownListeners: ShutdownListener[] = [];\n\n /**\n * Get framework version\n */\n public static get version() {\n return getFrameworkVersion();\n }\n\n /**\n * Set the runtime strategy\n */\n public static setRuntimeStrategy(strategy: RuntimeStrategy) {\n this.runtimeStrategy = strategy;\n }\n\n /**\n * Whether the application has finished booting — every connector in both\n * the early and late phases is active and all app files (locales, events,\n * main, routes) have been loaded.\n */\n public static get isBooted(): boolean {\n return this.booted;\n }\n\n /**\n * Record the http port this process bound.\n *\n * @internal The http connector calls this after a successful `listen`.\n */\n public static setServedPort(port: number): void {\n this.servedPort = port;\n }\n\n /**\n * Run a callback once the application is fully booted.\n *\n * App files (`main` / `events` / `routes` / locales) are imported BEFORE the\n * late-phase connectors (http, socket) start, so a listener registered at\n * import time waits for the whole sequence to finish before it runs. If the\n * application is already booted, the callback runs on the next microtask —\n * so a late subscriber never silently misses the event.\n *\n * @example\n * Application.onceBooted(({ environment }) => {\n * log.info(\"app\", \"booted\", `ready in ${environment}`);\n * });\n */\n public static onceBooted(listener: BootListener): void {\n if (this.booted && this.bootContext) {\n const context = this.bootContext;\n void this.runBootListener(listener, context);\n\n return;\n }\n\n this.bootListeners.push(listener);\n }\n\n /**\n * Promise form of {@link onceBooted} — resolves with the boot context once\n * the application is booted, or immediately if it already is.\n */\n public static whenBooted(): Promise<BootContext> {\n if (this.booted && this.bootContext) {\n return Promise.resolve(this.bootContext);\n }\n\n return new Promise<BootContext>((resolve) => {\n this.onceBooted((context) => resolve(context));\n });\n }\n\n /**\n * Flip the boot latch and drain every queued listener. The dev server and\n * the production entry call this once, right after the late phase starts. It\n * is idempotent — a second call is a no-op, so a double-invoke can never\n * double-fire listeners.\n *\n * Also reports readiness to a supervising parent process — this is the one\n * moment in the lifecycle where \"the application is serving\" is true, so it\n * is the only honest source for `warlock start`'s success banner. Sent before\n * the listeners run: a slow or hanging `onceBooted` listener must not delay\n * the parent's view of a server that is already accepting requests.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static markBooted(context: BootContext): void {\n if (this.booted) {\n return;\n }\n\n this.booted = true;\n this.bootContext = context;\n\n sendBootSignal({\n type: \"warlock:ready\",\n version: BOOT_SIGNAL_VERSION,\n pid: process.pid,\n at: new Date().toISOString(),\n environment: context.environment,\n runtimeStrategy: context.runtimeStrategy,\n bootDurationMs: context.bootDurationMs,\n port: this.servedPort,\n });\n\n const listeners = this.bootListeners;\n this.bootListeners = [];\n\n for (const listener of listeners) {\n void this.runBootListener(listener, context);\n }\n }\n\n /**\n * Invoke a single boot listener, isolating its failure so neither boot nor\n * the remaining listeners are affected.\n */\n private static async runBootListener(\n listener: BootListener,\n context: BootContext,\n ): Promise<void> {\n try {\n await listener(context);\n } catch (error) {\n log.error(\"application\", \"booted-listener\", error as Error);\n }\n }\n\n /**\n * Register a startup validator — distinct from {@link onceBooted}. A\n * rejecting validator aborts boot instead of being caught and logged, so\n * this is where configuration that must fail closed belongs (a signing\n * secret, a required integration key), not `onceBooted`.\n *\n * Must be registered before the framework calls `runStartupValidators` —\n * typically at module scope in `src/app/main.ts`, the same place a\n * `BootListener` would be registered. Registering one after validation has\n * already run throws: by then a rejection could no longer stop the app\n * from serving, so running it late would silently defeat the guarantee\n * this hook exists to give.\n *\n * @example\n * Application.onValidateBoot(() => {\n * if (!env(\"JWT_SECRET\")) {\n * throw new Error(\n * \"JWT_SECRET is not set. Sessions cannot be signed without it. \" +\n * \"Generate one with `yarn jwt` and put it in .env.\",\n * );\n * }\n * });\n */\n public static onValidateBoot(validator: BootValidator): void {\n if (this.validated) {\n throw new Error(\n \"Application.onValidateBoot(): startup validators already ran — \" +\n \"this validator was registered too late to block boot. Register it \" +\n \"before the framework calls Application.runStartupValidators(), \" +\n \"typically at module scope in `src/app/main.ts`.\",\n );\n }\n\n this.bootValidators.push(validator);\n }\n\n /**\n * Run every registered startup validator, in registration order, and abort\n * on the first rejection. Idempotent — a second call is a no-op.\n *\n * Distinct from `runBootListener`: a listener's failure is isolated so it\n * can never break boot; a validator's failure IS the boot failing — that is\n * the entire point of registering one instead of the other. The thrown\n * error names the failing validator and the original cause, so whatever\n * catches it (the dev server's boot try/catch, the production entry's\n * top-level await) reports which check failed and why, not a bare\n * rejection.\n *\n * @internal Framework entry points call this once app code (main.ts et al)\n * has loaded and BEFORE late-phase connectors (http, socket) start, so a\n * failing validator runs before anything can bind a port or accept a\n * request. Application code must not call this directly.\n */\n public static async runStartupValidators(): Promise<void> {\n if (this.validated) {\n return;\n }\n\n this.validated = true;\n\n const validators = this.bootValidators;\n this.bootValidators = [];\n\n for (const validator of validators) {\n try {\n await validator();\n } catch (error) {\n const name = validator.name || \"<anonymous>\";\n const cause = error instanceof Error ? error.message : String(error);\n\n throw new ErrorWithCause(\n `Startup validator \"${name}\" rejected boot: ${cause}. ` +\n `Fix the condition it checks, or remove the validator if it no ` +\n `longer applies — boot cannot proceed while it fails.`,\n { cause: error },\n );\n }\n }\n }\n\n /**\n * Whether the application has begun shutting down. Once true, readiness\n * checks report not-ready so a load balancer drains this instance before the\n * HTTP server stops accepting requests.\n */\n public static get isShuttingDown(): boolean {\n return this.shuttingDown;\n }\n\n /**\n * Register a teardown callback, run once when the application shuts down —\n * before the connectors (db, cache, http) are torn down, so it can still use\n * them. The natural place to close an app-owned resource opened in\n * `onceBooted`. If shutdown has already begun, the callback runs immediately.\n *\n * @example\n * Application.onShutdown(async () => {\n * await myQueueConsumer.stop();\n * });\n */\n public static onShutdown(listener: ShutdownListener): void {\n if (this.shuttingDown) {\n void this.runShutdownListener(listener);\n\n return;\n }\n\n this.shutdownListeners.push(listener);\n }\n\n /**\n * Flip the shutdown latch and run every teardown callback. The connectors\n * manager calls this at the start of shutdown — before any connector is torn\n * down. Idempotent and error-isolated: each callback is awaited, a thrown one\n * is logged and does not stop the rest. Listeners run LIFO (reverse of\n * registration), mirroring resource-unwind order.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static async runShutdownHooks(): Promise<void> {\n if (this.shuttingDown) {\n return;\n }\n\n this.shuttingDown = true;\n\n const listeners = this.shutdownListeners.reverse();\n this.shutdownListeners = [];\n\n for (const listener of listeners) {\n await this.runShutdownListener(listener);\n }\n }\n\n /**\n * Invoke a single teardown callback, isolating its failure so the rest still\n * run and shutdown is never blocked by one bad hook.\n */\n private static async runShutdownListener(listener: ShutdownListener): Promise<void> {\n try {\n await listener();\n } catch (error) {\n log.error(\"application\", \"shutdown-listener\", error as Error);\n }\n }\n\n /**\n * Get project uptime in milliseconds\n */\n public static get uptime(): number {\n return process.uptime() * 1000;\n }\n\n /**\n * Get the current environment\n */\n public static get environment(): Environment {\n return environment();\n }\n\n /**\n * Set the current environment\n */\n public static setEnvironment(env: Environment) {\n setEnvironment(env);\n }\n\n /**\n * Check if the application is running in production environment\n */\n public static get isProduction(): boolean {\n return this.environment === \"production\";\n }\n\n /**\n * Check if the application is running in development environment\n */\n public static get isDevelopment(): boolean {\n return this.environment === \"development\";\n }\n\n /**\n * Check if the application is running in test environment\n */\n public static get isTest(): boolean {\n return this.environment === \"test\";\n }\n\n /**\n * Get the root path\n */\n public static get rootPath(): string {\n return rootPath();\n }\n\n /**\n * Get the src path\n */\n public static get srcPath(): string {\n return srcPath();\n }\n\n /**\n * Get the app path\n */\n public static get appPath(): string {\n return appPath();\n }\n\n /**\n * Get the storage path\n */\n public static get storagePath(): string {\n return storagePath();\n }\n\n /**\n * Get the uploads path\n */\n public static get uploadsPath(): string {\n return uploadsPath();\n }\n\n /**\n * Get the public path\n */\n public static get publicPath(): string {\n return publicPath();\n }\n}\n"],"mappings":";;;;;;;AAuDA,MAAM,iBAAiB;AASvB,IAAa,cAAb,MAAyB;;mCAIY,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,OAAO,IAAI,GAAI;;;gBAUxD;;;uBAWuB,CAAC;;;mBAOrB;;;wBAMsB,CAAC;;;sBAcpB;;;2BAKyB,CAAC;;;;;CAKxD,WAAkB,UAAU;EAC1B,OAAO,oBAAoB;CAC7B;;;;CAKA,OAAc,mBAAmB,UAA2B;EAC1D,KAAK,kBAAkB;CACzB;;;;;;CAOA,WAAkB,WAAoB;EACpC,OAAO,KAAK;CACd;;;;;;CAOA,OAAc,cAAc,MAAoB;EAC9C,KAAK,aAAa;CACpB;;;;;;;;;;;;;;;CAgBA,OAAc,WAAW,UAA8B;EACrD,IAAI,KAAK,UAAU,KAAK,aAAa;GACnC,MAAM,UAAU,KAAK;GACrB,AAAK,KAAK,gBAAgB,UAAU,OAAO;GAE3C;EACF;EAEA,KAAK,cAAc,KAAK,QAAQ;CAClC;;;;;CAMA,OAAc,aAAmC;EAC/C,IAAI,KAAK,UAAU,KAAK,aACtB,OAAO,QAAQ,QAAQ,KAAK,WAAW;EAGzC,OAAO,IAAI,SAAsB,YAAY;GAC3C,KAAK,YAAY,YAAY,QAAQ,OAAO,CAAC;EAC/C,CAAC;CACH;;;;;;;;;;;;;;;CAgBA,OAAc,WAAW,SAA4B;EACnD,IAAI,KAAK,QACP;EAGF,KAAK,SAAS;EACd,KAAK,cAAc;EAEnB,eAAe;GACb,MAAM;GACN;GACA,KAAK,QAAQ;GACb,qBAAI,IAAI,KAAK,EAAC,CAAC,YAAY;GAC3B,aAAa,QAAQ;GACrB,iBAAiB,QAAQ;GACzB,gBAAgB,QAAQ;GACxB,MAAM,KAAK;EACb,CAAC;EAED,MAAM,YAAY,KAAK;EACvB,KAAK,gBAAgB,CAAC;EAEtB,KAAK,MAAM,YAAY,WACrB,AAAK,KAAK,gBAAgB,UAAU,OAAO;CAE/C;;;;;CAMA,aAAqB,gBACnB,UACA,SACe;EACf,IAAI;GACF,MAAM,SAAS,OAAO;EACxB,SAAS,OAAO;GACd,IAAI,MAAM,eAAe,mBAAmB,KAAc;EAC5D;CACF;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,OAAc,eAAe,WAAgC;EAC3D,IAAI,KAAK,WACP,MAAM,IAAI,MACR,iPAIF;EAGF,KAAK,eAAe,KAAK,SAAS;CACpC;;;;;;;;;;;;;;;;;;CAmBA,aAAoB,uBAAsC;EACxD,IAAI,KAAK,WACP;EAGF,KAAK,YAAY;EAEjB,MAAM,aAAa,KAAK;EACxB,KAAK,iBAAiB,CAAC;EAEvB,KAAK,MAAM,aAAa,YACtB,IAAI;GACF,MAAM,UAAU;EAClB,SAAS,OAAO;GAId,MAAM,IAAI,eACR,sBAJW,UAAU,QAAQ,cAIF,mBAHf,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAGb,uHAGpD,EAAE,OAAO,MAAM,CACjB;EACF;CAEJ;;;;;;CAOA,WAAkB,iBAA0B;EAC1C,OAAO,KAAK;CACd;;;;;;;;;;;;CAaA,OAAc,WAAW,UAAkC;EACzD,IAAI,KAAK,cAAc;GACrB,AAAK,KAAK,oBAAoB,QAAQ;GAEtC;EACF;EAEA,KAAK,kBAAkB,KAAK,QAAQ;CACtC;;;;;;;;;;CAWA,aAAoB,mBAAkC;EACpD,IAAI,KAAK,cACP;EAGF,KAAK,eAAe;EAEpB,MAAM,YAAY,KAAK,kBAAkB,QAAQ;EACjD,KAAK,oBAAoB,CAAC;EAE1B,KAAK,MAAM,YAAY,WACrB,MAAM,KAAK,oBAAoB,QAAQ;CAE3C;;;;;CAMA,aAAqB,oBAAoB,UAA2C;EAClF,IAAI;GACF,MAAM,SAAS;EACjB,SAAS,OAAO;GACd,IAAI,MAAM,eAAe,qBAAqB,KAAc;EAC9D;CACF;;;;CAKA,WAAkB,SAAiB;EACjC,OAAO,QAAQ,OAAO,IAAI;CAC5B;;;;CAKA,WAAkB,cAA2B;EAC3C,OAAO,YAAY;CACrB;;;;CAKA,OAAc,eAAe,KAAkB;EAC7C,eAAe,GAAG;CACpB;;;;CAKA,WAAkB,eAAwB;EACxC,OAAO,KAAK,gBAAgB;CAC9B;;;;CAKA,WAAkB,gBAAyB;EACzC,OAAO,KAAK,gBAAgB;CAC9B;;;;CAKA,WAAkB,SAAkB;EAClC,OAAO,KAAK,gBAAgB;CAC9B;;;;CAKA,WAAkB,WAAmB;EACnC,OAAO,SAAS;CAClB;;;;CAKA,WAAkB,UAAkB;EAClC,OAAO,QAAQ;CACjB;;;;CAKA,WAAkB,UAAkB;EAClC,OAAO,QAAQ;CACjB;;;;CAKA,WAAkB,cAAsB;EACtC,OAAO,YAAY;CACrB;;;;CAKA,WAAkB,cAAsB;EACtC,OAAO,YAAY;CACrB;;;;CAKA,WAAkB,aAAqB;EACrC,OAAO,WAAW;CACpB;AACF"}
1
+ {"version":3,"file":"application.mjs","names":[],"sources":["../../../../../../../core/src/application/application.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { BOOT_SIGNAL_VERSION, sendBootSignal } from \"./boot-signal\";\nimport {\n environment,\n setEnvironment,\n type Environment,\n type RuntimeStrategy,\n} from \"../utils/environment\";\nimport { getFrameworkVersion } from \"../utils/framework-vesion\";\nimport { appPath, publicPath, rootPath, srcPath, storagePath, uploadsPath } from \"../utils/paths\";\nimport { getRoles, getSites, hasRole, type AppRole } from \"./roles\";\n\n/**\n * Snapshot of the global facts known the moment the application finished\n * booting: every connector (early + late) is active and all app files\n * (locales, events, main, routes) are loaded. Passed to every\n * `Application.onceBooted` listener and resolved from `Application.whenBooted`.\n */\nexport type BootContext = {\n environment: Environment;\n runtimeStrategy: RuntimeStrategy;\n /**\n * Wall-clock milliseconds the boot took. The dev server measures it; the\n * production entry omits it.\n */\n bootDurationMs?: number;\n};\n\n/**\n * A callback registered through `Application.onceBooted`. May be async — its\n * rejection is caught and logged so a single listener can never break boot or\n * the other listeners.\n */\nexport type BootListener = (context: BootContext) => void | Promise<void>;\n\n/**\n * A callback registered through `Application.onValidateBoot`. May be async —\n * unlike {@link BootListener}, its rejection is NOT caught: it aborts boot.\n *\n * Use this for configuration that makes the application WRONG rather than\n * degraded when missing (a signing secret, a required integration key) — the\n * kind of check that must fail closed. Use `onceBooted` for everything else;\n * that contract stays fire-and-forget on purpose (it is the right behaviour\n * for plugins reacting to boot, not for gating it).\n */\nexport type BootValidator = () => void | Promise<void>;\n\n/**\n * `new Error(message, { cause })` is ES2022, and this package still compiles\n * against the ES2020 lib, where `Error` is typed as taking a message only.\n * Node has supported the option since v16, so this is the type layer\n * catching up with the runtime, not a change in what the code does. Same\n * workaround as `image.ts` (see #16).\n */\ntype ErrorWithCauseConstructor = new (message: string, options: { cause: unknown }) => Error;\n\nconst ErrorWithCause = Error as ErrorWithCauseConstructor;\n\n/**\n * A callback registered through `Application.onShutdown`. Runs once, while the\n * connectors (db, cache, http) are still up, so it can release app-owned\n * resources cleanly. May be async — its rejection is caught and logged.\n */\nexport type ShutdownListener = () => void | Promise<void>;\n\nexport class Application {\n /**\n * Project start time regarding the process start time\n */\n public static readonly startedAt = new Date(Date.now() - process.uptime() * 1000);\n\n /**\n * Runtime strategy\n */\n public static runtimeStrategy: RuntimeStrategy;\n\n /**\n * Whether the application has finished booting. Flipped once by `markBooted`.\n */\n private static booted = false;\n\n /**\n * The boot context, set once booted so it can be replayed to late subscribers.\n */\n private static bootContext: BootContext | undefined;\n\n /**\n * Listeners queued before boot completed, drained in registration order by\n * `markBooted`.\n */\n private static bootListeners: BootListener[] = [];\n\n /**\n * Whether `runStartupValidators` has already run. Flipped once, so a\n * validator registered afterwards is refused instead of silently skipped\n * or run too late to matter.\n */\n private static validated = false;\n\n /**\n * Startup validators queued before `runStartupValidators` ran, drained in\n * registration order.\n */\n private static bootValidators: BootValidator[] = [];\n\n /**\n * The http port this process actually bound, once it has. Reported in the\n * readiness signal so a supervisor knows where to send its health check\n * instead of re-deriving it from config it may not be able to read.\n * Undefined for an app with no http connector — a queue worker still boots.\n */\n private static servedPort: number | undefined;\n\n /**\n * Whether the application has begun (or finished) shutting down. Flipped once\n * by `runShutdownHooks`.\n */\n private static shuttingDown = false;\n\n /**\n * Teardown callbacks, run once when shutdown begins.\n */\n private static shutdownListeners: ShutdownListener[] = [];\n\n /**\n * Get framework version\n */\n public static get version() {\n return getFrameworkVersion();\n }\n\n /**\n * Set the runtime strategy\n */\n public static setRuntimeStrategy(strategy: RuntimeStrategy) {\n this.runtimeStrategy = strategy;\n }\n\n /**\n * Whether the application has finished booting — every connector in both\n * the early and late phases is active and all app files (locales, events,\n * main, routes) have been loaded.\n */\n public static get isBooted(): boolean {\n return this.booted;\n }\n\n /**\n * Record the http port this process bound.\n *\n * @internal The http connector calls this after a successful `listen`.\n */\n public static setServedPort(port: number): void {\n this.servedPort = port;\n }\n\n /**\n * Run a callback once the application is fully booted.\n *\n * App files (`main` / `events` / `routes` / locales) are imported BEFORE the\n * late-phase connectors (http, socket) start, so a listener registered at\n * import time waits for the whole sequence to finish before it runs. If the\n * application is already booted, the callback runs on the next microtask —\n * so a late subscriber never silently misses the event.\n *\n * @example\n * Application.onceBooted(({ environment }) => {\n * log.info(\"app\", \"booted\", `ready in ${environment}`);\n * });\n */\n public static onceBooted(listener: BootListener): void {\n if (this.booted && this.bootContext) {\n const context = this.bootContext;\n void this.runBootListener(listener, context);\n\n return;\n }\n\n this.bootListeners.push(listener);\n }\n\n /**\n * Promise form of {@link onceBooted} — resolves with the boot context once\n * the application is booted, or immediately if it already is.\n */\n public static whenBooted(): Promise<BootContext> {\n if (this.booted && this.bootContext) {\n return Promise.resolve(this.bootContext);\n }\n\n return new Promise<BootContext>((resolve) => {\n this.onceBooted((context) => resolve(context));\n });\n }\n\n /**\n * Flip the boot latch and drain every queued listener. The dev server and\n * the production entry call this once, right after the late phase starts. It\n * is idempotent — a second call is a no-op, so a double-invoke can never\n * double-fire listeners.\n *\n * Also reports readiness to a supervising parent process — this is the one\n * moment in the lifecycle where \"the application is serving\" is true, so it\n * is the only honest source for `warlock start`'s success banner. Sent before\n * the listeners run: a slow or hanging `onceBooted` listener must not delay\n * the parent's view of a server that is already accepting requests.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static markBooted(context: BootContext): void {\n if (this.booted) {\n return;\n }\n\n this.booted = true;\n this.bootContext = context;\n\n sendBootSignal({\n type: \"warlock:ready\",\n version: BOOT_SIGNAL_VERSION,\n pid: process.pid,\n at: new Date().toISOString(),\n environment: context.environment,\n runtimeStrategy: context.runtimeStrategy,\n bootDurationMs: context.bootDurationMs,\n port: this.servedPort,\n roles: [...this.roles],\n });\n\n const listeners = this.bootListeners;\n this.bootListeners = [];\n\n for (const listener of listeners) {\n void this.runBootListener(listener, context);\n }\n }\n\n /**\n * Invoke a single boot listener, isolating its failure so neither boot nor\n * the remaining listeners are affected.\n */\n private static async runBootListener(\n listener: BootListener,\n context: BootContext,\n ): Promise<void> {\n try {\n await listener(context);\n } catch (error) {\n log.error(\"application\", \"booted-listener\", error as Error);\n }\n }\n\n /**\n * Register a startup validator — distinct from {@link onceBooted}. A\n * rejecting validator aborts boot instead of being caught and logged, so\n * this is where configuration that must fail closed belongs (a signing\n * secret, a required integration key), not `onceBooted`.\n *\n * Must be registered before the framework calls `runStartupValidators` —\n * typically at module scope in `src/app/main.ts`, the same place a\n * `BootListener` would be registered. Registering one after validation has\n * already run throws: by then a rejection could no longer stop the app\n * from serving, so running it late would silently defeat the guarantee\n * this hook exists to give.\n *\n * @example\n * Application.onValidateBoot(() => {\n * if (!env(\"JWT_SECRET\")) {\n * throw new Error(\n * \"JWT_SECRET is not set. Sessions cannot be signed without it. \" +\n * \"Generate one with `yarn jwt` and put it in .env.\",\n * );\n * }\n * });\n */\n public static onValidateBoot(validator: BootValidator): void {\n if (this.validated) {\n throw new Error(\n \"Application.onValidateBoot(): startup validators already ran — \" +\n \"this validator was registered too late to block boot. Register it \" +\n \"before the framework calls Application.runStartupValidators(), \" +\n \"typically at module scope in `src/app/main.ts`.\",\n );\n }\n\n this.bootValidators.push(validator);\n }\n\n /**\n * Run every registered startup validator, in registration order, and abort\n * on the first rejection. Idempotent — a second call is a no-op.\n *\n * Distinct from `runBootListener`: a listener's failure is isolated so it\n * can never break boot; a validator's failure IS the boot failing — that is\n * the entire point of registering one instead of the other. The thrown\n * error names the failing validator and the original cause, so whatever\n * catches it (the dev server's boot try/catch, the production entry's\n * top-level await) reports which check failed and why, not a bare\n * rejection.\n *\n * @internal Framework entry points call this once app code (main.ts et al)\n * has loaded and BEFORE late-phase connectors (http, socket) start, so a\n * failing validator runs before anything can bind a port or accept a\n * request. Application code must not call this directly.\n */\n public static async runStartupValidators(): Promise<void> {\n if (this.validated) {\n return;\n }\n\n this.validated = true;\n\n const validators = this.bootValidators;\n this.bootValidators = [];\n\n for (const validator of validators) {\n try {\n await validator();\n } catch (error) {\n const name = validator.name || \"<anonymous>\";\n const cause = error instanceof Error ? error.message : String(error);\n\n throw new ErrorWithCause(\n `Startup validator \"${name}\" rejected boot: ${cause}. ` +\n `Fix the condition it checks, or remove the validator if it no ` +\n `longer applies — boot cannot proceed while it fails.`,\n { cause: error },\n );\n }\n }\n }\n\n /**\n * Whether the application has begun shutting down. Once true, readiness\n * checks report not-ready so a load balancer drains this instance before the\n * HTTP server stops accepting requests.\n */\n public static get isShuttingDown(): boolean {\n return this.shuttingDown;\n }\n\n /**\n * Register a teardown callback, run once when the application shuts down —\n * before the connectors (db, cache, http) are torn down, so it can still use\n * them. The natural place to close an app-owned resource opened in\n * `onceBooted`. If shutdown has already begun, the callback runs immediately.\n *\n * @example\n * Application.onShutdown(async () => {\n * await myQueueConsumer.stop();\n * });\n */\n public static onShutdown(listener: ShutdownListener): void {\n if (this.shuttingDown) {\n void this.runShutdownListener(listener);\n\n return;\n }\n\n this.shutdownListeners.push(listener);\n }\n\n /**\n * Flip the shutdown latch and run every teardown callback. The connectors\n * manager calls this at the start of shutdown — before any connector is torn\n * down. Idempotent and error-isolated: each callback is awaited, a thrown one\n * is logged and does not stop the rest. Listeners run LIFO (reverse of\n * registration), mirroring resource-unwind order.\n *\n * @internal Framework entry points call this; application code must not.\n */\n public static async runShutdownHooks(): Promise<void> {\n if (this.shuttingDown) {\n return;\n }\n\n this.shuttingDown = true;\n\n const listeners = this.shutdownListeners.reverse();\n this.shutdownListeners = [];\n\n for (const listener of listeners) {\n await this.runShutdownListener(listener);\n }\n }\n\n /**\n * Invoke a single teardown callback, isolating its failure so the rest still\n * run and shutdown is never blocked by one bad hook.\n */\n private static async runShutdownListener(listener: ShutdownListener): Promise<void> {\n try {\n await listener();\n } catch (error) {\n log.error(\"application\", \"shutdown-listener\", error as Error);\n }\n }\n\n /**\n * Get project uptime in milliseconds\n */\n public static get uptime(): number {\n return process.uptime() * 1000;\n }\n\n /**\n * Get the current environment\n */\n public static get environment(): Environment {\n return environment();\n }\n\n /**\n * Set the current environment\n */\n public static setEnvironment(env: Environment) {\n setEnvironment(env);\n }\n\n /**\n * Check if the application is running in production environment\n */\n public static get isProduction(): boolean {\n return this.environment === \"production\";\n }\n\n /**\n * Check if the application is running in development environment\n */\n public static get isDevelopment(): boolean {\n return this.environment === \"development\";\n }\n\n /**\n * Check if the application is running in test environment\n */\n public static get isTest(): boolean {\n return this.environment === \"test\";\n }\n\n /**\n * The roles this process serves — `WARLOCK_ROLES`, resolved once at\n * bootstrap. Every role is on when the variable is absent (plain `warlock\n * start`, `warlock dev`, and tests).\n */\n public static get roles(): ReadonlySet<AppRole> {\n return getRoles();\n }\n\n /**\n * The site keys this process installs pages for — `WARLOCK_SITES`,\n * resolved once at bootstrap. `undefined` means every site.\n */\n public static get sites(): ReadonlySet<string> | undefined {\n return getSites();\n }\n\n /**\n * Whether this process serves the given role.\n *\n * @example\n * if (Application.hasRole(\"worker\")) {\n * scheduler.start();\n * }\n */\n public static hasRole(role: AppRole): boolean {\n return hasRole(role);\n }\n\n /**\n * Get the root path\n */\n public static get rootPath(): string {\n return rootPath();\n }\n\n /**\n * Get the src path\n */\n public static get srcPath(): string {\n return srcPath();\n }\n\n /**\n * Get the app path\n */\n public static get appPath(): string {\n return appPath();\n }\n\n /**\n * Get the storage path\n */\n public static get storagePath(): string {\n return storagePath();\n }\n\n /**\n * Get the uploads path\n */\n public static get uploadsPath(): string {\n return uploadsPath();\n }\n\n /**\n * Get the public path\n */\n public static get publicPath(): string {\n return publicPath();\n }\n}\n"],"mappings":";;;;;;;;AAwDA,MAAM,iBAAiB;AASvB,IAAa,cAAb,MAAyB;;mCAIY,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,OAAO,IAAI,GAAI;;;gBAUxD;;;uBAWuB,CAAC;;;mBAOrB;;;wBAMsB,CAAC;;;sBAcpB;;;2BAKyB,CAAC;;;;;CAKxD,WAAkB,UAAU;EAC1B,OAAO,oBAAoB;CAC7B;;;;CAKA,OAAc,mBAAmB,UAA2B;EAC1D,KAAK,kBAAkB;CACzB;;;;;;CAOA,WAAkB,WAAoB;EACpC,OAAO,KAAK;CACd;;;;;;CAOA,OAAc,cAAc,MAAoB;EAC9C,KAAK,aAAa;CACpB;;;;;;;;;;;;;;;CAgBA,OAAc,WAAW,UAA8B;EACrD,IAAI,KAAK,UAAU,KAAK,aAAa;GACnC,MAAM,UAAU,KAAK;GACrB,AAAK,KAAK,gBAAgB,UAAU,OAAO;GAE3C;EACF;EAEA,KAAK,cAAc,KAAK,QAAQ;CAClC;;;;;CAMA,OAAc,aAAmC;EAC/C,IAAI,KAAK,UAAU,KAAK,aACtB,OAAO,QAAQ,QAAQ,KAAK,WAAW;EAGzC,OAAO,IAAI,SAAsB,YAAY;GAC3C,KAAK,YAAY,YAAY,QAAQ,OAAO,CAAC;EAC/C,CAAC;CACH;;;;;;;;;;;;;;;CAgBA,OAAc,WAAW,SAA4B;EACnD,IAAI,KAAK,QACP;EAGF,KAAK,SAAS;EACd,KAAK,cAAc;EAEnB,eAAe;GACb,MAAM;GACN;GACA,KAAK,QAAQ;GACb,qBAAI,IAAI,KAAK,EAAC,CAAC,YAAY;GAC3B,aAAa,QAAQ;GACrB,iBAAiB,QAAQ;GACzB,gBAAgB,QAAQ;GACxB,MAAM,KAAK;GACX,OAAO,CAAC,GAAG,KAAK,KAAK;EACvB,CAAC;EAED,MAAM,YAAY,KAAK;EACvB,KAAK,gBAAgB,CAAC;EAEtB,KAAK,MAAM,YAAY,WACrB,AAAK,KAAK,gBAAgB,UAAU,OAAO;CAE/C;;;;;CAMA,aAAqB,gBACnB,UACA,SACe;EACf,IAAI;GACF,MAAM,SAAS,OAAO;EACxB,SAAS,OAAO;GACd,IAAI,MAAM,eAAe,mBAAmB,KAAc;EAC5D;CACF;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,OAAc,eAAe,WAAgC;EAC3D,IAAI,KAAK,WACP,MAAM,IAAI,MACR,iPAIF;EAGF,KAAK,eAAe,KAAK,SAAS;CACpC;;;;;;;;;;;;;;;;;;CAmBA,aAAoB,uBAAsC;EACxD,IAAI,KAAK,WACP;EAGF,KAAK,YAAY;EAEjB,MAAM,aAAa,KAAK;EACxB,KAAK,iBAAiB,CAAC;EAEvB,KAAK,MAAM,aAAa,YACtB,IAAI;GACF,MAAM,UAAU;EAClB,SAAS,OAAO;GAId,MAAM,IAAI,eACR,sBAJW,UAAU,QAAQ,cAIF,mBAHf,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAGb,uHAGpD,EAAE,OAAO,MAAM,CACjB;EACF;CAEJ;;;;;;CAOA,WAAkB,iBAA0B;EAC1C,OAAO,KAAK;CACd;;;;;;;;;;;;CAaA,OAAc,WAAW,UAAkC;EACzD,IAAI,KAAK,cAAc;GACrB,AAAK,KAAK,oBAAoB,QAAQ;GAEtC;EACF;EAEA,KAAK,kBAAkB,KAAK,QAAQ;CACtC;;;;;;;;;;CAWA,aAAoB,mBAAkC;EACpD,IAAI,KAAK,cACP;EAGF,KAAK,eAAe;EAEpB,MAAM,YAAY,KAAK,kBAAkB,QAAQ;EACjD,KAAK,oBAAoB,CAAC;EAE1B,KAAK,MAAM,YAAY,WACrB,MAAM,KAAK,oBAAoB,QAAQ;CAE3C;;;;;CAMA,aAAqB,oBAAoB,UAA2C;EAClF,IAAI;GACF,MAAM,SAAS;EACjB,SAAS,OAAO;GACd,IAAI,MAAM,eAAe,qBAAqB,KAAc;EAC9D;CACF;;;;CAKA,WAAkB,SAAiB;EACjC,OAAO,QAAQ,OAAO,IAAI;CAC5B;;;;CAKA,WAAkB,cAA2B;EAC3C,OAAO,YAAY;CACrB;;;;CAKA,OAAc,eAAe,KAAkB;EAC7C,eAAe,GAAG;CACpB;;;;CAKA,WAAkB,eAAwB;EACxC,OAAO,KAAK,gBAAgB;CAC9B;;;;CAKA,WAAkB,gBAAyB;EACzC,OAAO,KAAK,gBAAgB;CAC9B;;;;CAKA,WAAkB,SAAkB;EAClC,OAAO,KAAK,gBAAgB;CAC9B;;;;;;CAOA,WAAkB,QAA8B;EAC9C,OAAO,SAAS;CAClB;;;;;CAMA,WAAkB,QAAyC;EACzD,OAAO,SAAS;CAClB;;;;;;;;;CAUA,OAAc,QAAQ,MAAwB;EAC5C,OAAO,QAAQ,IAAI;CACrB;;;;CAKA,WAAkB,WAAmB;EACnC,OAAO,SAAS;CAClB;;;;CAKA,WAAkB,UAAkB;EAClC,OAAO,QAAQ;CACjB;;;;CAKA,WAAkB,UAAkB;EAClC,OAAO,QAAQ;CACjB;;;;CAKA,WAAkB,cAAsB;EACtC,OAAO,YAAY;CACrB;;;;CAKA,WAAkB,cAAsB;EACtC,OAAO,YAAY;CACrB;;;;CAKA,WAAkB,aAAqB;EACrC,OAAO,WAAW;CACpB;AACF"}
@@ -31,7 +31,8 @@ type BootSignal = {
31
31
  environment: Environment;
32
32
  runtimeStrategy: RuntimeStrategy;
33
33
  bootDurationMs?: number; /** The bound http port, absent for an app with no http connector. */
34
- port?: number;
34
+ port?: number; /** The roles this process serves — see `Application.roles`. */
35
+ roles: string[];
35
36
  };
36
37
  /**
37
38
  * Environment flag `warlock start` sets on the process it supervises.
@@ -1 +1 @@
1
- {"version":3,"file":"boot-signal.d.mts","names":[],"sources":["../../../../../../../core/src/application/boot-signal.ts"],"mappings":";;;;;AAOA;;;KAAY,cAAA;AAAc;AAS1B;;;;AAAgC;AAUhC;AAnB0B,cASb,mBAAA;;;;;;;;;KAUD,UAAA;EACV,IAAA,EAAM,cAAA;EACN,OAAA,UAKA;EAHA,GAAA,UAIA;EAFA,EAAA;EACA,WAAA,EAAa,WAAA;EACb,eAAA,EAAiB,eAAA;EACjB,cAAA,WAEI;EAAJ,IAAA;AAAA;;;AAY8B;AAoBhC;;;;AAAiD;AA4BjD;cAhDa,mBAAA;;;;;;;AAgDwD;;;;;;;;;;;;iBA5BrD,cAAA,CAAe,MAAkB,EAAV,UAAU;;;;;;;;;;iBA4BjC,YAAA,CAAa,OAAA,YAAmB,OAAA,IAAW,UAAU"}
1
+ {"version":3,"file":"boot-signal.d.mts","names":[],"sources":["../../../../../../../core/src/application/boot-signal.ts"],"mappings":";;;;;AAOA;;;KAAY,cAAA;AAAc;AAS1B;;;;AAAgC;AAUhC;AAnB0B,cASb,mBAAA;;;;;;;;;KAUD,UAAA;EACV,IAAA,EAAM,cAAA;EACN,OAAA,UAKA;EAHA,GAAA,UAIA;EAFA,EAAA;EACA,WAAA,EAAa,WAAA;EACb,eAAA,EAAiB,eAAA;EACjB,cAAA,WAIK;EAFL,IAAA,WAcW;EAZX,KAAA;AAAA;;AAY8B;AAoBhC;;;;AAAiD;AA4BjD;;cAhDa,mBAAA;;;;;;AAgDwD;;;;;;;;;;;;;iBA5BrD,cAAA,CAAe,MAAkB,EAAV,UAAU;;;;;;;;;;iBA4BjC,YAAA,CAAa,OAAA,YAAmB,OAAA,IAAW,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"boot-signal.mjs","names":[],"sources":["../../../../../../../core/src/application/boot-signal.ts"],"sourcesContent":["import type { Environment, RuntimeStrategy } from \"../utils/environment\";\n\n/**\n * The only message type the boot channel carries today. Named rather than\n * inlined because both the sender (`sendBootSignal`) and the receiver\n * (`warlock start`) must agree on the exact literal.\n */\nexport type BootSignalType = \"warlock:ready\";\n\n/**\n * Schema version of {@link BootSignal}.\n *\n * Present from the first release on purpose: a version field cannot be added to\n * a shipped wire format without a flag day, and this one has to be able to\n * evolve while older CLIs are still supervising newer bundles.\n */\nexport const BOOT_SIGNAL_VERSION = 1;\n\n/**\n * What a booted child process tells its supervising parent over the IPC\n * channel `warlock start` opens for it.\n *\n * This is a wire contract, not an internal detail: `warlock start` prints its\n * success banner **only** when this arrives, so anything watching that banner\n * (a CI gate, a process supervisor) transitively depends on it.\n */\nexport type BootSignal = {\n type: BootSignalType;\n version: number;\n /** The application process, so a supervisor can address it directly. */\n pid: number;\n /** When the boot completed, ISO-8601. */\n at: string;\n environment: Environment;\n runtimeStrategy: RuntimeStrategy;\n bootDurationMs?: number;\n /** The bound http port, absent for an app with no http connector. */\n port?: number;\n};\n\n/**\n * Environment flag `warlock start` sets on the process it supervises.\n *\n * An IPC channel alone is NOT proof that the parent speaks this protocol —\n * pm2, a vitest worker fork, and any custom supervisor all hand their child a\n * channel they use for their own messages. Writing to one of those, and then\n * closing it, corrupts someone else's protocol and can kill the process. The\n * flag is the handshake: only a parent that set it gets signalled.\n */\nexport const BOOT_SIGNAL_ENV_KEY = \"WARLOCK_BOOT_SIGNAL\";\n\n/**\n * Report a completed boot to the parent process, if one is supervising.\n *\n * Two conditions must both hold: the process was spawned with an `ipc` stdio\n * channel (`process.send` exists), and the parent identified itself with\n * {@link BOOT_SIGNAL_ENV_KEY}. A bundle run directly (`node app.mjs`, a Docker\n * `CMD`) or under a foreign supervisor is a no-op, so the signal never changes\n * how a standalone process behaves and never touches a channel we don't own.\n *\n * The channel is closed as soon as the message is flushed. An open IPC channel\n * holds a `ref` on the child's event loop, which would keep a finished or\n * wedged process alive and indistinguishable from a healthy server — the exact\n * confusion this signal exists to remove. One message, then disconnect.\n *\n * Failure to send is deliberately swallowed: the parent may have already\n * detached or died, and a supervisor going away must never take down a\n * healthy application.\n */\nexport function sendBootSignal(signal: BootSignal): void {\n if (typeof process.send !== \"function\" || process.env[BOOT_SIGNAL_ENV_KEY] !== \"1\") {\n return;\n }\n\n // Consumed here so it cannot be inherited by anything this app itself spawns\n // and make a grandchild report readiness on a channel meant for someone else.\n delete process.env[BOOT_SIGNAL_ENV_KEY];\n\n try {\n process.send(signal, undefined, undefined, () => {\n process.disconnect?.();\n });\n } catch {\n // The channel closed between the check and the write — the app is up\n // regardless, and there is no longer anyone to tell.\n }\n}\n\n/**\n * Narrow an arbitrary IPC payload to a {@link BootSignal}.\n *\n * The parent receives whatever the child chooses to send, so the message is\n * untrusted input and is validated before it is allowed to flip the parent's\n * \"the server is up\" state. A *newer* version than this CLI knows is still\n * accepted — readiness is readiness, and refusing it would make an old\n * supervisor report a healthy app as failed.\n */\nexport function isBootSignal(message: unknown): message is BootSignal {\n if (typeof message !== \"object\" || message === null) {\n return false;\n }\n\n const candidate = message as { type?: unknown; version?: unknown };\n\n return candidate.type === \"warlock:ready\" && typeof candidate.version === \"number\";\n}\n"],"mappings":";;;;;;;;AAgBA,MAAa,sBAAsB;;;;;;;;;;AAiCnC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;AAoBnC,SAAgB,eAAe,QAA0B;CACvD,IAAI,OAAO,QAAQ,SAAS,cAAc,QAAQ,+BAA6B,KAC7E;CAKF,OAAO,QAAQ,IAAI;CAEnB,IAAI;EACF,QAAQ,KAAK,QAAQ,QAAW,cAAiB;GAC/C,QAAQ,aAAa;EACvB,CAAC;CACH,QAAQ,CAGR;AACF;;;;;;;;;;AAWA,SAAgB,aAAa,SAAyC;CACpE,IAAI,OAAO,YAAY,YAAY,YAAY,MAC7C,OAAO;CAGT,MAAM,YAAY;CAElB,OAAO,UAAU,SAAS,mBAAmB,OAAO,UAAU,YAAY;AAC5E"}
1
+ {"version":3,"file":"boot-signal.mjs","names":[],"sources":["../../../../../../../core/src/application/boot-signal.ts"],"sourcesContent":["import type { Environment, RuntimeStrategy } from \"../utils/environment\";\n\n/**\n * The only message type the boot channel carries today. Named rather than\n * inlined because both the sender (`sendBootSignal`) and the receiver\n * (`warlock start`) must agree on the exact literal.\n */\nexport type BootSignalType = \"warlock:ready\";\n\n/**\n * Schema version of {@link BootSignal}.\n *\n * Present from the first release on purpose: a version field cannot be added to\n * a shipped wire format without a flag day, and this one has to be able to\n * evolve while older CLIs are still supervising newer bundles.\n */\nexport const BOOT_SIGNAL_VERSION = 1;\n\n/**\n * What a booted child process tells its supervising parent over the IPC\n * channel `warlock start` opens for it.\n *\n * This is a wire contract, not an internal detail: `warlock start` prints its\n * success banner **only** when this arrives, so anything watching that banner\n * (a CI gate, a process supervisor) transitively depends on it.\n */\nexport type BootSignal = {\n type: BootSignalType;\n version: number;\n /** The application process, so a supervisor can address it directly. */\n pid: number;\n /** When the boot completed, ISO-8601. */\n at: string;\n environment: Environment;\n runtimeStrategy: RuntimeStrategy;\n bootDurationMs?: number;\n /** The bound http port, absent for an app with no http connector. */\n port?: number;\n /** The roles this process serves — see `Application.roles`. */\n roles: string[];\n};\n\n/**\n * Environment flag `warlock start` sets on the process it supervises.\n *\n * An IPC channel alone is NOT proof that the parent speaks this protocol —\n * pm2, a vitest worker fork, and any custom supervisor all hand their child a\n * channel they use for their own messages. Writing to one of those, and then\n * closing it, corrupts someone else's protocol and can kill the process. The\n * flag is the handshake: only a parent that set it gets signalled.\n */\nexport const BOOT_SIGNAL_ENV_KEY = \"WARLOCK_BOOT_SIGNAL\";\n\n/**\n * Report a completed boot to the parent process, if one is supervising.\n *\n * Two conditions must both hold: the process was spawned with an `ipc` stdio\n * channel (`process.send` exists), and the parent identified itself with\n * {@link BOOT_SIGNAL_ENV_KEY}. A bundle run directly (`node app.mjs`, a Docker\n * `CMD`) or under a foreign supervisor is a no-op, so the signal never changes\n * how a standalone process behaves and never touches a channel we don't own.\n *\n * The channel is closed as soon as the message is flushed. An open IPC channel\n * holds a `ref` on the child's event loop, which would keep a finished or\n * wedged process alive and indistinguishable from a healthy server — the exact\n * confusion this signal exists to remove. One message, then disconnect.\n *\n * Failure to send is deliberately swallowed: the parent may have already\n * detached or died, and a supervisor going away must never take down a\n * healthy application.\n */\nexport function sendBootSignal(signal: BootSignal): void {\n if (typeof process.send !== \"function\" || process.env[BOOT_SIGNAL_ENV_KEY] !== \"1\") {\n return;\n }\n\n // Consumed here so it cannot be inherited by anything this app itself spawns\n // and make a grandchild report readiness on a channel meant for someone else.\n delete process.env[BOOT_SIGNAL_ENV_KEY];\n\n try {\n process.send(signal, undefined, undefined, () => {\n process.disconnect?.();\n });\n } catch {\n // The channel closed between the check and the write — the app is up\n // regardless, and there is no longer anyone to tell.\n }\n}\n\n/**\n * Narrow an arbitrary IPC payload to a {@link BootSignal}.\n *\n * The parent receives whatever the child chooses to send, so the message is\n * untrusted input and is validated before it is allowed to flip the parent's\n * \"the server is up\" state. A *newer* version than this CLI knows is still\n * accepted — readiness is readiness, and refusing it would make an old\n * supervisor report a healthy app as failed.\n */\nexport function isBootSignal(message: unknown): message is BootSignal {\n if (typeof message !== \"object\" || message === null) {\n return false;\n }\n\n const candidate = message as { type?: unknown; version?: unknown };\n\n return candidate.type === \"warlock:ready\" && typeof candidate.version === \"number\";\n}\n"],"mappings":";;;;;;;;AAgBA,MAAa,sBAAsB;;;;;;;;;;AAmCnC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;AAoBnC,SAAgB,eAAe,QAA0B;CACvD,IAAI,OAAO,QAAQ,SAAS,cAAc,QAAQ,+BAA6B,KAC7E;CAKF,OAAO,QAAQ,IAAI;CAEnB,IAAI;EACF,QAAQ,KAAK,QAAQ,QAAW,cAAiB;GAC/C,QAAQ,aAAa;EACvB,CAAC;CACH,QAAQ,CAGR;AACF;;;;;;;;;;AAWA,SAAgB,aAAa,SAAyC;CACpE,IAAI,OAAO,YAAY,YAAY,YAAY,MAC7C,OAAO;CAGT,MAAM,YAAY;CAElB,OAAO,UAAU,SAAS,mBAAmB,OAAO,UAAU,YAAY;AAC5E"}
@@ -1,4 +1,5 @@
1
1
  import { app } from "./app.mjs";
2
+ import { APP_ROLES, AppRole, assertSitesRequireWebRole, getRoles, getSites, hasRole, parseRoles, parseSites, resetRolesCacheForTests, servesHttp } from "./roles.mjs";
2
3
  import { Application, BootContext, BootListener, BootValidator, ShutdownListener } from "./application.mjs";
3
4
  import { BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BootSignal, BootSignalType, isBootSignal, sendBootSignal } from "./boot-signal.mjs";
4
5
  import { AppConfigurations } from "./application-config-types.mjs";
@@ -1,4 +1,5 @@
1
1
  import { BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, isBootSignal, sendBootSignal } from "./boot-signal.mjs";
2
+ import { APP_ROLES, assertSitesRequireWebRole, getRoles, getSites, hasRole, parseRoles, parseSites, resetRolesCacheForTests, servesHttp } from "./roles.mjs";
2
3
  import { Application } from "./application.mjs";
3
4
  import { app } from "./app.mjs";
4
5
  import { getPublicUrl } from "./public-url.mjs";
@@ -0,0 +1,72 @@
1
+ //#region ../core/src/application/roles.d.ts
2
+ /**
3
+ * The three ways a process booted from the same build can serve traffic.
4
+ * `api` and `web` both bind the http port (§`servesHttp`); `worker` never
5
+ * does — it exists to run queue workers, the scheduler and `worker.ts`.
6
+ */
7
+ declare const APP_ROLES: readonly ["api", "web", "worker"];
8
+ /**
9
+ * One of {@link APP_ROLES}. What `Application.hasRole` checks and what
10
+ * `warlock start --role` accepts, comma-separated.
11
+ */
12
+ type AppRole = (typeof APP_ROLES)[number];
13
+ /**
14
+ * Parse a `--role` / `WARLOCK_ROLES` value into the set of roles a process
15
+ * should serve.
16
+ *
17
+ * `undefined` (the flag/variable absent) means "every role" — the same
18
+ * behaviour `warlock start` and `warlock dev` had before roles existed, so a
19
+ * plain `start` or a test run is unaffected. An explicit but empty list, or
20
+ * an entry that is not one of {@link APP_ROLES}, throws rather than silently
21
+ * falling back to "every role", since that would mask a typo as full traffic.
22
+ */
23
+ declare function parseRoles(value: string | undefined): ReadonlySet<AppRole>;
24
+ /**
25
+ * Parse a `--sites` / `WARLOCK_SITES` value into the set of site keys a
26
+ * `web` process should install pages for.
27
+ *
28
+ * `undefined` means "every site" (the default, unfiltered behaviour). An
29
+ * explicit but empty list throws rather than silently meaning "every site"
30
+ * or "no site" — both would be surprising for a flag that was actually
31
+ * passed. Cross-checking against the active roles and against `web.sites`
32
+ * config happens where both are known, not here.
33
+ */
34
+ declare function parseSites(value: string | undefined): ReadonlySet<string> | undefined;
35
+ /**
36
+ * `--sites` only makes sense alongside the `web` role — an `api`- or
37
+ * `worker`-only process never installs pages, so a sites filter on it could
38
+ * never do anything, and staying silent about that would hide a
39
+ * misconfigured deploy.
40
+ */
41
+ declare function assertSitesRequireWebRole(roles: ReadonlySet<AppRole>, sites: ReadonlySet<string> | undefined): void;
42
+ /**
43
+ * The roles this process serves. See {@link resolveRoles}.
44
+ */
45
+ declare function getRoles(): ReadonlySet<AppRole>;
46
+ /**
47
+ * The sites this process installs pages for, or `undefined` for every site.
48
+ * See {@link resolveSites}.
49
+ */
50
+ declare function getSites(): ReadonlySet<string> | undefined;
51
+ /**
52
+ * Whether this process serves the given role.
53
+ */
54
+ declare function hasRole(role: AppRole): boolean;
55
+ /**
56
+ * Whether this process should bind the http port — true when it serves
57
+ * `api` or `web`, false for a `worker`-only process. Shared by the http
58
+ * connector (skips `listen()`) and the production port preflight (skips the
59
+ * probe), so both agree on the same condition.
60
+ */
61
+ declare function servesHttp(): boolean;
62
+ /**
63
+ * Drop the cached roles/sites so the next call to {@link getRoles} /
64
+ * {@link getSites} re-reads `process.env`.
65
+ *
66
+ * @internal Test-only — production code resolves once per process on
67
+ * purpose (see {@link resolveRoles}).
68
+ */
69
+ declare function resetRolesCacheForTests(): void;
70
+ //#endregion
71
+ export { APP_ROLES, AppRole, assertSitesRequireWebRole, getRoles, getSites, hasRole, parseRoles, parseSites, resetRolesCacheForTests, servesHttp };
72
+ //# sourceMappingURL=roles.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.mts","names":[],"sources":["../../../../../../../core/src/application/roles.ts"],"mappings":";;AAKA;;;;cAAa,SAAA;AAMb;;;;AAAA,KAAY,OAAA,WAAkB,SAAS;AAcvC;;;;;;;;AAA0E;AAiC1E;AAjCA,iBAAgB,UAAA,CAAW,KAAA,uBAA4B,WAAW,CAAC,OAAA;;;AAiCD;AAuBlE;;;;;;;iBAvBgB,UAAA,CAAW,KAAA,uBAA4B,WAAW;;;;;;;iBAuBlD,yBAAA,CACd,KAAA,EAAO,WAAA,CAAY,OAAA,GACnB,KAAA,EAAO,WAAA;AA0CT;;;AAAA,iBAAgB,QAAA,IAAY,WAAW,CAAC,OAAA;AAAO;AAQ/C;;;AAR+C,iBAQ/B,QAAA,IAAY,WAAW;AAAA;AAOvC;;AAPuC,iBAOvB,OAAA,CAAQ,IAAa,EAAP,OAAO;;AAAA;AAUrC;;;;iBAAgB,UAAA;AAahB;;;;AAAuC;;;AAAvC,iBAAgB,uBAAA"}
@@ -0,0 +1,124 @@
1
+ //#region ../core/src/application/roles.ts
2
+ /**
3
+ * The three ways a process booted from the same build can serve traffic.
4
+ * `api` and `web` both bind the http port (§`servesHttp`); `worker` never
5
+ * does — it exists to run queue workers, the scheduler and `worker.ts`.
6
+ */
7
+ const APP_ROLES = [
8
+ "api",
9
+ "web",
10
+ "worker"
11
+ ];
12
+ const VALID_ROLES_LIST = APP_ROLES.join(", ");
13
+ /**
14
+ * Parse a `--role` / `WARLOCK_ROLES` value into the set of roles a process
15
+ * should serve.
16
+ *
17
+ * `undefined` (the flag/variable absent) means "every role" — the same
18
+ * behaviour `warlock start` and `warlock dev` had before roles existed, so a
19
+ * plain `start` or a test run is unaffected. An explicit but empty list, or
20
+ * an entry that is not one of {@link APP_ROLES}, throws rather than silently
21
+ * falling back to "every role", since that would mask a typo as full traffic.
22
+ */
23
+ function parseRoles(value) {
24
+ if (value === void 0) return new Set(APP_ROLES);
25
+ const entries = value.split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
26
+ if (entries.length === 0) throw new Error(`--role received an empty list. Valid roles: ${VALID_ROLES_LIST}.`);
27
+ for (const entry of entries) if (!APP_ROLES.includes(entry)) throw new Error(`Unknown role "${entry}". Valid roles: ${VALID_ROLES_LIST}.`);
28
+ return new Set(entries);
29
+ }
30
+ /**
31
+ * Parse a `--sites` / `WARLOCK_SITES` value into the set of site keys a
32
+ * `web` process should install pages for.
33
+ *
34
+ * `undefined` means "every site" (the default, unfiltered behaviour). An
35
+ * explicit but empty list throws rather than silently meaning "every site"
36
+ * or "no site" — both would be surprising for a flag that was actually
37
+ * passed. Cross-checking against the active roles and against `web.sites`
38
+ * config happens where both are known, not here.
39
+ */
40
+ function parseSites(value) {
41
+ if (value === void 0) return;
42
+ const entries = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
43
+ if (entries.length === 0) throw new Error("--sites received an empty list.");
44
+ return new Set(entries);
45
+ }
46
+ /**
47
+ * `--sites` only makes sense alongside the `web` role — an `api`- or
48
+ * `worker`-only process never installs pages, so a sites filter on it could
49
+ * never do anything, and staying silent about that would hide a
50
+ * misconfigured deploy.
51
+ */
52
+ function assertSitesRequireWebRole(roles, sites) {
53
+ if (sites !== void 0 && !roles.has("web")) throw new Error("--sites only applies to the web role");
54
+ }
55
+ let cachedRoles;
56
+ let cachedSites;
57
+ let sitesResolved = false;
58
+ /**
59
+ * The roles this process serves, resolved from `WARLOCK_ROLES` once and
60
+ * cached for the life of the process — the env var is bootstrap input, not
61
+ * something that can meaningfully change mid-run.
62
+ */
63
+ function resolveRoles() {
64
+ if (!cachedRoles) cachedRoles = parseRoles(process.env.WARLOCK_ROLES);
65
+ return cachedRoles;
66
+ }
67
+ /**
68
+ * The sites this process installs pages for, resolved from `WARLOCK_SITES`
69
+ * once and cached alongside {@link resolveRoles}. `undefined` means every
70
+ * site.
71
+ */
72
+ function resolveSites() {
73
+ if (!sitesResolved) {
74
+ cachedSites = parseSites(process.env.WARLOCK_SITES);
75
+ assertSitesRequireWebRole(resolveRoles(), cachedSites);
76
+ sitesResolved = true;
77
+ }
78
+ return cachedSites;
79
+ }
80
+ /**
81
+ * The roles this process serves. See {@link resolveRoles}.
82
+ */
83
+ function getRoles() {
84
+ return resolveRoles();
85
+ }
86
+ /**
87
+ * The sites this process installs pages for, or `undefined` for every site.
88
+ * See {@link resolveSites}.
89
+ */
90
+ function getSites() {
91
+ return resolveSites();
92
+ }
93
+ /**
94
+ * Whether this process serves the given role.
95
+ */
96
+ function hasRole(role) {
97
+ return resolveRoles().has(role);
98
+ }
99
+ /**
100
+ * Whether this process should bind the http port — true when it serves
101
+ * `api` or `web`, false for a `worker`-only process. Shared by the http
102
+ * connector (skips `listen()`) and the production port preflight (skips the
103
+ * probe), so both agree on the same condition.
104
+ */
105
+ function servesHttp() {
106
+ const roles = resolveRoles();
107
+ return roles.has("api") || roles.has("web");
108
+ }
109
+ /**
110
+ * Drop the cached roles/sites so the next call to {@link getRoles} /
111
+ * {@link getSites} re-reads `process.env`.
112
+ *
113
+ * @internal Test-only — production code resolves once per process on
114
+ * purpose (see {@link resolveRoles}).
115
+ */
116
+ function resetRolesCacheForTests() {
117
+ cachedRoles = void 0;
118
+ cachedSites = void 0;
119
+ sitesResolved = false;
120
+ }
121
+
122
+ //#endregion
123
+ export { APP_ROLES, assertSitesRequireWebRole, getRoles, getSites, hasRole, parseRoles, parseSites, resetRolesCacheForTests, servesHttp };
124
+ //# sourceMappingURL=roles.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.mjs","names":[],"sources":["../../../../../../../core/src/application/roles.ts"],"sourcesContent":["/**\n * The three ways a process booted from the same build can serve traffic.\n * `api` and `web` both bind the http port (§`servesHttp`); `worker` never\n * does — it exists to run queue workers, the scheduler and `worker.ts`.\n */\nexport const APP_ROLES = [\"api\", \"web\", \"worker\"] as const;\n\n/**\n * One of {@link APP_ROLES}. What `Application.hasRole` checks and what\n * `warlock start --role` accepts, comma-separated.\n */\nexport type AppRole = (typeof APP_ROLES)[number];\n\nconst VALID_ROLES_LIST = APP_ROLES.join(\", \");\n\n/**\n * Parse a `--role` / `WARLOCK_ROLES` value into the set of roles a process\n * should serve.\n *\n * `undefined` (the flag/variable absent) means \"every role\" — the same\n * behaviour `warlock start` and `warlock dev` had before roles existed, so a\n * plain `start` or a test run is unaffected. An explicit but empty list, or\n * an entry that is not one of {@link APP_ROLES}, throws rather than silently\n * falling back to \"every role\", since that would mask a typo as full traffic.\n */\nexport function parseRoles(value: string | undefined): ReadonlySet<AppRole> {\n if (value === undefined) {\n return new Set(APP_ROLES);\n }\n\n const entries = value\n .split(\",\")\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0);\n\n if (entries.length === 0) {\n throw new Error(`--role received an empty list. Valid roles: ${VALID_ROLES_LIST}.`);\n }\n\n for (const entry of entries) {\n if (!(APP_ROLES as readonly string[]).includes(entry)) {\n throw new Error(`Unknown role \"${entry}\". Valid roles: ${VALID_ROLES_LIST}.`);\n }\n }\n\n return new Set(entries as AppRole[]);\n}\n\n/**\n * Parse a `--sites` / `WARLOCK_SITES` value into the set of site keys a\n * `web` process should install pages for.\n *\n * `undefined` means \"every site\" (the default, unfiltered behaviour). An\n * explicit but empty list throws rather than silently meaning \"every site\"\n * or \"no site\" — both would be surprising for a flag that was actually\n * passed. Cross-checking against the active roles and against `web.sites`\n * config happens where both are known, not here.\n */\nexport function parseSites(value: string | undefined): ReadonlySet<string> | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n const entries = value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n\n if (entries.length === 0) {\n throw new Error(\"--sites received an empty list.\");\n }\n\n return new Set(entries);\n}\n\n/**\n * `--sites` only makes sense alongside the `web` role — an `api`- or\n * `worker`-only process never installs pages, so a sites filter on it could\n * never do anything, and staying silent about that would hide a\n * misconfigured deploy.\n */\nexport function assertSitesRequireWebRole(\n roles: ReadonlySet<AppRole>,\n sites: ReadonlySet<string> | undefined,\n): void {\n if (sites !== undefined && !roles.has(\"web\")) {\n throw new Error(\"--sites only applies to the web role\");\n }\n}\n\nlet cachedRoles: ReadonlySet<AppRole> | undefined;\nlet cachedSites: ReadonlySet<string> | undefined;\nlet sitesResolved = false;\n\n/**\n * The roles this process serves, resolved from `WARLOCK_ROLES` once and\n * cached for the life of the process — the env var is bootstrap input, not\n * something that can meaningfully change mid-run.\n */\nfunction resolveRoles(): ReadonlySet<AppRole> {\n if (!cachedRoles) {\n cachedRoles = parseRoles(process.env.WARLOCK_ROLES);\n }\n\n return cachedRoles;\n}\n\n/**\n * The sites this process installs pages for, resolved from `WARLOCK_SITES`\n * once and cached alongside {@link resolveRoles}. `undefined` means every\n * site.\n */\nfunction resolveSites(): ReadonlySet<string> | undefined {\n if (!sitesResolved) {\n cachedSites = parseSites(process.env.WARLOCK_SITES);\n assertSitesRequireWebRole(resolveRoles(), cachedSites);\n sitesResolved = true;\n }\n\n return cachedSites;\n}\n\n/**\n * The roles this process serves. See {@link resolveRoles}.\n */\nexport function getRoles(): ReadonlySet<AppRole> {\n return resolveRoles();\n}\n\n/**\n * The sites this process installs pages for, or `undefined` for every site.\n * See {@link resolveSites}.\n */\nexport function getSites(): ReadonlySet<string> | undefined {\n return resolveSites();\n}\n\n/**\n * Whether this process serves the given role.\n */\nexport function hasRole(role: AppRole): boolean {\n return resolveRoles().has(role);\n}\n\n/**\n * Whether this process should bind the http port — true when it serves\n * `api` or `web`, false for a `worker`-only process. Shared by the http\n * connector (skips `listen()`) and the production port preflight (skips the\n * probe), so both agree on the same condition.\n */\nexport function servesHttp(): boolean {\n const roles = resolveRoles();\n\n return roles.has(\"api\") || roles.has(\"web\");\n}\n\n/**\n * Drop the cached roles/sites so the next call to {@link getRoles} /\n * {@link getSites} re-reads `process.env`.\n *\n * @internal Test-only — production code resolves once per process on\n * purpose (see {@link resolveRoles}).\n */\nexport function resetRolesCacheForTests(): void {\n cachedRoles = undefined;\n cachedSites = undefined;\n sitesResolved = false;\n}\n"],"mappings":";;;;;;AAKA,MAAa,YAAY;CAAC;CAAO;CAAO;AAAQ;AAQhD,MAAM,mBAAmB,UAAU,KAAK,IAAI;;;;;;;;;;;AAY5C,SAAgB,WAAW,OAAiD;CAC1E,IAAI,UAAU,QACZ,OAAO,IAAI,IAAI,SAAS;CAG1B,MAAM,UAAU,MACb,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAC1C,QAAQ,UAAU,MAAM,SAAS,CAAC;CAErC,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MAAM,+CAA+C,iBAAiB,EAAE;CAGpF,KAAK,MAAM,SAAS,SAClB,IAAI,CAAE,UAAgC,SAAS,KAAK,GAClD,MAAM,IAAI,MAAM,iBAAiB,MAAM,kBAAkB,iBAAiB,EAAE;CAIhF,OAAO,IAAI,IAAI,OAAoB;AACrC;;;;;;;;;;;AAYA,SAAgB,WAAW,OAA4D;CACrF,IAAI,UAAU,QACZ;CAGF,MAAM,UAAU,MACb,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,QAAQ,UAAU,MAAM,SAAS,CAAC;CAErC,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MAAM,iCAAiC;CAGnD,OAAO,IAAI,IAAI,OAAO;AACxB;;;;;;;AAQA,SAAgB,0BACd,OACA,OACM;CACN,IAAI,UAAU,UAAa,CAAC,MAAM,IAAI,KAAK,GACzC,MAAM,IAAI,MAAM,sCAAsC;AAE1D;AAEA,IAAI;AACJ,IAAI;AACJ,IAAI,gBAAgB;;;;;;AAOpB,SAAS,eAAqC;CAC5C,IAAI,CAAC,aACH,cAAc,WAAW,QAAQ,IAAI,aAAa;CAGpD,OAAO;AACT;;;;;;AAOA,SAAS,eAAgD;CACvD,IAAI,CAAC,eAAe;EAClB,cAAc,WAAW,QAAQ,IAAI,aAAa;EAClD,0BAA0B,aAAa,GAAG,WAAW;EACrD,gBAAgB;CAClB;CAEA,OAAO;AACT;;;;AAKA,SAAgB,WAAiC;CAC/C,OAAO,aAAa;AACtB;;;;;AAMA,SAAgB,WAA4C;CAC1D,OAAO,aAAa;AACtB;;;;AAKA,SAAgB,QAAQ,MAAwB;CAC9C,OAAO,aAAa,CAAC,CAAC,IAAI,IAAI;AAChC;;;;;;;AAQA,SAAgB,aAAsB;CACpC,MAAM,QAAQ,aAAa;CAE3B,OAAO,MAAM,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK;AAC5C;;;;;;;;AASA,SAAgB,0BAAgC;CAC9C,cAAc;CACd,cAAc;CACd,gBAAgB;AAClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"add.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/add.command.ts"],"sourcesContent":["import { addCommandAction } from \"../../generations/add-command.action\";\r\nimport { command } from \"../../commands/cli-command\";\r\n\r\nexport const addCommand = command({\r\n name: \"add <features...>\",\r\n description: \"Add new feature(s) to the project\",\r\n action: addCommandAction,\r\n options: [\r\n {\r\n text: \"--package-manager, -pm\",\r\n description: \"Package manager to use, if not passed, it will be detected automatically\",\r\n },\r\n {\r\n text: \"--list, -l\",\r\n description: \"List available features\",\r\n type: \"boolean\",\r\n },\r\n {\r\n text: \"--no-install\",\r\n description:\r\n \"Record dependencies in package.json without installing them — still ejects configs, adds scripts, and runs setup hooks. Used by scaffolders that run a single install afterwards.\",\r\n type: \"boolean\",\r\n },\r\n ],\r\n});\r\n"],"mappings":";;;;AAGA,MAAa,aAAa,QAAQ;CAChC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;CACF;AACF,CAAC"}
1
+ {"version":3,"file":"add.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/add.command.ts"],"sourcesContent":["import { addCommandAction } from \"../../generations/add-command.action\";\nimport { command } from \"../../commands/cli-command\";\n\nexport const addCommand = command({\n name: \"add <features...>\",\n description: \"Add new feature(s) to the project\",\n action: addCommandAction,\n options: [\n {\n text: \"--package-manager, -pm\",\n description: \"Package manager to use, if not passed, it will be detected automatically\",\n },\n {\n text: \"--list, -l\",\n description: \"List available features\",\n type: \"boolean\",\n },\n {\n text: \"--no-install\",\n description:\n \"Record dependencies in package.json without installing them — still ejects configs, adds scripts, and runs setup hooks. Used by scaffolders that run a single install afterwards.\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;AAGA,MAAa,aAAa,QAAQ;CAChC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;CACF;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-database.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/create-database.command.ts"],"sourcesContent":["import { createDatabaseAction } from \"../../database/create-database-action\";\r\nimport { command } from \"../../commands/cli-command\";\r\n\r\nexport const createDatabaseCommand = command({\r\n name: \"create-database <name>\",\r\n alias: \"cdb\",\r\n description: \"Create a new database\",\r\n action: createDatabaseAction,\r\n preload: {\r\n config: [\"database\", \"log\"],\r\n env: true,\r\n connectors: [\"database\"],\r\n },\r\n options: [\r\n {\r\n text: \"--connection, -c\",\r\n description: \"Database connection name\",\r\n defaultValue: \"default\",\r\n },\r\n ],\r\n});\r\n"],"mappings":";;;;AAGA,MAAa,wBAAwB,QAAQ;CAC3C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP,QAAQ,CAAC,YAAY,KAAK;EAC1B,KAAK;EACL,YAAY,CAAC,UAAU;CACzB;CACA,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,cAAc;CAChB,CACF;AACF,CAAC"}
1
+ {"version":3,"file":"create-database.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/create-database.command.ts"],"sourcesContent":["import { createDatabaseAction } from \"../../database/create-database-action\";\nimport { command } from \"../../commands/cli-command\";\n\nexport const createDatabaseCommand = command({\n name: \"create-database <name>\",\n alias: \"cdb\",\n description: \"Create a new database\",\n action: createDatabaseAction,\n preload: {\n config: [\"database\", \"log\"],\n env: true,\n connectors: [\"database\"],\n },\n options: [\n {\n text: \"--connection, -c\",\n description: \"Database connection name\",\n defaultValue: \"default\",\n },\n ],\n});\n"],"mappings":";;;;AAGA,MAAa,wBAAwB,QAAQ;CAC3C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP,QAAQ,CAAC,YAAY,KAAK;EAC1B,KAAK;EACL,YAAY,CAAC,UAAU;CACzB;CACA,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,cAAc;CAChB,CACF;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/dev-server.command.ts"],"sourcesContent":["import { checkForFrameworkUpdate } from \"../../dev-server/check-for-updates\";\r\nimport { startDevelopmentServer } from \"../../dev-server/start-development-server\";\r\nimport { isDevWorker, superviseDevServer } from \"../../dev-server/supervisor\";\r\nimport { assertEsbuildBinaryIsLinked } from \"../../production/esbuild-preflight\";\r\nimport { command } from \"../../commands/cli-command\";\r\nimport { displayStartupBanner } from \"../cli-commands.utils\";\r\n\r\nexport const devServerCommand = command({\r\n name: \"dev\",\r\n description: \"Start development server (HMR, type-gen, health checks)\",\r\n persistent: true,\r\n preload: {\r\n runtimeStrategy: \"development\",\r\n // Overrides whatever NODE_ENV the shell exported. A shell with\r\n // NODE_ENV=production made Vite's dep optimiser pre-bundle the PRODUCTION\r\n // build of React, which silently suppresses hydration-mismatch warnings —\r\n // the dev server was hiding the exact class of bug it exists to surface.\r\n // Set here, in preload, because preloaders run before the action, and the\r\n // action is what eventually reaches Vite's `createServer`.\r\n environment: \"development\",\r\n config: true, // load all config\r\n bootstrap: true,\r\n prestart: true, // load prestart file (if exists)\r\n // Only the Early lifecycle phase starts here; the Late phase\r\n // (http, socket) starts after app modules load — see\r\n // development-server.ts STEP 8.5.\r\n connectors: true,\r\n },\r\n preAction: async () => {\r\n // Fail before either role (supervisor or worker) does any other work —\r\n // a missing/unlinked esbuild binary would otherwise surface much later\r\n // as an opaque low-level error from deep inside the transpile path.\r\n assertEsbuildBinaryIsLinked();\r\n\r\n // Two roles share this command. The first `warlock dev` becomes a thin\r\n // supervisor that owns the terminal and (re)spawns the real server; the\r\n // process it spawns carries WARLOCK_DEV_WORKER and falls through to the\r\n // server itself. See supervisor.ts for why the split exists.\r\n //\r\n // This branch has to sit in `preAction`, which runs BEFORE the command's\r\n // preloaders: the supervisor must never load config or start connectors,\r\n // or it would hold a second database connection open for the whole\r\n // session. `superviseDevServer()` never resolves, so nothing below it —\r\n // preload included — runs in the supervisor.\r\n if (!isDevWorker()) {\r\n return superviseDevServer();\r\n }\r\n\r\n await displayStartupBanner({ environment: \"development\" });\r\n },\r\n action: async (data) => {\r\n const devServer = await startDevelopmentServer({\r\n fresh: Boolean(data.options.fresh),\r\n // Pass `false` only when the CLI flag is explicitly set; `undefined`\r\n // lets `warlock.config.ts > devServer.*` defaults apply.\r\n generateTypings: data.options.skipTypings ? false : undefined,\r\n healthCheckers: data.options.skipHealth ? false : undefined,\r\n });\r\n\r\n // Fire-and-forget: once the server is ready, surface a one-line notice if\r\n // a newer @warlock.js/core has been published, and arm the `u` shortcut\r\n // that updates + restarts. Fully self-guarded — it never blocks startup\r\n // nor breaks dev if the registry is unreachable.\r\n void checkForFrameworkUpdate(devServer);\r\n },\r\n options: [\r\n {\r\n text: \"--fresh, -f\",\r\n description: \"Delete .warlock/manifest.json before start (force full re-parse from disk)\",\r\n type: \"boolean\",\r\n },\r\n {\r\n text: \"--skip-typings, -st\",\r\n description: \"Skip background type generation for this run\",\r\n type: \"boolean\",\r\n },\r\n {\r\n text: \"--skip-health, -sh\",\r\n description: \"Skip file health checkers for this run\",\r\n type: \"boolean\",\r\n },\r\n ],\r\n});\r\n"],"mappings":";;;;;;;;AAOA,MAAa,mBAAmB,QAAQ;CACtC,MAAM;CACN,aAAa;CACb,YAAY;CACZ,SAAS;EACP,iBAAiB;EAOjB,aAAa;EACb,QAAQ;EACR,WAAW;EACX,UAAU;EAIV,YAAY;CACd;CACA,WAAW,YAAY;EAIrB,4BAA4B;EAY5B,IAAI,CAAC,YAAY,GACf,OAAO,mBAAmB;EAG5B,MAAM,qBAAqB,EAAE,aAAa,cAAc,CAAC;CAC3D;CACA,QAAQ,OAAO,SAAS;EAatB,AAAK,wBAAwB,MAZL,uBAAuB;GAC7C,OAAO,QAAQ,KAAK,QAAQ,KAAK;GAGjC,iBAAiB,KAAK,QAAQ,cAAc,QAAQ;GACpD,gBAAgB,KAAK,QAAQ,aAAa,QAAQ;EACpD,CAAC,CAMqC;CACxC;CACA,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
1
+ {"version":3,"file":"dev-server.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/dev-server.command.ts"],"sourcesContent":["import { checkForFrameworkUpdate } from \"../../dev-server/check-for-updates\";\nimport { startDevelopmentServer } from \"../../dev-server/start-development-server\";\nimport { isDevWorker, superviseDevServer } from \"../../dev-server/supervisor\";\nimport { assertEsbuildBinaryIsLinked } from \"../../production/esbuild-preflight\";\nimport { command } from \"../../commands/cli-command\";\nimport { displayStartupBanner } from \"../cli-commands.utils\";\n\nexport const devServerCommand = command({\n name: \"dev\",\n description: \"Start development server (HMR, type-gen, health checks)\",\n persistent: true,\n preload: {\n runtimeStrategy: \"development\",\n // Overrides whatever NODE_ENV the shell exported. A shell with\n // NODE_ENV=production made Vite's dep optimiser pre-bundle the PRODUCTION\n // build of React, which silently suppresses hydration-mismatch warnings —\n // the dev server was hiding the exact class of bug it exists to surface.\n // Set here, in preload, because preloaders run before the action, and the\n // action is what eventually reaches Vite's `createServer`.\n environment: \"development\",\n config: true, // load all config\n bootstrap: true,\n prestart: true, // load prestart file (if exists)\n // Only the Early lifecycle phase starts here; the Late phase\n // (http, socket) starts after app modules load — see\n // development-server.ts STEP 8.5.\n connectors: true,\n },\n preAction: async () => {\n // Fail before either role (supervisor or worker) does any other work —\n // a missing/unlinked esbuild binary would otherwise surface much later\n // as an opaque low-level error from deep inside the transpile path.\n assertEsbuildBinaryIsLinked();\n\n // Two roles share this command. The first `warlock dev` becomes a thin\n // supervisor that owns the terminal and (re)spawns the real server; the\n // process it spawns carries WARLOCK_DEV_WORKER and falls through to the\n // server itself. See supervisor.ts for why the split exists.\n //\n // This branch has to sit in `preAction`, which runs BEFORE the command's\n // preloaders: the supervisor must never load config or start connectors,\n // or it would hold a second database connection open for the whole\n // session. `superviseDevServer()` never resolves, so nothing below it —\n // preload included — runs in the supervisor.\n if (!isDevWorker()) {\n return superviseDevServer();\n }\n\n await displayStartupBanner({ environment: \"development\" });\n },\n action: async (data) => {\n const devServer = await startDevelopmentServer({\n fresh: Boolean(data.options.fresh),\n // Pass `false` only when the CLI flag is explicitly set; `undefined`\n // lets `warlock.config.ts > devServer.*` defaults apply.\n generateTypings: data.options.skipTypings ? false : undefined,\n healthCheckers: data.options.skipHealth ? false : undefined,\n });\n\n // Fire-and-forget: once the server is ready, surface a one-line notice if\n // a newer @warlock.js/core has been published, and arm the `u` shortcut\n // that updates + restarts. Fully self-guarded — it never blocks startup\n // nor breaks dev if the registry is unreachable.\n void checkForFrameworkUpdate(devServer);\n },\n options: [\n {\n text: \"--fresh, -f\",\n description: \"Delete .warlock/manifest.json before start (force full re-parse from disk)\",\n type: \"boolean\",\n },\n {\n text: \"--skip-typings, -st\",\n description: \"Skip background type generation for this run\",\n type: \"boolean\",\n },\n {\n text: \"--skip-health, -sh\",\n description: \"Skip file health checkers for this run\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;AAOA,MAAa,mBAAmB,QAAQ;CACtC,MAAM;CACN,aAAa;CACb,YAAY;CACZ,SAAS;EACP,iBAAiB;EAOjB,aAAa;EACb,QAAQ;EACR,WAAW;EACX,UAAU;EAIV,YAAY;CACd;CACA,WAAW,YAAY;EAIrB,4BAA4B;EAY5B,IAAI,CAAC,YAAY,GACf,OAAO,mBAAmB;EAG5B,MAAM,qBAAqB,EAAE,aAAa,cAAc,CAAC;CAC3D;CACA,QAAQ,OAAO,SAAS;EAatB,AAAK,wBAAwB,MAZL,uBAAuB;GAC7C,OAAO,QAAQ,KAAK,QAAQ,KAAK;GAGjC,iBAAiB,KAAK,QAAQ,cAAc,QAAQ;GACpD,gBAAgB,KAAK,QAAQ,aAAa,QAAQ;EACpD,CAAC,CAMqC;CACxC;CACA,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}