@warlock.js/core 5.11.0 → 5.13.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 (149) hide show
  1. package/CHANGELOG.md +92 -54
  2. package/esm/application/app.d.mts +3 -3
  3. package/esm/application/app.mjs +6 -6
  4. package/esm/application/app.mjs.map +1 -1
  5. package/esm/cli/cli-commands.utils.mjs +7 -3
  6. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  7. package/esm/cli/commands/doctor/checks/health.check.mjs +1 -1
  8. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  9. package/esm/connectors/socket-connector.mjs +13 -16
  10. package/esm/connectors/socket-connector.mjs.map +1 -1
  11. package/esm/container/index.d.mts +20 -7
  12. package/esm/container/index.d.mts.map +1 -1
  13. package/esm/container/index.mjs +8 -5
  14. package/esm/container/index.mjs.map +1 -1
  15. package/esm/database/utils.d.mts +5 -1
  16. package/esm/database/utils.d.mts.map +1 -1
  17. package/esm/database/utils.mjs +7 -3
  18. package/esm/database/utils.mjs.map +1 -1
  19. package/esm/dev-server/dev-logger.mjs +12 -1
  20. package/esm/dev-server/dev-logger.mjs.map +1 -1
  21. package/esm/dev-server/development-server.mjs +1 -1
  22. package/esm/dev-server/development-server.mjs.map +1 -1
  23. package/esm/dev-server/file-event-handler.mjs +72 -13
  24. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  25. package/esm/dev-server/file-manager.d.mts +8 -0
  26. package/esm/dev-server/file-manager.d.mts.map +1 -1
  27. package/esm/dev-server/file-manager.mjs +26 -4
  28. package/esm/dev-server/file-manager.mjs.map +1 -1
  29. package/esm/dev-server/files-orchestrator.mjs +3 -3
  30. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  31. package/esm/dev-server/files-watcher.mjs +16 -2
  32. package/esm/dev-server/files-watcher.mjs.map +1 -1
  33. package/esm/dev-server/flags.mjs +16 -1
  34. package/esm/dev-server/flags.mjs.map +1 -1
  35. package/esm/dev-server/layer-executor.mjs +19 -2
  36. package/esm/dev-server/layer-executor.mjs.map +1 -1
  37. package/esm/dev-server/translation-type-generator.mjs +28 -0
  38. package/esm/dev-server/translation-type-generator.mjs.map +1 -0
  39. package/esm/dev-server/tsconfig-manager.mjs +1 -0
  40. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  41. package/esm/dev-server/type-generator.mjs +41 -5
  42. package/esm/dev-server/type-generator.mjs.map +1 -1
  43. package/esm/encryption/index.mjs +1 -1
  44. package/esm/errors/container-key-missing-error.mjs +18 -6
  45. package/esm/errors/container-key-missing-error.mjs.map +1 -1
  46. package/esm/generations/features/auth-google.feature.mjs +18 -0
  47. package/esm/generations/features/auth-google.feature.mjs.map +1 -0
  48. package/esm/generations/features/auth-passkeys.feature.mjs +19 -0
  49. package/esm/generations/features/auth-passkeys.feature.mjs.map +1 -0
  50. package/esm/generations/features/index.mjs +6 -0
  51. package/esm/generations/features/index.mjs.map +1 -1
  52. package/esm/generations/features/queue.feature.mjs +67 -0
  53. package/esm/generations/features/queue.feature.mjs.map +1 -0
  54. package/esm/generations/stubs.mjs +10 -10
  55. package/esm/generations/stubs.mjs.map +1 -1
  56. package/esm/http/context/request-context.d.mts +14 -11
  57. package/esm/http/context/request-context.d.mts.map +1 -1
  58. package/esm/http/context/request-context.mjs +13 -9
  59. package/esm/http/context/request-context.mjs.map +1 -1
  60. package/esm/http/createHttpApplication.d.mts.map +1 -1
  61. package/esm/http/createHttpApplication.mjs +2 -0
  62. package/esm/http/createHttpApplication.mjs.map +1 -1
  63. package/esm/http/csp.d.mts +139 -0
  64. package/esm/http/csp.d.mts.map +1 -0
  65. package/esm/http/csp.mjs +134 -0
  66. package/esm/http/csp.mjs.map +1 -0
  67. package/esm/http/errors/errors.d.mts +17 -1
  68. package/esm/http/errors/errors.d.mts.map +1 -1
  69. package/esm/http/errors/errors.mjs +20 -1
  70. package/esm/http/errors/errors.mjs.map +1 -1
  71. package/esm/http/index.d.mts +8 -3
  72. package/esm/http/index.mjs +6 -1
  73. package/esm/http/middleware/concurrency-limit.middleware.d.mts +2 -2
  74. package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
  75. package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
  76. package/esm/http/middleware/inject-request-context.d.mts +1 -1
  77. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  78. package/esm/http/middleware/inject-request-context.mjs +10 -0
  79. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  80. package/esm/http/middleware/rate-limit.middleware.d.mts +1 -1
  81. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  82. package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
  83. package/esm/http/middleware/utils/idempotency-key.mjs +13 -13
  84. package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
  85. package/esm/http/request-controller.d.mts +1 -1
  86. package/esm/http/request.d.mts +32 -44
  87. package/esm/http/request.d.mts.map +1 -1
  88. package/esm/http/request.mjs +52 -44
  89. package/esm/http/request.mjs.map +1 -1
  90. package/esm/http/response.d.mts +18 -0
  91. package/esm/http/response.d.mts.map +1 -1
  92. package/esm/http/response.mjs +31 -0
  93. package/esm/http/response.mjs.map +1 -1
  94. package/esm/http/stream-react-response.d.mts +65 -0
  95. package/esm/http/stream-react-response.d.mts.map +1 -0
  96. package/esm/http/stream-react-response.mjs +46 -0
  97. package/esm/http/stream-react-response.mjs.map +1 -0
  98. package/esm/http/tracing/index.mjs +4 -0
  99. package/esm/http/tracing/trace-id.d.mts +14 -0
  100. package/esm/http/tracing/trace-id.d.mts.map +1 -0
  101. package/esm/http/tracing/trace-id.mjs +37 -0
  102. package/esm/http/tracing/trace-id.mjs.map +1 -0
  103. package/esm/http/tracing/tracing-dispatcher.d.mts +42 -0
  104. package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -0
  105. package/esm/http/tracing/tracing-dispatcher.mjs +94 -0
  106. package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -0
  107. package/esm/http/tracing/tracing.type.d.mts +66 -0
  108. package/esm/http/tracing/tracing.type.d.mts.map +1 -0
  109. package/esm/http/types.d.mts +27 -21
  110. package/esm/http/types.d.mts.map +1 -1
  111. package/esm/index.d.mts +9 -4
  112. package/esm/index.mjs +7 -3
  113. package/esm/production/esbuild-preflight.mjs +47 -0
  114. package/esm/production/esbuild-preflight.mjs.map +1 -0
  115. package/esm/production/production-builder.mjs +19 -15
  116. package/esm/production/production-builder.mjs.map +1 -1
  117. package/esm/restful/restful.d.mts +1 -1
  118. package/esm/router/log-request-lifecycle.mjs +12 -2
  119. package/esm/router/log-request-lifecycle.mjs.map +1 -1
  120. package/esm/router/router.d.mts.map +1 -1
  121. package/esm/router/router.mjs +10 -0
  122. package/esm/router/router.mjs.map +1 -1
  123. package/esm/router/types.d.mts +1 -1
  124. package/esm/socket/utils.mjs +1 -2
  125. package/esm/socket/utils.mjs.map +1 -1
  126. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  127. package/esm/storage/drivers/cloud-driver.mjs +2 -5
  128. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  129. package/esm/warlock-config/types.d.mts +12 -0
  130. package/esm/warlock-config/types.d.mts.map +1 -1
  131. package/llms-full.txt +314 -46
  132. package/llms.txt +1 -0
  133. package/package.json +11 -12
  134. package/skills/add-connector/SKILL.md +1 -1
  135. package/skills/build-restful/SKILL.md +2 -2
  136. package/skills/configure-app/SKILL.md +31 -0
  137. package/skills/create-controller/SKILL.md +4 -4
  138. package/skills/request-tracing/SKILL.md +208 -0
  139. package/skills/send-response/SKILL.md +16 -0
  140. package/skills/store-file/SKILL.md +1 -1
  141. package/skills/upload-file/SKILL.md +2 -2
  142. package/skills/use-app-context/SKILL.md +2 -2
  143. package/skills/use-localization/SKILL.md +24 -21
  144. package/skills/use-middleware/SKILL.md +2 -2
  145. package/skills/use-repository/SKILL.md +1 -1
  146. package/skills/use-request-locals/SKILL.md +1 -1
  147. package/skills/wire-socket/SKILL.md +3 -3
  148. package/skills/write-middleware/SKILL.md +11 -5
  149. package/skills/write-use-case/SKILL.md +1 -1
