@warlock.js/core 5.34.3 → 5.35.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 (244) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/esm/cli/cli-commands.manager.mjs +1 -1
  3. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  4. package/esm/cli/commands/add.command.mjs +1 -0
  5. package/esm/cli/commands/add.command.mjs.map +1 -1
  6. package/esm/cli/commands/build.command.mjs +30 -3
  7. package/esm/cli/commands/build.command.mjs.map +1 -1
  8. package/esm/cli/commands/check/check-command.action.mjs +1 -0
  9. package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
  10. package/esm/cli/commands/check.command.mjs +5 -0
  11. package/esm/cli/commands/check.command.mjs.map +1 -1
  12. package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
  13. package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
  14. package/esm/cli/commands/explain.command.mjs +14 -4
  15. package/esm/cli/commands/explain.command.mjs.map +1 -1
  16. package/esm/cli/commands/generate/generate.command.mjs +19 -1
  17. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  18. package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
  19. package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
  20. package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
  21. package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
  22. package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
  23. package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
  24. package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
  25. package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
  26. package/esm/cli/commands/inspect.command.mjs +13 -8
  27. package/esm/cli/commands/inspect.command.mjs.map +1 -1
  28. package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
  29. package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
  30. package/esm/config/config-loader.d.mts.map +1 -1
  31. package/esm/config/config-loader.mjs +2 -0
  32. package/esm/config/config-loader.mjs.map +1 -1
  33. package/esm/config/load-config-files.mjs +4 -0
  34. package/esm/config/load-config-files.mjs.map +1 -1
  35. package/esm/dev-server/file-event-handler.mjs +11 -1
  36. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  37. package/esm/dev-server/file-manager.d.mts +1 -1
  38. package/esm/dev-server/file-manager.d.mts.map +1 -1
  39. package/esm/dev-server/file-manager.mjs.map +1 -1
  40. package/esm/dev-server/files-orchestrator.mjs +16 -1
  41. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  42. package/esm/dev-server/files-watcher.mjs +13 -4
  43. package/esm/dev-server/files-watcher.mjs.map +1 -1
  44. package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
  45. package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
  46. package/esm/dev-server/loader/register-loader.mjs +31 -12
  47. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  48. package/esm/dev-server/module-loader.mjs +14 -0
  49. package/esm/dev-server/module-loader.mjs.map +1 -1
  50. package/esm/dev-server/special-file-patterns.mjs +4 -1
  51. package/esm/dev-server/special-file-patterns.mjs.map +1 -1
  52. package/esm/e2e/index.d.mts +2 -2
  53. package/esm/e2e/page.d.mts +6 -1
  54. package/esm/e2e/page.d.mts.map +1 -1
  55. package/esm/e2e/page.mjs +36 -1
  56. package/esm/e2e/page.mjs.map +1 -1
  57. package/esm/e2e/types.d.mts +31 -0
  58. package/esm/e2e/types.d.mts.map +1 -1
  59. package/esm/e2e/types.mjs.map +1 -1
  60. package/esm/errors/error-codes.d.mts +48 -0
  61. package/esm/errors/error-codes.d.mts.map +1 -1
  62. package/esm/errors/error-codes.mjs +48 -0
  63. package/esm/errors/error-codes.mjs.map +1 -1
  64. package/esm/errors/index.d.mts +2 -2
  65. package/esm/errors/index.mjs +2 -2
  66. package/esm/generations/add-command.action.mjs +2 -0
  67. package/esm/generations/add-command.action.mjs.map +1 -1
  68. package/esm/generations/design-system-payload.mjs +53 -0
  69. package/esm/generations/design-system-payload.mjs.map +1 -0
  70. package/esm/generations/features/design-system.feature.mjs +38 -0
  71. package/esm/generations/features/design-system.feature.mjs.map +1 -0
  72. package/esm/generations/features/index.mjs +3 -1
  73. package/esm/generations/features/index.mjs.map +1 -1
  74. package/esm/generations/features/notifications.feature.mjs +13 -2
  75. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  76. package/esm/generations/features/test.feature.mjs +48 -13
  77. package/esm/generations/features/test.feature.mjs.map +1 -1
  78. package/esm/generations/stubs.mjs +9 -5
  79. package/esm/generations/stubs.mjs.map +1 -1
  80. package/esm/http/error-mapper.d.mts +1 -1
  81. package/esm/http/error-mapper.mjs +3 -3
  82. package/esm/http/error-mapper.mjs.map +1 -1
  83. package/esm/http/error-response.d.mts +3 -0
  84. package/esm/http/error-response.d.mts.map +1 -1
  85. package/esm/http/error-response.mjs +8 -2
  86. package/esm/http/error-response.mjs.map +1 -1
  87. package/esm/http/errors/errors.d.mts +8 -0
  88. package/esm/http/errors/errors.d.mts.map +1 -1
  89. package/esm/http/errors/errors.mjs +13 -1
  90. package/esm/http/errors/errors.mjs.map +1 -1
  91. package/esm/http/index.d.mts +3 -2
  92. package/esm/http/index.mjs +3 -2
  93. package/esm/http/list-endpoint.d.mts +35 -0
  94. package/esm/http/list-endpoint.d.mts.map +1 -0
  95. package/esm/http/list-endpoint.mjs +28 -0
  96. package/esm/http/list-endpoint.mjs.map +1 -0
  97. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  98. package/esm/http/middleware/inject-request-context.mjs +12 -2
  99. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  100. package/esm/http/server.mjs +1 -1
  101. package/esm/index.d.mts +8 -6
  102. package/esm/index.mjs +3 -2
  103. package/esm/openapi/security.mjs +12 -4
  104. package/esm/openapi/security.mjs.map +1 -1
  105. package/esm/openapi/types.d.mts +2 -0
  106. package/esm/openapi/types.d.mts.map +1 -1
  107. package/esm/production/build-budgets.d.mts +46 -0
  108. package/esm/production/build-budgets.d.mts.map +1 -0
  109. package/esm/production/build-budgets.mjs +109 -0
  110. package/esm/production/build-budgets.mjs.map +1 -0
  111. package/esm/production/production-builder.mjs +13 -1
  112. package/esm/production/production-builder.mjs.map +1 -1
  113. package/esm/production/resolve-build-config.d.mts +1 -1
  114. package/esm/production/resolve-build-config.d.mts.map +1 -1
  115. package/esm/production/resolve-build-config.mjs.map +1 -1
  116. package/esm/restful/restful.d.mts +1 -1
  117. package/esm/tests/fakes/storage-fake.d.mts +3 -1
  118. package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
  119. package/esm/tests/fakes/storage-fake.mjs +3 -3
  120. package/esm/tests/fakes/storage-fake.mjs.map +1 -1
  121. package/esm/tests/fakes/test-fakes.d.mts +3 -1
  122. package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
  123. package/esm/tests/fakes/test-fakes.mjs +3 -2
  124. package/esm/tests/fakes/test-fakes.mjs.map +1 -1
  125. package/esm/tests/index.d.mts +4 -2
  126. package/esm/tests/index.mjs +3 -1
  127. package/esm/tests/load-unit-config.mjs +24 -0
  128. package/esm/tests/load-unit-config.mjs.map +1 -0
  129. package/esm/tests/response-contract.d.mts +32 -0
  130. package/esm/tests/response-contract.d.mts.map +1 -0
  131. package/esm/tests/response-contract.mjs +116 -0
  132. package/esm/tests/response-contract.mjs.map +1 -0
  133. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  134. package/esm/tests/start-http-development-server.mjs +10 -2
  135. package/esm/tests/start-http-development-server.mjs.map +1 -1
  136. package/esm/tests/test-lifecycle-state.mjs.map +1 -1
  137. package/esm/tests/test-locales.mjs +33 -0
  138. package/esm/tests/test-locales.mjs.map +1 -0
  139. package/esm/tests/test-runtime-scope.d.mts +15 -0
  140. package/esm/tests/test-runtime-scope.d.mts.map +1 -0
  141. package/esm/tests/test-runtime-scope.mjs +80 -0
  142. package/esm/tests/test-runtime-scope.mjs.map +1 -0
  143. package/esm/tests/vitest-setup.d.mts +13 -3
  144. package/esm/tests/vitest-setup.d.mts.map +1 -1
  145. package/esm/tests/vitest-setup.mjs +146 -18
  146. package/esm/tests/vitest-setup.mjs.map +1 -1
  147. package/esm/tooling/check/affected-files.d.mts +5 -0
  148. package/esm/tooling/check/affected-files.d.mts.map +1 -0
  149. package/esm/tooling/check/affected-files.mjs +56 -0
  150. package/esm/tooling/check/affected-files.mjs.map +1 -0
  151. package/esm/tooling/check/changed-files.d.mts +2 -0
  152. package/esm/tooling/check/changed-files.d.mts.map +1 -1
  153. package/esm/tooling/check/changed-files.mjs +2 -2
  154. package/esm/tooling/check/changed-files.mjs.map +1 -1
  155. package/esm/tooling/check/conventions-step.d.mts +2 -1
  156. package/esm/tooling/check/conventions-step.d.mts.map +1 -1
  157. package/esm/tooling/check/conventions-step.mjs +48 -6
  158. package/esm/tooling/check/conventions-step.mjs.map +1 -1
  159. package/esm/tooling/check/fixes.d.mts +8 -0
  160. package/esm/tooling/check/fixes.d.mts.map +1 -0
  161. package/esm/tooling/check/fixes.mjs +31 -0
  162. package/esm/tooling/check/fixes.mjs.map +1 -0
  163. package/esm/tooling/check/index.d.mts +12 -0
  164. package/esm/tooling/check/index.mjs +3 -2
  165. package/esm/tooling/check/run-check.d.mts +2 -0
  166. package/esm/tooling/check/run-check.d.mts.map +1 -1
  167. package/esm/tooling/check/run-check.mjs +20 -4
  168. package/esm/tooling/check/run-check.mjs.map +1 -1
  169. package/esm/tooling/check/run-tests.mjs +1 -1
  170. package/esm/tooling/check/source-ast.d.mts +1 -0
  171. package/esm/tooling/check/web-conventions.d.mts +10 -0
  172. package/esm/tooling/check/web-conventions.d.mts.map +1 -0
  173. package/esm/tooling/check/web-conventions.mjs +126 -0
  174. package/esm/tooling/check/web-conventions.mjs.map +1 -0
  175. package/esm/tooling/index.d.mts +7 -2
  176. package/esm/tooling/index.mjs +4 -2
  177. package/esm/tooling/inspect/collect-files.mjs +172 -0
  178. package/esm/tooling/inspect/collect-files.mjs.map +1 -0
  179. package/esm/tooling/inspect/collect-pages.mjs +3 -2
  180. package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
  181. package/esm/tooling/inspect/collect-routes.mjs +1 -1
  182. package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
  183. package/esm/tooling/inspect/explain-file.d.mts +12 -0
  184. package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
  185. package/esm/tooling/inspect/explain-file.mjs +45 -0
  186. package/esm/tooling/inspect/explain-file.mjs.map +1 -0
  187. package/esm/tooling/inspect/index.d.mts +3 -2
  188. package/esm/tooling/inspect/index.mjs +2 -1
  189. package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
  190. package/esm/tooling/inspect/inspect-app.mjs +9 -3
  191. package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
  192. package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
  193. package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
  194. package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
  195. package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
  196. package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
  197. package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
  198. package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
  199. package/esm/tooling/mcp/mcp-tools.mjs +8 -2
  200. package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
  201. package/esm/tooling/problem.d.mts +4 -0
  202. package/esm/tooling/problem.d.mts.map +1 -1
  203. package/esm/tooling/problem.mjs +1 -0
  204. package/esm/tooling/problem.mjs.map +1 -1
  205. package/esm/tooling/syntax/ast.d.mts +2 -0
  206. package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
  207. package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
  208. package/esm/vite/index.d.mts +3 -1
  209. package/esm/vite/index.mjs +3 -1
  210. package/esm/vite/test-setup-reporter.d.mts +16 -0
  211. package/esm/vite/test-setup-reporter.d.mts.map +1 -0
  212. package/esm/vite/test-setup-reporter.mjs +55 -0
  213. package/esm/vite/test-setup-reporter.mjs.map +1 -0
  214. package/esm/vite/test-workers.d.mts +5 -0
  215. package/esm/vite/test-workers.d.mts.map +1 -0
  216. package/esm/vite/test-workers.mjs +38 -0
  217. package/esm/vite/test-workers.mjs.map +1 -0
  218. package/esm/warlock-config/index.d.mts +2 -1
  219. package/esm/warlock-config/types.d.mts +3 -0
  220. package/esm/warlock-config/types.d.mts.map +1 -1
  221. package/llms-full.txt +763 -9
  222. package/llms.txt +2 -0
  223. package/package.json +15 -14
  224. package/schemas/inspect-report.schema.json +47 -1
  225. package/skills/build-budgets/SKILL.md +51 -0
  226. package/skills/build-restful/SKILL.md +56 -0
  227. package/skills/configure-app/SKILL.md +7 -0
  228. package/skills/create-module/SKILL.md +9 -0
  229. package/skills/generate-web-ui/SKILL.md +71 -0
  230. package/skills/index.md +2 -2
  231. package/skills/run-app/SKILL.md +1 -1
  232. package/skills/send-mail/SKILL.md +25 -0
  233. package/skills/send-response/SKILL.md +50 -0
  234. package/skills/test-browser/SKILL.md +53 -0
  235. package/skills/test-http/SKILL.md +91 -0
  236. package/skills/test-service/SKILL.md +161 -3
  237. package/skills/upgrade-from-5-29/SKILL.md +18 -0
  238. package/skills/use-localization/SKILL.md +21 -0
  239. package/skills/warlock-check/SKILL.md +72 -1
  240. package/skills/warlock-inspect/SKILL.md +45 -2
  241. package/skills/warlock-mcp/SKILL.md +4 -0
  242. package/skills/write-cli-command/SKILL.md +18 -0
  243. package/esm/production/build-app-production.mjs +0 -14
  244. package/esm/production/build-app-production.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  > ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
8
8
 
9
+ ## 5.35.0 - 2026-10-21
10
+
11
+ ### Added
12
+
13
+ - Typed `AppError` domain failures with native HTTP handling and `http.errorResponse` context, preserving legacy HTTP error behavior.
14
+ - `listEndpoint` builds typed plural-key page responses from validated query controls and repository list callbacks, with bounded limits and explicit sort/filter whitelists.
15
+ - `assertResponseContracts` checks real HTTP responses against declared status schemas, with fixtures, resource support and optional complete status coverage.
16
+ - Web convention warnings for component size, multiple components, component-owned state/effects and handlers, native form controls and native validation; exported controller response schema warnings (`WL9013`–`WL9018`). Configure severity and component line limits through `.warlock-check.json`.
17
+ - `warlock check --fix` applies safe relative import extension fixes and exposes available edits through the optional JSON `fix` field.
18
+ - `warlock inspect --section files` lists every file under `src/` with its module, convention role, exports and resolved import graph without booting the app. The versioned report schema and MCP `inspect_files` tool expose the same section.
19
+ - `warlock explain <file>` prints exports, importers and transitively reaching routes/pages, with `--json` support. Error-code explanations continue to work.
20
+ - Per-page gzip JS/CSS build budgets with route overrides, warn/error enforcement, previous-build deltas, and `warlock build --json` reports. Existing apps remain report-only unless limits are configured.
21
+ - Browser `page.assertAccessible(options?)` using a pinned local axe-core injection over CDP, exact rule/selector allowances, violation diagnostics and accessibility evidence.
22
+ - Opt-in `tests.lifetime: "worker"` for Vitest forks in a single run with a shared module graph (`isolate: false`) and the generated cleanup runner. Connectors boot once per worker, file boundaries reset fakes/clock/IDs/private storage and await `tests.resetBetweenFiles`, and worker cleanup awaits shutdown. `getTestRuntimeStats()` exposes bootstrap/file counts. Unsafe pools, watch, isolation and missing runner configurations are refused.
23
+ - `setupTest({ mode: "unit", importModule })` loads configuration values once per module graph without bootstrap hooks, config special handlers, connectors, fake installation or native loader hooks.
24
+ - `tests.profileSetup` / `WARLOCK_TEST_PROFILE=1` report setup steps and teardown per file, with summaries per worker including final shutdown; the generated `TestSetupReporter` aggregates a run summary across workers. `setupTest({ file })` labels profiles.
25
+ - `recommendedTestWorkers()` from `@warlock.js/core/vite` respects CPU affinity and visible cgroup v2 CPU quotas. Testing docs/skills cover shared HTTP/database contention and affected-tests plus integration release gates, with a full suite nightly.
26
+ - `warlock add design-system` scaffolds app-owned accessible shadcn controls connected to Mongez React Form, with pinned dependencies, a Tailwind preflight before mutations, preservation of existing files, and dry-run support.
27
+ - `warlock generate page <path>` scaffolds a UI-only page and setup companion with route, loader and metadata; `--interactive` adds a feature hook.
28
+ - `warlock generate form <name> [--module <name>] [--endpoint]` scaffolds design-system controls, a shared Seal schema, a `useSubmitForm` hook and an optional guarded controller/route with validation and response schema.
29
+ - `warlock generate crud-ui <module>` scaffolds guarded list/create/edit pages, schema-backed forms, endpoint-derived filters, pagination and a delete action for conventional CRUD modules. All web generators support dry-run JSON reports and preserve existing files unless forced.
30
+
31
+ ### Changed
32
+
33
+ - Document the development devtools captured-mail preview without logging message bodies.
34
+ - `warlock check --changed` includes transitive import dependents for scoped lint, conventions and related tests. Changed typecheck diagnostics now include transitive importers.
35
+ - New `warlock add test` configs split pure `*.spec.ts` files from integration `*.test.ts` and specs under `tests/`; only integration loads framework/HTTP setup. Existing configs are preserved. The generated runner requires Vitest 4.1.10 or newer.
36
+ - Reference workload before this change: 1,335 files, 3,532.79 seconds summed setup, 4,465.55 seconds wall time (reported on 5.33.0). No after measurement was run under the implementation check-once rule. The startup cost model changes from one bootstrap per file to one per active worker plus reset cost; this is not a measured wall-time speedup. Use the new profiling switch to compare the same suite and worker count.
37
+
38
+ ### Fixed
39
+
40
+ - Restore application-config-only preload for feature generators, preserving configured notification locales and one-shot CLI exit. Correct cleanup test hooks to return void.
41
+
42
+ - Fix CRUD UI generation with the Oxc parser, ES2022-compatible tooling arrays, nullable component bodies, and app-free feature scaffolding. Align inspection CLI tests and convention error-code headings.
43
+ - Wait for owned MessagePort close events in the test-server disposal regression so referenced handles are actually released.
44
+
45
+ - Recognize required and optional auth descriptors in route inspection and generated agent summaries, including inherited session guards.
46
+ - Describe optional authentication as an anonymous security alternative in OpenAPI while keeping legacy guards required.
47
+ - Generate notification locale strings for every configured locale with an explicit English fallback.
48
+ - Exclude co-located tests and test-only directories from configuration discovery and reload.
49
+ - Register module JSON dictionaries in each test worker and remove them during teardown.
50
+ - Dispose HTTP test-server file runtime resources and pending loader acknowledgements on shutdown and failed startup.
51
+ - Native ESM loader registration shares a process-global promise across repeated initialization and module reloads; test startup skips the hook when an importer runs application modules through Vitest.
52
+
9
53
  ## 5.34.3 - 2026-10-20
10
54
 
11
55
  ### Fixed
@@ -385,7 +385,7 @@ var CLICommandsManager = class {
385
385
  }
386
386
  await loadEnvironmentFiles();
387
387
  await warlockConfigManager.load();