package/CHANGELOG.md CHANGED
@@ -4,7 +4,43 @@ All notable changes to `@warlock.js/core` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
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.**
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
+
9
+ ## 5.13.0 - 2026-09-17
10
+
11
+ ### Added
12
+
13
+ - `warlock add queue` installs `@warlock.js/queue`, generates Redis-backed `src/config/queue.ts`, and registers `queueConnector()` in `warlock.config.ts`.
14
+ - `warlock add auth-google` and `warlock add auth-passkeys` install `@warlock.js/auth`'s Google sign-in (`jose`) and passkey (`@simplewebauthn/server`) login methods.
15
+ - `warlock dev` generates `.warlock/typings/translations.d.ts` from literal `groupedTranslations` dictionaries, augmenting web's typed translation-key registry.
16
+
17
+ ### Changed
18
+
19
+ - `warlock dev` now uses a 12ms quiet window for isolated file saves while extending multi-file bursts up to a 60ms maximum, reducing routine HMR latency without splitting formatter or checkout reloads.
20
+
21
+ ### Fixed
22
+
23
+ - Safely return no alias resolution when a configured alias has no first target.
24
+ - `@warlock.js/core` no longer imports `@warlock.js/auth`; `useHashedPassword` now calls core's own `hashPassword` directly instead of an auth-service delegate that only called back into core, and `@warlock.js/auth` moves to a dev-only dependency. A guard spec now fails the build if any `core/src` module imports `@warlock.js/auth`.
25
+
26
+ ## 5.12.0 - 2026-09-16
27
+
28
+ ### Added
29
+
30
+ - Opt-in request tracing (`http.tracing`): vendor-neutral `onRequestStart` / `onRequestEnd` / `onPhase` hooks fired around `route.match`, `middleware`, `validation`, `handler`, and `response.write`, with the trace id derived from an inbound W3C `traceparent` header (falling back to `request.id`). Off by default and zero-overhead when disabled; a throwing hook is caught and reported once, never breaking the request. No new response header — apps still correlate through the existing `X-Request-Id` echo. See the `request-tracing` skill.
31
+ - Opt-in `Content-Security-Policy` header (`http.csp`), using the per-request nonce the framework already generates; report-only mode supported.
32
+ - `Response.streamReact()` and the underlying `streamReactResponse()` helper (`@warlock.js/core`'s Stage 1 streaming SSR seam): pipe a React server stream (`renderToPipeableStream`) onto the raw response after writing the already-committed status and headers, aborting the render if the client disconnects. `@warlock.js/web` uses this exclusively to stream a page document — it never touches the raw response itself.
33
+
34
+ ### Fixed
35
+
36
+ - Renaming or moving a file under `warlock dev` no longer prints a false `ENOENT` failure before the route is rewired.
37
+ - `warlock routes --json` now prints only JSON on stdout. The `› Running <command>...` header moved to stderr, like the completion banner.
38
+ - Production build contribution hooks (`generate` / `emit`) saw `outFile`, `entryPath`, `singleBundle`, `esmShim` and `banner` as `undefined`, because bundling deleted them from the shared build options. The bundler now works on its own copy.
39
+
40
+ ### Changed
41
+
42
+ - **`container.get(key)` now throws a named error when the key is not registered**, instead of returning `undefined` while typed as present. Use `container.tryGet(key)` where the value is genuinely optional.
43
+ - **BREAKING:** `request.user` and `clearCurrentUser()` removed from the HTTP request; `RequestUser` moved to `@warlock.js/auth`. The authenticated user now lives at `request.locals.user`, a key `@warlock.js/auth` declares via module augmentation on `RequestLocals` and writes from its middleware. Reading `request.user` in development throws a new `RequestUserMovedError` naming `request.locals.user` (kept for one release as a migration diagnostic; removal is documented, not silent). `decodedAccessToken` and its cache-mark behavior (`request.locals.authDerived`) are unchanged. `useCurrentUser()` / `requestContext.getUser()` now read `request.locals.user` and return `unknown`/the caller's generic instead of the removed `RequestUser` type — see `@warlock.js/auth`'s `currentUser()` for a typed wrapper.
8
44
 
9
45
  ## 5.11.0 - 2026-09-14
10
46
 
@@ -65,6 +101,7 @@ _Released in lockstep with the `@warlock.js/*` family; no package-specific chang
65
101
  - Internal type-safety hardening across the CLI, dev server and request handling; no other behaviour change.
66
102
 
67
103
  ## 5.6.0 - 2026-09-08
104
+
68
105
  ### Fixed
69
106
 
70
107
  - **A production build asked the APP to resolve packages only the framework declares, so a built app could not boot under a strict pnpm tree.** Every bare specifier was left external, including the `@fastify/*`, `find-my-way`, `fast-jwt` and `@mongez/*` imports that reach the bundle through the framework's own code — none of which an app has any reason to declare. Under npm/yarn hoisting they resolved by accident; under pnpm the app died with `ERR_MODULE_NOT_FOUND` at startup, after a build that reported success. Externality is now decided per import edge: a bare specifier stays external unless the importer is not the app's own code AND the specifier names a package that importer's own `dependencies` declare.
@@ -195,7 +232,7 @@ _Released in lockstep with the `@warlock.js/*` family; no package-specific chang
195
232
  first run every time.
196
233
 
197
234
  ⚠ **What a clean diff does not prove.** The manifest records what each page's route
198
- name was *derived* as, not what it was *registered* as: `warlock build` boots no
235
+ name was _derived_ as, not what it was _registered_ as: `warlock build` boots no
199
236
  connectors, so it cannot see the API routes a page name may collide with, and the
200
237
  router appends a `.<method>` suffix to a name another method already claimed —
201
238
  which the comparison accepts rather than reporting. `Page routes match` therefore
@@ -263,14 +300,14 @@ _Released in lockstep with the `@warlock.js/*` family; no package-specific chang
263
300
  verbatim and live, and its failure summary now reports whether a cause actually arrived:
264
301
  `the cause is printed above, in the application's own output` when output was seen, and
265
302
  `no output was captured from the application process — its cause did not reach this
266
- terminal` when none was. It previously pointed at "above" unconditionally, which on a
303
+ terminal` when none was. It previously pointed at "above" unconditionally, which on a
267
304
  silent child meant pointing at an empty terminal
268
305
 
269
306
  - **The HTTP connector now preflights its port before binding.** `warlock dev` and
270
307
  `warlock start` both go through `HttpConnector.start()`, which now calls
271
308
  `assertPortIsAvailable(port, host)` immediately before `listen()`. A collision now
272
309
  surfaces as `EADDRINUSE: Port <port> is already in use on <host>. Stop the dev server
273
- (or whatever else is listening on port <port>) and run again...` — the code and the
310
+ (or whatever else is listening on port <port>) and run again...` — the code and the
274
311
  port named in the same sentence — instead of a bare `EADDRINUSE` thrown from inside
275
312
  Fastify with no indication of which port it meant. `EACCES` on the port is treated
276
313
  the same way, since "cannot bind" is one problem from the operator's side. The test
@@ -288,8 +325,8 @@ _Released in lockstep with the `@warlock.js/*` family; no package-specific chang
288
325
  `src/app/contact/controllers/contact.controller.ts` (a `POST /api/contact` route
289
326
  validated with `@warlock.js/seal`), and `src/web/home.page.tsx` ships an
290
327
  interactive, localized (en/ar) contact form wired to that route via `@mongez/http`
291
- + `@mongez/react-form` + `@mongez/react-localization`. The `web` feature now also
292
- installs those three packages as dependencies.
328
+ - `@mongez/react-form` + `@mongez/react-localization`. The `web` feature now also
329
+ installs those three packages as dependencies.
293
330
 
294
331
  ### Fixed
295
332
 
@@ -318,7 +355,7 @@ _Released in lockstep with the `@warlock.js/*` family; no package-specific chang
318
355
  ### Added
319
356
 
320
357
  - **`warlock add tailwind`** — installs and wires Tailwind CSS v4 through PostCSS.
321
- - **`warlock add shadcn`** — sets up the prerequisites shadcn/ui expects. It is *not* a
358
+ - **`warlock add shadcn`** — sets up the prerequisites shadcn/ui expects. It is _not_ a
322
359
  wrapper around the shadcn CLI: you still run that yourself to add components, this
323
360
  only makes the project ready for it.
324
361
 
@@ -377,15 +414,15 @@ resolution pin and the `@warlock.js/web` peer narrowing — see those packages'
377
414
 
378
415
  ⚠ **If your app runs behind a proxy and relied on `detectIp()` reading the forwarding headers without setting `http.trustProxy`, set `http.trustProxy: true`** (or a Fastify `trustProxy` value matching your edge). With `true` set, behaviour is unchanged: `X-Real-IP` first, then the leftmost `X-Forwarded-For` hop, then the peer address. Only enable `true` when your edge overwrites those headers — it trusts them wholesale
379
416
 
380
- - **`http.trustProxy` now accepts a hop count or a trusted-proxy list, and `detectIp()` honours them.** `true` is the wrong shape for the common topology: an edge that *appends* to `X-Forwarded-For` leaves whatever the client prepended as the leftmost entry, so "trust the leftmost hop" hands the client its own IP back. The config value is passed to Fastify untouched, and `detectIp()` now reads the resolved client off `request.ip` instead of re-parsing the header — so both agree, and every Fastify shape works:
417
+ - **`http.trustProxy` now accepts a hop count or a trusted-proxy list, and `detectIp()` honours them.** `true` is the wrong shape for the common topology: an edge that _appends_ to `X-Forwarded-For` leaves whatever the client prepended as the leftmost entry, so "trust the leftmost hop" hands the client its own IP back. The config value is passed to Fastify untouched, and `detectIp()` now reads the resolved client off `request.ip` instead of re-parsing the header — so both agree, and every Fastify shape works:
381
418
 
382
- | `http.trustProxy` | Client IP |
383
- | --- | --- |
384
- | `false` *(default)* | Socket peer address; forwarding headers ignored |
385
- | `true` | Leftmost `X-Forwarded-For` entry (whole chain trusted) |
386
- | `2` | Walks past the 2 rightmost hops — for an edge that appends |
419
+ | `http.trustProxy` | Client IP |
420
+ | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
421
+ | `false` _(default)_ | Socket peer address; forwarding headers ignored |
422
+ | `true` | Leftmost `X-Forwarded-For` entry (whole chain trusted) |
423
+ | `2` | Walks past the 2 rightmost hops — for an edge that appends |
387
424
  | `"10.0.0.0/8"`, `"loopback, 10.0.0.0/8"`, `["10.0.0.0/8", "192.168.0.0/16"]` | Walks left while each hop is a listed proxy, stops at the first that isn't |
388
- | `(address, hop) => boolean` | Your predicate |
425
+ | `(address, hop) => boolean` | Your predicate |
389
426
 
390
427
  Prefer the narrowest shape your topology allows: with `true`, any client that can reach the process directly picks its own IP, and an `ipFilter` allowlist in front of it is decorative
391
428
 
@@ -413,8 +450,8 @@ resolution pin and the `@warlock.js/web` peer narrowing — see those packages'
413
450
  import { afterAll } from "vitest";
414
451
  import { setupTest, teardownTest } from "@warlock.js/core/tests";
415
452
 
416
- await setupTest(); // ← was setupTest({ connectors: true })
417
- afterAll(teardownTest); // ← is new
453
+ await setupTest(); // ← was setupTest({ connectors: true })
454
+ afterAll(teardownTest); // ← is new
418
455
  ```
419
456
 
420
457
  1. **`{ connectors: true }` must become a bare `setupTest()`.** Under the new precedence it is an **explicit** value, so it now overrides your `src/config/tests.ts` where it previously deferred to it.
@@ -423,12 +460,12 @@ afterAll(teardownTest); // ← is new
423
460
 
424
461
  **This is the migration step nobody can skip.** `warlock add test` emits the corrected file for new projects.
425
462
 
426
- | What changes | How you'll see it | What to do |
427
- |---|---|---|
463
+ | What changes | How you'll see it | What to do |
464
+ | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
428
465
  | **`setupTest({ connectors })` now beats `tests.connectors` config** — the precedence flipped | a test file that passes `connectors` explicitly starts a **different connector set** than it did in 4.13.0 | grep for `setupTest({` — a call passing `connectors` was previously **ignored** and is now honoured. **Including the one in your generated setup file** |
429
- | **A second `setupTest` call with different options now REJECTS** | an error naming the active and the requested selection, where 4.13.0 silently did nothing | call `teardownTest()` first, or don't call `setupTest` again at all |
430
- | **The generated setup file now registers `afterAll(teardownTest)`** | your test files tear the framework down when they finish, instead of leaving it running | **add it to your existing `src/test-setup.ts`** — see below |
431
- | **Docs corrected: `setupTest` is called per TEST FILE, not per worker** | no runtime effect on its own — the *invocation* always worked this way | fix the comment in `src/test-setup.ts` as above |
466
+ | **A second `setupTest` call with different options now REJECTS** | an error naming the active and the requested selection, where 4.13.0 silently did nothing | call `teardownTest()` first, or don't call `setupTest` again at all |
467
+ | **The generated setup file now registers `afterAll(teardownTest)`** | your test files tear the framework down when they finish, instead of leaving it running | **add it to your existing `src/test-setup.ts`** — see below |
468
+ | **Docs corrected: `setupTest` is called per TEST FILE, not per worker** | no runtime effect on its own — the _invocation_ always worked this way | fix the comment in `src/test-setup.ts` as above |
432
469
 
433
470
  ### Added
434
471
 
@@ -442,15 +479,15 @@ afterAll(teardownTest); // ← is new
442
479
 
443
480
  - **BREAKING — an explicit `setupTest({ connectors })` now wins over `tests.connectors` config.** The order was `config > parameter > true`; it is now **`explicit parameter > config > true`**
444
481
 
445
- 4.13.0's changelog said this question was open, not settled: *"a per-call override is a contract decision for a later release."* This is that decision. **Call-site intent should beat a project default** — a caller who names a connector set is being specific on purpose, and silently overruling them was the wrong behaviour
482
+ 4.13.0's changelog said this question was open, not settled: _"a per-call override is a contract decision for a later release."_ This is that decision. **Call-site intent should beat a project default** — a caller who names a connector set is being specific on purpose, and silently overruling them was the wrong behaviour
446
483
 
447
484
  **"Explicit" means a non-`undefined` value.** `setupTest()`, `setupTest({})` and `setupTest({ connectors: undefined })` **all fall through to config, then to `true`.** The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase project config
448
485
 
449
486
  ⚠ **The generated `src/test-setup.ts` now calls `setupTest()` with no argument**, where it previously passed `{ connectors: true }`. Under the new order, passing `true` explicitly would erase the `tests.connectors` layer for the entire project. **If you edit your setup file, leave the call bare**
450
487
 
451
- ⚠ **This is user-visible and it is why the change is marked BREAKING:** an application that sets `tests.connectors` *and* passes `connectors` from any test file will start a different connector set after upgrading
488
+ ⚠ **This is user-visible and it is why the change is marked BREAKING:** an application that sets `tests.connectors` _and_ passes `connectors` from any test file will start a different connector set after upgrading
452
489
 
453
- - **BREAKING — a conflicting `setupTest` call rejects instead of being ignored.** While a setup is starting or ready, a call with *different* effective options now rejects with an error naming both the active and the requested selection. The same options remain a no-op, and concurrent identical calls share one startup
490
+ - **BREAKING — a conflicting `setupTest` call rejects instead of being ignored.** While a setup is starting or ready, a call with _different_ effective options now rejects with an error naming both the active and the requested selection. The same options remain a no-op, and concurrent identical calls share one startup
454
491
 
455
492
  Through 4.13.0 this was a silent early-return on an `isSetupComplete` flag — so `setupTest({ connectors: false })` in a file whose `src/test-setup.ts` had already run **did nothing at all, reported nothing, and started every connector anyway.** Connector arrays are compared as **sets** after deduplication, so caller order never counts as a conflict
456
493
 
@@ -478,7 +515,7 @@ afterAll(teardownTest); // ← is new
478
515
 
479
516
  ⚠ **A stranded lifecycle must fail with a message, not a dead process** — a crash mid-file is indistinguishable from an infrastructure flake, which is the worst way for a framework to report its own bug
480
517
 
481
- ⚠ **Scope of the proof, stated because a green here is easy to over-read:** all nine guards were seen to fail under their own mutation, **but every spec injects its scheduler** — the default *value* is tested while the production timer, and whether its `unref` releases the worker, is not. **No spec observes a real hang**; the stuck attempt is a mock gate, not a socket that never returns
518
+ ⚠ **Scope of the proof, stated because a green here is easy to over-read:** all nine guards were seen to fail under their own mutation, **but every spec injects its scheduler** — the default _value_ is tested while the production timer, and whether its `unref` releases the worker, is not. **No spec observes a real hang**; the stuck attempt is a mock gate, not a socket that never returns
482
519
 
483
520
  ### Documentation
484
521
 
@@ -488,7 +525,7 @@ afterAll(teardownTest); // ← is new
488
525
 
489
526
  **The lifetime this release commits to is FILE-SCOPED:** the setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing, correct under every pool, every isolation setting, and watch mode**
490
527
 
491
- ⚠ **This deliberately declines a faster option.** Holding lifecycle state in the worker runtime makes a worker-scoped lifetime *possible* — bootstrap once, reuse across every file in that worker — and an earlier draft of this release simply left the framework running to get it. **We are not shipping that**, for two reasons neither of which is performance:
528
+ ⚠ **This deliberately declines a faster option.** Holding lifecycle state in the worker runtime makes a worker-scoped lifetime _possible_ — bootstrap once, reuse across every file in that worker — and an earlier draft of this release simply left the framework running to get it. **We are not shipping that**, for two reasons neither of which is performance:
492
529
 
493
530
  1. **Under `pool: "threads"` we cannot honestly claim the runner cleans up.** Vitest tears the thread down while the process lives, and whether Node reclaims that thread's sockets and pools is **unmeasured** — so "the runner owns cleanup by termination" would be a promise we cannot observe being kept
494
531
  2. **In watch mode Vitest reuses workers between reruns**, so there is no recycle and therefore **no cleanup owner at all** between reruns. Declaring watch mode unsupported was the alternative, and a test framework whose lifecycle is undefined in the mode people use all day does not have a lifecycle
@@ -501,12 +538,12 @@ afterAll(teardownTest); // ← is new
501
538
 
502
539
  **Four breaking changes. Every one of them fails visibly, and every one is fixed by a single line or a single config key.** Three are security defaults that were wrong; the fourth is an import path.
503
540
 
504
- | What breaks | How you'll see it | The fix |
505
- |---|---|---|
506
- | **`http.cors` now actually applies** — it never had any effect in any release through 4.12.0 | requests from origins you never allow-listed start being rejected | check `http.cors` before upgrading; it now means what it says |
507
- | **`http.bodyLimit` defaults to Fastify's 1 MB**, not 200 GB | large uploads that used to be accepted answer `413` | set `http.bodyLimit` explicitly if you need more |
508
- | **`http.trustProxy` defaults to `false`** | `request.ip` becomes the socket address instead of `X-Forwarded-For` | set `http.trustProxy: true` **only if** you are genuinely behind a proxy that strips the header |
509
- | **The package entry no longer re-exports the CLI, dev server, test helpers or Vite integration** | build fails with `has no exported member` | test helpers move to `@warlock.js/core/tests`, `lowerStage3Decorators` to `@warlock.js/core/vite` — **one line per import.** The CLI and dev-server internals are **not** public and have no replacement specifier |
541
+ | What breaks | How you'll see it | The fix |
542
+ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
543
+ | **`http.cors` now actually applies** — it never had any effect in any release through 4.12.0 | requests from origins you never allow-listed start being rejected | check `http.cors` before upgrading; it now means what it says |
544
+ | **`http.bodyLimit` defaults to Fastify's 1 MB**, not 200 GB | large uploads that used to be accepted answer `413` | set `http.bodyLimit` explicitly if you need more |
545
+ | **`http.trustProxy` defaults to `false`** | `request.ip` becomes the socket address instead of `X-Forwarded-For` | set `http.trustProxy: true` **only if** you are genuinely behind a proxy that strips the header |
546
+ | **The package entry no longer re-exports the CLI, dev server, test helpers or Vite integration** | build fails with `has no exported member` | test helpers move to `@warlock.js/core/tests`, `lowerStage3Decorators` to `@warlock.js/core/vite` — **one line per import.** The CLI and dev-server internals are **not** public and have no replacement specifier |
510
547
 
511
548
  **If your app configures none of the three HTTP keys, the first three changes make it strictly safer with no action from you.** The `trustProxy` default in particular meant per-IP rate limiting was bypassable by anyone sending their own `X-Forwarded-For`.
512
549
 
@@ -542,9 +579,9 @@ Details for each are in the entries below.
542
579
 
543
580
  - **Shutdown survives a throwing log channel.** A connector whose `shutdown()` failed was reported through `log.error(...)` **from inside the catch block** — and `Logger.log()` hands each entry to `channel.log()` with no isolation, so a channel that throws synchronously (a misconfigured transport, an unserialisable payload) made that report reject. The rejection escaped `shutdown()` entirely, and the consequences went well past a missing log line: **`log.flush()` never ran, so every buffered entry from the whole run was lost; the remaining connectors were never torn down; and `process.exit(0)` — the line `gracefulShutdown` runs once `shutdown()` resolves — was never reached, leaving the process alive on the handles those connectors still held**
544
581
 
545
- ⚠ **This hardens the shutdown path, not the logger.** `Logger.log()` still aborts its fan-out on the first throwing channel, so the other channels never receive that entry, and an *asynchronously* rejecting channel is not covered at all — `channel.log()` is never awaited. **Logger-wide isolation is a separate fix in a later release**
582
+ ⚠ **This hardens the shutdown path, not the logger.** `Logger.log()` still aborts its fan-out on the first throwing channel, so the other channels never receive that entry, and an _asynchronously_ rejecting channel is not covered at all — `channel.log()` is never awaited. **Logger-wide isolation is a separate fix in a later release**
546
583
 
547
- - **A test server that fails to start no longer leaves half of itself running.** `startHttpTestServer()` publishes the resolved port before the late connector phase and sets `isServerRunning` only on its last line, so a failure in between left **live early-phase connectors and a published port pointing at a server that never came up** — while `stopHttpTestServer()` in `globalTeardown` reported *"No server to stop"* and walked away from them. Startup now unwinds what it started, always withdraws the port and resets its state. ⚠ **The error you get back is unchanged — it always was.** Startup had no `catch` at all, so the original failure already propagated correctly; what was missing was the cleanup, and the new `catch` exists only to run it. A failure *during* that cleanup is reported and never substituted for the cause, which is the one propagation guarantee the wrapper had to be careful not to break
584
+ - **A test server that fails to start no longer leaves half of itself running.** `startHttpTestServer()` publishes the resolved port before the late connector phase and sets `isServerRunning` only on its last line, so a failure in between left **live early-phase connectors and a published port pointing at a server that never came up** — while `stopHttpTestServer()` in `globalTeardown` reported _"No server to stop"_ and walked away from them. Startup now unwinds what it started, always withdraws the port and resets its state. ⚠ **The error you get back is unchanged — it always was.** Startup had no `catch` at all, so the original failure already propagated correctly; what was missing was the cleanup, and the new `catch` exists only to run it. A failure _during_ that cleanup is reported and never substituted for the cause, which is the one propagation guarantee the wrapper had to be careful not to break
548
585
 
549
586
  **`stopHttpTestServer()` withdraws the port and resets state in a `finally`.** They previously ran after the `await`, so a shutdown that threw left the published port behind and the next run in the same process inherited it
550
587
 
@@ -554,17 +591,18 @@ Details for each are in the entries below.
554
591
 
555
592
  **Why it had to change:** those five put dev-only tooling into the **static module graph of every application that imports the framework** — 39 files, reaching ESLint and, through it, ESLint's optional `jiti` import. It cost nothing while the builder kept `packages: "external"`, because esbuild never walked into the framework. **Anything that bundles walks it, and the build fails.** That is why `singleBundle` could not build a single real application
556
593
 
557
- ⚠ **Making those imports lazy does not help and should not be attempted.** esbuild resolves `import()` at build time; a dynamic import defers *evaluation*, not resolution. Measured: `await import("jiti")` in an otherwise empty file still fails with `Could not resolve "jiti"`. **Only unreachability from the entry removes a module from the graph**
594
+ ⚠ **Making those imports lazy does not help and should not be attempted.** esbuild resolves `import()` at build time; a dynamic import defers _evaluation_, not resolution. Measured: `await import("jiti")` in an otherwise empty file still fails with `Could not resolve "jiti"`. **Only unreachability from the entry removes a module from the graph**
558
595
 
559
596
  **The two public halves are now reachable from a subpath — and in 4.12.0 and earlier they were reachable from nowhere at all.** `./tests` and `./vite` are **real build entries with their own emitted files and `exports` entries**, not reachable from the root barrel, which is the entire point:
560
597
 
561
598
  ```ts
562
599
  // before // after
563
- import { setupTest } from "@warlock.js/core"; import { setupTest } from "@warlock.js/core/tests";
600
+ import { setupTest } from "@warlock.js/core";
601
+ import { setupTest } from "@warlock.js/core/tests";
564
602
  import { startHttpTestServer } from "@warlock.js/core";
565
- import { startHttpTestServer } from "@warlock.js/core/tests";
603
+ import { startHttpTestServer } from "@warlock.js/core/tests";
566
604
  import { lowerStage3Decorators } from "@warlock.js/core";
567
- import { lowerStage3Decorators } from "@warlock.js/core/vite";
605
+ import { lowerStage3Decorators } from "@warlock.js/core/vite";
568
606
  ```
569
607
 
570
608
  **The CLI and dev-server internals are different — they were never a public API and have no replacement specifier.** If you were importing from those, you were reaching into framework internals; open an issue describing what you needed.
@@ -573,7 +611,7 @@ Details for each are in the entries below.
573
611
 
574
612
  `Path` is unaffected — it moved to a genuine utility module and remains exported
575
613
 
576
- **Removing the five lines was necessary but not sufficient.** Three production modules — `connectors/http-connector`, `connectors/connectors-manager` and `warlock-config/warlock-config.manager` — imported the dev server's console formatter directly. In `http-connector` the dev-console call was simply **deleted**: the same error was already routed through `log.fatal` on the following line. `connectors-manager` now logs a connector's shutdown failure through `@warlock.js/logger`, **awaited and then flushed** — `process.exit(0)` follows immediately, so an un-awaited log is a log that never happens. `warlock-config.manager`'s *"`warlock.config.ts` is missing"* warning writes **straight to the console instead**: it runs during CLI bootstrap, before the logger has a single channel configured, so routing it through the logger would drop it in every application
614
+ **Removing the five lines was necessary but not sufficient.** Three production modules — `connectors/http-connector`, `connectors/connectors-manager` and `warlock-config/warlock-config.manager` — imported the dev server's console formatter directly. In `http-connector` the dev-console call was simply **deleted**: the same error was already routed through `log.fatal` on the following line. `connectors-manager` now logs a connector's shutdown failure through `@warlock.js/logger`, **awaited and then flushed** — `process.exit(0)` follows immediately, so an un-awaited log is a log that never happens. `warlock-config.manager`'s _"`warlock.config.ts` is missing"_ warning writes **straight to the console instead**: it runs during CLI bootstrap, before the logger has a single channel configured, so routing it through the logger would drop it in every application
577
615
 
578
616
  `tests/unit/meta/production-entry-graph.test.ts` enforces this from now on. It keys on **our own directory names** rather than a denylist of third-party packages, because a denylist rots the moment a dependency changes its imports and can only catch names someone thought of
579
617
 
@@ -615,15 +653,15 @@ Details for each are in the entries below.
615
653
 
616
654
  - **`warlock migrate --pending` — what will run next, in the order it will run.** `migrate` could report what had already run (`--list`) and what files existed on disk (`--all`), but not the one thing an operator asks before a schema change against a live database. The pending set was already computed on every migrate run; it simply had no read-only exit
617
655
 
618
- The gap forced a workaround that is **unsound in the dangerous direction**. `--all` globs `src/app` only, so it cannot see migrations a *package* registers through `database.migrations` — `@warlock.js/auth` alone contributes two. `--list` reads the migrations table, which does contain them. Differencing the two counts subtracts populations that do not overlap, and it under-counts pending by roughly the number of package migrations installed — reporting "nothing else is pending" when something is
656
+ The gap forced a workaround that is **unsound in the dangerous direction**. `--all` globs `src/app` only, so it cannot see migrations a _package_ registers through `database.migrations` — `@warlock.js/auth` alone contributes two. `--list` reads the migrations table, which does contain them. Differencing the two counts subtracts populations that do not overlap, and it under-counts pending by roughly the number of package migrations installed — reporting "nothing else is pending" when something is
619
657
 
620
658
  **`migrate --list` now prints both sections**, executed and pending, so the question can be answered without knowing a second flag exists. The executed section prints **first and unconditionally**: it is a table read that cannot fail because of a broken file on disk, and `--list` is the command reached for while something is already wrong. `--list` always exits `0` — it is a report
621
659
 
622
- **`--pending` is the gate**, and its exit code is its entire API: **`0`** computed and nothing pending, **`1`** computed and N pending, **`2`** could not be computed. Two codes would fold "three migrations are waiting" into "I could not work out what is waiting", and those demand opposite responses — the first is *run them*, the second is *stop*. `migrate --pending && deploy` behaves correctly under all three
660
+ **`--pending` is the gate**, and its exit code is its entire API: **`0`** computed and nothing pending, **`1`** computed and N pending, **`2`** could not be computed. Two codes would fold "three migrations are waiting" into "I could not work out what is waiting", and those demand opposite responses — the first is _run them_, the second is _stop_. `migrate --pending && deploy` behaves correctly under all three
623
661
 
624
- **A failure to read the migrations never reports `0`.** Computing pending requires loading project code, and a single migration file missing its default export throws. That degrades to an explicit `Pending: unavailable — <reason>` line with the executed listing intact above it, and `--pending` exits `2`. An empty pending set means *nothing is pending*, and nothing else
662
+ **A failure to read the migrations never reports `0`.** Computing pending requires loading project code, and a single migration file missing its default export throws. That degrades to an explicit `Pending: unavailable — <reason>` line with the executed listing intact above it, and `--pending` exits `2`. An empty pending set means _nothing is pending_, and nothing else
625
663
 
626
- `--all` deliberately does **not** gain a migration name beside each path. The only identifier available without loading is the one derived from the filename, and that derivation is a *fallback* used when a migration does not set `migrationName` — so any migration that names itself (`auth`'s do) would be listed under a name that does not exist. A wrong identifier in a listing whose purpose is cross-referencing is worse than no identifier, and `--list`'s two sections answer the comparison directly
664
+ `--all` deliberately does **not** gain a migration name beside each path. The only identifier available without loading is the one derived from the filename, and that derivation is a _fallback_ used when a migration does not set `migrationName` — so any migration that names itself (`auth`'s do) would be listed under a name that does not exist. A wrong identifier in a listing whose purpose is cross-referencing is worse than no identifier, and `--list`'s two sections answer the comparison directly
627
665
 
628
666
  Proven against a real Postgres: an executed package migration and a pending local one land in the correct sections, a fully-migrated database reports an empty pending list rather than an absent one, and the reporter's `files − executed` arithmetic is pinned as a test that fails if it is ever reintroduced
629
667
 
@@ -635,17 +673,17 @@ Details for each are in the entries below.
635
673
 
636
674
  ### Fixed
637
675
 
638
- - **A build artifact that names an entry point it does not contain is now refused before it can be packed.** An interrupted build leaves a directory that looks finished — `package.json`, `README`, `CHANGELOG`, `bin/`, `skills/` — and holds no compiled code at all. Nineteen existed in this tree at once, and nothing in the release path noticed: the only related guard compares **modification times**, so a hollow directory with a freshly written manifest is *newer than source* and passes, and it runs solely on the artifact-reuse path, which is not how the hollow directories were produced
676
+ - **A build artifact that names an entry point it does not contain is now refused before it can be packed.** An interrupted build leaves a directory that looks finished — `package.json`, `README`, `CHANGELOG`, `bin/`, `skills/` — and holds no compiled code at all. Nineteen existed in this tree at once, and nothing in the release path noticed: the only related guard compares **modification times**, so a hollow directory with a freshly written manifest is _newer than source_ and passes, and it runs solely on the artifact-reuse path, which is not how the hollow directories were produced
639
677
 
640
678
  Each artifact is now verified immediately before `npm pack`, on the normal build path and the reuse path alike. **The manifest is the specification:** `main`, `module` and the typings field name the exact files the package promises to ship, so they are resolved against the artifact and must exist. Fields a manifest does not declare are skipped — `core` and `auth` point `main` at `esm/` while `cascade`, `ai` and `seal` point it at `cjs/`, and any check that assumed one build shape would have raised a false failure on packages that are entirely correct. A manifest declaring no entry point at all is also a failure: a published package nothing can import is not a package
641
679
 
642
680
  - **The production acceptance gate no longer inherits the environment it is supposed to be testing.** `run-pnpm-acceptance.mjs` spawned every child with `env: { ...process.env }` and set no `NODE_ENV`. It exercised the production path only because the shell it was written in happened to carry `NODE_ENV=production`; on a clean checkout, a new contributor's machine, or CI, the same gate boots the app in **development** — and does not fail, it passes while testing something other than the thing it is named after. That is the worst outcome available to a gate, and it sat underneath the proof for 4.11.0's headline fix
643
681
 
644
- `NODE_ENV=production` is now set explicitly on every spawn, and — more importantly — **asserted from inside the running app**: `/acceptance` reports the environment it actually booted in, and the run fails if it is anything else. Setting a variable and never checking it arrived is how the original defect survived. The remaining `{ ...process.env }` is documented as a deliberate inheritance of `PATH` and the package-manager store paths, with everything the *verdict* depends on set after it
682
+ `NODE_ENV=production` is now set explicitly on every spawn, and — more importantly — **asserted from inside the running app**: `/acceptance` reports the environment it actually booted in, and the run fails if it is anything else. Setting a variable and never checking it arrived is how the original defect survived. The remaining `{ ...process.env }` is documented as a deliberate inheritance of `PATH` and the package-manager store paths, with everything the _verdict_ depends on set after it
645
683
 
646
684
  Consequence for the roadmap, recorded because the ordering matters: **CI wiring for this gate is now blocked on this fix, not parallel to it.** Wiring it up first would have produced a green from CI — which carries more weight than a local one — for a run that never touched the production path
647
685
 
648
- - **`warlock migrate --rollback=false` no longer drops every table.** CLI options were parsed as raw strings and nothing ever coerced them: `--rollback=false` reached the action as the string `"false"`, `if (rollback)` saw a truthy value, and the run rolled back *everything*. The declared `type: "boolean"` on the option was decorative — used only to render help. The same shape existed on every boolean option, including `warlock drop.tables --force=false`, where it turned a confirmation prompt into an unattended drop
686
+ - **`warlock migrate --rollback=false` no longer drops every table.** CLI options were parsed as raw strings and nothing ever coerced them: `--rollback=false` reached the action as the string `"false"`, `if (rollback)` saw a truthy value, and the run rolled back _everything_. The declared `type: "boolean"` on the option was decorative — used only to render help. The same shape existed on every boolean option, including `warlock drop.tables --force=false`, where it turned a confirmation prompt into an unattended drop
649
687
 
650
688
  Its twin was worse. A bare `--flag` swallowed the following token as its value, so `warlock migrate --rollback 2024_users.ts` produced `rollback: "2024_users.ts"` — the filename was never read as a path, and every table went down while the operator believed they had named one file. **A declared boolean now never consumes the next positional**: `--rollback 2024_users.ts` is `rollback: true` plus the positional `2024_users.ts`
651
689
 
@@ -657,7 +695,7 @@ Details for each are in the entries below.
657
695
 
658
696
  - **`warlock generate.module users --force=false` no longer overwrites your files.** The coercion above is opt-in by design — it applies only to options a command declares `type: "boolean"`, so a string option whose value is genuinely the word `false` survives. The generate family and `add` never carried that declaration, so the fix reached none of them and both faces of the defect stayed live on the commands most likely to be run against existing source
659
697
 
660
- `--force=false` arrived at every generator as the truthy string `"false"` and the overwrite guard (`if (exists && !force)`) let it through — a flag written to *prevent* clobbering did the clobbering. Its twin ate the target: `warlock generate.module --force users` parsed `users` as the value of `--force`, so the module name was lost entirely and the generator ran with no name
698
+ `--force=false` arrived at every generator as the truthy string `"false"` and the overwrite guard (`if (exists && !force)`) let it through — a flag written to _prevent_ clobbering did the clobbering. Its twin ate the target: `warlock generate.module --force users` parsed `users` as the value of `--force`, so the module name was lost entirely and the generator ran with no name
661
699
 
662
700
  Twenty-three option declarations are now typed: `--force, -f` and `--dry-run` on all eight `generate.*` commands, plus `--minimal, -m`, `--with-validation, -v`, `--with-resource, -rs`, and both `--timestamps [bool]` declarations, and `--list, -l` / `--no-install` on `add`. Options that carry real data are deliberately untouched and still take a value — `--table`, `--add`, `--drop`, `--rename` on the generators, `--package-manager` on `add`, and `seed --drop="Seed Name"`, whose value scopes which seeder is undone
663
701
 
@@ -665,17 +703,17 @@ Details for each are in the entries below.
665
703
 
666
704
  The guard drives the real command objects through the manager's own resolution path (`tests/unit/cli/generate-flag-options.test.ts`) and asserts what the action is handed. Asserting the declaration object instead would pass against a fixture while the CLI stayed broken
667
705
 
668
- - **`new Image(...)` no longer fails depending on how soon you call it.** The `Image` module fired `import("sharp")` at load time without awaiting it, and the constructor only checked whether that import had *failed* — never whether it was still in flight. Constructing an image in the first tick after importing the package therefore ran with an undefined sharp function and died with `TypeError: sharpFn is not a function`; the exact same code passed if something had awaited a timer first. Anything that builds an image during boot — a startup thumbnail job, a module-level warm-up — hit it, and it presented as a mysterious "works locally, breaks in prod" timing bug rather than as a missing dependency
706
+ - **`new Image(...)` no longer fails depending on how soon you call it.** The `Image` module fired `import("sharp")` at load time without awaiting it, and the constructor only checked whether that import had _failed_ — never whether it was still in flight. Constructing an image in the first tick after importing the package therefore ran with an undefined sharp function and died with `TypeError: sharpFn is not a function`; the exact same code passed if something had awaited a timer first. Anything that builds an image during boot — a startup thumbnail job, a module-level warm-up — hit it, and it presented as a mysterious "works locally, breaks in prod" timing bug rather than as a missing dependency
669
707
 
670
708
  Sharp is now resolved **synchronously on the first construction that needs it**, via `createRequire`, and the outcome is cached for the process. There is no longer a window in which the constructor can proceed without a real sharp function: it either has the module or throws. A missing sharp still throws the same install-hint error, at the same point (construction), with the same wording
671
709
 
672
710
  Resolution stays **lazy** — importing `@warlock.js/core` still does not load sharp's native binary, so apps that never touch images pay nothing — and constructing an `Image` from an existing sharp instance short-circuits before any module load
673
711
 
674
- The guard for this is a spawned fresh Node process that imports and constructs with nothing in between (`tests/unit/image/image-sharp-resolution.test.ts`). A same-process test cannot catch it: importing at collection time and constructing later *is* the delay that hides the bug
712
+ The guard for this is a spawned fresh Node process that imports and constructs with nothing in between (`tests/unit/image/image-sharp-resolution.test.ts`). A same-process test cannot catch it: importing at collection time and constructing later _is_ the delay that hides the bug
675
713
 
676
714
  - **A sharp that is installed but will not load no longer reports itself as "not installed".** The resolution above swallowed every failure into a single outcome, so the most common real-world sharp problem — the package present but its native binary built for another platform — arrived as `sharp is not installed.` plus instructions to run `npm install sharp`, which cannot fix it. sharp throws its own long, actionable error naming the runtime, the failing `.node` file and the exact install flags to use; that text was discarded and replaced with a different, wrong cause
677
715
 
678
- Only **genuine absence** now produces the install hint: a `MODULE_NOT_FOUND` whose message names the specifier `'sharp'` exactly. Matching on the code alone is not sufficient — a dependency missing *inside* sharp raises the very same code (`Cannot find module 'color'`), and would have been reported as sharp itself being absent. Any other failure surfaces sharp's own message, inlined as `Failed to load "sharp": …` **and** chained as `cause`, so a terminal that never prints `cause` still shows the text that helps. The absent-sharp path is unchanged, wording included
716
+ Only **genuine absence** now produces the install hint: a `MODULE_NOT_FOUND` whose message names the specifier `'sharp'` exactly. Matching on the code alone is not sufficient — a dependency missing _inside_ sharp raises the very same code (`Cannot find module 'color'`), and would have been reported as sharp itself being absent. Any other failure surfaces sharp's own message, inlined as `Failed to load "sharp": …` **and** chained as `cause`, so a terminal that never prints `cause` still shows the text that helps. The absent-sharp path is unchanged, wording included
679
717
 
680
718
  **The failure reason is cached, not just the fact of failure.** The resolution attempt runs once per process; a second `new Image(...)` skips the load entirely, so caching only "there is no sharp function" would have re-told the same lie one call later. The guard therefore constructs **twice** in each spawned process and asserts the second error matches the first (`tests/unit/image/image-sharp-load-failure.test.ts`) — a one-shot test passes even with that bug present
681
719
 
@@ -691,7 +729,7 @@ Details for each are in the entries below.
691
729
 
692
730
  As with sharp, the failure **reason** is cached rather than only the fact of failure, so the second call cannot fall through to the "not installed" branch and re-tell a lie the first call got right
693
731
 
694
- The guard is a spawned fresh Node process (`tests/unit/react/react-module-resolution.test.ts`); a same-process test cannot catch this, which is why the pre-existing suite was green against it. Against the live bug all five cases failed, and — the detail that shows how much the race hid — all five failed with the *same* `Cannot read properties of undefined` symptom, including the case that only asserts the install message. The present-but-broken states are staged by copying the module source next to a fixture `node_modules`, since resolution anchors to the importing file; the real workspace is not mutated
732
+ The guard is a spawned fresh Node process (`tests/unit/react/react-module-resolution.test.ts`); a same-process test cannot catch this, which is why the pre-existing suite was green against it. Against the live bug all five cases failed, and — the detail that shows how much the race hid — all five failed with the _same_ `Cannot read properties of undefined` symptom, including the case that only asserts the install message. The present-but-broken states are staged by copying the module source next to a fixture `node_modules`, since resolution anchors to the importing file; the real workspace is not mutated
695
733
 
696
734
  ## 4.11.0
697
735
 
@@ -714,17 +752,17 @@ Details for each are in the entries below.
714
752
 
715
753
  ### Fixed
716
754
 
717
- - **A production bundle no longer imports a package your app does not declare.** `warlock build`'s generated config loader emitted `import config from "@mongez/config"` — one of *core's* dependencies, never the app's. npm and yarn hoist flat so it resolved by accident; under pnpm's strict layout the shipped bundle died at boot with `ERR_MODULE_NOT_FOUND` for a package the app had no reason to install. The generator now emits `setConfig` from `@warlock.js/core`, which the app does declare, and Node resolves `@mongez/config` from core's own install — correct under pnpm, and portable, unlike baking absolute paths into an artifact meant to be copied between machines
755
+ - **A production bundle no longer imports a package your app does not declare.** `warlock build`'s generated config loader emitted `import config from "@mongez/config"` — one of _core's_ dependencies, never the app's. npm and yarn hoist flat so it resolved by accident; under pnpm's strict layout the shipped bundle died at boot with `ERR_MODULE_NOT_FOUND` for a package the app had no reason to install. The generator now emits `setConfig` from `@warlock.js/core`, which the app does declare, and Node resolves `@mongez/config` from core's own install — correct under pnpm, and portable, unlike baking absolute paths into an artifact meant to be copied between machines
718
756
 
719
757
  **The rule is now enforced, not just followed.** `warlock build` fails if any specifier written into generated code is missing from the app's `dependencies`, listing every violation at once. Rewriting the one bad import fixes today's bundle; the check is what stops the next change to the generator from reintroducing it invisibly — under npm and yarn the mistake never surfaces
720
758
 
721
759
  Two scaffolding sites had the same defect and are fixed with it: `warlock generate.module` emitted `groupedTranslations` from `@mongez/localization`, and the communicators config stub emitted `env` from `@mongez/dotenv`. Both now come from `@warlock.js/core`, which already re-exports them
722
760
 
723
- - **`env()` inside `warlock.config.ts` no longer always returns its default.** The config module was evaluated *before* any `.env` file was read, so a project following the documented `build: { outdir: env("BUILD_OUT", "dist") }` recipe silently got `dist` no matter what the environment said — under every command, `dev` included, and under `build` and `start` env was never loaded at all. Env files are now loaded before `warlock.config.ts` is evaluated, for every command
761
+ - **`env()` inside `warlock.config.ts` no longer always returns its default.** The config module was evaluated _before_ any `.env` file was read, so a project following the documented `build: { outdir: env("BUILD_OUT", "dist") }` recipe silently got `dist` no matter what the environment said — under every command, `dev` included, and under `build` and `start` env was never loaded at all. Env files are now loaded before `warlock.config.ts` is evaluated, for every command
724
762
 
725
763
  Loading is guarded: a project with no `.env` is legitimate and must not start failing `warlock build` now that env loads everywhere. `NODE_ENV` remains authoritative for which file is chosen — no command forces the environment, so a deliberate `NODE_ENV=staging` build still reads `.env.staging`. With `NODE_ENV` unset, plain `.env` is read
726
764
 
727
- - **An application without `src/config/storage.ts` can boot again.** The storage connector starts unconditionally, on the documented grounds that `storage.init()` falls back to a built-in `local` driver so file storage works out of the box. That fallback was never implemented: `init()` resolved the default driver *name* and then found nothing registered under it, so any app without a storage config died at boot with `Storage driver "local" is not configured`. A built-in `local` driver rooted at `uploadsPath()` is now registered before configured drivers — so an app defining its own `local` still overrides it, and naming a driver that genuinely does not exist still fails loudly
765
+ - **An application without `src/config/storage.ts` can boot again.** The storage connector starts unconditionally, on the documented grounds that `storage.init()` falls back to a built-in `local` driver so file storage works out of the box. That fallback was never implemented: `init()` resolved the default driver _name_ and then found nothing registered under it, so any app without a storage config died at boot with `Storage driver "local" is not configured`. A built-in `local` driver rooted at `uploadsPath()` is now registered before configured drivers — so an app defining its own `local` still overrides it, and naming a driver that genuinely does not exist still fails loudly
728
766
 
729
767
  Only scaffolded apps hid this, because `create-warlock` always ships a storage config
730
768
 
@@ -5,10 +5,10 @@ import { Server } from "socket.io";
5
5
 
6
6
  //#region ../core/src/application/app.d.ts
7
7
  type RuntimeApplication = {
8
- socket: Server;
9
- http: FastifyInstance;
8
+ socket: Server | undefined;
9
+ http: FastifyInstance | undefined;
10
10
  router: Router;
11
- database: DataSource;
11
+ database: DataSource | undefined;
12
12
  };
13
13
  declare const app: RuntimeApplication;
14
14
  //#endregion
@@ -7,14 +7,14 @@ const app = {
7
7
  * Available only if socket.io config file exists
8
8
  */
9
9
  get socket() {
10
- return container.get("socket");
10
+ return container.tryGet("socket");
11
11
  },
12
12
  /**
13
13
  * HTTP Server Instance
14
14
  * Available only if http config file exists
15
15
  */
16
16
  get http() {
17
- return container.get("http.server");
17
+ return container.tryGet("http.server");
18
18
  },
19
19
  /**
20
20
  * Router Instance
@@ -22,18 +22,18 @@ const app = {
22
22
  * Unlike `socket`/`database`, the router is set unconditionally at
23
23
  * `router.ts` module load (there is no "no router configured" case), so a
24
24
  * miss here is never a legitimate absence — it means either a genuine bug
25
- * or a duplicate-instance dev-path read. `getOrFail` surfaces that instead
26
- * of silently returning `undefined`.
25
+ * or a duplicate-instance dev-path read. `get` surfaces that instead of
26
+ * silently returning `undefined`.
27
27
  */
28
28
  get router() {
29
- return container.getOrFail("router");
29
+ return container.get("router");
30
30
  },
31
31
  /**
32
32
  * Database Instance
33
33
  * Available only if database config file exists
34
34
  */
35
35
  get database() {
36
- return container.get("database.source");
36
+ return container.tryGet("database.source");
37
37
  }
38
38
  };
39
39
 
@@ -1 +1 @@
1
- {"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../../core/src/application/app.ts"],"sourcesContent":["import { type DataSource } from \"@warlock.js/cascade\";\nimport { type FastifyInstance } from \"fastify\";\nimport { type Server } from \"socket.io\";\nimport { type Router } from \"../router\";\nimport { container } from \"./../container\";\n\ntype RuntimeApplication = {\n socket: Server;\n http: FastifyInstance;\n router: Router;\n database: DataSource;\n};\n\nexport const app: RuntimeApplication = {\n /**\n * Socket Io Instance\n * Available only if socket.io config file exists\n */\n get socket() {\n return container.get(\"socket\");\n },\n /**\n * HTTP Server Instance\n * Available only if http config file exists\n */\n get http() {\n return container.get(\"http.server\");\n },\n /**\n * Router Instance\n *\n * Unlike `socket`/`database`, the router is set unconditionally at\n * `router.ts` module load (there is no \"no router configured\" case), so a\n * miss here is never a legitimate absence — it means either a genuine bug\n * or a duplicate-instance dev-path read. `getOrFail` surfaces that instead\n * of silently returning `undefined`.\n */\n get router() {\n return container.getOrFail(\"router\");\n },\n /**\n * Database Instance\n * Available only if database config file exists\n */\n get database() {\n return container.get(\"database.source\");\n },\n};\n"],"mappings":";;;AAaA,MAAa,MAA0B;;;;;CAKrC,IAAI,SAAS;EACX,OAAO,UAAU,IAAI,QAAQ;CAC/B;;;;;CAKA,IAAI,OAAO;EACT,OAAO,UAAU,IAAI,aAAa;CACpC;;;;;;;;;;CAUA,IAAI,SAAS;EACX,OAAO,UAAU,UAAU,QAAQ;CACrC;;;;;CAKA,IAAI,WAAW;EACb,OAAO,UAAU,IAAI,iBAAiB;CACxC;AACF"}
1
+ {"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../../core/src/application/app.ts"],"sourcesContent":["import { type DataSource } from \"@warlock.js/cascade\";\nimport { type FastifyInstance } from \"fastify\";\nimport { type Server } from \"socket.io\";\nimport { type Router } from \"../router\";\nimport { container } from \"./../container\";\n\ntype RuntimeApplication = {\n socket: Server | undefined;\n http: FastifyInstance | undefined;\n router: Router;\n database: DataSource | undefined;\n};\n\nexport const app: RuntimeApplication = {\n /**\n * Socket Io Instance\n * Available only if socket.io config file exists\n */\n get socket() {\n return container.tryGet(\"socket\");\n },\n /**\n * HTTP Server Instance\n * Available only if http config file exists\n */\n get http() {\n return container.tryGet(\"http.server\");\n },\n /**\n * Router Instance\n *\n * Unlike `socket`/`database`, the router is set unconditionally at\n * `router.ts` module load (there is no \"no router configured\" case), so a\n * miss here is never a legitimate absence — it means either a genuine bug\n * or a duplicate-instance dev-path read. `get` surfaces that instead of\n * silently returning `undefined`.\n */\n get router() {\n return container.get(\"router\");\n },\n /**\n * Database Instance\n * Available only if database config file exists\n */\n get database() {\n return container.tryGet(\"database.source\");\n },\n};\n"],"mappings":";;;AAaA,MAAa,MAA0B;;;;;CAKrC,IAAI,SAAS;EACX,OAAO,UAAU,OAAO,QAAQ;CAClC;;;;;CAKA,IAAI,OAAO;EACT,OAAO,UAAU,OAAO,aAAa;CACvC;;;;;;;;;;CAUA,IAAI,SAAS;EACX,OAAO,UAAU,IAAI,QAAQ;CAC/B;;;;;CAKA,IAAI,WAAW;EACb,OAAO,UAAU,OAAO,iBAAiB;CAC3C;AACF"}
@@ -92,11 +92,15 @@ function displayMissingReadinessNotice(waitedMs) {
92
92
  console.error();
93
93
  }
94
94
  /**
95
- * Display command execution header
95
+ * Display command execution header.
96
+ *
97
+ * On stderr for the same reason as {@link displayCommandSuccess}: it prints
98
+ * before every command, so on stdout it prefixed `warlock routes --json` with
99
+ * `› Running routes...` and the payload no longer parsed.
96
100
  */
97
101
  function displayExecutingCommand(commandName) {
98
- console.log(` ${colors.cyan("›")} Running ${colors.bold(colors.white(commandName))}...`);
99
- console.log();
102
+ console.error(` ${colors.cyan("›")} Running ${colors.bold(colors.white(commandName))}...`);
103
+ console.error();
100
104
  }
101
105
  /**
102
106
  * Display command not found error with optional suggestions
@@ -1 +1 @@
1
- {"version":3,"file":"cli-commands.utils.mjs","names":[],"sources":["../../../../../../../core/src/cli/cli-commands.utils.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { Environment } from \"../utils\";\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\n\nexport function isMatchingCommandName(commandName: string, targetingCommandName: string) {\n return commandName.split(\" \")[0] === targetingCommandName;\n}\n\n/**\n * Display the Warlock.js version banner\n */\nexport async function displayWarlockVersionInTerminal() {\n const version = await getWarlockVersion();\n console.log(`⚡ ${colors.bold(\"Warlock.js\")} ${colors.greenBright(`v${version}`)}`);\n}\n\ntype StartBannerOptions = {\n environment: Environment;\n};\n\nfunction getTextColorMethod(environment: Environment) {\n switch (environment) {\n case \"development\":\n return colors.yellowBright;\n case \"production\":\n return colors.greenBright;\n case \"test\":\n return colors.blueBright;\n default:\n return colors.whiteBright;\n }\n}\n\n/**\n * Display CLI startup banner\n */\nexport async function displayStartupBanner({ environment }: StartBannerOptions) {\n const version = await getWarlockVersion();\n const textColorMethod = getTextColorMethod(environment);\n console.log(` ⚡ ${colors.bold(textColorMethod(\"Warlock.js\"))} ${colors.dim(`v${version}`)}`);\n console.log();\n}\n\ntype ProductionReadyBannerOptions = {\n bootDurationMs?: number;\n};\n\n/**\n * Announce that the production server is up — printed on **stdout**, and only\n * after the child process reported a completed boot.\n *\n * Stdout is the channel a supervisor or CI gate greps to decide the service is\n * healthy, so nothing optimistic may be written to it: this function is the\n * single place allowed to say \"started\", and it is called from exactly one\n * place, the `warlock:ready` handler in the `start` command.\n */\nexport async function displayProductionReadyBanner({\n bootDurationMs,\n}: ProductionReadyBannerOptions = {}) {\n const version = await getWarlockVersion();\n const duration = bootDurationMs ? colors.dim(` in ${bootDurationMs}ms`) : \"\";\n\n console.log(\n ` ⚡ ${colors.bold(colors.greenBright(\"Warlock.js\"))} ${colors.dim(`v${version}`)} ${colors.green(\"✔\")} production server started${duration}`,\n );\n console.log();\n}\n\n/**\n * Report that the production server died before it ever served anything.\n *\n * Written to **stderr** only. `warlock start` inherits both child streams, and\n * production launchers commonly merge them (`2>&1`), so mirroring this block to\n * stdout prints every line twice. The command's non-zero exit code is the\n * machine-readable failure signal; stdout remains reserved for the ready\n * banner.\n *\n * `causeWasCaptured` gates the \"the cause is printed above\" line. The\n * supervisor cannot always guarantee the child wrote anything before it\n * died — an import that throws before the logger configures its channels,\n * a bundle with no console channel at all — and claiming a cause is above\n * when nothing was ever captured sends the developer looking for output\n * that doesn't exist, which is worse than admitting the gap.\n */\nexport function displayProductionStartFailure(exitCode: number, causeWasCaptured: boolean) {\n const causeLine = causeWasCaptured\n ? ` ${colors.dim(\"the cause is printed above, in the application's own output\")}`\n : ` ${colors.dim(\"no output was captured from the application process — its cause did not reach this terminal\")}`;\n\n const lines = [\n \"\",\n ` ${colors.red(\"✖\")} ${colors.bold(\"warlock start\")} failed — the server never finished booting`,\n ` ${colors.dim(`the application process exited with code ${exitCode}`)}`,\n causeLine,\n \"\",\n ];\n\n for (const line of lines) {\n console.error(line);\n }\n}\n\n/**\n * Note that a running child has not reported readiness yet.\n *\n * Emitted on **stderr** only. The process may be perfectly healthy — an older\n * bundle has no readiness signal at all — so saying anything on stdout would be\n * the very false-green this channel exists to prevent.\n *\n * The wording offers the likely causes rather than asserting one. It cannot\n * distinguish an old bundle from a slow boot from a boot that is about to fail,\n * and an earlier version claimed \"the bundle predates readiness reporting\" —\n * which was simply wrong when a current bundle was mid-crash, and sent the\n * reader after the wrong thing.\n */\nexport function displayMissingReadinessNotice(waitedMs: number) {\n console.error();\n console.error(\n ` ${colors.yellow(\"!\")} still running after ${Math.round(waitedMs / 1000)}s with no readiness signal`,\n );\n console.error(\n ` ${colors.dim(\"either the boot is still in progress, or this bundle was built\")}`,\n );\n console.error(\n ` ${colors.dim(\"before readiness reporting — re-run\")} ${colors.cyan(\"warlock build\")} ${colors.dim(\"if the banner never appears\")}`,\n );\n console.error();\n}\n\n/**\n * Display command execution header\n */\nexport function displayExecutingCommand(commandName: string) {\n console.log(` ${colors.cyan(\"›\")} Running ${colors.bold(colors.white(commandName))}...`);\n console.log();\n}\n\n/**\n * Display command not found error with optional suggestions\n */\nexport function displayCommandNotFound(commandName: string, suggestions?: string[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Command ${colors.magenta(commandName)} not found`);\n\n if (suggestions && suggestions.length > 0) {\n console.log();\n console.log(` ${colors.yellow(\"Did you mean?\")}`);\n suggestions.forEach((suggestion) => {\n console.log(` ${colors.cyan(\"→\")} ${colors.white(suggestion)}`);\n });\n }\n\n console.log();\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display missing command error\n */\nexport function displayMissingCommand() {\n console.log();\n console.log(` ${colors.red(\"✖\")} No command specified`);\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display command success message.\n *\n * Written to STDERR, not stdout. This banner is printed around EVERY command,\n * including ones whose stdout is a machine payload a script is meant to parse —\n * `warlock routes --json` is the clearest case. On stdout it produced this,\n * which is not JSON and cannot be piped anywhere:\n *\n * ```\n * []\n *\n * ✔ routes completed successfully (696ms)\n * ```\n *\n * stdout belongs to the command's output; status chrome belongs to stderr,\n * where it stays visible to a person in a terminal and out of a pipe.\n */\nexport function displayCommandSuccess(commandName: string, durationMs?: number) {\n const duration = durationMs ? colors.dim(` (${durationMs}ms)`) : \"\";\n console.error();\n console.error(\n ` ${colors.green(\"✔\")} ${colors.bold(commandName)} completed successfully${duration}`,\n );\n console.error();\n}\n\n/**\n * Display command error message.\n *\n * On stderr for the same reason as {@link displayCommandSuccess}, and one\n * stronger: a failure interleaved into a machine payload corrupts the payload\n * AND hides the failure from anyone reading stderr for it.\n */\nexport function displayCommandError(commandName: string, error: Error) {\n console.error();\n console.error(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed`);\n console.error(` ${colors.dim(error.message)}`);\n console.error();\n}\n\n/**\n * Display a FATAL boot/preload failure and exit information.\n *\n * Preload failures — a bad import in a config file, a connector that throws\n * on startup, a missing module export — happen BEFORE the command's run loop\n * exists, so there is nothing to recover into: they are always fatal. Unlike\n * `displayCommandError`, this prints the full stack (not just `error.message`)\n * because the stack names the exact file + line of the offending import, which\n * is the single most useful clue when a config file pulls in a broken module.\n *\n * Surfacing this loudly is the difference between a clear error and a silent\n * hang: an unhandled preload rejection used to escape while the already-started\n * loader worker thread kept the process alive, leaving `warlock dev` frozen\n * just after the banner with no message at all.\n *\n * @example\n * // SyntaxError: The requested module '@warlock.js/cascade' does not\n * // provide an export named 'belongsTo'\n * // at src/app/.../permission.model.ts:2\n */\nexport function displayBootError(commandName: string, error: Error) {\n console.log();\n console.log(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed to start`);\n console.log(` ${colors.red(error.message)}`);\n if (error.stack) {\n console.log();\n console.log(colors.dim(error.stack));\n }\n console.log();\n}\n\n/**\n * Display missing required options error\n */\nexport function displayMissingOptions(options: { name: string; text: string }[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Missing required options:`);\n options.forEach((opt) => {\n console.log(` ${colors.yellow(opt.text)} ${colors.dim(`(--${opt.name})`)}`);\n });\n console.log();\n}\n\n/**\n * Command info for help display\n */\nexport type HelpCommandInfo = {\n name: string;\n alias?: string;\n description?: string;\n source: \"framework\" | \"plugin\" | \"project\";\n};\n\n/**\n * Display global help with all commands grouped by source\n */\nexport async function displayHelp(commands: HelpCommandInfo[]) {\n const version = await getWarlockVersion();\n\n console.log();\n console.log(\n ` ⚡ ${colors.bold(colors.yellowBright(\"Warlock.js\"))} CLI ${colors.dim(`v${version}`)}`,\n );\n console.log();\n console.log(\n ` ${colors.bold(\"Usage:\")} ${colors.cyan(\"warlock\")} ${colors.dim(\"<command>\")} ${colors.dim(\"[options]\")}`,\n );\n console.log();\n\n // Group by source\n const grouped: Record<string, HelpCommandInfo[]> = {\n framework: [],\n plugin: [],\n project: [],\n };\n\n commands.forEach((cmd) => {\n grouped[cmd.source]?.push(cmd);\n });\n\n // Display each group\n const groupLabels: Record<string, string> = {\n framework: \"Framework Commands\",\n plugin: \"Plugin Commands\",\n project: \"Project Commands\",\n };\n\n for (const [source, cmds] of Object.entries(grouped)) {\n if (cmds.length === 0) continue;\n\n console.log(` ${colors.bold(colors.white(groupLabels[source]))}`);\n console.log();\n\n // Find max name length for alignment\n const maxLen = Math.max(...cmds.map((c) => c.name.length + (c.alias ? c.alias.length + 4 : 0)));\n\n cmds.forEach((cmd) => {\n const aliasStr = cmd.alias ? colors.dim(` (${cmd.alias})`) : \"\";\n const nameWithAlias = cmd.name + (cmd.alias ? ` (${cmd.alias})` : \"\");\n const padding = \" \".repeat(maxLen - nameWithAlias.length + 2);\n // const desc = cmd.description || colors.dim(\"No description\");\n const desc = cmd.description || \"\";\n console.log(` ${colors.cyan(cmd.name)}${aliasStr}${padding}${desc}`);\n });\n console.log();\n }\n\n // Display global flags\n console.log(` ${colors.bold(colors.white(\"Global Flags\"))}`);\n console.log();\n\n const globalFlags = [\n { flag: \"--help, -h\", description: \"Show help for a command\" },\n { flag: \"--version, -v\", description: \"Show Warlock version\" },\n { flag: \"--no-cache\", description: \"Force reload without cache\" },\n { flag: \"--warm-cache\", description: \"Pre-cache all project commands\" },\n ];\n\n const maxFlagLen = Math.max(...globalFlags.map((f) => f.flag.length));\n\n globalFlags.forEach(({ flag, description }) => {\n const padding = \" \".repeat(maxFlagLen - flag.length + 2);\n console.log(` ${colors.yellow(flag)}${padding}${description}`);\n });\n console.log();\n\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock <command> --help\")} ${colors.dim(\"for command-specific help\")}`,\n );\n console.log();\n}\n\n/**\n * Display help for a specific command\n */\nexport function displayCommandHelp(command: {\n name: string;\n alias?: string;\n description?: string;\n options?: { name: string; text: string; description?: string; required?: boolean }[];\n}) {\n console.log();\n console.log(\n ` ${colors.bold(colors.cyan(command.name))}${command.alias ? colors.dim(` (${command.alias})`) : \"\"}`,\n );\n\n if (command.description) {\n console.log(` ${command.description}`);\n }\n console.log();\n\n if (command.options && command.options.length > 0) {\n console.log(` ${colors.bold(\"Options:\")}`);\n console.log();\n\n const maxLen = Math.max(...command.options.map((o) => o.text.length));\n\n command.options.forEach((opt) => {\n const padding = \" \".repeat(maxLen - opt.text.length + 2);\n const required = opt.required ? colors.red(\" (required)\") : \"\";\n const desc = opt.description || \"\";\n console.log(` ${colors.green(opt.text)}${padding}${desc}${required}`);\n });\n console.log();\n } else {\n console.log(` ${colors.dim(\"No options available\")}`);\n console.log();\n }\n}\n"],"mappings":";;;;AAIA,SAAgB,sBAAsB,aAAqB,sBAA8B;CACvF,OAAO,YAAY,MAAM,GAAG,CAAC,CAAC,OAAO;AACvC;;;;AAKA,eAAsB,kCAAkC;CACtD,MAAM,UAAU,MAAM,kBAAkB;CACxC,QAAQ,IAAI,KAAK,OAAO,KAAK,YAAY,EAAE,GAAG,OAAO,YAAY,IAAI,SAAS,GAAG;AACnF;AAMA,SAAS,mBAAmB,aAA0B;CACpD,QAAQ,aAAR;EACE,KAAK,eACH,OAAO,OAAO;EAChB,KAAK,cACH,OAAO,OAAO;EAChB,KAAK,QACH,OAAO,OAAO;EAChB,SACE,OAAO,OAAO;CAClB;AACF;;;;AAKA,eAAsB,qBAAqB,EAAE,eAAmC;CAC9E,MAAM,UAAU,MAAM,kBAAkB;CACxC,MAAM,kBAAkB,mBAAmB,WAAW;CACtD,QAAQ,IAAI,OAAO,OAAO,KAAK,gBAAgB,YAAY,CAAC,EAAE,GAAG,OAAO,IAAI,IAAI,SAAS,GAAG;CAC5F,QAAQ,IAAI;AACd;;;;;;;;;;AAeA,eAAsB,6BAA6B,EACjD,mBACgC,CAAC,GAAG;CACpC,MAAM,UAAU,MAAM,kBAAkB;CACxC,MAAM,WAAW,iBAAiB,OAAO,IAAI,OAAO,eAAe,GAAG,IAAI;CAE1E,QAAQ,IACN,OAAO,OAAO,KAAK,OAAO,YAAY,YAAY,CAAC,EAAE,GAAG,OAAO,IAAI,IAAI,SAAS,EAAE,GAAG,OAAO,MAAM,GAAG,EAAE,4BAA4B,UACrI;CACA,QAAQ,IAAI;AACd;;;;;;;;;;;;;;;;;AAkBA,SAAgB,8BAA8B,UAAkB,kBAA2B;CACzF,MAAM,YAAY,mBACd,KAAK,OAAO,IAAI,6DAA6D,MAC7E,KAAK,OAAO,IAAI,6FAA6F;CAEjH,MAAM,QAAQ;EACZ;EACA,KAAK,OAAO,IAAI,GAAG,EAAE,GAAG,OAAO,KAAK,eAAe,EAAE;EACrD,KAAK,OAAO,IAAI,4CAA4C,UAAU;EACtE;EACA;CACF;CAEA,KAAK,MAAM,QAAQ,OACjB,QAAQ,MAAM,IAAI;AAEtB;;;;;;;;;;;;;;AAeA,SAAgB,8BAA8B,UAAkB;CAC9D,QAAQ,MAAM;CACd,QAAQ,MACN,KAAK,OAAO,OAAO,GAAG,EAAE,uBAAuB,KAAK,MAAM,WAAW,GAAI,EAAE,2BAC7E;CACA,QAAQ,MACN,KAAK,OAAO,IAAI,gEAAgE,GAClF;CACA,QAAQ,MACN,KAAK,OAAO,IAAI,qCAAqC,EAAE,GAAG,OAAO,KAAK,eAAe,EAAE,GAAG,OAAO,IAAI,6BAA6B,GACpI;CACA,QAAQ,MAAM;AAChB;;;;AAKA,SAAgB,wBAAwB,aAAqB;CAC3D,QAAQ,IAAI,KAAK,OAAO,KAAK,GAAG,EAAE,WAAW,OAAO,KAAK,OAAO,MAAM,WAAW,CAAC,EAAE,IAAI;CACxF,QAAQ,IAAI;AACd;;;;AAKA,SAAgB,uBAAuB,aAAqB,aAAwB;CAClF,QAAQ,IAAI;CACZ,QAAQ,IAAI,KAAK,OAAO,IAAI,GAAG,EAAE,WAAW,OAAO,QAAQ,WAAW,EAAE,WAAW;CAEnF,IAAI,eAAe,YAAY,SAAS,GAAG;EACzC,QAAQ,IAAI;EACZ,QAAQ,IAAI,KAAK,OAAO,OAAO,eAAe,GAAG;EACjD,YAAY,SAAS,eAAe;GAClC,QAAQ,IAAI,OAAO,OAAO,KAAK,GAAG,EAAE,GAAG,OAAO,MAAM,UAAU,GAAG;EACnE,CAAC;CACH;CAEA,QAAQ,IAAI;CACZ,QAAQ,IACN,KAAK,OAAO,IAAI,KAAK,EAAE,GAAG,OAAO,KAAK,gBAAgB,EAAE,GAAG,OAAO,IAAI,2BAA2B,GACnG;CACA,QAAQ,IAAI;AACd;;;;AAKA,SAAgB,wBAAwB;CACtC,QAAQ,IAAI;CACZ,QAAQ,IAAI,KAAK,OAAO,IAAI,GAAG,EAAE,sBAAsB;CACvD,QAAQ,IACN,KAAK,OAAO,IAAI,KAAK,EAAE,GAAG,OAAO,KAAK,gBAAgB,EAAE,GAAG,OAAO,IAAI,2BAA2B,GACnG;CACA,QAAQ,IAAI;AACd;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,sBAAsB,aAAqB,YAAqB;CAC9E,MAAM,WAAW,aAAa,OAAO,IAAI,KAAK,WAAW,IAAI,IAAI;CACjE,QAAQ,MAAM;CACd,QAAQ,MACN,KAAK,OAAO,MAAM,GAAG,EAAE,GAAG,OAAO,KAAK,WAAW,EAAE,yBAAyB,UAC9E;CACA,QAAQ,MAAM;AAChB;;;;;;;;AASA,SAAgB,oBAAoB,aAAqB,OAAc;CACrE,QAAQ,MAAM;CACd,QAAQ,MAAM,KAAK,OAAO,IAAI,GAAG,EAAE,GAAG,OAAO,KAAK,WAAW,EAAE,QAAQ;CACvE,QAAQ,MAAM,KAAK,OAAO,IAAI,MAAM,OAAO,GAAG;CAC9C,QAAQ,MAAM;AAChB;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,iBAAiB,aAAqB,OAAc;CAClE,QAAQ,IAAI;CACZ,QAAQ,IAAI,KAAK,OAAO,IAAI,GAAG,EAAE,GAAG,OAAO,KAAK,WAAW,EAAE,iBAAiB;CAC9E,QAAQ,IAAI,KAAK,OAAO,IAAI,MAAM,OAAO,GAAG;CAC5C,IAAI,MAAM,OAAO;EACf,QAAQ,IAAI;EACZ,QAAQ,IAAI,OAAO,IAAI,MAAM,KAAK,CAAC;CACrC;CACA,QAAQ,IAAI;AACd;;;;AAKA,SAAgB,sBAAsB,SAA2C;CAC/E,QAAQ,IAAI;CACZ,QAAQ,IAAI,KAAK,OAAO,IAAI,GAAG,EAAE,2BAA2B;CAC5D,QAAQ,SAAS,QAAQ;EACvB,QAAQ,IAAI,QAAQ,OAAO,OAAO,IAAI,IAAI,EAAE,GAAG,OAAO,IAAI,MAAM,IAAI,KAAK,EAAE,GAAG;CAChF,CAAC;CACD,QAAQ,IAAI;AACd;;;;AAeA,eAAsB,YAAY,UAA6B;CAC7D,MAAM,UAAU,MAAM,kBAAkB;CAExC,QAAQ,IAAI;CACZ,QAAQ,IACN,OAAO,OAAO,KAAK,OAAO,aAAa,YAAY,CAAC,EAAE,OAAO,OAAO,IAAI,IAAI,SAAS,GACvF;CACA,QAAQ,IAAI;CACZ,QAAQ,IACN,KAAK,OAAO,KAAK,QAAQ,EAAE,GAAG,OAAO,KAAK,SAAS,EAAE,GAAG,OAAO,IAAI,WAAW,EAAE,GAAG,OAAO,IAAI,WAAW,GAC3G;CACA,QAAQ,IAAI;CAGZ,MAAM,UAA6C;EACjD,WAAW,CAAC;EACZ,QAAQ,CAAC;EACT,SAAS,CAAC;CACZ;CAEA,SAAS,SAAS,QAAQ;EACxB,QAAQ,IAAI,OAAO,EAAE,KAAK,GAAG;CAC/B,CAAC;CAGD,MAAM,cAAsC;EAC1C,WAAW;EACX,QAAQ;EACR,SAAS;CACX;CAEA,KAAK,MAAM,CAAC,QAAQ,SAAS,OAAO,QAAQ,OAAO,GAAG;EACpD,IAAI,KAAK,WAAW,GAAG;EAEvB,QAAQ,IAAI,KAAK,OAAO,KAAK,OAAO,MAAM,YAAY,OAAO,CAAC,GAAG;EACjE,QAAQ,IAAI;EAGZ,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,EAAE,KAAK,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,IAAI,EAAE,CAAC;EAE9F,KAAK,SAAS,QAAQ;GACpB,MAAM,WAAW,IAAI,QAAQ,OAAO,IAAI,KAAK,IAAI,MAAM,EAAE,IAAI;GAC7D,MAAM,gBAAgB,IAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI,MAAM,KAAK;GAClE,MAAM,UAAU,IAAI,OAAO,SAAS,cAAc,SAAS,CAAC;GAE5D,MAAM,OAAO,IAAI,eAAe;GAChC,QAAQ,IAAI,OAAO,OAAO,KAAK,IAAI,IAAI,IAAI,WAAW,UAAU,MAAM;EACxE,CAAC;EACD,QAAQ,IAAI;CACd;CAGA,QAAQ,IAAI,KAAK,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC,GAAG;CAC5D,QAAQ,IAAI;CAEZ,MAAM,cAAc;EAClB;GAAE,MAAM;GAAc,aAAa;EAA0B;EAC7D;GAAE,MAAM;GAAiB,aAAa;EAAuB;EAC7D;GAAE,MAAM;GAAc,aAAa;EAA6B;EAChE;GAAE,MAAM;GAAgB,aAAa;EAAiC;CACxE;CAEA,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,KAAK,MAAM,EAAE,KAAK,MAAM,CAAC;CAEpE,YAAY,SAAS,EAAE,MAAM,kBAAkB;EAC7C,MAAM,UAAU,IAAI,OAAO,aAAa,KAAK,SAAS,CAAC;EACvD,QAAQ,IAAI,OAAO,OAAO,OAAO,IAAI,IAAI,UAAU,aAAa;CAClE,CAAC;CACD,QAAQ,IAAI;CAEZ,QAAQ,IACN,KAAK,OAAO,IAAI,KAAK,EAAE,GAAG,OAAO,KAAK,0BAA0B,EAAE,GAAG,OAAO,IAAI,2BAA2B,GAC7G;CACA,QAAQ,IAAI;AACd;;;;AAKA,SAAgB,mBAAmB,SAKhC;CACD,QAAQ,IAAI;CACZ,QAAQ,IACN,KAAK,OAAO,KAAK,OAAO,KAAK,QAAQ,IAAI,CAAC,IAAI,QAAQ,QAAQ,OAAO,IAAI,KAAK,QAAQ,MAAM,EAAE,IAAI,IACpG;CAEA,IAAI,QAAQ,aACV,QAAQ,IAAI,KAAK,QAAQ,aAAa;CAExC,QAAQ,IAAI;CAEZ,IAAI,QAAQ,WAAW,QAAQ,QAAQ,SAAS,GAAG;EACjD,QAAQ,IAAI,KAAK,OAAO,KAAK,UAAU,GAAG;EAC1C,QAAQ,IAAI;EAEZ,MAAM,SAAS,KAAK,IAAI,GAAG,QAAQ,QAAQ,KAAK,MAAM,EAAE,KAAK,MAAM,CAAC;EAEpE,QAAQ,QAAQ,SAAS,QAAQ;GAC/B,MAAM,UAAU,IAAI,OAAO,SAAS,IAAI,KAAK,SAAS,CAAC;GACvD,MAAM,WAAW,IAAI,WAAW,OAAO,IAAI,aAAa,IAAI;GAC5D,MAAM,OAAO,IAAI,eAAe;GAChC,QAAQ,IAAI,OAAO,OAAO,MAAM,IAAI,IAAI,IAAI,UAAU,OAAO,UAAU;EACzE,CAAC;EACD,QAAQ,IAAI;CACd,OAAO;EACL,QAAQ,IAAI,KAAK,OAAO,IAAI,sBAAsB,GAAG;EACrD,QAAQ,IAAI;CACd;AACF"}
1
+ {"version":3,"file":"cli-commands.utils.mjs","names":[],"sources":["../../../../../../../core/src/cli/cli-commands.utils.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { Environment } from \"../utils\";\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\n\nexport function isMatchingCommandName(commandName: string, targetingCommandName: string) {\n return commandName.split(\" \")[0] === targetingCommandName;\n}\n\n/**\n * Display the Warlock.js version banner\n */\nexport async function displayWarlockVersionInTerminal() {\n const version = await getWarlockVersion();\n console.log(`⚡ ${colors.bold(\"Warlock.js\")} ${colors.greenBright(`v${version}`)}`);\n}\n\ntype StartBannerOptions = {\n environment: Environment;\n};\n\nfunction getTextColorMethod(environment: Environment) {\n switch (environment) {\n case \"development\":\n return colors.yellowBright;\n case \"production\":\n return colors.greenBright;\n case \"test\":\n return colors.blueBright;\n default:\n return colors.whiteBright;\n }\n}\n\n/**\n * Display CLI startup banner\n */\nexport async function displayStartupBanner({ environment }: StartBannerOptions) {\n const version = await getWarlockVersion();\n const textColorMethod = getTextColorMethod(environment);\n console.log(` ⚡ ${colors.bold(textColorMethod(\"Warlock.js\"))} ${colors.dim(`v${version}`)}`);\n console.log();\n}\n\ntype ProductionReadyBannerOptions = {\n bootDurationMs?: number;\n};\n\n/**\n * Announce that the production server is up — printed on **stdout**, and only\n * after the child process reported a completed boot.\n *\n * Stdout is the channel a supervisor or CI gate greps to decide the service is\n * healthy, so nothing optimistic may be written to it: this function is the\n * single place allowed to say \"started\", and it is called from exactly one\n * place, the `warlock:ready` handler in the `start` command.\n */\nexport async function displayProductionReadyBanner({\n bootDurationMs,\n}: ProductionReadyBannerOptions = {}) {\n const version = await getWarlockVersion();\n const duration = bootDurationMs ? colors.dim(` in ${bootDurationMs}ms`) : \"\";\n\n console.log(\n ` ⚡ ${colors.bold(colors.greenBright(\"Warlock.js\"))} ${colors.dim(`v${version}`)} ${colors.green(\"✔\")} production server started${duration}`,\n );\n console.log();\n}\n\n/**\n * Report that the production server died before it ever served anything.\n *\n * Written to **stderr** only. `warlock start` inherits both child streams, and\n * production launchers commonly merge them (`2>&1`), so mirroring this block to\n * stdout prints every line twice. The command's non-zero exit code is the\n * machine-readable failure signal; stdout remains reserved for the ready\n * banner.\n *\n * `causeWasCaptured` gates the \"the cause is printed above\" line. The\n * supervisor cannot always guarantee the child wrote anything before it\n * died — an import that throws before the logger configures its channels,\n * a bundle with no console channel at all — and claiming a cause is above\n * when nothing was ever captured sends the developer looking for output\n * that doesn't exist, which is worse than admitting the gap.\n */\nexport function displayProductionStartFailure(exitCode: number, causeWasCaptured: boolean) {\n const causeLine = causeWasCaptured\n ? ` ${colors.dim(\"the cause is printed above, in the application's own output\")}`\n : ` ${colors.dim(\"no output was captured from the application process — its cause did not reach this terminal\")}`;\n\n const lines = [\n \"\",\n ` ${colors.red(\"✖\")} ${colors.bold(\"warlock start\")} failed — the server never finished booting`,\n ` ${colors.dim(`the application process exited with code ${exitCode}`)}`,\n causeLine,\n \"\",\n ];\n\n for (const line of lines) {\n console.error(line);\n }\n}\n\n/**\n * Note that a running child has not reported readiness yet.\n *\n * Emitted on **stderr** only. The process may be perfectly healthy — an older\n * bundle has no readiness signal at all — so saying anything on stdout would be\n * the very false-green this channel exists to prevent.\n *\n * The wording offers the likely causes rather than asserting one. It cannot\n * distinguish an old bundle from a slow boot from a boot that is about to fail,\n * and an earlier version claimed \"the bundle predates readiness reporting\" —\n * which was simply wrong when a current bundle was mid-crash, and sent the\n * reader after the wrong thing.\n */\nexport function displayMissingReadinessNotice(waitedMs: number) {\n console.error();\n console.error(\n ` ${colors.yellow(\"!\")} still running after ${Math.round(waitedMs / 1000)}s with no readiness signal`,\n );\n console.error(\n ` ${colors.dim(\"either the boot is still in progress, or this bundle was built\")}`,\n );\n console.error(\n ` ${colors.dim(\"before readiness reporting — re-run\")} ${colors.cyan(\"warlock build\")} ${colors.dim(\"if the banner never appears\")}`,\n );\n console.error();\n}\n\n/**\n * Display command execution header.\n *\n * On stderr for the same reason as {@link displayCommandSuccess}: it prints\n * before every command, so on stdout it prefixed `warlock routes --json` with\n * `› Running routes...` and the payload no longer parsed.\n */\nexport function displayExecutingCommand(commandName: string) {\n console.error(` ${colors.cyan(\"›\")} Running ${colors.bold(colors.white(commandName))}...`);\n console.error();\n}\n\n/**\n * Display command not found error with optional suggestions\n */\nexport function displayCommandNotFound(commandName: string, suggestions?: string[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Command ${colors.magenta(commandName)} not found`);\n\n if (suggestions && suggestions.length > 0) {\n console.log();\n console.log(` ${colors.yellow(\"Did you mean?\")}`);\n suggestions.forEach((suggestion) => {\n console.log(` ${colors.cyan(\"→\")} ${colors.white(suggestion)}`);\n });\n }\n\n console.log();\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display missing command error\n */\nexport function displayMissingCommand() {\n console.log();\n console.log(` ${colors.red(\"✖\")} No command specified`);\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display command success message.\n *\n * Written to STDERR, not stdout. This banner is printed around EVERY command,\n * including ones whose stdout is a machine payload a script is meant to parse —\n * `warlock routes --json` is the clearest case. On stdout it produced this,\n * which is not JSON and cannot be piped anywhere:\n *\n * ```\n * []\n *\n * ✔ routes completed successfully (696ms)\n * ```\n *\n * stdout belongs to the command's output; status chrome belongs to stderr,\n * where it stays visible to a person in a terminal and out of a pipe.\n */\nexport function displayCommandSuccess(commandName: string, durationMs?: number) {\n const duration = durationMs ? colors.dim(` (${durationMs}ms)`) : \"\";\n console.error();\n console.error(\n ` ${colors.green(\"✔\")} ${colors.bold(commandName)} completed successfully${duration}`,\n );\n console.error();\n}\n\n/**\n * Display command error message.\n *\n * On stderr for the same reason as {@link displayCommandSuccess}, and one\n * stronger: a failure interleaved into a machine payload corrupts the payload\n * AND hides the failure from anyone reading stderr for it.\n */\nexport function displayCommandError(commandName: string, error: Error) {\n console.error();\n console.error(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed`);\n console.error(` ${colors.dim(error.message)}`);\n console.error();\n}\n\n/**\n * Display a FATAL boot/preload failure and exit information.\n *\n * Preload failures — a bad import in a config file, a connector that throws\n * on startup, a missing module export — happen BEFORE the command's run loop\n * exists, so there is nothing to recover into: they are always fatal. Unlike\n * `displayCommandError`, this prints the full stack (not just `error.message`)\n * because the stack names the exact file + line of the offending import, which\n * is the single most useful clue when a config file pulls in a broken module.\n *\n * Surfacing this loudly is the difference between a clear error and a silent\n * hang: an unhandled preload rejection used to escape while the already-started\n * loader worker thread kept the process alive, leaving `warlock dev` frozen\n * just after the banner with no message at all.\n *\n * @example\n * // SyntaxError: The requested module '@warlock.js/cascade' does not\n * // provide an export named 'belongsTo'\n * // at src/app/.../permission.model.ts:2\n */\nexport function displayBootError(commandName: string, error: Error) {\n console.log();\n console.log(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed to start`);\n console.log(` ${colors.red(error.message)}`);\n if (error.stack) {\n console.log();\n console.log(colors.dim(error.stack));\n }\n console.log();\n}\n\n/**\n * Display missing required options error\n */\nexport function displayMissingOptions(options: { name: string; text: string }[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Missing required options:`);\n options.forEach((opt) => {\n console.log(` ${colors.yellow(opt.text)} ${colors.dim(`(--${opt.name})`)}`);\n });\n console.log();\n}\n\n/**\n * Command info for help display\n */\nexport type HelpCommandInfo = {\n name: string;\n alias?: string;\n description?: string;\n source: \"framework\" | \"plugin\" | \"project\";\n};\n\n/**\n * Display global help with all commands grouped by source\n */\nexport async function displayHelp(commands: HelpCommandInfo[]) {\n const version = await getWarlockVersion();\n\n console.log();\n console.log(\n ` ⚡ ${colors.bold(colors.yellowBright(\"Warlock.js\"))} CLI ${colors.dim(`v${version}`)}`,\n );\n console.log();\n console.log(\n ` ${colors.bold(\"Usage:\")} ${colors.cyan(\"warlock\")} ${colors.dim(\"<command>\")} ${colors.dim(\"[options]\")}`,\n );\n console.log();\n\n // Group by source\n const grouped: Record<string, HelpCommandInfo[]> = {\n framework: [],\n plugin: [],\n project: [],\n };\n\n commands.forEach((cmd) => {\n grouped[cmd.source]?.push(cmd);\n });\n\n // Display each group\n const groupLabels: Record<string, string> = {\n framework: \"Framework Commands\",\n plugin: \"Plugin Commands\",\n project: \"Project Commands\",\n };\n\n for (const [source, cmds] of Object.entries(grouped)) {\n if (cmds.length === 0) continue;\n\n console.log(` ${colors.bold(colors.white(groupLabels[source]))}`);\n console.log();\n\n // Find max name length for alignment\n const maxLen = Math.max(...cmds.map((c) => c.name.length + (c.alias ? c.alias.length + 4 : 0)));\n\n cmds.forEach((cmd) => {\n const aliasStr = cmd.alias ? colors.dim(` (${cmd.alias})`) : \"\";\n const nameWithAlias = cmd.name + (cmd.alias ? ` (${cmd.alias})` : \"\");\n const padding = \" \".repeat(maxLen - nameWithAlias.length + 2);\n // const desc = cmd.description || colors.dim(\"No description\");\n const desc = cmd.description || \"\";\n console.log(` ${colors.cyan(cmd.name)}${aliasStr}${padding}${desc}`);\n });\n console.log();\n }\n\n // Display global flags\n console.log(` ${colors.bold(colors.white(\"Global Flags\"))}`);\n console.log();\n\n const globalFlags = [\n { flag: \"--help, -h\", description: \"Show help for a command\" },\n { flag: \"--version, -v\", description: \"Show Warlock version\" },\n { flag: \"--no-cache\", description: \"Force reload without cache\" },\n { flag: \"--warm-cache\", description: \"Pre-cache all project commands\" },\n ];\n\n const maxFlagLen = Math.max(...globalFlags.map((f) => f.flag.length));\n\n globalFlags.forEach(({ flag, description }) => {\n const padding = \" \".repeat(maxFlagLen - flag.length + 2);\n console.log(` ${colors.yellow(flag)}${padding}${description}`);\n });\n console.log();\n\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock <command> --help\")} ${colors.dim(\"for command-specific help\")}`,\n );\n console.log();\n}\n\n/**\n * Display help for a specific command\n */\nexport function displayCommandHelp(command: {\n name: string;\n alias?: string;\n description?: string;\n options?: { name: string; text: string; description?: string; required?: boolean }[];\n}) {\n console.log();\n console.log(\n ` ${colors.bold(colors.cyan(command.name))}${command.alias ? colors.dim(` (${command.alias})`) : \"\"}`,\n );\n\n if (command.description) {\n console.log(` ${command.description}`);\n }\n console.log();\n\n if (command.options && command.options.length > 0) {\n console.log(` ${colors.bold(\"Options:\")}`);\n console.log();\n\n const maxLen = Math.max(...command.options.map((o) => o.text.length));\n\n command.options.forEach((opt) => {\n const padding = \" \".repeat(maxLen - opt.text.length + 2);\n const required = opt.required ? colors.red(\" (required)\") : \"\";\n const desc = opt.description || \"\";\n console.log(` ${colors.green(opt.text)}${padding}${desc}${required}`);\n });\n console.log();\n } else {\n console.log(` ${colors.dim(\"No options available\")}`);\n console.log();\n }\n}\n"],"mappings":"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"}
@@ -13,7 +13,7 @@ import { router } from "../../../../router/router.mjs";
13
13
  * verdict from the config flag it is meant to be verifying.
14
14
  */
15
15
  function fastifyHasGet(path) {
16
- const http = container.has("http.server") ? container.get("http.server") : void 0;
16
+ const http = container.tryGet("http.server");
17
17
  if (!http || typeof http.hasRoute !== "function") return void 0;
18
18
  try {
19
19
  return Boolean(http.hasRoute({