388
- if (preloaders.config || preloaders.bootstrap || preloaders.prestart) await filesOrchestrator.init();
388
+ if (preloaders.bootstrap || preloaders.prestart) await filesOrchestrator.init();
389
389
  if (preloaders.bootstrap) {
390
390
  await bootstrap();
391
391
  if (await fileExistsAsync(appPath("bootstrap.ts"))) await filesOrchestrator.load("src/app/bootstrap.ts");
@@ -1 +1 @@
1
- {"version":3,"file":"cli-commands.manager.mjs","names":[],"sources":["../../../../../../../../packages/core/src/cli/cli-commands.manager.ts"],"sourcesContent":["import { toCamelCase } from \"@mongez/reinforcements\";\nimport { colors } from \"@mongez/copper\";\nimport { fileExistsAsync } from \"@warlock.js/fs\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { registerConfiguredConnectors } from \"../connectors/register-configured-connectors\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { isDevWorker } from \"../dev-server/supervisor\";\nimport { preflightConfiguredHttpPort, shouldPreflightHttpPort } from \"../http/boot-port-preflight\";\nimport { manifestManager } from \"../manifest/manifest-manager\";\nimport { appPath } from \"../utils\";\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { CLICommand } from \"../commands/cli-command\";\nimport {\n displayBootError,\n displayCommandError,\n displayCommandHelp,\n displayCommandNotFound,\n displayCommandSuccess,\n displayExecutingCommand,\n displayHelp,\n displayMissingCommand,\n displayMissingOptions,\n displayWarlockVersionInTerminal,\n type HelpCommandInfo,\n isMatchingCommandName,\n} from \"./cli-commands.utils\";\nimport { cliCommandsLoader } from \"./commands-loader\";\nimport { frameworkCommands } from \"./framework-cli-commands\";\nimport { CliOptionValueError, type ParsedCliArgs, parseCliArgs } from \"./parse-cli-args\";\nimport { findSimilar } from \"./string-similarity\";\nimport type { CommandActionData, ResolvedCLICommandOption } from \"../commands/types\";\n\n/**\n * Best-effort budget (ms) for draining stdout/stderr before a forced exit.\n * Bounded so a stuck stream can never hang the CLI.\n */\nconst EXIT_FLUSH_BUDGET = 500;\n\n/**\n * Exit with `code` once stdout/stderr have drained.\n *\n * `process.exit()` truncates writes still queued on a non-TTY stdout — a pipe,\n * a CI log — which is how the *reason* for a failure gets lost even when the\n * exit code is right. `exitCode` is set up front so that a natural exit (should\n * the drain outlive the process's remaining work) is non-zero too.\n */\nfunction exitAfterFlush(code: number) {\n process.exitCode = code;\n\n const timer = setTimeout(() => process.exit(code), EXIT_FLUSH_BUDGET);\n timer.unref?.();\n\n let pending = 2;\n const drained = () => {\n if (--pending > 0) return;\n\n clearTimeout(timer);\n process.exit(code);\n };\n\n // A zero-length write's callback fires once everything queued ahead of it has\n // been handed to the OS.\n process.stdout.write(\"\", drained);\n process.stderr.write(\"\", drained);\n}\n\n/**\n * How a command run ended, from the process's point of view.\n *\n * - `succeeded` — finished, nothing rejected afterwards; the CLI may exit 0.\n * - `failed` — reported an error; the exit code is owned by whoever reported it.\n * - `running` — a persistent command (dev/start) now owns the process.\n *\n * `execute` returns this instead of exiting on success itself. A method that\n * terminates the process out from under its caller cannot be awaited honestly:\n * the exit it schedules outlives the call, so the success it represents is\n * still unsettled when `execute` resolves — which is exactly how a green check\n * ends up printed next to the failure that contradicts it.\n */\nexport type CommandRunVerdict = \"succeeded\" | \"failed\" | \"running\";\n\n/**\n * Whether this process is the CHILD half of a supervised command.\n *\n * ## The finding this encodes\n *\n * `warlock dev` printing `› Running dev...` twice was NOT a double boot, and\n * the distinction is the whole point of this function existing rather than the\n * line simply being deleted. The sequence, in file order:\n *\n * 1. `execute()` prints the header (below) — BEFORE `commandPreAction`.\n * 2. `cli/commands/dev-server.command.ts` `preAction` calls\n * `superviseDevServer()` when `!isDevWorker()`.\n * 3. `dev-server/supervisor.ts` `spawnWorker()` re-spawns `process.argv` with\n * `WARLOCK_DEV_WORKER=1`, so the child runs `warlock dev` again and prints\n * the header too.\n * 4. `superviseDevServer()` returns a promise that never settles, so the\n * supervisor's `await commandPreAction(...)` never resolves — `loadPreloaders`\n * and `command.execute` are unreachable in the supervisor.\n *\n * Step 4 is why only ONE process ever loads config, connectors, app modules or\n * a scheduler. Verified by running it: one `APP-CODE-LOADED`, one\n * `Application.markBooted`, one `⚡ Warlock.js` banner, one route registration,\n * two processes. The `2 job(s) registered` in the original report is two jobs\n * in one scheduler, not two schedulers — a genuine double boot would have\n * printed that warning twice.\n *\n * So the defect is exactly one duplicated line, and this is the guard that\n * keeps it one. `tests/unit/cli/dev-command-single-boot.test.ts` fails if the\n * supervisor ever starts running preloaders — i.e. if the double boot the\n * duplicated line looked like ever becomes real.\n */\nfunction isRespawnedWorker(): boolean {\n return isDevWorker();\n}\n\nexport class CLICommandsManager {\n /**\n * List of commands\n */\n public commands: CLICommand[] = [];\n\n /**\n * Commands map (name -> command)\n */\n public commandsMap: Map<string, CLICommand> = new Map();\n\n /**\n * Alias map (alias -> command name)\n */\n public aliasMap: Map<string, string> = new Map();\n\n /**\n * Register the given commands\n */\n public register(...commands: CLICommand[]): this {\n this.commands.push(...commands);\n\n commands.forEach((command) => {\n // `split` always yields a first element, so this holds. Falling back to\n // the whole name rather than asserting keeps the map keyed by a REAL\n // command name — a key of `undefined` would register the command under\n // the literal string \"undefined\" and make it unreachable by its own name.\n const commandKey = command.name.split(\" \")[0] ?? command.name;\n\n manifestManager.addCommandToList(command.name, {\n relativePath: command.commandRelativePath,\n source: command.commandSource || \"project\",\n description: command.commandDescription,\n alias: command.commandAlias,\n options: command.commandOptions.length > 0 ? command.commandOptions : undefined,\n });\n\n this.commandsMap.set(commandKey, command);\n\n // Register alias if exists\n if (command.commandAlias) {\n this.aliasMap.set(command.commandAlias, commandKey);\n }\n });\n\n return this;\n }\n\n /**\n * Get command by name or alias\n */\n public getCommand(name: string): CLICommand | undefined {\n // First try direct lookup\n let command = this.commandsMap.get(name);\n if (command) return command;\n\n // Try alias lookup\n const realName = this.aliasMap.get(name);\n if (realName) {\n return this.commandsMap.get(realName);\n }\n\n return;\n }\n\n /**\n * Get all command names and aliases\n * Used for fuzzy matching suggestions\n */\n public getAllCommandNames(): string[] {\n const names: string[] = [];\n\n // Add all command names\n for (const name of this.commandsMap.keys()) {\n names.push(name);\n }\n\n // Add all aliases\n for (const alias of this.aliasMap.keys()) {\n names.push(alias);\n }\n\n // Also include cached commands from manifest\n const manifestCommands = manifestManager.commandsJson?.commands || {};\n // `Object.entries` rather than keys + index: the index read is\n // `T | undefined` under the strictness contract even for a key that came\n // from the object itself, and these names feed the CLI's \"did you mean?\"\n // suggestions — a literal \"undefined\" in that list is a suggestion no\n // operator can act on.\n for (const [name, meta] of Object.entries(manifestCommands)) {\n const baseName = name.split(\" \")[0] ?? name;\n\n if (!names.includes(baseName)) {\n names.push(baseName);\n }\n\n const alias = meta?.alias;\n\n if (alias && !names.includes(alias)) {\n names.push(alias);\n }\n }\n\n return names;\n }\n\n /**\n * Start the cli manager\n */\n public async start() {\n // Schema-less first pass: it exists to discover the command name and the\n // command-independent flags (--help, --version, --no-cache). Values here are\n // raw strings; the typed pass below is the one whose options reach a command.\n const { name, options } = parseCliArgs(process.argv);\n\n if (options.noCache) {\n manifestManager.clearCommandsCache();\n // remove the commands.json file as wel\n await manifestManager.removeCommandsFile();\n }\n\n // `-v` is only the version flag when no command was given: after a command\n // name it is that command's own short flag (`--with-validation`).\n if (options.version || (options.v && !name)) {\n await displayWarlockVersionInTerminal();\n process.exit(0);\n }\n\n // Try to load from manifest first (fastest path)\n await manifestManager.loadCommands();\n\n // Register framework commands first (needed for help)\n this.register(\n ...frameworkCommands.map((command) => {\n if (!command.commandSource) {\n command.commandSource = \"framework\";\n }\n return command;\n }),\n );\n\n const isHelpCommand = options.help || options.h;\n\n // Handle global help (no command)\n if (!name && isHelpCommand) {\n await this.showGlobalHelp();\n process.exit(0);\n }\n\n // Handle warm cache\n if (options.warmCache) {\n await this.warmCache();\n process.exit(0);\n }\n\n if (!name) {\n displayMissingCommand();\n process.exit(1);\n }\n\n const command = await this.lazyGetCommand(name);\n\n if (manifestManager.hasChanges) {\n await manifestManager.saveCommands();\n }\n\n if (!command) {\n // Find similar commands for suggestions\n const allCommandNames = this.getAllCommandNames();\n const suggestions = findSimilar(name, allCommandNames).map((s) => s.value);\n\n displayCommandNotFound(name, suggestions);\n process.exit(1);\n }\n\n // Handle command-specific help\n if (isHelpCommand) {\n displayCommandHelp({\n name: command.name,\n alias: command.commandAlias,\n description: command.commandDescription,\n options: command.commandOptions,\n });\n process.exit(0);\n }\n\n // The first parse above had no command to consult, so every value in it is\n // a raw string. Now that the command is resolved, re-read argv against its\n // declared options so a `type: \"boolean\"` option arrives as a boolean.\n const resolved = this.resolveCommandArgs(command);\n\n const verdict = await this.execute(command, {\n options: resolved.options,\n args: resolved.args,\n });\n\n // The process-level exit belongs to the entrypoint, not to `execute`.\n // A one-shot command can leave a handle open (a db connection, a watcher a\n // plugin forgot to close), so the CLI exits explicitly rather than waiting\n // for the loop to drain — but only on a verdict of `succeeded`. `failed`\n // already exited non-zero through whoever reported it, and `running` means\n // a persistent command owns the process now.\n if (verdict === \"succeeded\") {\n exitAfterFlush(0);\n }\n }\n\n /**\n * Re-parse argv against the resolved command's declared options.\n *\n * parseCliArgs runs twice on purpose. The first run produces the command\n * name, and there is no schema to consult yet; the second one, here, can be\n * type-aware. Re-reading argv (rather than post-processing the first result)\n * is what makes `--rollback users.ts` recoverable: by the time the first pass\n * returns, the swallowed positional is indistinguishable from a value.\n */\n protected resolveCommandArgs(command: CLICommand, argv: string[] = process.argv): ParsedCliArgs {\n try {\n return parseCliArgs(argv, command.commandOptions);\n } catch (error) {\n if (error instanceof CliOptionValueError) {\n console.log();\n console.log(` ${colors.redBright(\"Error:\")} ${error.message}`);\n console.log();\n process.exit(1);\n }\n\n throw error;\n }\n }\n\n /**\n * Show global help with all commands\n * Uses manifest if available for fast display\n */\n protected async showGlobalHelp() {\n // Always rescan rather than trusting `.warlock/commands.json`: the manifest\n // has no invalidation, so a newly added project command stayed invisible\n // and a removed one lingered in help until `--warm-cache`.\n await this.loadPluginsCommands();\n\n const projectCommands = await cliCommandsLoader.scanAll();\n\n this.register(...projectCommands);\n\n await manifestManager.saveCommands();\n\n // Build help info from registered commands\n const helpCommands: HelpCommandInfo[] = this.commands.map((cmd) => ({\n name: cmd.name,\n alias: cmd.commandAlias,\n description: cmd.commandDescription,\n source: cmd.commandSource || \"project\",\n }));\n\n await displayHelp(helpCommands);\n }\n\n /**\n * Warm cache - scan all project commands and save to manifest\n */\n protected async warmCache() {\n console.log();\n console.log(` ${colors.cyan(\"›\")} Scanning project commands...`);\n\n const projectCommands = await cliCommandsLoader.scanAll();\n\n this.register(...projectCommands);\n\n await manifestManager.saveCommands();\n\n console.log(\n ` ${colors.green(\"✔\")} Cached ${colors.bold(String(projectCommands.length))} project commands`,\n );\n console.log();\n }\n\n /**\n * Load plugins commands\n */\n protected async loadPluginsCommands() {\n await warlockConfigManager.load();\n\n if (warlockConfigManager.isLoaded) {\n this.register(\n ...(warlockConfigManager.get(\"cli\")?.commands || []).map((command) => {\n if (!command.commandSource) {\n command.commandSource = \"plugin\";\n }\n return command;\n }),\n );\n }\n }\n\n /**\n * Try to find the command based on the given command name or alias\n */\n protected async lazyGetCommand(name: string) {\n // first step, try to find it directly through current commands\n let command = this.getCommand(name);\n if (command) {\n return command;\n }\n\n // second step, try to find it through warlock config commands\n await this.loadPluginsCommands();\n\n command = this.getCommand(name);\n\n if (command) {\n return command;\n }\n\n // third step, try to find it through project commands\n await this.loadProjectCommands(name);\n\n command = this.getCommand(name);\n if (command) {\n return command;\n }\n\n return null;\n }\n\n /**\n * Load project commands\n */\n protected async loadProjectCommands(name: string) {\n // first get the commands.json contents as an object\n const jsonCommandsFile = await manifestManager.loadCommands();\n\n if (jsonCommandsFile) {\n // Check by name or alias\n // `Object.entries` hands the metadata over already narrowed; a `for...in`\n // index read is `T | undefined` and every field below is read off it.\n for (const [fullCommandName, cmdMeta] of Object.entries(jsonCommandsFile.commands)) {\n if (cmdMeta === undefined) continue;\n\n // Match by name or alias\n if (isMatchingCommandName(fullCommandName, name) || cmdMeta.alias === name) {\n const commandPath = cmdMeta.relativePath;\n if (commandPath) {\n const executedCommand = await cliCommandsLoader.load(commandPath);\n\n if (executedCommand) {\n executedCommand.$relativePath(commandPath);\n this.register(executedCommand);\n return;\n }\n }\n }\n }\n }\n\n const command = await cliCommandsLoader.locate(name);\n\n if (command) {\n this.register(command);\n return;\n }\n }\n\n /**\n * Validate required options\n */\n protected validateOptions(\n command: CLICommand,\n options: Record<string, string | boolean | number>,\n ): ResolvedCLICommandOption[] {\n const missing: ResolvedCLICommandOption[] = [];\n\n command.commandOptions.forEach((opt) => {\n if (opt.required) {\n // Check if option is provided by name or alias\n const name = toCamelCase(opt.name);\n const alias = opt.alias ? toCamelCase(opt.alias) : \"\";\n const hasOption =\n options[name] !== undefined ||\n options[opt.name] !== undefined ||\n (alias && options[alias] !== undefined);\n if (!hasOption) {\n missing.push(opt);\n }\n }\n });\n\n return missing;\n }\n\n /**\n * Apply default values to options\n */\n protected applyDefaultOptions(\n command: CLICommand,\n options: Record<string, string | boolean | number>,\n ): Record<string, string | boolean | number> {\n const result = { ...options };\n\n command.commandOptions.forEach((declared) => {\n // Parsed keys are camelCase; normalise so commands built with raw kebab\n // names still resolve, and mirror the value to the kebab key.\n const camelName = toCamelCase(declared.name);\n const opt = {\n ...declared,\n name: camelName,\n alias: declared.alias ? toCamelCase(declared.alias) : declared.alias,\n };\n\n const declaredValue = result[declared.name];\n if (result[camelName] === undefined && declaredValue !== undefined) {\n result[camelName] = declaredValue;\n }\n\n if (opt.defaultValue !== undefined) {\n const hasOption =\n result[opt.name] !== undefined || (opt.alias && result[opt.alias] !== undefined);\n if (!hasOption) {\n result[opt.name] = opt.defaultValue;\n }\n }\n\n // `!== undefined`, not truthiness: `-r=false` now resolves to the boolean\n // `false`, and a truthiness test would drop it before it ever reached the\n // canonical name — leaving `rollback` undefined instead of explicitly off.\n if (\n opt.alias !== undefined &&\n result[opt.alias] !== undefined &&\n result[opt.name] === undefined\n ) {\n // The `!== undefined` guard on the line above already proves this read\n // is defined, but the compiler does not carry a guard on one index\n // expression to a second, textually identical one. Reading it into a\n // local makes the narrowing survive, WITHOUT weakening the check — an\n // `?? ` fallback here would resurrect exactly the bug the comment above\n // describes, turning an explicit `-r=false` into a default.\n const aliasValue = result[opt.alias];\n\n if (aliasValue !== undefined) {\n result[opt.name] = aliasValue;\n }\n }\n });\n\n return result;\n }\n\n /**\n * Execute the given command and report how it ended.\n *\n * Returning the verdict — rather than exiting on success from in here — is\n * what makes the awaited result trustworthy: see {@link CommandRunVerdict}.\n */\n public async execute(command: CLICommand, data: CommandActionData): Promise<CommandRunVerdict> {\n const startTime = Date.now();\n\n // Validate required options\n const missingOptions = this.validateOptions(command, data.options);\n if (missingOptions.length > 0) {\n displayMissingOptions(missingOptions);\n process.exit(1);\n }\n\n // Apply default values\n data.options = this.applyDefaultOptions(command, data.options);\n\n // ONE header per `warlock <command>` the user typed — not one per process.\n //\n // `warlock dev` is TWO processes by design: the invocation the user typed\n // becomes a thin supervisor (`dev-server/supervisor.ts`) that re-spawns\n // `process.argv` with `WARLOCK_DEV_WORKER=1`, and the child is the actual\n // server. Both are `warlock dev`, so both reached this line and the user saw\n //\n // › Running dev...\n // › Running dev...\n //\n // which reads exactly like the application booting twice. It is not — see\n // the block comment on `isRespawnedWorker` — but the line was still wrong,\n // and it re-printed on every crash-restart on top of the supervisor's own\n // \"restarting\" notice.\n //\n // The SUPERVISOR keeps the header, not the worker: it prints immediately\n // (so a slow boot is not silent), it prints once for the whole session\n // (restarts are already announced as restarts), and it is the process the\n // user actually started.\n if (!isRespawnedWorker()) {\n displayExecutingCommand(command.name);\n }\n\n if (command.commandPreAction) {\n await command.commandPreAction(data);\n }\n\n // load preloaders\n if (command.commandPreload) {\n try {\n await this.loadPreloaders(command);\n } catch (error) {\n // A preload failure (a bad import in a config file, a connector that\n // throws on boot, a missing module export) happens BEFORE the command's\n // action runs — there is no run loop to recover into, so it is ALWAYS\n // fatal, persistent command (dev/start) or not. Surface the real cause\n // and the offending file, then exit. This MUST stay outside the\n // command-execute try/catch below: letting a preload rejection escape\n // turned `warlock dev` into a silent hang — the unhandled rejection slid\n // past while the already-started loader worker thread kept the process\n // alive, freezing just after the banner with no error printed.\n displayBootError(command.name, error as Error);\n process.exit(1);\n }\n }\n\n this.captureFatalAsyncErrors(command);\n\n try {\n await command.execute(data);\n\n // Persistent commands (dev/start) hand control to their run loop here.\n // There is no success banner to print and nothing to exit for.\n if (command.isPersistent) return \"running\";\n\n // Yield one full event-loop turn before declaring success, and AWAIT that\n // yield. Node flags an unhandled rejection only after the current turn's\n // microtasks drain, so reporting success synchronously here fired FIRST\n // and erased the failure — a `build` whose async work rejected exited 0\n // having emitted nothing.\n //\n // The await is the load-bearing part, not the yield. Scheduling this as a\n // bare `setImmediate` let `execute()` RESOLVE while the verdict was still\n // pending: the callback outlived the call it belonged to, so the green\n // check could surface after the caller had already moved on — in a later\n // command, or after a rejection reported in the interim, which the guard\n // below cannot see because by then it reads the state of a run that has\n // already ended. Awaiting binds the banner to THIS invocation: `execute`\n // does not resolve until the verdict it is about to report has settled.\n //\n // `setImmediate` runs in the same loop iteration's check phase, so this\n // cannot hang on a lingering handle the way dropping the exit would; it\n // only lets `captureFatalAsyncErrors` win the race when there is one.\n await new Promise<void>((resolve) => setImmediate(resolve));\n\n // The yield is what gives a floating rejection the chance to be reported;\n // this is what acts on it. A green check printed after that error would\n // contradict it, and the exit that follows would reset `process.exitCode`\n // to 0 — re-erasing the very failure this whole path exists to surface.\n if (this.fatalAsyncErrorReported) return \"failed\";\n\n displayCommandSuccess(command.name, Date.now() - startTime);\n\n return \"succeeded\";\n } catch (error) {\n displayCommandError(command.name, error as Error);\n // Persistent commands (dev/start) own their startup-failure exits;\n // by the time an error reaches here they've already entered their\n // run loop, so it's a runtime error we log but don't crash on —\n // HMR or process supervisors can recover. Non-persistent\n // one-shot commands always exit on failure.\n if (!command.isPersistent) {\n process.exit(1);\n }\n\n return \"failed\";\n }\n }\n\n /**\n * Fail loudly on async errors that escape the command's own promise.\n *\n * `execute` awaits `command.execute(data)`, but work the command merely\n * *started* settles outside that await — `@mongez/events` `trigger` /\n * `triggerAll` are synchronous and do not await their listeners, so an async\n * listener that throws leaves a floating rejection behind. Node reports it at\n * the end of the turn, by which point the CLI had already exited 0: the\n * failure was erased and the command looked green.\n *\n * Persistent commands (dev/start) are exempt from the exit for the same\n * reason as the catch block in `execute`: by the time they reach this point\n * they own a run loop that is expected to survive runtime errors so HMR or a\n * supervisor can recover. They still print — silence is what made this class\n * of failure invisible in the first place.\n */\n protected fatalAsyncErrorReported = false;\n\n protected captureFatalAsyncErrors(command: CLICommand) {\n const onFatalAsyncError = (reason: unknown) => {\n // A rejection reason is not necessarily an Error (`Promise.reject(\"x\")`),\n // and a crash handler must never itself throw.\n const error = reason instanceof Error ? reason : new Error(String(reason));\n\n // Set before printing, so the deferred success banner is suppressed even\n // if displaying the error yields.\n this.fatalAsyncErrorReported = true;\n\n displayCommandError(command.name, error);\n\n if (!command.isPersistent) {\n exitAfterFlush(1);\n }\n };\n\n process.on(\"unhandledRejection\", onFatalAsyncError);\n process.on(\"uncaughtException\", onFatalAsyncError);\n }\n\n /**\n * Load preloaders\n */\n protected async loadPreloaders(command: CLICommand) {\n const preloaders = command.commandPreload || {};\n\n if (preloaders.runtimeStrategy) {\n Application.setRuntimeStrategy(preloaders.runtimeStrategy);\n }\n\n // Override, not a default: a command that declares an environment gets it\n // regardless of what the shell exported. Then read it back — `process.env`\n // is process-global and anything may have written to it, so a value that\n // did not take must fail here, loudly, rather than surface later as a\n // production React bundle under `warlock dev`.\n if (preloaders.environment) {\n Application.setEnvironment(preloaders.environment);\n\n if (process.env.NODE_ENV !== preloaders.environment) {\n throw new Error(\n `Command \"${command.name}\" set NODE_ENV to \"${preloaders.environment}\", but it read back as ` +\n `\"${process.env.NODE_ENV}\". The environment did not take, and every decision made ` +\n `downstream of it — env file selection, bundler dep optimisation — would be wrong.`,\n );\n }\n }\n\n // Env BEFORE config, unconditionally, for every command.\n //\n // `warlock.config.ts` is a module whose body runs on import, and projects\n // legitimately call `env(\"BUILD_OUT\", \"dist\")` in it. Loading config first\n // evaluated that body against an empty env store, so every such call\n // silently returned its default — under `build` and `start`, which never\n // loaded env at all, and under `dev` too, because config was loaded first\n // regardless. The ordering is load-bearing in the other direction as well:\n // `loadEnv()` reads `process.env.NODE_ENV` at call time to choose\n // `.env.<NODE_ENV>` over `.env`, so it must follow `setEnvironment` above.\n await loadEnvironmentFiles();\n\n await warlockConfigManager.load();\n\n if (preloaders.config || preloaders.bootstrap || preloaders.prestart) {\n await filesOrchestrator.init();\n }\n\n // `preloaders.env` is gone: env is loaded above for every command, so the\n // branch that used to serve it could only re-parse the same files and\n // re-override `process.env` a second time.\n if (preloaders.bootstrap) {\n await bootstrap();\n\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\n await filesOrchestrator.load(\"src/app/bootstrap.ts\");\n }\n }\n\n // Load configuration files\n if (preloaders.config) {\n await loadConfigFiles(preloaders.config);\n }\n\n if (preloaders.prestart) {\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\n await filesOrchestrator.load(\"src/app/prestart.ts\");\n }\n }\n\n // Initialize connectors.\n // `connectors: true` only starts the early phase here — late-phase\n // connectors (http, socket) start after the command's action loads\n // app code. Explicit lists bypass phase splitting (caller knows\n // exactly which connectors they want).\n if (preloaders.connectors) {\n // REGISTER before starting — the non-bundled counterpart of section 2.5\n // of the generated production entry, and the \"dev preloader\" path\n // `register-configured-connectors.ts` documents.\n //\n // Without this, `warlock.config.ts > connectors` drove the BUILD only:\n // `warlock build` read the array to drain each connector's contribution,\n // but under `warlock dev` nothing ever registered it, so an app had to\n // call `connectorsManager.register(...)` from its own `main.ts` to get a\n // connector running. Apps that did BOTH then registered the same\n // connector twice in production — once here from the baked config, once\n // from their own app code — and it booted twice, which surfaced as\n // `Route name \"...\" is already taken` while installing page routes.\n //\n // The array is omitted deliberately, so the function reads the config\n // loaded above; `expectedNames` is omitted too, because drift is a\n // build↔runtime relationship and dev has no build to drift from.\n //\n // The idempotency this function provides is against ITSELF — a second\n // call skips names the first registered. It does NOT protect against an\n // app calling `connectorsManager.register()` directly, which pushes\n // unconditionally, and which runs AFTER this point in both modes. So a\n // connector belongs in the config array or in app code, never both:\n // listing it in both is the duplicate this comment's route-name error\n // describes.\n registerConfiguredConnectors();\n\n // Fail fast on a busy port BEFORE the early-phase connectors (database,\n // cache, ...) connect. `http` itself is a LATE-phase connector, so its\n // own preflight is not reached until after those early connectors are\n // up and every app module has been imported — 7-13s of work discarded\n // before anyone learns the port was taken. This is the dev-preload\n // counterpart of the check the generated production entry runs at the\n // same point in its own boot sequence (`production-builder.ts`, step\n // 2.6) — a cheap bind-and-release probe, not a connector reorder.\n // Only when this boot will actually start http: a scoped list without\n // \"http\" (seed, migrate, …) never binds the port, so probing it would\n // collide with a running dev server on a port it never uses (f9ace89e).\n if (shouldPreflightHttpPort(preloaders.connectors)) {\n await preflightConfiguredHttpPort();\n }\n\n if (preloaders.connectors === true) {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n } else {\n await connectorsManager.start(preloaders.connectors);\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-commands.manager.mjs","names":[],"sources":["../../../../../../../../packages/core/src/cli/cli-commands.manager.ts"],"sourcesContent":["import { toCamelCase } from \"@mongez/reinforcements\";\nimport { colors } from \"@mongez/copper\";\nimport { fileExistsAsync } from \"@warlock.js/fs\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { registerConfiguredConnectors } from \"../connectors/register-configured-connectors\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { isDevWorker } from \"../dev-server/supervisor\";\nimport { preflightConfiguredHttpPort, shouldPreflightHttpPort } from \"../http/boot-port-preflight\";\nimport { manifestManager } from \"../manifest/manifest-manager\";\nimport { appPath } from \"../utils\";\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { CLICommand } from \"../commands/cli-command\";\nimport {\n displayBootError,\n displayCommandError,\n displayCommandHelp,\n displayCommandNotFound,\n displayCommandSuccess,\n displayExecutingCommand,\n displayHelp,\n displayMissingCommand,\n displayMissingOptions,\n displayWarlockVersionInTerminal,\n type HelpCommandInfo,\n isMatchingCommandName,\n} from \"./cli-commands.utils\";\nimport { cliCommandsLoader } from \"./commands-loader\";\nimport { frameworkCommands } from \"./framework-cli-commands\";\nimport { CliOptionValueError, type ParsedCliArgs, parseCliArgs } from \"./parse-cli-args\";\nimport { findSimilar } from \"./string-similarity\";\nimport type { CommandActionData, ResolvedCLICommandOption } from \"../commands/types\";\n\n/**\n * Best-effort budget (ms) for draining stdout/stderr before a forced exit.\n * Bounded so a stuck stream can never hang the CLI.\n */\nconst EXIT_FLUSH_BUDGET = 500;\n\n/**\n * Exit with `code` once stdout/stderr have drained.\n *\n * `process.exit()` truncates writes still queued on a non-TTY stdout — a pipe,\n * a CI log — which is how the *reason* for a failure gets lost even when the\n * exit code is right. `exitCode` is set up front so that a natural exit (should\n * the drain outlive the process's remaining work) is non-zero too.\n */\nfunction exitAfterFlush(code: number) {\n process.exitCode = code;\n\n const timer = setTimeout(() => process.exit(code), EXIT_FLUSH_BUDGET);\n timer.unref?.();\n\n let pending = 2;\n const drained = () => {\n if (--pending > 0) return;\n\n clearTimeout(timer);\n process.exit(code);\n };\n\n // A zero-length write's callback fires once everything queued ahead of it has\n // been handed to the OS.\n process.stdout.write(\"\", drained);\n process.stderr.write(\"\", drained);\n}\n\n/**\n * How a command run ended, from the process's point of view.\n *\n * - `succeeded` — finished, nothing rejected afterwards; the CLI may exit 0.\n * - `failed` — reported an error; the exit code is owned by whoever reported it.\n * - `running` — a persistent command (dev/start) now owns the process.\n *\n * `execute` returns this instead of exiting on success itself. A method that\n * terminates the process out from under its caller cannot be awaited honestly:\n * the exit it schedules outlives the call, so the success it represents is\n * still unsettled when `execute` resolves — which is exactly how a green check\n * ends up printed next to the failure that contradicts it.\n */\nexport type CommandRunVerdict = \"succeeded\" | \"failed\" | \"running\";\n\n/**\n * Whether this process is the CHILD half of a supervised command.\n *\n * ## The finding this encodes\n *\n * `warlock dev` printing `› Running dev...` twice was NOT a double boot, and\n * the distinction is the whole point of this function existing rather than the\n * line simply being deleted. The sequence, in file order:\n *\n * 1. `execute()` prints the header (below) — BEFORE `commandPreAction`.\n * 2. `cli/commands/dev-server.command.ts` `preAction` calls\n * `superviseDevServer()` when `!isDevWorker()`.\n * 3. `dev-server/supervisor.ts` `spawnWorker()` re-spawns `process.argv` with\n * `WARLOCK_DEV_WORKER=1`, so the child runs `warlock dev` again and prints\n * the header too.\n * 4. `superviseDevServer()` returns a promise that never settles, so the\n * supervisor's `await commandPreAction(...)` never resolves — `loadPreloaders`\n * and `command.execute` are unreachable in the supervisor.\n *\n * Step 4 is why only ONE process ever loads config, connectors, app modules or\n * a scheduler. Verified by running it: one `APP-CODE-LOADED`, one\n * `Application.markBooted`, one `⚡ Warlock.js` banner, one route registration,\n * two processes. The `2 job(s) registered` in the original report is two jobs\n * in one scheduler, not two schedulers — a genuine double boot would have\n * printed that warning twice.\n *\n * So the defect is exactly one duplicated line, and this is the guard that\n * keeps it one. `tests/unit/cli/dev-command-single-boot.test.ts` fails if the\n * supervisor ever starts running preloaders — i.e. if the double boot the\n * duplicated line looked like ever becomes real.\n */\nfunction isRespawnedWorker(): boolean {\n return isDevWorker();\n}\n\nexport class CLICommandsManager {\n /**\n * List of commands\n */\n public commands: CLICommand[] = [];\n\n /**\n * Commands map (name -> command)\n */\n public commandsMap: Map<string, CLICommand> = new Map();\n\n /**\n * Alias map (alias -> command name)\n */\n public aliasMap: Map<string, string> = new Map();\n\n /**\n * Register the given commands\n */\n public register(...commands: CLICommand[]): this {\n this.commands.push(...commands);\n\n commands.forEach((command) => {\n // `split` always yields a first element, so this holds. Falling back to\n // the whole name rather than asserting keeps the map keyed by a REAL\n // command name — a key of `undefined` would register the command under\n // the literal string \"undefined\" and make it unreachable by its own name.\n const commandKey = command.name.split(\" \")[0] ?? command.name;\n\n manifestManager.addCommandToList(command.name, {\n relativePath: command.commandRelativePath,\n source: command.commandSource || \"project\",\n description: command.commandDescription,\n alias: command.commandAlias,\n options: command.commandOptions.length > 0 ? command.commandOptions : undefined,\n });\n\n this.commandsMap.set(commandKey, command);\n\n // Register alias if exists\n if (command.commandAlias) {\n this.aliasMap.set(command.commandAlias, commandKey);\n }\n });\n\n return this;\n }\n\n /**\n * Get command by name or alias\n */\n public getCommand(name: string): CLICommand | undefined {\n // First try direct lookup\n let command = this.commandsMap.get(name);\n if (command) return command;\n\n // Try alias lookup\n const realName = this.aliasMap.get(name);\n if (realName) {\n return this.commandsMap.get(realName);\n }\n\n return;\n }\n\n /**\n * Get all command names and aliases\n * Used for fuzzy matching suggestions\n */\n public getAllCommandNames(): string[] {\n const names: string[] = [];\n\n // Add all command names\n for (const name of this.commandsMap.keys()) {\n names.push(name);\n }\n\n // Add all aliases\n for (const alias of this.aliasMap.keys()) {\n names.push(alias);\n }\n\n // Also include cached commands from manifest\n const manifestCommands = manifestManager.commandsJson?.commands || {};\n // `Object.entries` rather than keys + index: the index read is\n // `T | undefined` under the strictness contract even for a key that came\n // from the object itself, and these names feed the CLI's \"did you mean?\"\n // suggestions — a literal \"undefined\" in that list is a suggestion no\n // operator can act on.\n for (const [name, meta] of Object.entries(manifestCommands)) {\n const baseName = name.split(\" \")[0] ?? name;\n\n if (!names.includes(baseName)) {\n names.push(baseName);\n }\n\n const alias = meta?.alias;\n\n if (alias && !names.includes(alias)) {\n names.push(alias);\n }\n }\n\n return names;\n }\n\n /**\n * Start the cli manager\n */\n public async start() {\n // Schema-less first pass: it exists to discover the command name and the\n // command-independent flags (--help, --version, --no-cache). Values here are\n // raw strings; the typed pass below is the one whose options reach a command.\n const { name, options } = parseCliArgs(process.argv);\n\n if (options.noCache) {\n manifestManager.clearCommandsCache();\n // remove the commands.json file as wel\n await manifestManager.removeCommandsFile();\n }\n\n // `-v` is only the version flag when no command was given: after a command\n // name it is that command's own short flag (`--with-validation`).\n if (options.version || (options.v && !name)) {\n await displayWarlockVersionInTerminal();\n process.exit(0);\n }\n\n // Try to load from manifest first (fastest path)\n await manifestManager.loadCommands();\n\n // Register framework commands first (needed for help)\n this.register(\n ...frameworkCommands.map((command) => {\n if (!command.commandSource) {\n command.commandSource = \"framework\";\n }\n return command;\n }),\n );\n\n const isHelpCommand = options.help || options.h;\n\n // Handle global help (no command)\n if (!name && isHelpCommand) {\n await this.showGlobalHelp();\n process.exit(0);\n }\n\n // Handle warm cache\n if (options.warmCache) {\n await this.warmCache();\n process.exit(0);\n }\n\n if (!name) {\n displayMissingCommand();\n process.exit(1);\n }\n\n const command = await this.lazyGetCommand(name);\n\n if (manifestManager.hasChanges) {\n await manifestManager.saveCommands();\n }\n\n if (!command) {\n // Find similar commands for suggestions\n const allCommandNames = this.getAllCommandNames();\n const suggestions = findSimilar(name, allCommandNames).map((s) => s.value);\n\n displayCommandNotFound(name, suggestions);\n process.exit(1);\n }\n\n // Handle command-specific help\n if (isHelpCommand) {\n displayCommandHelp({\n name: command.name,\n alias: command.commandAlias,\n description: command.commandDescription,\n options: command.commandOptions,\n });\n process.exit(0);\n }\n\n // The first parse above had no command to consult, so every value in it is\n // a raw string. Now that the command is resolved, re-read argv against its\n // declared options so a `type: \"boolean\"` option arrives as a boolean.\n const resolved = this.resolveCommandArgs(command);\n\n const verdict = await this.execute(command, {\n options: resolved.options,\n args: resolved.args,\n });\n\n // The process-level exit belongs to the entrypoint, not to `execute`.\n // A one-shot command can leave a handle open (a db connection, a watcher a\n // plugin forgot to close), so the CLI exits explicitly rather than waiting\n // for the loop to drain — but only on a verdict of `succeeded`. `failed`\n // already exited non-zero through whoever reported it, and `running` means\n // a persistent command owns the process now.\n if (verdict === \"succeeded\") {\n exitAfterFlush(0);\n }\n }\n\n /**\n * Re-parse argv against the resolved command's declared options.\n *\n * parseCliArgs runs twice on purpose. The first run produces the command\n * name, and there is no schema to consult yet; the second one, here, can be\n * type-aware. Re-reading argv (rather than post-processing the first result)\n * is what makes `--rollback users.ts` recoverable: by the time the first pass\n * returns, the swallowed positional is indistinguishable from a value.\n */\n protected resolveCommandArgs(command: CLICommand, argv: string[] = process.argv): ParsedCliArgs {\n try {\n return parseCliArgs(argv, command.commandOptions);\n } catch (error) {\n if (error instanceof CliOptionValueError) {\n console.log();\n console.log(` ${colors.redBright(\"Error:\")} ${error.message}`);\n console.log();\n process.exit(1);\n }\n\n throw error;\n }\n }\n\n /**\n * Show global help with all commands\n * Uses manifest if available for fast display\n */\n protected async showGlobalHelp() {\n // Always rescan rather than trusting `.warlock/commands.json`: the manifest\n // has no invalidation, so a newly added project command stayed invisible\n // and a removed one lingered in help until `--warm-cache`.\n await this.loadPluginsCommands();\n\n const projectCommands = await cliCommandsLoader.scanAll();\n\n this.register(...projectCommands);\n\n await manifestManager.saveCommands();\n\n // Build help info from registered commands\n const helpCommands: HelpCommandInfo[] = this.commands.map((cmd) => ({\n name: cmd.name,\n alias: cmd.commandAlias,\n description: cmd.commandDescription,\n source: cmd.commandSource || \"project\",\n }));\n\n await displayHelp(helpCommands);\n }\n\n /**\n * Warm cache - scan all project commands and save to manifest\n */\n protected async warmCache() {\n console.log();\n console.log(` ${colors.cyan(\"›\")} Scanning project commands...`);\n\n const projectCommands = await cliCommandsLoader.scanAll();\n\n this.register(...projectCommands);\n\n await manifestManager.saveCommands();\n\n console.log(\n ` ${colors.green(\"✔\")} Cached ${colors.bold(String(projectCommands.length))} project commands`,\n );\n console.log();\n }\n\n /**\n * Load plugins commands\n */\n protected async loadPluginsCommands() {\n await warlockConfigManager.load();\n\n if (warlockConfigManager.isLoaded) {\n this.register(\n ...(warlockConfigManager.get(\"cli\")?.commands || []).map((command) => {\n if (!command.commandSource) {\n command.commandSource = \"plugin\";\n }\n return command;\n }),\n );\n }\n }\n\n /**\n * Try to find the command based on the given command name or alias\n */\n protected async lazyGetCommand(name: string) {\n // first step, try to find it directly through current commands\n let command = this.getCommand(name);\n if (command) {\n return command;\n }\n\n // second step, try to find it through warlock config commands\n await this.loadPluginsCommands();\n\n command = this.getCommand(name);\n\n if (command) {\n return command;\n }\n\n // third step, try to find it through project commands\n await this.loadProjectCommands(name);\n\n command = this.getCommand(name);\n if (command) {\n return command;\n }\n\n return null;\n }\n\n /**\n * Load project commands\n */\n protected async loadProjectCommands(name: string) {\n // first get the commands.json contents as an object\n const jsonCommandsFile = await manifestManager.loadCommands();\n\n if (jsonCommandsFile) {\n // Check by name or alias\n // `Object.entries` hands the metadata over already narrowed; a `for...in`\n // index read is `T | undefined` and every field below is read off it.\n for (const [fullCommandName, cmdMeta] of Object.entries(jsonCommandsFile.commands)) {\n if (cmdMeta === undefined) continue;\n\n // Match by name or alias\n if (isMatchingCommandName(fullCommandName, name) || cmdMeta.alias === name) {\n const commandPath = cmdMeta.relativePath;\n if (commandPath) {\n const executedCommand = await cliCommandsLoader.load(commandPath);\n\n if (executedCommand) {\n executedCommand.$relativePath(commandPath);\n this.register(executedCommand);\n return;\n }\n }\n }\n }\n }\n\n const command = await cliCommandsLoader.locate(name);\n\n if (command) {\n this.register(command);\n return;\n }\n }\n\n /**\n * Validate required options\n */\n protected validateOptions(\n command: CLICommand,\n options: Record<string, string | boolean | number>,\n ): ResolvedCLICommandOption[] {\n const missing: ResolvedCLICommandOption[] = [];\n\n command.commandOptions.forEach((opt) => {\n if (opt.required) {\n // Check if option is provided by name or alias\n const name = toCamelCase(opt.name);\n const alias = opt.alias ? toCamelCase(opt.alias) : \"\";\n const hasOption =\n options[name] !== undefined ||\n options[opt.name] !== undefined ||\n (alias && options[alias] !== undefined);\n if (!hasOption) {\n missing.push(opt);\n }\n }\n });\n\n return missing;\n }\n\n /**\n * Apply default values to options\n */\n protected applyDefaultOptions(\n command: CLICommand,\n options: Record<string, string | boolean | number>,\n ): Record<string, string | boolean | number> {\n const result = { ...options };\n\n command.commandOptions.forEach((declared) => {\n // Parsed keys are camelCase; normalise so commands built with raw kebab\n // names still resolve, and mirror the value to the kebab key.\n const camelName = toCamelCase(declared.name);\n const opt = {\n ...declared,\n name: camelName,\n alias: declared.alias ? toCamelCase(declared.alias) : declared.alias,\n };\n\n const declaredValue = result[declared.name];\n if (result[camelName] === undefined && declaredValue !== undefined) {\n result[camelName] = declaredValue;\n }\n\n if (opt.defaultValue !== undefined) {\n const hasOption =\n result[opt.name] !== undefined || (opt.alias && result[opt.alias] !== undefined);\n if (!hasOption) {\n result[opt.name] = opt.defaultValue;\n }\n }\n\n // `!== undefined`, not truthiness: `-r=false` now resolves to the boolean\n // `false`, and a truthiness test would drop it before it ever reached the\n // canonical name — leaving `rollback` undefined instead of explicitly off.\n if (\n opt.alias !== undefined &&\n result[opt.alias] !== undefined &&\n result[opt.name] === undefined\n ) {\n // The `!== undefined` guard on the line above already proves this read\n // is defined, but the compiler does not carry a guard on one index\n // expression to a second, textually identical one. Reading it into a\n // local makes the narrowing survive, WITHOUT weakening the check — an\n // `?? ` fallback here would resurrect exactly the bug the comment above\n // describes, turning an explicit `-r=false` into a default.\n const aliasValue = result[opt.alias];\n\n if (aliasValue !== undefined) {\n result[opt.name] = aliasValue;\n }\n }\n });\n\n return result;\n }\n\n /**\n * Execute the given command and report how it ended.\n *\n * Returning the verdict — rather than exiting on success from in here — is\n * what makes the awaited result trustworthy: see {@link CommandRunVerdict}.\n */\n public async execute(command: CLICommand, data: CommandActionData): Promise<CommandRunVerdict> {\n const startTime = Date.now();\n\n // Validate required options\n const missingOptions = this.validateOptions(command, data.options);\n if (missingOptions.length > 0) {\n displayMissingOptions(missingOptions);\n process.exit(1);\n }\n\n // Apply default values\n data.options = this.applyDefaultOptions(command, data.options);\n\n // ONE header per `warlock <command>` the user typed — not one per process.\n //\n // `warlock dev` is TWO processes by design: the invocation the user typed\n // becomes a thin supervisor (`dev-server/supervisor.ts`) that re-spawns\n // `process.argv` with `WARLOCK_DEV_WORKER=1`, and the child is the actual\n // server. Both are `warlock dev`, so both reached this line and the user saw\n //\n // › Running dev...\n // › Running dev...\n //\n // which reads exactly like the application booting twice. It is not — see\n // the block comment on `isRespawnedWorker` — but the line was still wrong,\n // and it re-printed on every crash-restart on top of the supervisor's own\n // \"restarting\" notice.\n //\n // The SUPERVISOR keeps the header, not the worker: it prints immediately\n // (so a slow boot is not silent), it prints once for the whole session\n // (restarts are already announced as restarts), and it is the process the\n // user actually started.\n if (!isRespawnedWorker()) {\n displayExecutingCommand(command.name);\n }\n\n if (command.commandPreAction) {\n await command.commandPreAction(data);\n }\n\n // load preloaders\n if (command.commandPreload) {\n try {\n await this.loadPreloaders(command);\n } catch (error) {\n // A preload failure (a bad import in a config file, a connector that\n // throws on boot, a missing module export) happens BEFORE the command's\n // action runs — there is no run loop to recover into, so it is ALWAYS\n // fatal, persistent command (dev/start) or not. Surface the real cause\n // and the offending file, then exit. This MUST stay outside the\n // command-execute try/catch below: letting a preload rejection escape\n // turned `warlock dev` into a silent hang — the unhandled rejection slid\n // past while the already-started loader worker thread kept the process\n // alive, freezing just after the banner with no error printed.\n displayBootError(command.name, error as Error);\n process.exit(1);\n }\n }\n\n this.captureFatalAsyncErrors(command);\n\n try {\n await command.execute(data);\n\n // Persistent commands (dev/start) hand control to their run loop here.\n // There is no success banner to print and nothing to exit for.\n if (command.isPersistent) return \"running\";\n\n // Yield one full event-loop turn before declaring success, and AWAIT that\n // yield. Node flags an unhandled rejection only after the current turn's\n // microtasks drain, so reporting success synchronously here fired FIRST\n // and erased the failure — a `build` whose async work rejected exited 0\n // having emitted nothing.\n //\n // The await is the load-bearing part, not the yield. Scheduling this as a\n // bare `setImmediate` let `execute()` RESOLVE while the verdict was still\n // pending: the callback outlived the call it belonged to, so the green\n // check could surface after the caller had already moved on — in a later\n // command, or after a rejection reported in the interim, which the guard\n // below cannot see because by then it reads the state of a run that has\n // already ended. Awaiting binds the banner to THIS invocation: `execute`\n // does not resolve until the verdict it is about to report has settled.\n //\n // `setImmediate` runs in the same loop iteration's check phase, so this\n // cannot hang on a lingering handle the way dropping the exit would; it\n // only lets `captureFatalAsyncErrors` win the race when there is one.\n await new Promise<void>((resolve) => setImmediate(resolve));\n\n // The yield is what gives a floating rejection the chance to be reported;\n // this is what acts on it. A green check printed after that error would\n // contradict it, and the exit that follows would reset `process.exitCode`\n // to 0 — re-erasing the very failure this whole path exists to surface.\n if (this.fatalAsyncErrorReported) return \"failed\";\n\n displayCommandSuccess(command.name, Date.now() - startTime);\n\n return \"succeeded\";\n } catch (error) {\n displayCommandError(command.name, error as Error);\n // Persistent commands (dev/start) own their startup-failure exits;\n // by the time an error reaches here they've already entered their\n // run loop, so it's a runtime error we log but don't crash on —\n // HMR or process supervisors can recover. Non-persistent\n // one-shot commands always exit on failure.\n if (!command.isPersistent) {\n process.exit(1);\n }\n\n return \"failed\";\n }\n }\n\n /**\n * Fail loudly on async errors that escape the command's own promise.\n *\n * `execute` awaits `command.execute(data)`, but work the command merely\n * *started* settles outside that await — `@mongez/events` `trigger` /\n * `triggerAll` are synchronous and do not await their listeners, so an async\n * listener that throws leaves a floating rejection behind. Node reports it at\n * the end of the turn, by which point the CLI had already exited 0: the\n * failure was erased and the command looked green.\n *\n * Persistent commands (dev/start) are exempt from the exit for the same\n * reason as the catch block in `execute`: by the time they reach this point\n * they own a run loop that is expected to survive runtime errors so HMR or a\n * supervisor can recover. They still print — silence is what made this class\n * of failure invisible in the first place.\n */\n protected fatalAsyncErrorReported = false;\n\n protected captureFatalAsyncErrors(command: CLICommand) {\n const onFatalAsyncError = (reason: unknown) => {\n // A rejection reason is not necessarily an Error (`Promise.reject(\"x\")`),\n // and a crash handler must never itself throw.\n const error = reason instanceof Error ? reason : new Error(String(reason));\n\n // Set before printing, so the deferred success banner is suppressed even\n // if displaying the error yields.\n this.fatalAsyncErrorReported = true;\n\n displayCommandError(command.name, error);\n\n if (!command.isPersistent) {\n exitAfterFlush(1);\n }\n };\n\n process.on(\"unhandledRejection\", onFatalAsyncError);\n process.on(\"uncaughtException\", onFatalAsyncError);\n }\n\n /**\n * Load preloaders\n */\n protected async loadPreloaders(command: CLICommand) {\n const preloaders = command.commandPreload || {};\n\n if (preloaders.runtimeStrategy) {\n Application.setRuntimeStrategy(preloaders.runtimeStrategy);\n }\n\n // Override, not a default: a command that declares an environment gets it\n // regardless of what the shell exported. Then read it back — `process.env`\n // is process-global and anything may have written to it, so a value that\n // did not take must fail here, loudly, rather than surface later as a\n // production React bundle under `warlock dev`.\n if (preloaders.environment) {\n Application.setEnvironment(preloaders.environment);\n\n if (process.env.NODE_ENV !== preloaders.environment) {\n throw new Error(\n `Command \"${command.name}\" set NODE_ENV to \"${preloaders.environment}\", but it read back as ` +\n `\"${process.env.NODE_ENV}\". The environment did not take, and every decision made ` +\n `downstream of it — env file selection, bundler dep optimisation — would be wrong.`,\n );\n }\n }\n\n // Env BEFORE config, unconditionally, for every command.\n //\n // `warlock.config.ts` is a module whose body runs on import, and projects\n // legitimately call `env(\"BUILD_OUT\", \"dist\")` in it. Loading config first\n // evaluated that body against an empty env store, so every such call\n // silently returned its default — under `build` and `start`, which never\n // loaded env at all, and under `dev` too, because config was loaded first\n // regardless. The ordering is load-bearing in the other direction as well:\n // `loadEnv()` reads `process.env.NODE_ENV` at call time to choose\n // `.env.<NODE_ENV>` over `.env`, so it must follow `setEnvironment` above.\n await loadEnvironmentFiles();\n\n await warlockConfigManager.load();\n\n if (preloaders.bootstrap || preloaders.prestart) {\n await filesOrchestrator.init();\n }\n\n // `preloaders.env` is gone: env is loaded above for every command, so the\n // branch that used to serve it could only re-parse the same files and\n // re-override `process.env` a second time.\n if (preloaders.bootstrap) {\n await bootstrap();\n\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\n await filesOrchestrator.load(\"src/app/bootstrap.ts\");\n }\n }\n\n // Load configuration files\n if (preloaders.config) {\n await loadConfigFiles(preloaders.config);\n }\n\n if (preloaders.prestart) {\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\n await filesOrchestrator.load(\"src/app/prestart.ts\");\n }\n }\n\n // Initialize connectors.\n // `connectors: true` only starts the early phase here — late-phase\n // connectors (http, socket) start after the command's action loads\n // app code. Explicit lists bypass phase splitting (caller knows\n // exactly which connectors they want).\n if (preloaders.connectors) {\n // REGISTER before starting — the non-bundled counterpart of section 2.5\n // of the generated production entry, and the \"dev preloader\" path\n // `register-configured-connectors.ts` documents.\n //\n // Without this, `warlock.config.ts > connectors` drove the BUILD only:\n // `warlock build` read the array to drain each connector's contribution,\n // but under `warlock dev` nothing ever registered it, so an app had to\n // call `connectorsManager.register(...)` from its own `main.ts` to get a\n // connector running. Apps that did BOTH then registered the same\n // connector twice in production — once here from the baked config, once\n // from their own app code — and it booted twice, which surfaced as\n // `Route name \"...\" is already taken` while installing page routes.\n //\n // The array is omitted deliberately, so the function reads the config\n // loaded above; `expectedNames` is omitted too, because drift is a\n // build↔runtime relationship and dev has no build to drift from.\n //\n // The idempotency this function provides is against ITSELF — a second\n // call skips names the first registered. It does NOT protect against an\n // app calling `connectorsManager.register()` directly, which pushes\n // unconditionally, and which runs AFTER this point in both modes. So a\n // connector belongs in the config array or in app code, never both:\n // listing it in both is the duplicate this comment's route-name error\n // describes.\n registerConfiguredConnectors();\n\n // Fail fast on a busy port BEFORE the early-phase connectors (database,\n // cache, ...) connect. `http` itself is a LATE-phase connector, so its\n // own preflight is not reached until after those early connectors are\n // up and every app module has been imported — 7-13s of work discarded\n // before anyone learns the port was taken. This is the dev-preload\n // counterpart of the check the generated production entry runs at the\n // same point in its own boot sequence (`production-builder.ts`, step\n // 2.6) — a cheap bind-and-release probe, not a connector reorder.\n // Only when this boot will actually start http: a scoped list without\n // \"http\" (seed, migrate, …) never binds the port, so probing it would\n // collide with a running dev server on a port it never uses (f9ace89e).\n if (shouldPreflightHttpPort(preloaders.connectors)) {\n await preflightConfiguredHttpPort();\n }\n\n if (preloaders.connectors === true) {\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n } else {\n await connectorsManager.start(preloaders.connectors);\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -6,6 +6,7 @@ import { addCommandAction } from "../../generations/add-command.action.mjs";
6
6
  const addCommand = command({
7
7
  name: "add <features...>",
8
8
  description: "Add new feature(s) to the project",
9
+ preload: { config: ["app"] },
9
10
  action: (data) => runGenerationCommand("add", data, addCommandAction),
10
11
  options: [
11
12
  {
@@ -1 +1 @@
1
- {"version":3,"file":"add.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/add.command.ts"],"sourcesContent":["import { addCommandAction } from \"../../generations/add-command.action\";\nimport { generationOutputOptions, runGenerationCommand } from \"../../generations/generation-run\";\nimport { command } from \"../../commands/cli-command\";\n\nexport const addCommand = command({\n name: \"add <features...>\",\n description: \"Add new feature(s) to the project\",\n action: (data) => runGenerationCommand(\"add\", data, addCommandAction),\n options: [\n {\n text: \"--package-manager, -pm\",\n description: \"Package manager to use, if not passed, it will be detected automatically\",\n },\n {\n text: \"--list, -l\",\n description: \"List available features\",\n type: \"boolean\",\n },\n {\n text: \"--no-install\",\n description:\n \"Record dependencies in package.json without installing them — still ejects configs, adds scripts, and runs setup hooks. Used by scaffolders that run a single install afterwards.\",\n type: \"boolean\",\n },\n ...generationOutputOptions,\n ],\n});\n"],"mappings":";;;;;AAIA,MAAa,aAAa,QAAQ;CAChC,MAAM;CACN,aAAa;CACb,SAAS,SAAS,qBAAqB,OAAO,MAAM,gBAAgB;CACpE,SAAS;EACP;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA,GAAG;CACL;AACF,CAAC"}
1
+ {"version":3,"file":"add.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/add.command.ts"],"sourcesContent":["import { addCommandAction } from \"../../generations/add-command.action\";\nimport { generationOutputOptions, runGenerationCommand } from \"../../generations/generation-run\";\nimport { command } from \"../../commands/cli-command\";\n\nexport const addCommand = command({\n name: \"add <features...>\",\n description: \"Add new feature(s) to the project\",\n preload: { config: [\"app\"] },\n action: (data) => runGenerationCommand(\"add\", data, addCommandAction),\n options: [\n {\n text: \"--package-manager, -pm\",\n description: \"Package manager to use, if not passed, it will be detected automatically\",\n },\n {\n text: \"--list, -l\",\n description: \"List available features\",\n type: \"boolean\",\n },\n {\n text: \"--no-install\",\n description:\n \"Record dependencies in package.json without installing them — still ejects configs, adds scripts, and runs setup hooks. Used by scaffolders that run a single install afterwards.\",\n type: \"boolean\",\n },\n ...generationOutputOptions,\n ],\n});\n"],"mappings":";;;;;AAIA,MAAa,aAAa,QAAQ;CAChC,MAAM;CACN,aAAa;CACb,SAAS,EAAE,QAAQ,CAAC,KAAK,EAAE;CAC3B,SAAS,SAAS,qBAAqB,OAAO,MAAM,gBAAgB;CACpE,SAAS;EACP;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA,GAAG;CACL;AACF,CAAC"}
@@ -1,19 +1,46 @@
1
1
  import { unverifiedBuildRuntimeNotice } from "../../utils/runtime.mjs";
2
2
  import { command } from "../../commands/cli-command.mjs";
3
- import { buildAppProduction } from "../../production/build-app-production.mjs";
3
+ import { withConsoleOnStderr } from "./routes/with-console-on-stderr.mjs";
4
+ import { ProductionBuilder } from "../../production/production-builder.mjs";
4
5
 
5
6
  //#region ../../packages/core/src/cli/commands/build.command.ts
6
7
  const buildCommand = command({
7
8
  name: "build",
8
9
  description: "Build the project for production",
9
- action: buildAppProduction,
10
+ action: buildCommandAction,
11
+ options: [{
12
+ text: "--json",
13
+ description: "Print the build budget report as JSON",
14
+ type: "boolean"
15
+ }],
10
16
  preload: { warlockConfig: true },
11
17
  preAction: () => {
12
18
  const notice = unverifiedBuildRuntimeNotice();
13
19
  if (notice !== void 0) console.error(notice);
14
20
  }
15
21
  });
22
+ async function buildCommandAction({ options }) {
23
+ const builder = new ProductionBuilder();
24
+ if (!options.json) {
25
+ await builder.build();
26
+ return;
27
+ }
28
+ try {
29
+ await withConsoleOnStderr(() => builder.build());
30
+ console.log(JSON.stringify({
31
+ success: true,
32
+ budgets: builder.budgetReport
33
+ }));
34
+ } catch (error) {
35
+ console.log(JSON.stringify({
36
+ success: false,
37
+ budgets: builder.budgetReport,
38
+ error: error instanceof Error ? error.message : String(error)
39
+ }));
40
+ throw error;
41
+ }
42
+ }
16
43
 
17
44
  //#endregion
18
- export { buildCommand };
45
+ export { buildCommand, buildCommandAction };
19
46
  //# sourceMappingURL=build.command.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/build.command.ts"],"sourcesContent":["import { buildAppProduction } from \"../../production/build-app-production\";\nimport { command } from \"../../commands/cli-command\";\nimport { unverifiedBuildRuntimeNotice } from \"../../utils/runtime\";\n\nexport const buildCommand = command({\n name: \"build\",\n description: \"Build the project for production\",\n action: buildAppProduction,\n preload: {\n warlockConfig: true,\n },\n preAction: () => {\n // Bun and Deno can usually run the build, but only the Node build is\n // verified; say so on stderr without failing the command.\n const notice = unverifiedBuildRuntimeNotice();\n\n if (notice !== undefined) {\n console.error(notice);\n }\n },\n});\n"],"mappings":";;;;;AAIA,MAAa,eAAe,QAAQ;CAClC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS,EACP,eAAe,KACjB;CACA,iBAAiB;EAGf,MAAM,SAAS,6BAA6B;EAE5C,IAAI,WAAW,QACb,QAAQ,MAAM,MAAM;CAExB;AACF,CAAC"}
1
+ {"version":3,"file":"build.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/build.command.ts"],"sourcesContent":["import { ProductionBuilder } from \"../../production/production-builder\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { withConsoleOnStderr } from \"./routes/with-console-on-stderr\";\nimport { command } from \"../../commands/cli-command\";\nimport { unverifiedBuildRuntimeNotice } from \"../../utils/runtime\";\n\nexport const buildCommand = command({\n name: \"build\",\n description: \"Build the project for production\",\n action: buildCommandAction,\n options: [\n { text: \"--json\", description: \"Print the build budget report as JSON\", type: \"boolean\" },\n ],\n preload: {\n warlockConfig: true,\n },\n preAction: () => {\n // Bun and Deno can usually run the build, but only the Node build is\n // verified; say so on stderr without failing the command.\n const notice = unverifiedBuildRuntimeNotice();\n\n if (notice !== undefined) {\n console.error(notice);\n }\n },\n});\n\nexport async function buildCommandAction({ options }: CommandActionData): Promise<void> {\n const builder = new ProductionBuilder();\n if (!options.json) {\n await builder.build();\n return;\n }\n try {\n await withConsoleOnStderr(() => builder.build());\n console.log(JSON.stringify({ success: true, budgets: builder.budgetReport }));\n } catch (error) {\n console.log(\n JSON.stringify({\n success: false,\n budgets: builder.budgetReport,\n error: error instanceof Error ? error.message : String(error),\n }),\n );\n throw error;\n }\n}\n"],"mappings":";;;;;;AAMA,MAAa,eAAe,QAAQ;CAClC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EAAE,MAAM;EAAU,aAAa;EAAyC,MAAM;CAAU,CAC1F;CACA,SAAS,EACP,eAAe,KACjB;CACA,iBAAiB;EAGf,MAAM,SAAS,6BAA6B;EAE5C,IAAI,WAAW,QACb,QAAQ,MAAM,MAAM;CAExB;AACF,CAAC;AAED,eAAsB,mBAAmB,EAAE,WAA6C;CACtF,MAAM,UAAU,IAAI,kBAAkB;CACtC,IAAI,CAAC,QAAQ,MAAM;EACjB,MAAM,QAAQ,MAAM;EACpB;CACF;CACA,IAAI;EACF,MAAM,0BAA0B,QAAQ,MAAM,CAAC;EAC/C,QAAQ,IAAI,KAAK,UAAU;GAAE,SAAS;GAAM,SAAS,QAAQ;EAAa,CAAC,CAAC;CAC9E,SAAS,OAAO;EACd,QAAQ,IACN,KAAK,UAAU;GACb,SAAS;GACT,SAAS,QAAQ;GACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAC9D,CAAC,CACH;EACA,MAAM;CACR;AACF"}
@@ -41,6 +41,7 @@ async function checkCommandAction({ options }) {
41
41
  const flags = [...changed ? ["--changed"] : [], ...base ? ["--base", base] : []];
42
42
  const report = await runCheck({
43
43
  lint,
44
+ fix: options.fix === true,
44
45
  changed,
45
46
  base,
46
47
  skip,
@@ -1 +1 @@
1
- {"version":3,"file":"check-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/check/check-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\nimport { CHECK_STEPS, type CheckStepName } from \"../../../tooling/check/check-report.types\";\nimport { runCheck } from \"../../../tooling/check/run-check\";\nimport { flushAndExit } from \"../../flush-and-exit\";\nimport { childTypecheckRunner } from \"./child-typecheck\";\nimport { formatCheckSummary, printProblems } from \"./format-problems\";\n\nfunction stringOption(value: string | number | boolean | undefined): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\nfunction isStepName(value: string): value is CheckStepName {\n return (CHECK_STEPS as readonly string[]).includes(value);\n}\n\n/** Parse `--skip typecheck,lint`; throws on an unknown step name. */\nexport function parseSkippedSteps(value: string | undefined): CheckStepName[] {\n if (!value) return [];\n\n const names = value\n .split(\",\")\n .map((name) => name.trim())\n .filter((name) => name.length > 0);\n const unknown = names.filter((name) => !isStepName(name));\n\n if (unknown.length > 0) {\n throw new Error(\n `Unknown check step(s): ${unknown.join(\", \")}. Valid steps: ${CHECK_STEPS.join(\", \")}.`,\n );\n }\n\n return names.filter(isStepName);\n}\n\n/**\n * Action behind `warlock check`.\n *\n * Runs {@link runCheck} with the typecheck in a child `warlock typecheck`\n * process (so a heap overflow is reported, not fatal), prints the problems and\n * the step summary — or, with `--json`, exactly the report — and exits with\n * the report's code: 0 clean, 1 your code, 2 this machine.\n *\n * @param data Parsed CLI args: `changed`, `base`, `skip`, `project`, `json`.\n */\nexport async function checkCommandAction({ options }: CommandActionData): Promise<void> {\n const lint = stringOption(options.lint);\n if (lint === \"eslint\") {\n throw new Error(\n \"ESLint support was removed in 5.34.0. Oxlint/Oxfmt is the default. Run npx warlock upgrade --from 5.33 --to 5.34.0.\",\n );\n }\n if (lint !== undefined && lint !== \"oxlint\") {\n throw new Error(\"Unknown lint tool. Use --lint oxlint.\");\n }\n const base = stringOption(options.base);\n const changed = options.changed === true || base !== undefined;\n const project = stringOption(options.project);\n const skip = parseSkippedSteps(stringOption(options.skip));\n const cliEntry = process.argv[1];\n const flags = [...(changed ? [\"--changed\"] : []), ...(base ? [\"--base\", base] : [])];\n\n const report = await runCheck({\n lint,\n changed,\n base,\n skip,\n project,\n ...(cliEntry ? { typecheck: childTypecheckRunner(cliEntry, flags) } : {}),\n });\n\n if (options.json === true) {\n console.log(JSON.stringify(report, null, 2));\n } else {\n printProblems(report.problems);\n\n if (report.problems.length > 0) console.log(\"\");\n\n for (const line of formatCheckSummary(report)) {\n console.log(line);\n }\n }\n\n if (report.exitCode !== 0) {\n await flushAndExit(report.exitCode);\n }\n}\n"],"mappings":";;;;;;;AAOA,SAAS,aAAa,OAAkE;CACtF,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;AAEA,SAAS,WAAW,OAAuC;CACzD,OAAQ,YAAkC,SAAS,KAAK;AAC1D;;AAGA,SAAgB,kBAAkB,OAA4C;CAC5E,IAAI,CAAC,OAAO,OAAO,CAAC;CAEpB,MAAM,QAAQ,MACX,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,QAAQ,SAAS,KAAK,SAAS,CAAC;CACnC,MAAM,UAAU,MAAM,QAAQ,SAAS,CAAC,WAAW,IAAI,CAAC;CAExD,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,0BAA0B,QAAQ,KAAK,IAAI,EAAE,iBAAiB,YAAY,KAAK,IAAI,EAAE,EACvF;CAGF,OAAO,MAAM,OAAO,UAAU;AAChC;;;;;;;;;;;AAYA,eAAsB,mBAAmB,EAAE,WAA6C;CACtF,MAAM,OAAO,aAAa,QAAQ,IAAI;CACtC,IAAI,SAAS,UACX,MAAM,IAAI,MACR,qHACF;CAEF,IAAI,SAAS,UAAa,SAAS,UACjC,MAAM,IAAI,MAAM,uCAAuC;CAEzD,MAAM,OAAO,aAAa,QAAQ,IAAI;CACtC,MAAM,UAAU,QAAQ,YAAY,QAAQ,SAAS;CACrD,MAAM,UAAU,aAAa,QAAQ,OAAO;CAC5C,MAAM,OAAO,kBAAkB,aAAa,QAAQ,IAAI,CAAC;CACzD,MAAM,WAAW,QAAQ,KAAK;CAC9B,MAAM,QAAQ,CAAC,GAAI,UAAU,CAAC,WAAW,IAAI,CAAC,GAAI,GAAI,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,CAAE;CAEnF,MAAM,SAAS,MAAM,SAAS;EAC5B;EACA;EACA;EACA;EACA;EACA,GAAI,WAAW,EAAE,WAAW,qBAAqB,UAAU,KAAK,EAAE,IAAI,CAAC;CACzE,CAAC;CAED,IAAI,QAAQ,SAAS,MACnB,QAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;MACtC;EACL,cAAc,OAAO,QAAQ;EAE7B,IAAI,OAAO,SAAS,SAAS,GAAG,QAAQ,IAAI,EAAE;EAE9C,KAAK,MAAM,QAAQ,mBAAmB,MAAM,GAC1C,QAAQ,IAAI,IAAI;CAEpB;CAEA,IAAI,OAAO,aAAa,GACtB,MAAM,aAAa,OAAO,QAAQ;AAEtC"}
1
+ {"version":3,"file":"check-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/check/check-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\nimport { CHECK_STEPS, type CheckStepName } from \"../../../tooling/check/check-report.types\";\nimport { runCheck } from \"../../../tooling/check/run-check\";\nimport { flushAndExit } from \"../../flush-and-exit\";\nimport { childTypecheckRunner } from \"./child-typecheck\";\nimport { formatCheckSummary, printProblems } from \"./format-problems\";\n\nfunction stringOption(value: string | number | boolean | undefined): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\nfunction isStepName(value: string): value is CheckStepName {\n return (CHECK_STEPS as readonly string[]).includes(value);\n}\n\n/** Parse `--skip typecheck,lint`; throws on an unknown step name. */\nexport function parseSkippedSteps(value: string | undefined): CheckStepName[] {\n if (!value) return [];\n\n const names = value\n .split(\",\")\n .map((name) => name.trim())\n .filter((name) => name.length > 0);\n const unknown = names.filter((name) => !isStepName(name));\n\n if (unknown.length > 0) {\n throw new Error(\n `Unknown check step(s): ${unknown.join(\", \")}. Valid steps: ${CHECK_STEPS.join(\", \")}.`,\n );\n }\n\n return names.filter(isStepName);\n}\n\n/**\n * Action behind `warlock check`.\n *\n * Runs {@link runCheck} with the typecheck in a child `warlock typecheck`\n * process (so a heap overflow is reported, not fatal), prints the problems and\n * the step summary — or, with `--json`, exactly the report — and exits with\n * the report's code: 0 clean, 1 your code, 2 this machine.\n *\n * @param data Parsed CLI args: `changed`, `base`, `skip`, `project`, `json`.\n */\nexport async function checkCommandAction({ options }: CommandActionData): Promise<void> {\n const lint = stringOption(options.lint);\n if (lint === \"eslint\") {\n throw new Error(\n \"ESLint support was removed in 5.34.0. Oxlint/Oxfmt is the default. Run npx warlock upgrade --from 5.33 --to 5.34.0.\",\n );\n }\n if (lint !== undefined && lint !== \"oxlint\") {\n throw new Error(\"Unknown lint tool. Use --lint oxlint.\");\n }\n const base = stringOption(options.base);\n const changed = options.changed === true || base !== undefined;\n const project = stringOption(options.project);\n const skip = parseSkippedSteps(stringOption(options.skip));\n const cliEntry = process.argv[1];\n const flags = [...(changed ? [\"--changed\"] : []), ...(base ? [\"--base\", base] : [])];\n\n const report = await runCheck({\n lint,\n fix: options.fix === true,\n changed,\n base,\n skip,\n project,\n ...(cliEntry ? { typecheck: childTypecheckRunner(cliEntry, flags) } : {}),\n });\n\n if (options.json === true) {\n console.log(JSON.stringify(report, null, 2));\n } else {\n printProblems(report.problems);\n\n if (report.problems.length > 0) console.log(\"\");\n\n for (const line of formatCheckSummary(report)) {\n console.log(line);\n }\n }\n\n if (report.exitCode !== 0) {\n await flushAndExit(report.exitCode);\n }\n}\n"],"mappings":";;;;;;;AAOA,SAAS,aAAa,OAAkE;CACtF,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;AAEA,SAAS,WAAW,OAAuC;CACzD,OAAQ,YAAkC,SAAS,KAAK;AAC1D;;AAGA,SAAgB,kBAAkB,OAA4C;CAC5E,IAAI,CAAC,OAAO,OAAO,CAAC;CAEpB,MAAM,QAAQ,MACX,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,QAAQ,SAAS,KAAK,SAAS,CAAC;CACnC,MAAM,UAAU,MAAM,QAAQ,SAAS,CAAC,WAAW,IAAI,CAAC;CAExD,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,0BAA0B,QAAQ,KAAK,IAAI,EAAE,iBAAiB,YAAY,KAAK,IAAI,EAAE,EACvF;CAGF,OAAO,MAAM,OAAO,UAAU;AAChC;;;;;;;;;;;AAYA,eAAsB,mBAAmB,EAAE,WAA6C;CACtF,MAAM,OAAO,aAAa,QAAQ,IAAI;CACtC,IAAI,SAAS,UACX,MAAM,IAAI,MACR,qHACF;CAEF,IAAI,SAAS,UAAa,SAAS,UACjC,MAAM,IAAI,MAAM,uCAAuC;CAEzD,MAAM,OAAO,aAAa,QAAQ,IAAI;CACtC,MAAM,UAAU,QAAQ,YAAY,QAAQ,SAAS;CACrD,MAAM,UAAU,aAAa,QAAQ,OAAO;CAC5C,MAAM,OAAO,kBAAkB,aAAa,QAAQ,IAAI,CAAC;CACzD,MAAM,WAAW,QAAQ,KAAK;CAC9B,MAAM,QAAQ,CAAC,GAAI,UAAU,CAAC,WAAW,IAAI,CAAC,GAAI,GAAI,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,CAAE;CAEnF,MAAM,SAAS,MAAM,SAAS;EAC5B;EACA,KAAK,QAAQ,QAAQ;EACrB;EACA;EACA;EACA;EACA,GAAI,WAAW,EAAE,WAAW,qBAAqB,UAAU,KAAK,EAAE,IAAI,CAAC;CACzE,CAAC;CAED,IAAI,QAAQ,SAAS,MACnB,QAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;MACtC;EACL,cAAc,OAAO,QAAQ;EAE7B,IAAI,OAAO,SAAS,SAAS,GAAG,QAAQ,IAAI,EAAE;EAE9C,KAAK,MAAM,QAAQ,mBAAmB,MAAM,GAC1C,QAAQ,IAAI,IAAI;CAEpB;CAEA,IAAI,OAAO,aAAa,GACtB,MAAM,aAAa,OAAO,QAAQ;AAEtC"}
@@ -17,6 +17,11 @@ const checkCommand = command({
17
17
  description: "Typecheck, lint, run the affected tests and check conventions; one problem list, exit 1 for your code and 2 for this machine",
18
18
  action: checkCommandAction,
19
19
  options: [
20
+ {
21
+ text: "--fix",
22
+ description: "Apply safe convention fixes before checking",
23
+ type: "boolean"
24
+ },
20
25
  {
21
26
  text: "--lint",
22
27
  description: "Lint tool: oxlint (default)",
@@ -1 +1 @@
1
- {"version":3,"file":"check.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/check.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { checkCommandAction } from \"./check/check-command.action\";\n\n/**\n * `warlock check` — the single verify command: typecheck, lint (the app's\n * Oxlint config), the tests affected by the change, and framework\n * convention checks, reported as one problem list. Exit 0 clean, 1 problems\n * in the code, 2 problems with this machine.\n *\n * No preload: the steps read files and run tools; nothing boots, connects or\n * binds a port, so `check` behaves the same with or without a database or\n * Redis running.\n */\nexport const checkCommand = command({\n name: \"check\",\n description:\n \"Typecheck, lint, run the affected tests and check conventions; one problem list, exit 1 for your code and 2 for this machine\",\n action: checkCommandAction,\n options: [\n {\n text: \"--lint\",\n description: \"Lint tool: oxlint (default)\",\n type: \"string\",\n },\n {\n text: \"--changed, -c\",\n description:\n \"Only the files changed against HEAD (plus untracked files) and what they affect\",\n type: \"boolean\",\n },\n {\n text: \"--base, -b\",\n description: \"Compare with the merge base of this ref instead of HEAD (implies --changed)\",\n type: \"string\",\n },\n {\n text: \"--skip, -s\",\n description:\n \"Comma-separated steps to skip: environment, typecheck, lint, tests, conventions\",\n type: \"string\",\n },\n {\n text: \"--project, -p\",\n description: \"tsconfig path (default tsconfig.json)\",\n type: \"string\",\n },\n {\n text: \"--json, -j\",\n description: \"Print the report as JSON\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAa,eAAe,QAAQ;CAClC,MAAM;CACN,aACE;CACF,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
1
+ {"version":3,"file":"check.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/check.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { checkCommandAction } from \"./check/check-command.action\";\n\n/**\n * `warlock check` — the single verify command: typecheck, lint (the app's\n * Oxlint config), the tests affected by the change, and framework\n * convention checks, reported as one problem list. Exit 0 clean, 1 problems\n * in the code, 2 problems with this machine.\n *\n * No preload: the steps read files and run tools; nothing boots, connects or\n * binds a port, so `check` behaves the same with or without a database or\n * Redis running.\n */\nexport const checkCommand = command({\n name: \"check\",\n description:\n \"Typecheck, lint, run the affected tests and check conventions; one problem list, exit 1 for your code and 2 for this machine\",\n action: checkCommandAction,\n options: [\n { text: \"--fix\", description: \"Apply safe convention fixes before checking\", type: \"boolean\" },\n {\n text: \"--lint\",\n description: \"Lint tool: oxlint (default)\",\n type: \"string\",\n },\n {\n text: \"--changed, -c\",\n description:\n \"Only the files changed against HEAD (plus untracked files) and what they affect\",\n type: \"boolean\",\n },\n {\n text: \"--base, -b\",\n description: \"Compare with the merge base of this ref instead of HEAD (implies --changed)\",\n type: \"string\",\n },\n {\n text: \"--skip, -s\",\n description:\n \"Comma-separated steps to skip: environment, typecheck, lint, tests, conventions\",\n type: \"string\",\n },\n {\n text: \"--project, -p\",\n description: \"tsconfig path (default tsconfig.json)\",\n type: \"string\",\n },\n {\n text: \"--json, -j\",\n description: \"Print the report as JSON\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAa,eAAe,QAAQ;CAClC,MAAM;CACN,aACE;CACF,QAAQ;CACR,SAAS;EACP;GAAE,MAAM;GAAS,aAAa;GAA+C,MAAM;EAAU;EAC7F;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
@@ -1,4 +1,8 @@
1
1
  import { ERROR_CODES, errorCodeDocsUrl, explainErrorCode, isWarlockErrorCode } from "../../../errors/error-codes.mjs";
2
+ import { bootForInspect, inspectOptionsFromBoot } from "../../../tooling/inspect/boot-for-inspect.mjs";
3
+ import { inspectApp } from "../../../tooling/inspect/inspect-app.mjs";
4
+ import { withConsoleOnStderr } from "../routes/with-console-on-stderr.mjs";
5
+ import { explainFile, formatFileExplanation } from "../../../tooling/inspect/explain-file.mjs";
2
6
  import { colors } from "@mongez/copper";
3
7
 
4
8
  //#region ../../packages/core/src/cli/commands/explain/explain-command.action.ts
@@ -48,6 +52,23 @@ async function explainCommandAction({ args, options }) {
48
52
  for (const line of formatCodeList()) console.log(line);
49
53
  return;
50
54
  }
55
+ if (!/^WL\d+$/i.test(requested.trim())) {
56
+ const collect = async () => {
57
+ const boot = await bootForInspect();
58
+ return explainFile(requested, await inspectApp({
59
+ ...inspectOptionsFromBoot(boot),
60
+ sections: [
61
+ "files",
62
+ "routes",
63
+ "pages"
64
+ ]
65
+ }));
66
+ };
67
+ const file = await withConsoleOnStderr(collect);
68
+ if (options.json) console.log(JSON.stringify(file, null, 2));
69
+ else for (const line of formatFileExplanation(file)) console.log(line);
70
+ return;
71
+ }
51
72
  const definition = explainErrorCode(requested);
52
73
  if (!definition) throw new Error(`Unknown error code ${JSON.stringify(requested)}. Run \`warlock explain\` to list every code.`);
53
74
  if (options.json) {
@@ -1 +1 @@
1
- {"version":3,"file":"explain-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/explain/explain-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport {\n ERROR_CODES,\n type ErrorCodeDefinition,\n errorCodeDocsUrl,\n explainErrorCode,\n isWarlockErrorCode,\n} from \"../../../errors/error-codes\";\n\n/** What `warlock explain <code> --json` prints: the registry entry plus its docs URL. */\nexport type ExplainedErrorCode = ErrorCodeDefinition & { docs: string };\n\nfunction withDocs(definition: ErrorCodeDefinition): ExplainedErrorCode {\n return {\n ...definition,\n docs: isWarlockErrorCode(definition.code) ? errorCodeDocsUrl(definition.code) : \"\",\n };\n}\n\n/**\n * The lines `warlock explain <code>` prints, uncolored, so they are testable.\n *\n * @param definition The registry entry to describe.\n */\nexport function formatExplanation(definition: ErrorCodeDefinition): string[] {\n const { docs } = withDocs(definition);\n\n return [\n `${definition.code} ${definition.title}`,\n \"\",\n ` Area: ${definition.area} (${definition.category === \"environment\" ? \"this machine\" : \"your code\"})`,\n ` Fix: ${definition.fix}`,\n ` Skill: ${definition.skill}`,\n ` Docs: ${docs}`,\n ...(definition.errorNames?.length ? [` Thrown as: ${definition.errorNames.join(\", \")}`] : []),\n ];\n}\n\n/** One line per registered code, for `warlock explain` without an argument. */\nexport function formatCodeList(): string[] {\n return Object.values(ERROR_CODES).map(\n (definition: ErrorCodeDefinition) => `${definition.code} ${definition.title}`,\n );\n}\n\n/**\n * Action behind `warlock explain [code]`.\n *\n * With a code, prints what it means and what to do; with `--json`, the registry\n * entry as JSON. Without a code, lists every code (or, with `--json`, every\n * entry). An unknown code is thrown, so the CLI exits 1.\n *\n * @param data Parsed CLI args: the code as the first positional, `options.json`.\n */\nexport async function explainCommandAction({ args, options }: CommandActionData): Promise<void> {\n const requested = args[0];\n\n if (requested === undefined || requested.trim() === \"\") {\n if (options.json) {\n console.log(JSON.stringify(Object.values(ERROR_CODES).map(withDocs), null, 2));\n return;\n }\n\n for (const line of formatCodeList()) {\n console.log(line);\n }\n\n return;\n }\n\n const definition = explainErrorCode(requested);\n\n if (!definition) {\n // Thrown, not printed: the CLI reports it on stderr and exits 1.\n throw new Error(\n `Unknown error code ${JSON.stringify(requested)}. Run \\`warlock explain\\` to list every code.`,\n );\n }\n\n if (options.json) {\n console.log(JSON.stringify(withDocs(definition), null, 2));\n return;\n }\n\n const [heading, ...rest] = formatExplanation(definition);\n\n console.log(colors.bold(heading ?? definition.code));\n\n for (const line of rest) {\n console.log(line);\n }\n}\n"],"mappings":";;;;AAaA,SAAS,SAAS,YAAqD;CACrE,OAAO;EACL,GAAG;EACH,MAAM,mBAAmB,WAAW,IAAI,IAAI,iBAAiB,WAAW,IAAI,IAAI;CAClF;AACF;;;;;;AAOA,SAAgB,kBAAkB,YAA2C;CAC3E,MAAM,EAAE,SAAS,SAAS,UAAU;CAEpC,OAAO;EACL,GAAG,WAAW,KAAK,IAAI,WAAW;EAClC;EACA,eAAe,WAAW,KAAK,IAAI,WAAW,aAAa,gBAAgB,iBAAiB,YAAY;EACxG,eAAe,WAAW;EAC1B,eAAe,WAAW;EAC1B,eAAe;EACf,GAAI,WAAW,YAAY,SAAS,CAAC,gBAAgB,WAAW,WAAW,KAAK,IAAI,GAAG,IAAI,CAAC;CAC9F;AACF;;AAGA,SAAgB,iBAA2B;CACzC,OAAO,OAAO,OAAO,WAAW,CAAC,CAAC,KAC/B,eAAoC,GAAG,WAAW,KAAK,IAAI,WAAW,OACzE;AACF;;;;;;;;;;AAWA,eAAsB,qBAAqB,EAAE,MAAM,WAA6C;CAC9F,MAAM,YAAY,KAAK;CAEvB,IAAI,cAAc,UAAa,UAAU,KAAK,MAAM,IAAI;EACtD,IAAI,QAAQ,MAAM;GAChB,QAAQ,IAAI,KAAK,UAAU,OAAO,OAAO,WAAW,CAAC,CAAC,IAAI,QAAQ,GAAG,MAAM,CAAC,CAAC;GAC7E;EACF;EAEA,KAAK,MAAM,QAAQ,eAAe,GAChC,QAAQ,IAAI,IAAI;EAGlB;CACF;CAEA,MAAM,aAAa,iBAAiB,SAAS;CAE7C,IAAI,CAAC,YAEH,MAAM,IAAI,MACR,sBAAsB,KAAK,UAAU,SAAS,EAAE,8CAClD;CAGF,IAAI,QAAQ,MAAM;EAChB,QAAQ,IAAI,KAAK,UAAU,SAAS,UAAU,GAAG,MAAM,CAAC,CAAC;EACzD;CACF;CAEA,MAAM,CAAC,SAAS,GAAG,QAAQ,kBAAkB,UAAU;CAEvD,QAAQ,IAAI,OAAO,KAAK,WAAW,WAAW,IAAI,CAAC;CAEnD,KAAK,MAAM,QAAQ,MACjB,QAAQ,IAAI,IAAI;AAEpB"}
1
+ {"version":3,"file":"explain-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/explain/explain-command.action.ts"],"sourcesContent":["import { bootForInspect, inspectOptionsFromBoot } from \"../../../tooling/inspect/boot-for-inspect\";\nimport { inspectApp } from \"../../../tooling/inspect/inspect-app\";\nimport { explainFile, formatFileExplanation } from \"../../../tooling/inspect/explain-file\";\nimport { withConsoleOnStderr } from \"../routes/with-console-on-stderr\";\nimport { colors } from \"@mongez/copper\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport {\n ERROR_CODES,\n type ErrorCodeDefinition,\n errorCodeDocsUrl,\n explainErrorCode,\n isWarlockErrorCode,\n} from \"../../../errors/error-codes\";\n\n/** What `warlock explain <code> --json` prints: the registry entry plus its docs URL. */\nexport type ExplainedErrorCode = ErrorCodeDefinition & { docs: string };\n\nfunction withDocs(definition: ErrorCodeDefinition): ExplainedErrorCode {\n return {\n ...definition,\n docs: isWarlockErrorCode(definition.code) ? errorCodeDocsUrl(definition.code) : \"\",\n };\n}\n\n/**\n * The lines `warlock explain <code>` prints, uncolored, so they are testable.\n *\n * @param definition The registry entry to describe.\n */\nexport function formatExplanation(definition: ErrorCodeDefinition): string[] {\n const { docs } = withDocs(definition);\n\n return [\n `${definition.code} ${definition.title}`,\n \"\",\n ` Area: ${definition.area} (${definition.category === \"environment\" ? \"this machine\" : \"your code\"})`,\n ` Fix: ${definition.fix}`,\n ` Skill: ${definition.skill}`,\n ` Docs: ${docs}`,\n ...(definition.errorNames?.length ? [` Thrown as: ${definition.errorNames.join(\", \")}`] : []),\n ];\n}\n\n/** One line per registered code, for `warlock explain` without an argument. */\nexport function formatCodeList(): string[] {\n return Object.values(ERROR_CODES).map(\n (definition: ErrorCodeDefinition) => `${definition.code} ${definition.title}`,\n );\n}\n\n/**\n * Action behind `warlock explain [code]`.\n *\n * With a code, prints what it means and what to do; with `--json`, the registry\n * entry as JSON. Without a code, lists every code (or, with `--json`, every\n * entry). An unknown code is thrown, so the CLI exits 1.\n *\n * @param data Parsed CLI args: the code as the first positional, `options.json`.\n */\nexport async function explainCommandAction({ args, options }: CommandActionData): Promise<void> {\n const requested = args[0];\n\n if (requested === undefined || requested.trim() === \"\") {\n if (options.json) {\n console.log(JSON.stringify(Object.values(ERROR_CODES).map(withDocs), null, 2));\n return;\n }\n\n for (const line of formatCodeList()) {\n console.log(line);\n }\n\n return;\n }\n\n if (!/^WL\\d+$/i.test(requested.trim())) {\n const collect = async () => {\n const boot = await bootForInspect();\n return explainFile(\n requested,\n await inspectApp({\n ...inspectOptionsFromBoot(boot),\n sections: [\"files\", \"routes\", \"pages\"],\n }),\n );\n };\n const file = await withConsoleOnStderr(collect);\n if (options.json) console.log(JSON.stringify(file, null, 2));\n else for (const line of formatFileExplanation(file)) console.log(line);\n return;\n }\n\n const definition = explainErrorCode(requested);\n\n if (!definition) {\n // Thrown, not printed: the CLI reports it on stderr and exits 1.\n throw new Error(\n `Unknown error code ${JSON.stringify(requested)}. Run \\`warlock explain\\` to list every code.`,\n );\n }\n\n if (options.json) {\n console.log(JSON.stringify(withDocs(definition), null, 2));\n return;\n }\n\n const [heading, ...rest] = formatExplanation(definition);\n\n console.log(colors.bold(heading ?? definition.code));\n\n for (const line of rest) {\n console.log(line);\n }\n}\n"],"mappings":";;;;;;;;AAiBA,SAAS,SAAS,YAAqD;CACrE,OAAO;EACL,GAAG;EACH,MAAM,mBAAmB,WAAW,IAAI,IAAI,iBAAiB,WAAW,IAAI,IAAI;CAClF;AACF;;;;;;AAOA,SAAgB,kBAAkB,YAA2C;CAC3E,MAAM,EAAE,SAAS,SAAS,UAAU;CAEpC,OAAO;EACL,GAAG,WAAW,KAAK,IAAI,WAAW;EAClC;EACA,eAAe,WAAW,KAAK,IAAI,WAAW,aAAa,gBAAgB,iBAAiB,YAAY;EACxG,eAAe,WAAW;EAC1B,eAAe,WAAW;EAC1B,eAAe;EACf,GAAI,WAAW,YAAY,SAAS,CAAC,gBAAgB,WAAW,WAAW,KAAK,IAAI,GAAG,IAAI,CAAC;CAC9F;AACF;;AAGA,SAAgB,iBAA2B;CACzC,OAAO,OAAO,OAAO,WAAW,CAAC,CAAC,KAC/B,eAAoC,GAAG,WAAW,KAAK,IAAI,WAAW,OACzE;AACF;;;;;;;;;;AAWA,eAAsB,qBAAqB,EAAE,MAAM,WAA6C;CAC9F,MAAM,YAAY,KAAK;CAEvB,IAAI,cAAc,UAAa,UAAU,KAAK,MAAM,IAAI;EACtD,IAAI,QAAQ,MAAM;GAChB,QAAQ,IAAI,KAAK,UAAU,OAAO,OAAO,WAAW,CAAC,CAAC,IAAI,QAAQ,GAAG,MAAM,CAAC,CAAC;GAC7E;EACF;EAEA,KAAK,MAAM,QAAQ,eAAe,GAChC,QAAQ,IAAI,IAAI;EAGlB;CACF;CAEA,IAAI,CAAC,WAAW,KAAK,UAAU,KAAK,CAAC,GAAG;EACtC,MAAM,UAAU,YAAY;GAC1B,MAAM,OAAO,MAAM,eAAe;GAClC,OAAO,YACL,WACA,MAAM,WAAW;IACf,GAAG,uBAAuB,IAAI;IAC9B,UAAU;KAAC;KAAS;KAAU;IAAO;GACvC,CAAC,CACH;EACF;EACA,MAAM,OAAO,MAAM,oBAAoB,OAAO;EAC9C,IAAI,QAAQ,MAAM,QAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;OACtD,KAAK,MAAM,QAAQ,sBAAsB,IAAI,GAAG,QAAQ,IAAI,IAAI;EACrE;CACF;CAEA,MAAM,aAAa,iBAAiB,SAAS;CAE7C,IAAI,CAAC,YAEH,MAAM,IAAI,MACR,sBAAsB,KAAK,UAAU,SAAS,EAAE,8CAClD;CAGF,IAAI,QAAQ,MAAM;EAChB,QAAQ,IAAI,KAAK,UAAU,SAAS,UAAU,GAAG,MAAM,CAAC,CAAC;EACzD;CACF;CAEA,MAAM,CAAC,SAAS,GAAG,QAAQ,kBAAkB,UAAU;CAEvD,QAAQ,IAAI,OAAO,KAAK,WAAW,WAAW,IAAI,CAAC;CAEnD,KAAK,MAAM,QAAQ,MACjB,QAAQ,IAAI,IAAI;AAEpB"}
@@ -5,15 +5,25 @@ import { explainCommandAction } from "./explain/explain-command.action.mjs";
5
5
  /**
6
6
  * `warlock explain [code]` — print what a stable Warlock error code (`WL3001`)
7
7
  * means, what to do about it, the skill that covers it and its docs link.
8
- * Without a code it lists every code. Reads a static table: no preload.
8
+ * Without an argument it lists every code. File explanations boot read-only
9
+ * to discover route/page entry points; error codes read a static table.
9
10
  */
10
11
  const explainCommand = command({
11
- name: "explain [code]",
12
- description: "Explain a Warlock error code (e.g. WL3001): what it means and how to fix it",
12
+ name: "explain [code-or-file]",
13
+ description: "Explain a source file and its routes/pages, or a Warlock error code (e.g. WL3001)",
13
14
  action: explainCommandAction,
15
+ preAction: ({ args }) => {
16
+ const requested = args[0]?.trim();
17
+ explainCommand.commandPreload = requested && !/^WL\d+$/i.test(requested) ? {
18
+ runtimeStrategy: "development",
19
+ config: true,
20
+ env: true,
21
+ bootstrap: true
22
+ } : void 0;
23
+ },
14
24
  options: [{
15
25
  text: "--json, -j",
16
- description: "Print the registry entry as JSON",
26
+ description: "Print the explanation as JSON",
17
27
  type: "boolean"
18
28
  }]
19
29
  });
@@ -1 +1 @@
1
- {"version":3,"file":"explain.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/explain.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { explainCommandAction } from \"./explain/explain-command.action\";\n\n/**\n * `warlock explain [code]` — print what a stable Warlock error code (`WL3001`)\n * means, what to do about it, the skill that covers it and its docs link.\n * Without a code it lists every code. Reads a static table: no preload.\n */\nexport const explainCommand = command({\n name: \"explain [code]\",\n description: \"Explain a Warlock error code (e.g. WL3001): what it means and how to fix it\",\n action: explainCommandAction,\n options: [\n {\n text: \"--json, -j\",\n description: \"Print the registry entry as JSON\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;AAQA,MAAa,iBAAiB,QAAQ;CACpC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC"}
1
+ {"version":3,"file":"explain.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/explain.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { explainCommandAction } from \"./explain/explain-command.action\";\n\n/**\n * `warlock explain [code]` — print what a stable Warlock error code (`WL3001`)\n * means, what to do about it, the skill that covers it and its docs link.\n * Without an argument it lists every code. File explanations boot read-only\n * to discover route/page entry points; error codes read a static table.\n */\nexport const explainCommand = command({\n name: \"explain [code-or-file]\",\n description: \"Explain a source file and its routes/pages, or a Warlock error code (e.g. WL3001)\",\n action: explainCommandAction,\n preAction: ({ args }) => {\n const requested = args[0]?.trim();\n explainCommand.commandPreload =\n requested && !/^WL\\d+$/i.test(requested)\n ? { runtimeStrategy: \"development\", config: true, env: true, bootstrap: true }\n : undefined;\n },\n options: [\n {\n text: \"--json, -j\",\n description: \"Print the explanation as JSON\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;AASA,MAAa,iBAAiB,QAAQ;CACpC,MAAM;CACN,aAAa;CACb,QAAQ;CACR,YAAY,EAAE,WAAW;EACvB,MAAM,YAAY,KAAK,EAAE,EAAE,KAAK;EAChC,eAAe,iBACb,aAAa,CAAC,WAAW,KAAK,SAAS,IACnC;GAAE,iBAAiB;GAAe,QAAQ;GAAM,KAAK;GAAM,WAAW;EAAK,IAC3E;CACR;CACA,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC"}
@@ -9,10 +9,14 @@ import { generateRepository } from "./generators/repository.generator.mjs";
9
9
  import { generateResource } from "./generators/resource.generator.mjs";
10
10
  import { generateService } from "./generators/service.generator.mjs";
11
11
  import { generateUseCase } from "./generators/use-case.generator.mjs";
12
+ import { generateCrudUi, generateForm, generatePage } from "./generators/web.generator.mjs";
12
13
  import { colors } from "@mongez/copper";
13
14
 
14
15
  //#region ../../packages/core/src/cli/commands/generate/generate.command.ts
15
16
  const generators = {
17
+ page: generatePage,
18
+ form: generateForm,
19
+ "crud-ui": generateCrudUi,
16
20
  module: generateModule,
17
21
  endpoint: generateEndpoint,
18
22
  controller: generateController,
@@ -104,7 +108,21 @@ const generatorOptionsForDispatch = [
104
108
  description: "With --force, also replace existing files that differ from the generated output (default: skip and list them)",
105
109
  type: "boolean"
106
110
  },
107
- ...endpointOptions
111
+ ...endpointOptions,
112
+ {
113
+ text: "--module <name>",
114
+ description: "Module owning the generated form"
115
+ },
116
+ {
117
+ text: "--endpoint",
118
+ description: "Generate a guarded form endpoint",
119
+ type: "boolean"
120
+ },
121
+ {
122
+ text: "--interactive",
123
+ description: "Generate a page feature hook",
124
+ type: "boolean"
125
+ }
108
126
  ];
109
127
  const generateCommand = command({
110
128
  name: "generate <generator> [args...]",