@warlock.js/core 5.5.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/esm/application/app.d.mts.map +1 -1
  3. package/esm/application/app.mjs +7 -1
  4. package/esm/application/app.mjs.map +1 -1
  5. package/esm/benchmark/profiler.d.mts.map +1 -1
  6. package/esm/benchmark/profiler.mjs +4 -3
  7. package/esm/benchmark/profiler.mjs.map +1 -1
  8. package/esm/cli/cli-commands.manager.mjs +10 -7
  9. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  10. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  11. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  12. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
  13. package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
  14. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  15. package/esm/cli/commands/routes/format-routes-table.mjs +3 -3
  16. package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
  17. package/esm/cli/commands/routes/route-row.mjs.map +1 -1
  18. package/esm/cli/commands/routes.command.mjs.map +1 -1
  19. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  20. package/esm/cli/parse-cli-args.mjs +1 -0
  21. package/esm/cli/parse-cli-args.mjs.map +1 -1
  22. package/esm/cli/string-similarity.mjs +13 -4
  23. package/esm/cli/string-similarity.mjs.map +1 -1
  24. package/esm/commands/cli-command.d.mts.map +1 -1
  25. package/esm/commands/cli-command.mjs +5 -2
  26. package/esm/commands/cli-command.mjs.map +1 -1
  27. package/esm/config/config-loader.mjs +3 -1
  28. package/esm/config/config-loader.mjs.map +1 -1
  29. package/esm/config/locale-configuration.mjs +11 -1
  30. package/esm/config/locale-configuration.mjs.map +1 -1
  31. package/esm/container/container-instance-registry.mjs +40 -0
  32. package/esm/container/container-instance-registry.mjs.map +1 -0
  33. package/esm/container/index.d.mts +11 -0
  34. package/esm/container/index.d.mts.map +1 -1
  35. package/esm/container/index.mjs +11 -0
  36. package/esm/container/index.mjs.map +1 -1
  37. package/esm/database/drop-tables-action.mjs.map +1 -1
  38. package/esm/database/migrate-action.mjs.map +1 -1
  39. package/esm/database/seed-command-action.mjs.map +1 -1
  40. package/esm/dev-server/dependency-graph.d.mts.map +1 -1
  41. package/esm/dev-server/dependency-graph.mjs +2 -1
  42. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  43. package/esm/dev-server/dev-logger.mjs +7 -5
  44. package/esm/dev-server/dev-logger.mjs.map +1 -1
  45. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  46. package/esm/dev-server/files-orchestrator.mjs +1 -1
  47. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  48. package/esm/dev-server/layer-executor.mjs.map +1 -1
  49. package/esm/dev-server/loader/build-transpile-init.mjs.map +1 -1
  50. package/esm/dev-server/loader/load-hook.mjs.map +1 -1
  51. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  52. package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
  53. package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
  54. package/esm/dev-server/module-loader.mjs.map +1 -1
  55. package/esm/dev-server/parse-imports.mjs +22 -8
  56. package/esm/dev-server/parse-imports.mjs.map +1 -1
  57. package/esm/dev-server/type-generator.mjs.map +1 -1
  58. package/esm/dev-server/types.d.mts.map +1 -1
  59. package/esm/dev-server/utils.mjs.map +1 -1
  60. package/esm/encryption/encrypt.mjs +1 -0
  61. package/esm/encryption/encrypt.mjs.map +1 -1
  62. package/esm/errors/container-key-missing-error.mjs +28 -0
  63. package/esm/errors/container-key-missing-error.mjs.map +1 -0
  64. package/esm/errors/unknown-locale-error.mjs +28 -0
  65. package/esm/errors/unknown-locale-error.mjs.map +1 -0
  66. package/esm/generations/add-command.action.mjs +8 -2
  67. package/esm/generations/add-command.action.mjs.map +1 -1
  68. package/esm/generations/features/access.feature.mjs.map +1 -1
  69. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
  70. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
  71. package/esm/generations/features/ai-google.feature.mjs.map +1 -1
  72. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
  73. package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
  74. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
  75. package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
  76. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
  77. package/esm/generations/features/ai.feature.mjs.map +1 -1
  78. package/esm/generations/features/herald.feature.mjs.map +1 -1
  79. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  80. package/esm/generations/features/scheduler.feature.mjs.map +1 -1
  81. package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
  82. package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +6 -1
  83. package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
  84. package/esm/generations/features/tailwind.feature.mjs.map +1 -1
  85. package/esm/generations/features/types.mjs.map +1 -1
  86. package/esm/generations/features/web.feature.mjs.map +1 -1
  87. package/esm/http/context/request-memo.mjs.map +1 -1
  88. package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
  89. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  90. package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
  91. package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
  92. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  93. package/esm/http/middleware/maintenance.middleware.mjs +1 -2
  94. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  95. package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
  96. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  97. package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
  98. package/esm/http/middleware/utils/cidr-match.mjs +1 -0
  99. package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
  100. package/esm/http/port-preflight.d.mts.map +1 -1
  101. package/esm/http/port-preflight.mjs.map +1 -1
  102. package/esm/http/request.d.mts.map +1 -1
  103. package/esm/http/request.mjs +16 -9
  104. package/esm/http/request.mjs.map +1 -1
  105. package/esm/http/response.d.mts +38 -0
  106. package/esm/http/response.d.mts.map +1 -1
  107. package/esm/http/response.mjs +47 -0
  108. package/esm/http/response.mjs.map +1 -1
  109. package/esm/http/server.d.mts.map +1 -1
  110. package/esm/http/server.mjs.map +1 -1
  111. package/esm/http/types.d.mts.map +1 -1
  112. package/esm/image/image.d.mts.map +1 -1
  113. package/esm/image/image.mjs.map +1 -1
  114. package/esm/mail/send-mail.mjs.map +1 -1
  115. package/esm/production/build-contributions.mjs.map +1 -1
  116. package/esm/production/bundle-framework-dependencies.mjs +152 -0
  117. package/esm/production/bundle-framework-dependencies.mjs.map +1 -0
  118. package/esm/production/production-builder.mjs +30 -3
  119. package/esm/production/production-builder.mjs.map +1 -1
  120. package/esm/production/production-supervisor.mjs.map +1 -1
  121. package/esm/production/tsconfig-path-aliases.mjs +99 -0
  122. package/esm/production/tsconfig-path-aliases.mjs.map +1 -0
  123. package/esm/react/index.d.mts.map +1 -1
  124. package/esm/react/index.mjs.map +1 -1
  125. package/esm/repositories/adapters/cascade/filter-applicator.mjs +4 -2
  126. package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
  127. package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
  128. package/esm/repositories/contracts/types.d.mts.map +1 -1
  129. package/esm/resource/resource-field-builder.d.mts.map +1 -1
  130. package/esm/resource/resource-field-builder.mjs.map +1 -1
  131. package/esm/resource/types.d.mts.map +1 -1
  132. package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
  133. package/esm/router/positional-handler-diagnostics.mjs +9 -3
  134. package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
  135. package/esm/router/route-registry.mjs +1 -1
  136. package/esm/router/route-registry.mjs.map +1 -1
  137. package/esm/router/router.d.mts.map +1 -1
  138. package/esm/router/router.mjs +1 -2
  139. package/esm/router/router.mjs.map +1 -1
  140. package/esm/router/types.d.mts.map +1 -1
  141. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  142. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  143. package/esm/storage/scoped-storage.d.mts.map +1 -1
  144. package/esm/storage/scoped-storage.mjs +1 -0
  145. package/esm/storage/scoped-storage.mjs.map +1 -1
  146. package/esm/storage/storage.d.mts.map +1 -1
  147. package/esm/storage/storage.mjs +6 -2
  148. package/esm/storage/storage.mjs.map +1 -1
  149. package/esm/storage/types.d.mts.map +1 -1
  150. package/esm/storage/utils/contain-path.mjs.map +1 -1
  151. package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
  152. package/esm/storage/utils/safe-fetch.mjs +6 -1
  153. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  154. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  155. package/esm/tests/start-http-development-server.mjs.map +1 -1
  156. package/esm/tests/test-lifecycle-state.mjs.map +1 -1
  157. package/esm/tests/test-setup-timeout.mjs.map +1 -1
  158. package/esm/tests/vitest-setup.d.mts.map +1 -1
  159. package/esm/tests/vitest-setup.mjs.map +1 -1
  160. package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
  161. package/esm/use-cases/use-cases-registry.mjs.map +1 -1
  162. package/esm/utils/app-log.mjs.map +1 -1
  163. package/esm/utils/queue.mjs.map +1 -1
  164. package/esm/utils/sleep.mjs.map +1 -1
  165. package/esm/utils/version-compare.d.mts.map +1 -1
  166. package/esm/utils/version-compare.mjs +12 -4
  167. package/esm/utils/version-compare.mjs.map +1 -1
  168. package/esm/validation/plugins/database-plugin.mjs.map +1 -1
  169. package/esm/vite/lower-stage3-decorators.mjs +1 -0
  170. package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
  171. package/package.json +12 -12
package/CHANGELOG.md CHANGED
@@ -6,6 +6,38 @@ 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.7.0 - 2026-09-11
10
+
11
+ ### Security
12
+
13
+ - **The SSRF guard now fails closed on an address it cannot classify.** An unclassifiable IP was previously treated as public, so an outbound request could reach an internal address the guard exists to block.
14
+
15
+ ### Fixed
16
+
17
+ - **The dev server no longer says it is watching for changes before it is serving.** On a slow boot that line could arrive minutes before the port was bound.
18
+ - The dev-server error formatter can no longer throw while formatting an error, which replaced the developer's real error with its own crash.
19
+ - `warlock add` with an unknown feature now names it, instead of dying with `Cannot read properties of undefined`.
20
+ - Bracket-notation request bodies (`items[0][name]`) no longer silently drop values.
21
+ - `warlock routes` and the circular-import report degrade to unaligned output instead of throwing on an unexpected shape.
22
+
23
+ ### Changed
24
+
25
+ - Internal type-safety hardening across the CLI, dev server and request handling; no other behaviour change.
26
+
27
+ ## 5.6.0 - 2026-09-08
28
+ ### Fixed
29
+
30
+ - **A production build asked the APP to resolve packages only the framework declares, so a built app could not boot under a strict pnpm tree.** Every bare specifier was left external, including the `@fastify/*`, `find-my-way`, `fast-jwt` and `@mongez/*` imports that reach the bundle through the framework's own code — none of which an app has any reason to declare. Under npm/yarn hoisting they resolved by accident; under pnpm the app died with `ERR_MODULE_NOT_FOUND` at startup, after a build that reported success. Externality is now decided per import edge: a bare specifier stays external unless the importer is not the app's own code AND the specifier names a package that importer's own `dependencies` declare.
31
+ - **An optional peer still stays external, and now by construction rather than by a list.** `nodemailer`, `socket.io`, `mongodb`, `vite`, `redis`, `pg`, the AI SDKs and `@aws-sdk/*` are declared as peer dependencies, never dependencies, so the rule above leaves every one of them alone without anyone having to maintain a list of their names.
32
+ - **Wildcard `paths` in `tsconfig.json` were resolved by nothing.** `app/*` and `web/*` — the aliases an app uses to refer to its own source — were skipped because esbuild's `alias` option cannot express a wildcard, and nothing took over. `import { User } from "app/users/models/user.model"` survived into the artifact as a bare specifier and `warlock start` failed with `Cannot find package 'app'`. They now resolve, and a wildcard that matches a declared alias but resolves to no file is a build error rather than a silent external.
33
+ - `response.setLocale()` wrote the locale cookie under a name spelled independently of the one `request` read it back under. One constant now owns that name at both ends, with a test that reads back whatever the writer emitted rather than naming the cookie itself.
34
+ - A container lookup that missed reported only that the key was not bound. When more than one copy of `@warlock.js/core` is loaded — which a source checkout or a mixed install can produce — that message described a real condition as if it were a wiring mistake. The failure now names the duplicate-instance condition and how many copies it found, on the failure path only.
35
+
36
+ ### Changed
37
+
38
+ - `response.clearCookies()` documents what it cannot do, in its first sentence: a cookie set on one path is not cleared by a call made from another. The behaviour is unchanged — the promise it appeared to make was never one it could keep.
39
+ - The router folds its own route prefixes through the same normaliser the rest of the framework uses, so a prefix cannot be normalised two different ways.
40
+
9
41
  ## 5.5.0 - 2026-09-07
10
42
 
11
43
  ### Fixed
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.mts","names":[],"sources":["../../../../../../../core/src/application/app.ts"],"mappings":";;;;;;KAMK,kBAAA;EACH,MAAA,EAAQ,MAAA;EACR,IAAA,EAAM,eAAA;EACN,MAAA,EAAQ,MAAA;EACR,QAAA,EAAU,UAAA;AAAA;AAAA,cAGC,GAAA,EAAK,kBA4BjB"}
1
+ {"version":3,"file":"app.d.mts","names":[],"sources":["../../../../../../../core/src/application/app.ts"],"mappings":";;;;;;KAMK,kBAAA;EACH,MAAA,EAAQ,MAAA;EACR,IAAA,EAAM,eAAA;EACN,MAAA,EAAQ,MAAA;EACR,QAAA,EAAU,UAAA;AAAA;AAAA,cAGC,GAAA,EAAK,kBAkCjB"}
@@ -18,9 +18,15 @@ const app = {
18
18
  },
19
19
  /**
20
20
  * Router Instance
21
+ *
22
+ * Unlike `socket`/`database`, the router is set unconditionally at
23
+ * `router.ts` module load (there is no "no router configured" case), so a
24
+ * miss here is never a legitimate absence — it means either a genuine bug
25
+ * or a duplicate-instance dev-path read. `getOrFail` surfaces that instead
26
+ * of silently returning `undefined`.
21
27
  */
22
28
  get router() {
23
- return container.get("router");
29
+ return container.getOrFail("router");
24
30
  },
25
31
  /**
26
32
  * Database Instance
@@ -1 +1 @@
1
- {"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../../core/src/application/app.ts"],"sourcesContent":["import { type DataSource } from \"@warlock.js/cascade\";\nimport { type FastifyInstance } from \"fastify\";\nimport { type Server } from \"socket.io\";\nimport { type Router } from \"../router\";\nimport { container } from \"./../container\";\n\ntype RuntimeApplication = {\n socket: Server;\n http: FastifyInstance;\n router: Router;\n database: DataSource;\n};\n\nexport const app: RuntimeApplication = {\n /**\n * Socket Io Instance\n * Available only if socket.io config file exists\n */\n get socket() {\n return container.get(\"socket\");\n },\n /**\n * HTTP Server Instance\n * Available only if http config file exists\n */\n get http() {\n return container.get(\"http.server\");\n },\n /**\n * Router Instance\n */\n get router() {\n return container.get(\"router\");\n },\n /**\n * Database Instance\n * Available only if database config file exists\n */\n get database() {\n return container.get(\"database.source\");\n },\n};\n"],"mappings":";;;AAaA,MAAa,MAA0B;;;;;CAKrC,IAAI,SAAS;EACX,OAAO,UAAU,IAAI,QAAQ;CAC/B;;;;;CAKA,IAAI,OAAO;EACT,OAAO,UAAU,IAAI,aAAa;CACpC;;;;CAIA,IAAI,SAAS;EACX,OAAO,UAAU,IAAI,QAAQ;CAC/B;;;;;CAKA,IAAI,WAAW;EACb,OAAO,UAAU,IAAI,iBAAiB;CACxC;AACF"}
1
+ {"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../../core/src/application/app.ts"],"sourcesContent":["import { type DataSource } from \"@warlock.js/cascade\";\nimport { type FastifyInstance } from \"fastify\";\nimport { type Server } from \"socket.io\";\nimport { type Router } from \"../router\";\nimport { container } from \"./../container\";\n\ntype RuntimeApplication = {\n socket: Server;\n http: FastifyInstance;\n router: Router;\n database: DataSource;\n};\n\nexport const app: RuntimeApplication = {\n /**\n * Socket Io Instance\n * Available only if socket.io config file exists\n */\n get socket() {\n return container.get(\"socket\");\n },\n /**\n * HTTP Server Instance\n * Available only if http config file exists\n */\n get http() {\n return container.get(\"http.server\");\n },\n /**\n * Router Instance\n *\n * Unlike `socket`/`database`, the router is set unconditionally at\n * `router.ts` module load (there is no \"no router configured\" case), so a\n * miss here is never a legitimate absence — it means either a genuine bug\n * or a duplicate-instance dev-path read. `getOrFail` surfaces that instead\n * of silently returning `undefined`.\n */\n get router() {\n return container.getOrFail(\"router\");\n },\n /**\n * Database Instance\n * Available only if database config file exists\n */\n get database() {\n return container.get(\"database.source\");\n },\n};\n"],"mappings":";;;AAaA,MAAa,MAA0B;;;;;CAKrC,IAAI,SAAS;EACX,OAAO,UAAU,IAAI,QAAQ;CAC/B;;;;;CAKA,IAAI,OAAO;EACT,OAAO,UAAU,IAAI,aAAa;CACpC;;;;;;;;;;CAUA,IAAI,SAAS;EACX,OAAO,UAAU,UAAU,QAAQ;CACrC;;;;;CAKA,IAAI,WAAW;EACb,OAAO,UAAU,IAAI,iBAAiB;CACxC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"profiler.d.mts","names":[],"sources":["../../../../../../../core/src/benchmark/profiler.ts"],"mappings":";;;cAkBa,iBAAA;EAAA,iBACM,UAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;EAAA,QACT,QAAA;cAEW,OAAA,GAAU,wBAAA;EAoBP;;;;;;;;EAAf,MAAA,CAAO,MAAA,EAAQ,sBAAA,YAAkC,oBAAA;EAxBvC;;;;;;;;;;EAqEV,KAAA,CAAM,IAAA,WAAe,cAAA;EAAf;;;;;;;;EAoCN,QAAA,IAAY,MAAA,SAAe,cAAA;EAyD3B;;AAAO;;;;EAxCD,KAAA,IAAS,OAAA;;;;;;;;;;;EAmBf,KAAA,CAAM,IAAA;;;;;;;EAqBN,OAAA;AAAA"}
1
+ {"version":3,"file":"profiler.d.mts","names":[],"sources":["../../../../../../../core/src/benchmark/profiler.ts"],"mappings":";;;cAkBa,iBAAA;EAAA,iBACM,UAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;EAAA,QACT,QAAA;cAEW,OAAA,GAAU,wBAAA;EAoBP;;;;;;;;EAAf,MAAA,CAAO,MAAA,EAAQ,sBAAA,YAAkC,oBAAA;EAxBvC;;;;;;;;;;EAqEV,KAAA,CAAM,IAAA,WAAe,cAAA;EAAf;;;;;;;;EAsCN,QAAA,IAAY,MAAA,SAAe,cAAA;EAyD3B;;AAAO;;;;EAxCD,KAAA,IAAS,OAAA;;;;;;;;;;;EAmBf,KAAA,CAAM,IAAA;;;;;;;EAqBN,OAAA;AAAA"}
@@ -56,8 +56,9 @@ var BenchmarkProfiler = class {
56
56
  if (!entry || entry.latencies.length === 0) return void 0;
57
57
  const latencies = [...entry.latencies].sort((a, b) => a - b);
58
58
  const count = latencies.length;
59
+ if (count === 0) return void 0;
59
60
  const getP = (percentile) => {
60
- return latencies[Math.min(count - 1, Math.floor(count * percentile))];
61
+ return latencies[Math.min(count - 1, Math.floor(count * percentile))] ?? 0;
61
62
  };
62
63
  return {
63
64
  p50: getP(.5),
@@ -65,8 +66,8 @@ var BenchmarkProfiler = class {
65
66
  p95: getP(.95),
66
67
  p99: getP(.99),
67
68
  avg: Math.round(entry.sum / count * 100) / 100,
68
- min: latencies[0],
69
- max: latencies[count - 1],
69
+ min: latencies[0] ?? 0,
70
+ max: latencies[count - 1] ?? 0,
70
71
  count: entry.total,
71
72
  errors: entry.errors,
72
73
  errorRate: Math.round(entry.errors / entry.total * 100) / 100,
@@ -1 +1 @@
1
- {"version":3,"file":"profiler.mjs","names":[],"sources":["../../../../../../../core/src/benchmark/profiler.ts"],"sourcesContent":["import { NoopChannel } from \"./channels/noop.channel\";\nimport type {\n BenchmarkChannel,\n BenchmarkErrorResult,\n BenchmarkProfilerOptions,\n BenchmarkStats,\n BenchmarkSuccessResult,\n} from \"./types\";\n\ntype BenchmarkEntry = {\n latencies: number[];\n sum: number;\n total: number;\n errors: number;\n firstSeenAt: Date;\n lastSeenAt: Date;\n};\n\nexport class BenchmarkProfiler {\n private readonly maxSamples: number;\n private readonly channels: BenchmarkChannel[];\n private readonly entries = new Map<string, BenchmarkEntry>();\n private interval?: ReturnType<typeof setInterval>;\n\n public constructor(options?: BenchmarkProfilerOptions) {\n this.maxSamples = options?.maxSamples ?? 1000;\n this.channels = options?.channels ?? [new NoopChannel()];\n\n if (options?.flushEvery) {\n this.interval = setInterval(() => {\n // We use void to explicitly ignore the promise, as this is a background interval\n void this.flush();\n }, options.flushEvery);\n }\n }\n\n /**\n * Record one measurement result. Called automatically by measure() when a profiler is set.\n *\n * @param result - The success or error result from measure()\n *\n * @example\n * profiler.record(result);\n */\n public record(result: BenchmarkSuccessResult<unknown> | BenchmarkErrorResult): void {\n let entry = this.entries.get(result.name);\n\n if (!entry) {\n entry = {\n latencies: [],\n sum: 0,\n total: 0,\n errors: 0,\n firstSeenAt: result.startedAt,\n lastSeenAt: result.endedAt,\n };\n\n this.entries.set(result.name, entry);\n }\n\n entry.latencies.push(result.latency);\n entry.sum += result.latency;\n\n if (entry.latencies.length > this.maxSamples) {\n const removed = entry.latencies.shift();\n if (removed !== undefined) {\n entry.sum -= removed;\n }\n }\n\n entry.total += 1;\n\n if (!result.success) {\n entry.errors += 1;\n }\n\n entry.lastSeenAt = result.endedAt;\n }\n\n /**\n * Get aggregated stats for one operation name.\n * Computes p50/p95/p99 by sorting the ring buffer on demand.\n *\n * @param name - The operation name to get stats for.\n * @returns Stats object or undefined if no data yet.\n *\n * @example\n * const stats = profiler.stats(\"db-query\");\n */\n public stats(name: string): BenchmarkStats | undefined {\n const entry = this.entries.get(name);\n if (!entry || entry.latencies.length === 0) return undefined;\n\n const latencies = [...entry.latencies].sort((a, b) => a - b);\n const count = latencies.length;\n\n const getP = (percentile: number) => {\n const index = Math.min(count - 1, Math.floor(count * percentile));\n return latencies[index];\n };\n\n return {\n p50: getP(0.5),\n p90: getP(0.9),\n p95: getP(0.95),\n p99: getP(0.99),\n avg: Math.round((entry.sum / count) * 100) / 100,\n min: latencies[0],\n max: latencies[count - 1],\n count: entry.total,\n errors: entry.errors,\n errorRate: Math.round((entry.errors / entry.total) * 100) / 100,\n firstSeenAt: entry.firstSeenAt,\n lastSeenAt: entry.lastSeenAt,\n };\n }\n\n /**\n * Get stats for all tracked operations.\n *\n * @returns A record mapping operation names to their stats.\n *\n * @example\n * const all = profiler.allStats();\n */\n public allStats(): Record<string, BenchmarkStats> {\n const all: Record<string, BenchmarkStats> = {};\n for (const name of this.entries.keys()) {\n const operationStats = this.stats(name);\n if (operationStats) {\n all[name] = operationStats;\n }\n }\n return all;\n }\n\n /**\n * Send allStats() to all registered channels.\n *\n * @example\n * await profiler.flush();\n */\n public async flush(): Promise<void> {\n const stats = this.allStats();\n if (Object.keys(stats).length === 0) return;\n\n for (const channel of this.channels) {\n await channel.onFlush(stats);\n }\n }\n\n /**\n * Clear ring buffer for one or all operations.\n * Does NOT reset unbounded total/error counters.\n *\n * @param name - Optional operation name. If omitted, clears all ring buffers.\n *\n * @example\n * profiler.reset(\"db-query\");\n * profiler.reset();\n */\n public reset(name?: string): void {\n if (name) {\n const entry = this.entries.get(name);\n if (entry) {\n entry.latencies = [];\n entry.sum = 0;\n }\n } else {\n for (const entry of this.entries.values()) {\n entry.latencies = [];\n entry.sum = 0;\n }\n }\n }\n\n /**\n * Dispose the profiler, clearing its auto-flush interval.\n *\n * @example\n * profiler.dispose();\n */\n public dispose(): void {\n if (this.interval) {\n clearInterval(this.interval);\n this.interval = undefined;\n }\n }\n}\n"],"mappings":";;;AAkBA,IAAa,oBAAb,MAA+B;CAM7B,AAAO,YAAY,SAAoC;iCAH5B,IAAI,IAA4B;EAIzD,KAAK,aAAa,SAAS,cAAc;EACzC,KAAK,WAAW,SAAS,YAAY,CAAC,IAAI,YAAY,CAAC;EAEvD,IAAI,SAAS,YACX,KAAK,WAAW,kBAAkB;GAEhC,AAAK,KAAK,MAAM;EAClB,GAAG,QAAQ,UAAU;CAEzB;;;;;;;;;CAUA,AAAO,OAAO,QAAsE;EAClF,IAAI,QAAQ,KAAK,QAAQ,IAAI,OAAO,IAAI;EAExC,IAAI,CAAC,OAAO;GACV,QAAQ;IACN,WAAW,CAAC;IACZ,KAAK;IACL,OAAO;IACP,QAAQ;IACR,aAAa,OAAO;IACpB,YAAY,OAAO;GACrB;GAEA,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK;EACrC;EAEA,MAAM,UAAU,KAAK,OAAO,OAAO;EACnC,MAAM,OAAO,OAAO;EAEpB,IAAI,MAAM,UAAU,SAAS,KAAK,YAAY;GAC5C,MAAM,UAAU,MAAM,UAAU,MAAM;GACtC,IAAI,YAAY,QACd,MAAM,OAAO;EAEjB;EAEA,MAAM,SAAS;EAEf,IAAI,CAAC,OAAO,SACV,MAAM,UAAU;EAGlB,MAAM,aAAa,OAAO;CAC5B;;;;;;;;;;;CAYA,AAAO,MAAM,MAA0C;EACrD,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI;EACnC,IAAI,CAAC,SAAS,MAAM,UAAU,WAAW,GAAG,OAAO;EAEnD,MAAM,YAAY,CAAC,GAAG,MAAM,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;EAC3D,MAAM,QAAQ,UAAU;EAExB,MAAM,QAAQ,eAAuB;GAEnC,OAAO,UADO,KAAK,IAAI,QAAQ,GAAG,KAAK,MAAM,QAAQ,UAAU,CAC1C;EACvB;EAEA,OAAO;GACL,KAAK,KAAK,EAAG;GACb,KAAK,KAAK,EAAG;GACb,KAAK,KAAK,GAAI;GACd,KAAK,KAAK,GAAI;GACd,KAAK,KAAK,MAAO,MAAM,MAAM,QAAS,GAAG,IAAI;GAC7C,KAAK,UAAU;GACf,KAAK,UAAU,QAAQ;GACvB,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,WAAW,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,GAAG,IAAI;GAC5D,aAAa,MAAM;GACnB,YAAY,MAAM;EACpB;CACF;;;;;;;;;CAUA,AAAO,WAA2C;EAChD,MAAM,MAAsC,CAAC;EAC7C,KAAK,MAAM,QAAQ,KAAK,QAAQ,KAAK,GAAG;GACtC,MAAM,iBAAiB,KAAK,MAAM,IAAI;GACtC,IAAI,gBACF,IAAI,QAAQ;EAEhB;EACA,OAAO;CACT;;;;;;;CAQA,MAAa,QAAuB;EAClC,MAAM,QAAQ,KAAK,SAAS;EAC5B,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,GAAG;EAErC,KAAK,MAAM,WAAW,KAAK,UACzB,MAAM,QAAQ,QAAQ,KAAK;CAE/B;;;;;;;;;;;CAYA,AAAO,MAAM,MAAqB;EAChC,IAAI,MAAM;GACR,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI;GACnC,IAAI,OAAO;IACT,MAAM,YAAY,CAAC;IACnB,MAAM,MAAM;GACd;EACF,OACE,KAAK,MAAM,SAAS,KAAK,QAAQ,OAAO,GAAG;GACzC,MAAM,YAAY,CAAC;GACnB,MAAM,MAAM;EACd;CAEJ;;;;;;;CAQA,AAAO,UAAgB;EACrB,IAAI,KAAK,UAAU;GACjB,cAAc,KAAK,QAAQ;GAC3B,KAAK,WAAW;EAClB;CACF;AACF"}
1
+ {"version":3,"file":"profiler.mjs","names":[],"sources":["../../../../../../../core/src/benchmark/profiler.ts"],"sourcesContent":["import { NoopChannel } from \"./channels/noop.channel\";\nimport type {\n BenchmarkChannel,\n BenchmarkErrorResult,\n BenchmarkProfilerOptions,\n BenchmarkStats,\n BenchmarkSuccessResult,\n} from \"./types\";\n\ntype BenchmarkEntry = {\n latencies: number[];\n sum: number;\n total: number;\n errors: number;\n firstSeenAt: Date;\n lastSeenAt: Date;\n};\n\nexport class BenchmarkProfiler {\n private readonly maxSamples: number;\n private readonly channels: BenchmarkChannel[];\n private readonly entries = new Map<string, BenchmarkEntry>();\n private interval?: ReturnType<typeof setInterval>;\n\n public constructor(options?: BenchmarkProfilerOptions) {\n this.maxSamples = options?.maxSamples ?? 1000;\n this.channels = options?.channels ?? [new NoopChannel()];\n\n if (options?.flushEvery) {\n this.interval = setInterval(() => {\n // We use void to explicitly ignore the promise, as this is a background interval\n void this.flush();\n }, options.flushEvery);\n }\n }\n\n /**\n * Record one measurement result. Called automatically by measure() when a profiler is set.\n *\n * @param result - The success or error result from measure()\n *\n * @example\n * profiler.record(result);\n */\n public record(result: BenchmarkSuccessResult<unknown> | BenchmarkErrorResult): void {\n let entry = this.entries.get(result.name);\n\n if (!entry) {\n entry = {\n latencies: [],\n sum: 0,\n total: 0,\n errors: 0,\n firstSeenAt: result.startedAt,\n lastSeenAt: result.endedAt,\n };\n\n this.entries.set(result.name, entry);\n }\n\n entry.latencies.push(result.latency);\n entry.sum += result.latency;\n\n if (entry.latencies.length > this.maxSamples) {\n const removed = entry.latencies.shift();\n if (removed !== undefined) {\n entry.sum -= removed;\n }\n }\n\n entry.total += 1;\n\n if (!result.success) {\n entry.errors += 1;\n }\n\n entry.lastSeenAt = result.endedAt;\n }\n\n /**\n * Get aggregated stats for one operation name.\n * Computes p50/p95/p99 by sorting the ring buffer on demand.\n *\n * @param name - The operation name to get stats for.\n * @returns Stats object or undefined if no data yet.\n *\n * @example\n * const stats = profiler.stats(\"db-query\");\n */\n public stats(name: string): BenchmarkStats | undefined {\n const entry = this.entries.get(name);\n if (!entry || entry.latencies.length === 0) return undefined;\n\n const latencies = [...entry.latencies].sort((a, b) => a - b);\n const count = latencies.length;\n\n if (count === 0) return undefined;\n\n const getP = (percentile: number) => {\n const index = Math.min(count - 1, Math.floor(count * percentile));\n return latencies[index] ?? 0;\n };\n\n return {\n p50: getP(0.5),\n p90: getP(0.9),\n p95: getP(0.95),\n p99: getP(0.99),\n avg: Math.round((entry.sum / count) * 100) / 100,\n min: latencies[0] ?? 0,\n max: latencies[count - 1] ?? 0,\n count: entry.total,\n errors: entry.errors,\n errorRate: Math.round((entry.errors / entry.total) * 100) / 100,\n firstSeenAt: entry.firstSeenAt,\n lastSeenAt: entry.lastSeenAt,\n };\n }\n\n /**\n * Get stats for all tracked operations.\n *\n * @returns A record mapping operation names to their stats.\n *\n * @example\n * const all = profiler.allStats();\n */\n public allStats(): Record<string, BenchmarkStats> {\n const all: Record<string, BenchmarkStats> = {};\n for (const name of this.entries.keys()) {\n const operationStats = this.stats(name);\n if (operationStats) {\n all[name] = operationStats;\n }\n }\n return all;\n }\n\n /**\n * Send allStats() to all registered channels.\n *\n * @example\n * await profiler.flush();\n */\n public async flush(): Promise<void> {\n const stats = this.allStats();\n if (Object.keys(stats).length === 0) return;\n\n for (const channel of this.channels) {\n await channel.onFlush(stats);\n }\n }\n\n /**\n * Clear ring buffer for one or all operations.\n * Does NOT reset unbounded total/error counters.\n *\n * @param name - Optional operation name. If omitted, clears all ring buffers.\n *\n * @example\n * profiler.reset(\"db-query\");\n * profiler.reset();\n */\n public reset(name?: string): void {\n if (name) {\n const entry = this.entries.get(name);\n if (entry) {\n entry.latencies = [];\n entry.sum = 0;\n }\n } else {\n for (const entry of this.entries.values()) {\n entry.latencies = [];\n entry.sum = 0;\n }\n }\n }\n\n /**\n * Dispose the profiler, clearing its auto-flush interval.\n *\n * @example\n * profiler.dispose();\n */\n public dispose(): void {\n if (this.interval) {\n clearInterval(this.interval);\n this.interval = undefined;\n }\n }\n}\n"],"mappings":";;;AAkBA,IAAa,oBAAb,MAA+B;CAM7B,AAAO,YAAY,SAAoC;iCAH5B,IAAI,IAA4B;EAIzD,KAAK,aAAa,SAAS,cAAc;EACzC,KAAK,WAAW,SAAS,YAAY,CAAC,IAAI,YAAY,CAAC;EAEvD,IAAI,SAAS,YACX,KAAK,WAAW,kBAAkB;GAEhC,AAAK,KAAK,MAAM;EAClB,GAAG,QAAQ,UAAU;CAEzB;;;;;;;;;CAUA,AAAO,OAAO,QAAsE;EAClF,IAAI,QAAQ,KAAK,QAAQ,IAAI,OAAO,IAAI;EAExC,IAAI,CAAC,OAAO;GACV,QAAQ;IACN,WAAW,CAAC;IACZ,KAAK;IACL,OAAO;IACP,QAAQ;IACR,aAAa,OAAO;IACpB,YAAY,OAAO;GACrB;GAEA,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK;EACrC;EAEA,MAAM,UAAU,KAAK,OAAO,OAAO;EACnC,MAAM,OAAO,OAAO;EAEpB,IAAI,MAAM,UAAU,SAAS,KAAK,YAAY;GAC5C,MAAM,UAAU,MAAM,UAAU,MAAM;GACtC,IAAI,YAAY,QACd,MAAM,OAAO;EAEjB;EAEA,MAAM,SAAS;EAEf,IAAI,CAAC,OAAO,SACV,MAAM,UAAU;EAGlB,MAAM,aAAa,OAAO;CAC5B;;;;;;;;;;;CAYA,AAAO,MAAM,MAA0C;EACrD,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI;EACnC,IAAI,CAAC,SAAS,MAAM,UAAU,WAAW,GAAG,OAAO;EAEnD,MAAM,YAAY,CAAC,GAAG,MAAM,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;EAC3D,MAAM,QAAQ,UAAU;EAExB,IAAI,UAAU,GAAG,OAAO;EAExB,MAAM,QAAQ,eAAuB;GAEnC,OAAO,UADO,KAAK,IAAI,QAAQ,GAAG,KAAK,MAAM,QAAQ,UAAU,CAC1C,MAAM;EAC7B;EAEA,OAAO;GACL,KAAK,KAAK,EAAG;GACb,KAAK,KAAK,EAAG;GACb,KAAK,KAAK,GAAI;GACd,KAAK,KAAK,GAAI;GACd,KAAK,KAAK,MAAO,MAAM,MAAM,QAAS,GAAG,IAAI;GAC7C,KAAK,UAAU,MAAM;GACrB,KAAK,UAAU,QAAQ,MAAM;GAC7B,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,WAAW,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,GAAG,IAAI;GAC5D,aAAa,MAAM;GACnB,YAAY,MAAM;EACpB;CACF;;;;;;;;;CAUA,AAAO,WAA2C;EAChD,MAAM,MAAsC,CAAC;EAC7C,KAAK,MAAM,QAAQ,KAAK,QAAQ,KAAK,GAAG;GACtC,MAAM,iBAAiB,KAAK,MAAM,IAAI;GACtC,IAAI,gBACF,IAAI,QAAQ;EAEhB;EACA,OAAO;CACT;;;;;;;CAQA,MAAa,QAAuB;EAClC,MAAM,QAAQ,KAAK,SAAS;EAC5B,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,GAAG;EAErC,KAAK,MAAM,WAAW,KAAK,UACzB,MAAM,QAAQ,QAAQ,KAAK;CAE/B;;;;;;;;;;;CAYA,AAAO,MAAM,MAAqB;EAChC,IAAI,MAAM;GACR,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI;GACnC,IAAI,OAAO;IACT,MAAM,YAAY,CAAC;IACnB,MAAM,MAAM;GACd;EACF,OACE,KAAK,MAAM,SAAS,KAAK,QAAQ,OAAO,GAAG;GACzC,MAAM,YAAY,CAAC;GACnB,MAAM,MAAM;EACd;CAEJ;;;;;;;CAQA,AAAO,UAAgB;EACrB,IAAI,KAAK,UAAU;GACjB,cAAc,KAAK,QAAQ;GAC3B,KAAK,WAAW;EAClB;CACF;AACF"}
@@ -95,7 +95,7 @@ var CLICommandsManager = class {
95
95
  register(...commands) {
96
96
  this.commands.push(...commands);
97
97
  commands.forEach((command) => {
98
- const commandKey = command.name.split(" ")[0];
98
+ const commandKey = command.name.split(" ")[0] ?? command.name;
99
99
  manifestManager.addCommandToList(command.name, {
100
100
  relativePath: command.commandRelativePath,
101
101
  source: command.commandSource || "project",
@@ -126,10 +126,10 @@ var CLICommandsManager = class {
126
126
  for (const name of this.commandsMap.keys()) names.push(name);
127
127
  for (const alias of this.aliasMap.keys()) names.push(alias);
128
128
  const manifestCommands = manifestManager.commandsJson?.commands || {};
129
- for (const name of Object.keys(manifestCommands)) {
130
- const baseName = name.split(" ")[0];
129
+ for (const [name, meta] of Object.entries(manifestCommands)) {
130
+ const baseName = name.split(" ")[0] ?? name;
131
131
  if (!names.includes(baseName)) names.push(baseName);
132
- const alias = manifestCommands[name].alias;
132
+ const alias = meta?.alias;
133
133
  if (alias && !names.includes(alias)) names.push(alias);
134
134
  }
135
135
  return names;
@@ -274,8 +274,8 @@ var CLICommandsManager = class {
274
274
  */
275
275
  async loadProjectCommands(name) {
276
276
  const jsonCommandsFile = await manifestManager.loadCommands();
277
- if (jsonCommandsFile) for (const fullCommandName in jsonCommandsFile.commands) {
278
- const cmdMeta = jsonCommandsFile.commands[fullCommandName];
277
+ if (jsonCommandsFile) for (const [fullCommandName, cmdMeta] of Object.entries(jsonCommandsFile.commands)) {
278
+ if (cmdMeta === void 0) continue;
279
279
  if (isMatchingCommandName(fullCommandName, name) || cmdMeta.alias === name) {
280
280
  const commandPath = cmdMeta.relativePath;
281
281
  if (commandPath) {
@@ -315,7 +315,10 @@ var CLICommandsManager = class {
315
315
  if (opt.defaultValue !== void 0) {
316
316
  if (!(result[opt.name] !== void 0 || opt.alias && result[opt.alias] !== void 0)) result[opt.name] = opt.defaultValue;
317
317
  }
318
- if (opt.alias !== void 0 && result[opt.alias] !== void 0 && result[opt.name] === void 0) result[opt.name] = result[opt.alias];
318
+ if (opt.alias !== void 0 && result[opt.alias] !== void 0 && result[opt.name] === void 0) {
319
+ const aliasValue = result[opt.alias];
320
+ if (aliasValue !== void 0) result[opt.name] = aliasValue;
321
+ }
319
322
  });
320
323
  return result;
321
324
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cli-commands.manager.mjs","names":[],"sources":["../../../../../../../core/src/cli/cli-commands.manager.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { Application } from \"../application\";\r\nimport { bootstrap } from \"../bootstrap\";\r\nimport { loadConfigFiles } from \"../config/load-config-files\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { registerConfiguredConnectors } from \"../connectors/register-configured-connectors\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { isDevWorker } from \"../dev-server/supervisor\";\r\nimport { preflightConfiguredHttpPort } from \"../http/boot-port-preflight\";\r\nimport { manifestManager } from \"../manifest/manifest-manager\";\r\nimport { appPath } from \"../utils\";\r\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { CLICommand } from \"../commands/cli-command\";\r\nimport {\r\n displayBootError,\r\n displayCommandError,\r\n displayCommandHelp,\r\n displayCommandNotFound,\r\n displayCommandSuccess,\r\n displayExecutingCommand,\r\n displayHelp,\r\n displayMissingCommand,\r\n displayMissingOptions,\r\n displayWarlockVersionInTerminal,\r\n type HelpCommandInfo,\r\n isMatchingCommandName,\r\n} from \"./cli-commands.utils\";\r\nimport { cliCommandsLoader } from \"./commands-loader\";\r\nimport { frameworkCommands } from \"./framework-cli-commands\";\r\nimport { CliOptionValueError, type ParsedCliArgs, parseCliArgs } from \"./parse-cli-args\";\r\nimport { findSimilar } from \"./string-similarity\";\r\nimport type { CommandActionData, ResolvedCLICommandOption } from \"../commands/types\";\r\n\r\n/**\r\n * Best-effort budget (ms) for draining stdout/stderr before a forced exit.\r\n * Bounded so a stuck stream can never hang the CLI.\r\n */\r\nconst EXIT_FLUSH_BUDGET = 500;\r\n\r\n/**\r\n * Exit with `code` once stdout/stderr have drained.\r\n *\r\n * `process.exit()` truncates writes still queued on a non-TTY stdout — a pipe,\r\n * a CI log — which is how the *reason* for a failure gets lost even when the\r\n * exit code is right. `exitCode` is set up front so that a natural exit (should\r\n * the drain outlive the process's remaining work) is non-zero too.\r\n */\r\nfunction exitAfterFlush(code: number) {\r\n process.exitCode = code;\r\n\r\n const timer = setTimeout(() => process.exit(code), EXIT_FLUSH_BUDGET);\r\n timer.unref?.();\r\n\r\n let pending = 2;\r\n const drained = () => {\r\n if (--pending > 0) return;\r\n\r\n clearTimeout(timer);\r\n process.exit(code);\r\n };\r\n\r\n // A zero-length write's callback fires once everything queued ahead of it has\r\n // been handed to the OS.\r\n process.stdout.write(\"\", drained);\r\n process.stderr.write(\"\", drained);\r\n}\r\n\r\n/**\r\n * How a command run ended, from the process's point of view.\r\n *\r\n * - `succeeded` — finished, nothing rejected afterwards; the CLI may exit 0.\r\n * - `failed` — reported an error; the exit code is owned by whoever reported it.\r\n * - `running` — a persistent command (dev/start) now owns the process.\r\n *\r\n * `execute` returns this instead of exiting on success itself. A method that\r\n * terminates the process out from under its caller cannot be awaited honestly:\r\n * the exit it schedules outlives the call, so the success it represents is\r\n * still unsettled when `execute` resolves — which is exactly how a green check\r\n * ends up printed next to the failure that contradicts it.\r\n */\r\nexport type CommandRunVerdict = \"succeeded\" | \"failed\" | \"running\";\r\n\r\n/**\r\n * Whether this process is the CHILD half of a supervised command.\r\n *\r\n * ## The finding this encodes\r\n *\r\n * `warlock dev` printing `› Running dev...` twice was NOT a double boot, and\r\n * the distinction is the whole point of this function existing rather than the\r\n * line simply being deleted. The sequence, in file order:\r\n *\r\n * 1. `execute()` prints the header (below) — BEFORE `commandPreAction`.\r\n * 2. `cli/commands/dev-server.command.ts` `preAction` calls\r\n * `superviseDevServer()` when `!isDevWorker()`.\r\n * 3. `dev-server/supervisor.ts` `spawnWorker()` re-spawns `process.argv` with\r\n * `WARLOCK_DEV_WORKER=1`, so the child runs `warlock dev` again and prints\r\n * the header too.\r\n * 4. `superviseDevServer()` returns a promise that never settles, so the\r\n * supervisor's `await commandPreAction(...)` never resolves — `loadPreloaders`\r\n * and `command.execute` are unreachable in the supervisor.\r\n *\r\n * Step 4 is why only ONE process ever loads config, connectors, app modules or\r\n * a scheduler. Verified by running it: one `APP-CODE-LOADED`, one\r\n * `Application.markBooted`, one `⚡ Warlock.js` banner, one route registration,\r\n * two processes. The `2 job(s) registered` in the original report is two jobs\r\n * in one scheduler, not two schedulers — a genuine double boot would have\r\n * printed that warning twice.\r\n *\r\n * So the defect is exactly one duplicated line, and this is the guard that\r\n * keeps it one. `tests/unit/cli/dev-command-single-boot.test.ts` fails if the\r\n * supervisor ever starts running preloaders — i.e. if the double boot the\r\n * duplicated line looked like ever becomes real.\r\n */\r\nfunction isRespawnedWorker(): boolean {\r\n return isDevWorker();\r\n}\r\n\r\nexport class CLICommandsManager {\r\n /**\r\n * List of commands\r\n */\r\n public commands: CLICommand[] = [];\r\n\r\n /**\r\n * Commands map (name -> command)\r\n */\r\n public commandsMap: Map<string, CLICommand> = new Map();\r\n\r\n /**\r\n * Alias map (alias -> command name)\r\n */\r\n public aliasMap: Map<string, string> = new Map();\r\n\r\n /**\r\n * Register the given commands\r\n */\r\n public register(...commands: CLICommand[]): this {\r\n this.commands.push(...commands);\r\n\r\n commands.forEach((command) => {\r\n const commandKey = command.name.split(\" \")[0];\r\n\r\n manifestManager.addCommandToList(command.name, {\r\n relativePath: command.commandRelativePath,\r\n source: command.commandSource || \"project\",\r\n description: command.commandDescription,\r\n alias: command.commandAlias,\r\n options: command.commandOptions.length > 0 ? command.commandOptions : undefined,\r\n });\r\n\r\n this.commandsMap.set(commandKey, command);\r\n\r\n // Register alias if exists\r\n if (command.commandAlias) {\r\n this.aliasMap.set(command.commandAlias, commandKey);\r\n }\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get command by name or alias\r\n */\r\n public getCommand(name: string): CLICommand | undefined {\r\n // First try direct lookup\r\n let command = this.commandsMap.get(name);\r\n if (command) return command;\r\n\r\n // Try alias lookup\r\n const realName = this.aliasMap.get(name);\r\n if (realName) {\r\n return this.commandsMap.get(realName);\r\n }\r\n\r\n return;\r\n }\r\n\r\n /**\r\n * Get all command names and aliases\r\n * Used for fuzzy matching suggestions\r\n */\r\n public getAllCommandNames(): string[] {\r\n const names: string[] = [];\r\n\r\n // Add all command names\r\n for (const name of this.commandsMap.keys()) {\r\n names.push(name);\r\n }\r\n\r\n // Add all aliases\r\n for (const alias of this.aliasMap.keys()) {\r\n names.push(alias);\r\n }\r\n\r\n // Also include cached commands from manifest\r\n const manifestCommands = manifestManager.commandsJson?.commands || {};\r\n for (const name of Object.keys(manifestCommands)) {\r\n const baseName = name.split(\" \")[0];\r\n if (!names.includes(baseName)) {\r\n names.push(baseName);\r\n }\r\n const alias = manifestCommands[name].alias;\r\n if (alias && !names.includes(alias)) {\r\n names.push(alias);\r\n }\r\n }\r\n\r\n return names;\r\n }\r\n\r\n /**\r\n * Start the cli manager\r\n */\r\n public async start() {\r\n // Schema-less first pass: it exists to discover the command name and the\r\n // command-independent flags (--help, --version, --no-cache). Values here are\r\n // raw strings; the typed pass below is the one whose options reach a command.\r\n const { name, options } = parseCliArgs(process.argv);\r\n\r\n if (options.noCache) {\r\n manifestManager.clearCommandsCache();\r\n // remove the commands.json file as wel\r\n await manifestManager.removeCommandsFile();\r\n }\r\n\r\n if (options.version || options.v) {\r\n await displayWarlockVersionInTerminal();\r\n process.exit(0);\r\n }\r\n\r\n // Try to load from manifest first (fastest path)\r\n await manifestManager.loadCommands();\r\n\r\n // Register framework commands first (needed for help)\r\n this.register(\r\n ...frameworkCommands.map((command) => {\r\n if (!command.commandSource) {\r\n command.commandSource = \"framework\";\r\n }\r\n return command;\r\n }),\r\n );\r\n\r\n const isHelpCommand = options.help || options.h;\r\n\r\n // Handle global help (no command)\r\n if (!name && isHelpCommand) {\r\n await this.showGlobalHelp();\r\n process.exit(0);\r\n }\r\n\r\n // Handle warm cache\r\n if (options.warmCache) {\r\n await this.warmCache();\r\n process.exit(0);\r\n }\r\n\r\n if (!name) {\r\n displayMissingCommand();\r\n process.exit(1);\r\n }\r\n\r\n const command = await this.lazyGetCommand(name);\r\n\r\n if (manifestManager.hasChanges) {\r\n await manifestManager.saveCommands();\r\n }\r\n\r\n if (!command) {\r\n // Find similar commands for suggestions\r\n const allCommandNames = this.getAllCommandNames();\r\n const suggestions = findSimilar(name, allCommandNames).map((s) => s.value);\r\n\r\n displayCommandNotFound(name, suggestions);\r\n process.exit(1);\r\n }\r\n\r\n // Handle command-specific help\r\n if (isHelpCommand) {\r\n displayCommandHelp({\r\n name: command.name,\r\n alias: command.commandAlias,\r\n description: command.commandDescription,\r\n options: command.commandOptions,\r\n });\r\n process.exit(0);\r\n }\r\n\r\n // The first parse above had no command to consult, so every value in it is\r\n // a raw string. Now that the command is resolved, re-read argv against its\r\n // declared options so a `type: \"boolean\"` option arrives as a boolean.\r\n const resolved = this.resolveCommandArgs(command);\r\n\r\n const verdict = await this.execute(command, {\r\n options: resolved.options,\r\n args: resolved.args,\r\n });\r\n\r\n // The process-level exit belongs to the entrypoint, not to `execute`.\r\n // A one-shot command can leave a handle open (a db connection, a watcher a\r\n // plugin forgot to close), so the CLI exits explicitly rather than waiting\r\n // for the loop to drain — but only on a verdict of `succeeded`. `failed`\r\n // already exited non-zero through whoever reported it, and `running` means\r\n // a persistent command owns the process now.\r\n if (verdict === \"succeeded\") {\r\n exitAfterFlush(0);\r\n }\r\n }\r\n\r\n /**\r\n * Re-parse argv against the resolved command's declared options.\r\n *\r\n * parseCliArgs runs twice on purpose. The first run produces the command\r\n * name, and there is no schema to consult yet; the second one, here, can be\r\n * type-aware. Re-reading argv (rather than post-processing the first result)\r\n * is what makes `--rollback users.ts` recoverable: by the time the first pass\r\n * returns, the swallowed positional is indistinguishable from a value.\r\n */\r\n protected resolveCommandArgs(command: CLICommand, argv: string[] = process.argv): ParsedCliArgs {\r\n try {\r\n return parseCliArgs(argv, command.commandOptions);\r\n } catch (error) {\r\n if (error instanceof CliOptionValueError) {\r\n console.log();\r\n console.log(` ${colors.redBright(\"Error:\")} ${error.message}`);\r\n console.log();\r\n process.exit(1);\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Show global help with all commands\r\n * Uses manifest if available for fast display\r\n */\r\n protected async showGlobalHelp() {\r\n if (\r\n manifestManager.isCommandLoaded &&\r\n Object.keys(manifestManager.commandsJson?.commands || {}).length > 0\r\n ) {\r\n // Use manifest directly - no need to load command files\r\n const helpCommands: HelpCommandInfo[] = Object.entries(\r\n manifestManager.commandsJson?.commands || {},\r\n ).map(([name, cmd]) => ({\r\n name,\r\n alias: cmd.alias,\r\n description: cmd.description,\r\n source: cmd.source,\r\n }));\r\n\r\n await displayHelp(helpCommands);\r\n return;\r\n }\r\n\r\n // Fallback: No manifest, build from registered commands\r\n // This happens on first run before warm-cache\r\n await this.loadPluginsCommands();\r\n\r\n const projectCommands = await cliCommandsLoader.scanAll();\r\n\r\n this.register(...projectCommands);\r\n\r\n await manifestManager.saveCommands();\r\n\r\n // Build help info from registered commands\r\n const helpCommands: HelpCommandInfo[] = this.commands.map((cmd) => ({\r\n name: cmd.name,\r\n alias: cmd.commandAlias,\r\n description: cmd.commandDescription,\r\n source: cmd.commandSource || \"project\",\r\n }));\r\n\r\n await displayHelp(helpCommands);\r\n }\r\n\r\n /**\r\n * Warm cache - scan all project commands and save to manifest\r\n */\r\n protected async warmCache() {\r\n console.log();\r\n console.log(` ${colors.cyan(\"›\")} Scanning project commands...`);\r\n\r\n const projectCommands = await cliCommandsLoader.scanAll();\r\n\r\n this.register(...projectCommands);\r\n\r\n await manifestManager.saveCommands();\r\n\r\n console.log(\r\n ` ${colors.green(\"✔\")} Cached ${colors.bold(String(projectCommands.length))} project commands`,\r\n );\r\n console.log();\r\n }\r\n\r\n /**\r\n * Load plugins commands\r\n */\r\n protected async loadPluginsCommands() {\r\n await warlockConfigManager.load();\r\n\r\n if (warlockConfigManager.isLoaded) {\r\n this.register(\r\n ...(warlockConfigManager.get(\"cli\")?.commands || []).map((command) => {\r\n if (!command.commandSource) {\r\n command.commandSource = \"plugin\";\r\n }\r\n return command;\r\n }),\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Try to find the command based on the given command name or alias\r\n */\r\n protected async lazyGetCommand(name: string) {\r\n // first step, try to find it directly through current commands\r\n let command = this.getCommand(name);\r\n if (command) {\r\n return command;\r\n }\r\n\r\n // second step, try to find it through warlock config commands\r\n await this.loadPluginsCommands();\r\n\r\n command = this.getCommand(name);\r\n\r\n if (command) {\r\n return command;\r\n }\r\n\r\n // third step, try to find it through project commands\r\n await this.loadProjectCommands(name);\r\n\r\n command = this.getCommand(name);\r\n if (command) {\r\n return command;\r\n }\r\n\r\n return null;\r\n }\r\n\r\n /**\r\n * Load project commands\r\n */\r\n protected async loadProjectCommands(name: string) {\r\n // first get the commands.json contents as an object\r\n const jsonCommandsFile = await manifestManager.loadCommands();\r\n\r\n if (jsonCommandsFile) {\r\n // Check by name or alias\r\n for (const fullCommandName in jsonCommandsFile.commands) {\r\n const cmdMeta = jsonCommandsFile.commands[fullCommandName];\r\n\r\n // Match by name or alias\r\n if (isMatchingCommandName(fullCommandName, name) || cmdMeta.alias === name) {\r\n const commandPath = cmdMeta.relativePath;\r\n if (commandPath) {\r\n const executedCommand = await cliCommandsLoader.load(commandPath);\r\n\r\n if (executedCommand) {\r\n executedCommand.$relativePath(commandPath);\r\n this.register(executedCommand);\r\n return;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n const command = await cliCommandsLoader.locate(name);\r\n\r\n if (command) {\r\n this.register(command);\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * Validate required options\r\n */\r\n protected validateOptions(\r\n command: CLICommand,\r\n options: Record<string, string | boolean | number>,\r\n ): ResolvedCLICommandOption[] {\r\n const missing: ResolvedCLICommandOption[] = [];\r\n\r\n command.commandOptions.forEach((opt) => {\r\n if (opt.required) {\r\n // Check if option is provided by name or alias\r\n const hasOption =\r\n options[opt.name] !== undefined || (opt.alias && options[opt.alias] !== undefined);\r\n if (!hasOption) {\r\n missing.push(opt);\r\n }\r\n }\r\n });\r\n\r\n return missing;\r\n }\r\n\r\n /**\r\n * Apply default values to options\r\n */\r\n protected applyDefaultOptions(\r\n command: CLICommand,\r\n options: Record<string, string | boolean | number>,\r\n ): Record<string, string | boolean | number> {\r\n const result = { ...options };\r\n\r\n command.commandOptions.forEach((opt) => {\r\n if (opt.defaultValue !== undefined) {\r\n const hasOption =\r\n result[opt.name] !== undefined || (opt.alias && result[opt.alias] !== undefined);\r\n if (!hasOption) {\r\n result[opt.name] = opt.defaultValue;\r\n }\r\n }\r\n\r\n // `!== undefined`, not truthiness: `-r=false` now resolves to the boolean\r\n // `false`, and a truthiness test would drop it before it ever reached the\r\n // canonical name — leaving `rollback` undefined instead of explicitly off.\r\n if (\r\n opt.alias !== undefined &&\r\n result[opt.alias] !== undefined &&\r\n result[opt.name] === undefined\r\n ) {\r\n result[opt.name] = result[opt.alias];\r\n }\r\n });\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Execute the given command and report how it ended.\r\n *\r\n * Returning the verdict — rather than exiting on success from in here — is\r\n * what makes the awaited result trustworthy: see {@link CommandRunVerdict}.\r\n */\r\n public async execute(command: CLICommand, data: CommandActionData): Promise<CommandRunVerdict> {\r\n const startTime = Date.now();\r\n\r\n // Validate required options\r\n const missingOptions = this.validateOptions(command, data.options);\r\n if (missingOptions.length > 0) {\r\n displayMissingOptions(missingOptions);\r\n process.exit(1);\r\n }\r\n\r\n // Apply default values\r\n data.options = this.applyDefaultOptions(command, data.options);\r\n\r\n // ONE header per `warlock <command>` the user typed — not one per process.\r\n //\r\n // `warlock dev` is TWO processes by design: the invocation the user typed\r\n // becomes a thin supervisor (`dev-server/supervisor.ts`) that re-spawns\r\n // `process.argv` with `WARLOCK_DEV_WORKER=1`, and the child is the actual\r\n // server. Both are `warlock dev`, so both reached this line and the user saw\r\n //\r\n // › Running dev...\r\n // › Running dev...\r\n //\r\n // which reads exactly like the application booting twice. It is not — see\r\n // the block comment on `isRespawnedWorker` — but the line was still wrong,\r\n // and it re-printed on every crash-restart on top of the supervisor's own\r\n // \"restarting\" notice.\r\n //\r\n // The SUPERVISOR keeps the header, not the worker: it prints immediately\r\n // (so a slow boot is not silent), it prints once for the whole session\r\n // (restarts are already announced as restarts), and it is the process the\r\n // user actually started.\r\n if (!isRespawnedWorker()) {\r\n displayExecutingCommand(command.name);\r\n }\r\n\r\n if (command.commandPreAction) {\r\n await command.commandPreAction(data);\r\n }\r\n\r\n // load preloaders\r\n if (command.commandPreload) {\r\n try {\r\n await this.loadPreloaders(command);\r\n } catch (error) {\r\n // A preload failure (a bad import in a config file, a connector that\r\n // throws on boot, a missing module export) happens BEFORE the command's\r\n // action runs — there is no run loop to recover into, so it is ALWAYS\r\n // fatal, persistent command (dev/start) or not. Surface the real cause\r\n // and the offending file, then exit. This MUST stay outside the\r\n // command-execute try/catch below: letting a preload rejection escape\r\n // turned `warlock dev` into a silent hang — the unhandled rejection slid\r\n // past while the already-started loader worker thread kept the process\r\n // alive, freezing just after the banner with no error printed.\r\n displayBootError(command.name, error as Error);\r\n process.exit(1);\r\n }\r\n }\r\n\r\n this.captureFatalAsyncErrors(command);\r\n\r\n try {\r\n await command.execute(data);\r\n\r\n // Persistent commands (dev/start) hand control to their run loop here.\r\n // There is no success banner to print and nothing to exit for.\r\n if (command.isPersistent) return \"running\";\r\n\r\n // Yield one full event-loop turn before declaring success, and AWAIT that\r\n // yield. Node flags an unhandled rejection only after the current turn's\r\n // microtasks drain, so reporting success synchronously here fired FIRST\r\n // and erased the failure — a `build` whose async work rejected exited 0\r\n // having emitted nothing.\r\n //\r\n // The await is the load-bearing part, not the yield. Scheduling this as a\r\n // bare `setImmediate` let `execute()` RESOLVE while the verdict was still\r\n // pending: the callback outlived the call it belonged to, so the green\r\n // check could surface after the caller had already moved on — in a later\r\n // command, or after a rejection reported in the interim, which the guard\r\n // below cannot see because by then it reads the state of a run that has\r\n // already ended. Awaiting binds the banner to THIS invocation: `execute`\r\n // does not resolve until the verdict it is about to report has settled.\r\n //\r\n // `setImmediate` runs in the same loop iteration's check phase, so this\r\n // cannot hang on a lingering handle the way dropping the exit would; it\r\n // only lets `captureFatalAsyncErrors` win the race when there is one.\r\n await new Promise<void>((resolve) => setImmediate(resolve));\r\n\r\n // The yield is what gives a floating rejection the chance to be reported;\r\n // this is what acts on it. A green check printed after that error would\r\n // contradict it, and the exit that follows would reset `process.exitCode`\r\n // to 0 — re-erasing the very failure this whole path exists to surface.\r\n if (this.fatalAsyncErrorReported) return \"failed\";\r\n\r\n displayCommandSuccess(command.name, Date.now() - startTime);\r\n\r\n return \"succeeded\";\r\n } catch (error) {\r\n displayCommandError(command.name, error as Error);\r\n // Persistent commands (dev/start) own their startup-failure exits;\r\n // by the time an error reaches here they've already entered their\r\n // run loop, so it's a runtime error we log but don't crash on —\r\n // HMR or process supervisors can recover. Non-persistent\r\n // one-shot commands always exit on failure.\r\n if (!command.isPersistent) {\r\n process.exit(1);\r\n }\r\n\r\n return \"failed\";\r\n }\r\n }\r\n\r\n /**\r\n * Fail loudly on async errors that escape the command's own promise.\r\n *\r\n * `execute` awaits `command.execute(data)`, but work the command merely\r\n * *started* settles outside that await — `@mongez/events` `trigger` /\r\n * `triggerAll` are synchronous and do not await their listeners, so an async\r\n * listener that throws leaves a floating rejection behind. Node reports it at\r\n * the end of the turn, by which point the CLI had already exited 0: the\r\n * failure was erased and the command looked green.\r\n *\r\n * Persistent commands (dev/start) are exempt from the exit for the same\r\n * reason as the catch block in `execute`: by the time they reach this point\r\n * they own a run loop that is expected to survive runtime errors so HMR or a\r\n * supervisor can recover. They still print — silence is what made this class\r\n * of failure invisible in the first place.\r\n */\r\n protected fatalAsyncErrorReported = false;\r\n\r\n protected captureFatalAsyncErrors(command: CLICommand) {\r\n const onFatalAsyncError = (reason: unknown) => {\r\n // A rejection reason is not necessarily an Error (`Promise.reject(\"x\")`),\r\n // and a crash handler must never itself throw.\r\n const error = reason instanceof Error ? reason : new Error(String(reason));\r\n\r\n // Set before printing, so the deferred success banner is suppressed even\r\n // if displaying the error yields.\r\n this.fatalAsyncErrorReported = true;\r\n\r\n displayCommandError(command.name, error);\r\n\r\n if (!command.isPersistent) {\r\n exitAfterFlush(1);\r\n }\r\n };\r\n\r\n process.on(\"unhandledRejection\", onFatalAsyncError);\r\n process.on(\"uncaughtException\", onFatalAsyncError);\r\n }\r\n\r\n /**\r\n * Load preloaders\r\n */\r\n protected async loadPreloaders(command: CLICommand) {\r\n const preloaders = command.commandPreload || {};\r\n\r\n if (preloaders.runtimeStrategy) {\r\n Application.setRuntimeStrategy(preloaders.runtimeStrategy);\r\n }\r\n\r\n // Override, not a default: a command that declares an environment gets it\r\n // regardless of what the shell exported. Then read it back — `process.env`\r\n // is process-global and anything may have written to it, so a value that\r\n // did not take must fail here, loudly, rather than surface later as a\r\n // production React bundle under `warlock dev`.\r\n if (preloaders.environment) {\r\n Application.setEnvironment(preloaders.environment);\r\n\r\n if (process.env.NODE_ENV !== preloaders.environment) {\r\n throw new Error(\r\n `Command \"${command.name}\" set NODE_ENV to \"${preloaders.environment}\", but it read back as ` +\r\n `\"${process.env.NODE_ENV}\". The environment did not take, and every decision made ` +\r\n `downstream of it — env file selection, bundler dep optimisation — would be wrong.`,\r\n );\r\n }\r\n }\r\n\r\n // Env BEFORE config, unconditionally, for every command.\r\n //\r\n // `warlock.config.ts` is a module whose body runs on import, and projects\r\n // legitimately call `env(\"BUILD_OUT\", \"dist\")` in it. Loading config first\r\n // evaluated that body against an empty env store, so every such call\r\n // silently returned its default — under `build` and `start`, which never\r\n // loaded env at all, and under `dev` too, because config was loaded first\r\n // regardless. The ordering is load-bearing in the other direction as well:\r\n // `loadEnv()` reads `process.env.NODE_ENV` at call time to choose\r\n // `.env.<NODE_ENV>` over `.env`, so it must follow `setEnvironment` above.\r\n await loadEnvironmentFiles();\r\n\r\n await warlockConfigManager.load();\r\n\r\n if (preloaders.config || preloaders.bootstrap || preloaders.prestart) {\r\n await filesOrchestrator.init();\r\n }\r\n\r\n // `preloaders.env` is gone: env is loaded above for every command, so the\r\n // branch that used to serve it could only re-parse the same files and\r\n // re-override `process.env` a second time.\r\n if (preloaders.bootstrap) {\r\n await bootstrap();\r\n\r\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\r\n await filesOrchestrator.load(\"src/app/bootstrap.ts\");\r\n }\r\n }\r\n\r\n // Load configuration files\r\n if (preloaders.config) {\r\n await loadConfigFiles(preloaders.config);\r\n }\r\n\r\n if (preloaders.prestart) {\r\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\r\n await filesOrchestrator.load(\"src/app/prestart.ts\");\r\n }\r\n }\r\n\r\n // Initialize connectors.\r\n // `connectors: true` only starts the early phase here — late-phase\r\n // connectors (http, socket) start after the command's action loads\r\n // app code. Explicit lists bypass phase splitting (caller knows\r\n // exactly which connectors they want).\r\n if (preloaders.connectors) {\r\n // REGISTER before starting — the non-bundled counterpart of section 2.5\r\n // of the generated production entry, and the \"dev preloader\" path\r\n // `register-configured-connectors.ts` documents.\r\n //\r\n // Without this, `warlock.config.ts > connectors` drove the BUILD only:\r\n // `warlock build` read the array to drain each connector's contribution,\r\n // but under `warlock dev` nothing ever registered it, so an app had to\r\n // call `connectorsManager.register(...)` from its own `main.ts` to get a\r\n // connector running. Apps that did BOTH then registered the same\r\n // connector twice in production — once here from the baked config, once\r\n // from their own app code — and it booted twice, which surfaced as\r\n // `Route name \"...\" is already taken` while installing page routes.\r\n //\r\n // The array is omitted deliberately, so the function reads the config\r\n // loaded above; `expectedNames` is omitted too, because drift is a\r\n // build↔runtime relationship and dev has no build to drift from.\r\n //\r\n // The idempotency this function provides is against ITSELF — a second\r\n // call skips names the first registered. It does NOT protect against an\r\n // app calling `connectorsManager.register()` directly, which pushes\r\n // unconditionally, and which runs AFTER this point in both modes. So a\r\n // connector belongs in the config array or in app code, never both:\r\n // listing it in both is the duplicate this comment's route-name error\r\n // describes.\r\n registerConfiguredConnectors();\r\n\r\n // Fail fast on a busy port BEFORE the early-phase connectors (database,\r\n // cache, ...) connect. `http` itself is a LATE-phase connector, so its\r\n // own preflight is not reached until after those early connectors are\r\n // up and every app module has been imported — 7-13s of work discarded\r\n // before anyone learns the port was taken. This is the dev-preload\r\n // counterpart of the check the generated production entry runs at the\r\n // same point in its own boot sequence (`production-builder.ts`, step\r\n // 2.6) — a cheap bind-and-release probe, not a connector reorder.\r\n await preflightConfiguredHttpPort();\r\n\r\n if (preloaders.connectors === true) {\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\r\n } else {\r\n await connectorsManager.start(preloaders.connectors);\r\n }\r\n }\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-commands.manager.mjs","names":[],"sources":["../../../../../../../core/src/cli/cli-commands.manager.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { Application } from \"../application\";\r\nimport { bootstrap } from \"../bootstrap\";\r\nimport { loadConfigFiles } from \"../config/load-config-files\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { registerConfiguredConnectors } from \"../connectors/register-configured-connectors\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { isDevWorker } from \"../dev-server/supervisor\";\r\nimport { preflightConfiguredHttpPort } from \"../http/boot-port-preflight\";\r\nimport { manifestManager } from \"../manifest/manifest-manager\";\r\nimport { appPath } from \"../utils\";\r\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { CLICommand } from \"../commands/cli-command\";\r\nimport {\r\n displayBootError,\r\n displayCommandError,\r\n displayCommandHelp,\r\n displayCommandNotFound,\r\n displayCommandSuccess,\r\n displayExecutingCommand,\r\n displayHelp,\r\n displayMissingCommand,\r\n displayMissingOptions,\r\n displayWarlockVersionInTerminal,\r\n type HelpCommandInfo,\r\n isMatchingCommandName,\r\n} from \"./cli-commands.utils\";\r\nimport { cliCommandsLoader } from \"./commands-loader\";\r\nimport { frameworkCommands } from \"./framework-cli-commands\";\r\nimport { CliOptionValueError, type ParsedCliArgs, parseCliArgs } from \"./parse-cli-args\";\r\nimport { findSimilar } from \"./string-similarity\";\r\nimport type { CommandActionData, ResolvedCLICommandOption } from \"../commands/types\";\r\n\r\n/**\r\n * Best-effort budget (ms) for draining stdout/stderr before a forced exit.\r\n * Bounded so a stuck stream can never hang the CLI.\r\n */\r\nconst EXIT_FLUSH_BUDGET = 500;\r\n\r\n/**\r\n * Exit with `code` once stdout/stderr have drained.\r\n *\r\n * `process.exit()` truncates writes still queued on a non-TTY stdout — a pipe,\r\n * a CI log — which is how the *reason* for a failure gets lost even when the\r\n * exit code is right. `exitCode` is set up front so that a natural exit (should\r\n * the drain outlive the process's remaining work) is non-zero too.\r\n */\r\nfunction exitAfterFlush(code: number) {\r\n process.exitCode = code;\r\n\r\n const timer = setTimeout(() => process.exit(code), EXIT_FLUSH_BUDGET);\r\n timer.unref?.();\r\n\r\n let pending = 2;\r\n const drained = () => {\r\n if (--pending > 0) return;\r\n\r\n clearTimeout(timer);\r\n process.exit(code);\r\n };\r\n\r\n // A zero-length write's callback fires once everything queued ahead of it has\r\n // been handed to the OS.\r\n process.stdout.write(\"\", drained);\r\n process.stderr.write(\"\", drained);\r\n}\r\n\r\n/**\r\n * How a command run ended, from the process's point of view.\r\n *\r\n * - `succeeded` — finished, nothing rejected afterwards; the CLI may exit 0.\r\n * - `failed` — reported an error; the exit code is owned by whoever reported it.\r\n * - `running` — a persistent command (dev/start) now owns the process.\r\n *\r\n * `execute` returns this instead of exiting on success itself. A method that\r\n * terminates the process out from under its caller cannot be awaited honestly:\r\n * the exit it schedules outlives the call, so the success it represents is\r\n * still unsettled when `execute` resolves — which is exactly how a green check\r\n * ends up printed next to the failure that contradicts it.\r\n */\r\nexport type CommandRunVerdict = \"succeeded\" | \"failed\" | \"running\";\r\n\r\n/**\r\n * Whether this process is the CHILD half of a supervised command.\r\n *\r\n * ## The finding this encodes\r\n *\r\n * `warlock dev` printing `› Running dev...` twice was NOT a double boot, and\r\n * the distinction is the whole point of this function existing rather than the\r\n * line simply being deleted. The sequence, in file order:\r\n *\r\n * 1. `execute()` prints the header (below) — BEFORE `commandPreAction`.\r\n * 2. `cli/commands/dev-server.command.ts` `preAction` calls\r\n * `superviseDevServer()` when `!isDevWorker()`.\r\n * 3. `dev-server/supervisor.ts` `spawnWorker()` re-spawns `process.argv` with\r\n * `WARLOCK_DEV_WORKER=1`, so the child runs `warlock dev` again and prints\r\n * the header too.\r\n * 4. `superviseDevServer()` returns a promise that never settles, so the\r\n * supervisor's `await commandPreAction(...)` never resolves — `loadPreloaders`\r\n * and `command.execute` are unreachable in the supervisor.\r\n *\r\n * Step 4 is why only ONE process ever loads config, connectors, app modules or\r\n * a scheduler. Verified by running it: one `APP-CODE-LOADED`, one\r\n * `Application.markBooted`, one `⚡ Warlock.js` banner, one route registration,\r\n * two processes. The `2 job(s) registered` in the original report is two jobs\r\n * in one scheduler, not two schedulers — a genuine double boot would have\r\n * printed that warning twice.\r\n *\r\n * So the defect is exactly one duplicated line, and this is the guard that\r\n * keeps it one. `tests/unit/cli/dev-command-single-boot.test.ts` fails if the\r\n * supervisor ever starts running preloaders — i.e. if the double boot the\r\n * duplicated line looked like ever becomes real.\r\n */\r\nfunction isRespawnedWorker(): boolean {\r\n return isDevWorker();\r\n}\r\n\r\nexport class CLICommandsManager {\r\n /**\r\n * List of commands\r\n */\r\n public commands: CLICommand[] = [];\r\n\r\n /**\r\n * Commands map (name -> command)\r\n */\r\n public commandsMap: Map<string, CLICommand> = new Map();\r\n\r\n /**\r\n * Alias map (alias -> command name)\r\n */\r\n public aliasMap: Map<string, string> = new Map();\r\n\r\n /**\r\n * Register the given commands\r\n */\r\n public register(...commands: CLICommand[]): this {\r\n this.commands.push(...commands);\r\n\r\n commands.forEach((command) => {\r\n // `split` always yields a first element, so this holds. Falling back to\r\n // the whole name rather than asserting keeps the map keyed by a REAL\r\n // command name — a key of `undefined` would register the command under\r\n // the literal string \"undefined\" and make it unreachable by its own name.\r\n const commandKey = command.name.split(\" \")[0] ?? command.name;\r\n\r\n manifestManager.addCommandToList(command.name, {\r\n relativePath: command.commandRelativePath,\r\n source: command.commandSource || \"project\",\r\n description: command.commandDescription,\r\n alias: command.commandAlias,\r\n options: command.commandOptions.length > 0 ? command.commandOptions : undefined,\r\n });\r\n\r\n this.commandsMap.set(commandKey, command);\r\n\r\n // Register alias if exists\r\n if (command.commandAlias) {\r\n this.aliasMap.set(command.commandAlias, commandKey);\r\n }\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get command by name or alias\r\n */\r\n public getCommand(name: string): CLICommand | undefined {\r\n // First try direct lookup\r\n let command = this.commandsMap.get(name);\r\n if (command) return command;\r\n\r\n // Try alias lookup\r\n const realName = this.aliasMap.get(name);\r\n if (realName) {\r\n return this.commandsMap.get(realName);\r\n }\r\n\r\n return;\r\n }\r\n\r\n /**\r\n * Get all command names and aliases\r\n * Used for fuzzy matching suggestions\r\n */\r\n public getAllCommandNames(): string[] {\r\n const names: string[] = [];\r\n\r\n // Add all command names\r\n for (const name of this.commandsMap.keys()) {\r\n names.push(name);\r\n }\r\n\r\n // Add all aliases\r\n for (const alias of this.aliasMap.keys()) {\r\n names.push(alias);\r\n }\r\n\r\n // Also include cached commands from manifest\r\n const manifestCommands = manifestManager.commandsJson?.commands || {};\r\n // `Object.entries` rather than keys + index: the index read is\r\n // `T | undefined` under the strictness contract even for a key that came\r\n // from the object itself, and these names feed the CLI's \"did you mean?\"\r\n // suggestions — a literal \"undefined\" in that list is a suggestion no\r\n // operator can act on.\r\n for (const [name, meta] of Object.entries(manifestCommands)) {\r\n const baseName = name.split(\" \")[0] ?? name;\r\n\r\n if (!names.includes(baseName)) {\r\n names.push(baseName);\r\n }\r\n\r\n const alias = meta?.alias;\r\n\r\n if (alias && !names.includes(alias)) {\r\n names.push(alias);\r\n }\r\n }\r\n\r\n return names;\r\n }\r\n\r\n /**\r\n * Start the cli manager\r\n */\r\n public async start() {\r\n // Schema-less first pass: it exists to discover the command name and the\r\n // command-independent flags (--help, --version, --no-cache). Values here are\r\n // raw strings; the typed pass below is the one whose options reach a command.\r\n const { name, options } = parseCliArgs(process.argv);\r\n\r\n if (options.noCache) {\r\n manifestManager.clearCommandsCache();\r\n // remove the commands.json file as wel\r\n await manifestManager.removeCommandsFile();\r\n }\r\n\r\n if (options.version || options.v) {\r\n await displayWarlockVersionInTerminal();\r\n process.exit(0);\r\n }\r\n\r\n // Try to load from manifest first (fastest path)\r\n await manifestManager.loadCommands();\r\n\r\n // Register framework commands first (needed for help)\r\n this.register(\r\n ...frameworkCommands.map((command) => {\r\n if (!command.commandSource) {\r\n command.commandSource = \"framework\";\r\n }\r\n return command;\r\n }),\r\n );\r\n\r\n const isHelpCommand = options.help || options.h;\r\n\r\n // Handle global help (no command)\r\n if (!name && isHelpCommand) {\r\n await this.showGlobalHelp();\r\n process.exit(0);\r\n }\r\n\r\n // Handle warm cache\r\n if (options.warmCache) {\r\n await this.warmCache();\r\n process.exit(0);\r\n }\r\n\r\n if (!name) {\r\n displayMissingCommand();\r\n process.exit(1);\r\n }\r\n\r\n const command = await this.lazyGetCommand(name);\r\n\r\n if (manifestManager.hasChanges) {\r\n await manifestManager.saveCommands();\r\n }\r\n\r\n if (!command) {\r\n // Find similar commands for suggestions\r\n const allCommandNames = this.getAllCommandNames();\r\n const suggestions = findSimilar(name, allCommandNames).map((s) => s.value);\r\n\r\n displayCommandNotFound(name, suggestions);\r\n process.exit(1);\r\n }\r\n\r\n // Handle command-specific help\r\n if (isHelpCommand) {\r\n displayCommandHelp({\r\n name: command.name,\r\n alias: command.commandAlias,\r\n description: command.commandDescription,\r\n options: command.commandOptions,\r\n });\r\n process.exit(0);\r\n }\r\n\r\n // The first parse above had no command to consult, so every value in it is\r\n // a raw string. Now that the command is resolved, re-read argv against its\r\n // declared options so a `type: \"boolean\"` option arrives as a boolean.\r\n const resolved = this.resolveCommandArgs(command);\r\n\r\n const verdict = await this.execute(command, {\r\n options: resolved.options,\r\n args: resolved.args,\r\n });\r\n\r\n // The process-level exit belongs to the entrypoint, not to `execute`.\r\n // A one-shot command can leave a handle open (a db connection, a watcher a\r\n // plugin forgot to close), so the CLI exits explicitly rather than waiting\r\n // for the loop to drain — but only on a verdict of `succeeded`. `failed`\r\n // already exited non-zero through whoever reported it, and `running` means\r\n // a persistent command owns the process now.\r\n if (verdict === \"succeeded\") {\r\n exitAfterFlush(0);\r\n }\r\n }\r\n\r\n /**\r\n * Re-parse argv against the resolved command's declared options.\r\n *\r\n * parseCliArgs runs twice on purpose. The first run produces the command\r\n * name, and there is no schema to consult yet; the second one, here, can be\r\n * type-aware. Re-reading argv (rather than post-processing the first result)\r\n * is what makes `--rollback users.ts` recoverable: by the time the first pass\r\n * returns, the swallowed positional is indistinguishable from a value.\r\n */\r\n protected resolveCommandArgs(command: CLICommand, argv: string[] = process.argv): ParsedCliArgs {\r\n try {\r\n return parseCliArgs(argv, command.commandOptions);\r\n } catch (error) {\r\n if (error instanceof CliOptionValueError) {\r\n console.log();\r\n console.log(` ${colors.redBright(\"Error:\")} ${error.message}`);\r\n console.log();\r\n process.exit(1);\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Show global help with all commands\r\n * Uses manifest if available for fast display\r\n */\r\n protected async showGlobalHelp() {\r\n if (\r\n manifestManager.isCommandLoaded &&\r\n Object.keys(manifestManager.commandsJson?.commands || {}).length > 0\r\n ) {\r\n // Use manifest directly - no need to load command files\r\n const helpCommands: HelpCommandInfo[] = Object.entries(\r\n manifestManager.commandsJson?.commands || {},\r\n ).map(([name, cmd]) => ({\r\n name,\r\n alias: cmd.alias,\r\n description: cmd.description,\r\n source: cmd.source,\r\n }));\r\n\r\n await displayHelp(helpCommands);\r\n return;\r\n }\r\n\r\n // Fallback: No manifest, build from registered commands\r\n // This happens on first run before warm-cache\r\n await this.loadPluginsCommands();\r\n\r\n const projectCommands = await cliCommandsLoader.scanAll();\r\n\r\n this.register(...projectCommands);\r\n\r\n await manifestManager.saveCommands();\r\n\r\n // Build help info from registered commands\r\n const helpCommands: HelpCommandInfo[] = this.commands.map((cmd) => ({\r\n name: cmd.name,\r\n alias: cmd.commandAlias,\r\n description: cmd.commandDescription,\r\n source: cmd.commandSource || \"project\",\r\n }));\r\n\r\n await displayHelp(helpCommands);\r\n }\r\n\r\n /**\r\n * Warm cache - scan all project commands and save to manifest\r\n */\r\n protected async warmCache() {\r\n console.log();\r\n console.log(` ${colors.cyan(\"›\")} Scanning project commands...`);\r\n\r\n const projectCommands = await cliCommandsLoader.scanAll();\r\n\r\n this.register(...projectCommands);\r\n\r\n await manifestManager.saveCommands();\r\n\r\n console.log(\r\n ` ${colors.green(\"✔\")} Cached ${colors.bold(String(projectCommands.length))} project commands`,\r\n );\r\n console.log();\r\n }\r\n\r\n /**\r\n * Load plugins commands\r\n */\r\n protected async loadPluginsCommands() {\r\n await warlockConfigManager.load();\r\n\r\n if (warlockConfigManager.isLoaded) {\r\n this.register(\r\n ...(warlockConfigManager.get(\"cli\")?.commands || []).map((command) => {\r\n if (!command.commandSource) {\r\n command.commandSource = \"plugin\";\r\n }\r\n return command;\r\n }),\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Try to find the command based on the given command name or alias\r\n */\r\n protected async lazyGetCommand(name: string) {\r\n // first step, try to find it directly through current commands\r\n let command = this.getCommand(name);\r\n if (command) {\r\n return command;\r\n }\r\n\r\n // second step, try to find it through warlock config commands\r\n await this.loadPluginsCommands();\r\n\r\n command = this.getCommand(name);\r\n\r\n if (command) {\r\n return command;\r\n }\r\n\r\n // third step, try to find it through project commands\r\n await this.loadProjectCommands(name);\r\n\r\n command = this.getCommand(name);\r\n if (command) {\r\n return command;\r\n }\r\n\r\n return null;\r\n }\r\n\r\n /**\r\n * Load project commands\r\n */\r\n protected async loadProjectCommands(name: string) {\r\n // first get the commands.json contents as an object\r\n const jsonCommandsFile = await manifestManager.loadCommands();\r\n\r\n if (jsonCommandsFile) {\r\n // Check by name or alias\r\n // `Object.entries` hands the metadata over already narrowed; a `for...in`\r\n // index read is `T | undefined` and every field below is read off it.\r\n for (const [fullCommandName, cmdMeta] of Object.entries(jsonCommandsFile.commands)) {\r\n if (cmdMeta === undefined) continue;\r\n\r\n // Match by name or alias\r\n if (isMatchingCommandName(fullCommandName, name) || cmdMeta.alias === name) {\r\n const commandPath = cmdMeta.relativePath;\r\n if (commandPath) {\r\n const executedCommand = await cliCommandsLoader.load(commandPath);\r\n\r\n if (executedCommand) {\r\n executedCommand.$relativePath(commandPath);\r\n this.register(executedCommand);\r\n return;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n const command = await cliCommandsLoader.locate(name);\r\n\r\n if (command) {\r\n this.register(command);\r\n return;\r\n }\r\n }\r\n\r\n /**\r\n * Validate required options\r\n */\r\n protected validateOptions(\r\n command: CLICommand,\r\n options: Record<string, string | boolean | number>,\r\n ): ResolvedCLICommandOption[] {\r\n const missing: ResolvedCLICommandOption[] = [];\r\n\r\n command.commandOptions.forEach((opt) => {\r\n if (opt.required) {\r\n // Check if option is provided by name or alias\r\n const hasOption =\r\n options[opt.name] !== undefined || (opt.alias && options[opt.alias] !== undefined);\r\n if (!hasOption) {\r\n missing.push(opt);\r\n }\r\n }\r\n });\r\n\r\n return missing;\r\n }\r\n\r\n /**\r\n * Apply default values to options\r\n */\r\n protected applyDefaultOptions(\r\n command: CLICommand,\r\n options: Record<string, string | boolean | number>,\r\n ): Record<string, string | boolean | number> {\r\n const result = { ...options };\r\n\r\n command.commandOptions.forEach((opt) => {\r\n if (opt.defaultValue !== undefined) {\r\n const hasOption =\r\n result[opt.name] !== undefined || (opt.alias && result[opt.alias] !== undefined);\r\n if (!hasOption) {\r\n result[opt.name] = opt.defaultValue;\r\n }\r\n }\r\n\r\n // `!== undefined`, not truthiness: `-r=false` now resolves to the boolean\r\n // `false`, and a truthiness test would drop it before it ever reached the\r\n // canonical name — leaving `rollback` undefined instead of explicitly off.\r\n if (\r\n opt.alias !== undefined &&\r\n result[opt.alias] !== undefined &&\r\n result[opt.name] === undefined\r\n ) {\r\n // The `!== undefined` guard on the line above already proves this read\r\n // is defined, but the compiler does not carry a guard on one index\r\n // expression to a second, textually identical one. Reading it into a\r\n // local makes the narrowing survive, WITHOUT weakening the check — an\r\n // `?? ` fallback here would resurrect exactly the bug the comment above\r\n // describes, turning an explicit `-r=false` into a default.\r\n const aliasValue = result[opt.alias];\r\n\r\n if (aliasValue !== undefined) {\r\n result[opt.name] = aliasValue;\r\n }\r\n }\r\n });\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Execute the given command and report how it ended.\r\n *\r\n * Returning the verdict — rather than exiting on success from in here — is\r\n * what makes the awaited result trustworthy: see {@link CommandRunVerdict}.\r\n */\r\n public async execute(command: CLICommand, data: CommandActionData): Promise<CommandRunVerdict> {\r\n const startTime = Date.now();\r\n\r\n // Validate required options\r\n const missingOptions = this.validateOptions(command, data.options);\r\n if (missingOptions.length > 0) {\r\n displayMissingOptions(missingOptions);\r\n process.exit(1);\r\n }\r\n\r\n // Apply default values\r\n data.options = this.applyDefaultOptions(command, data.options);\r\n\r\n // ONE header per `warlock <command>` the user typed — not one per process.\r\n //\r\n // `warlock dev` is TWO processes by design: the invocation the user typed\r\n // becomes a thin supervisor (`dev-server/supervisor.ts`) that re-spawns\r\n // `process.argv` with `WARLOCK_DEV_WORKER=1`, and the child is the actual\r\n // server. Both are `warlock dev`, so both reached this line and the user saw\r\n //\r\n // › Running dev...\r\n // › Running dev...\r\n //\r\n // which reads exactly like the application booting twice. It is not — see\r\n // the block comment on `isRespawnedWorker` — but the line was still wrong,\r\n // and it re-printed on every crash-restart on top of the supervisor's own\r\n // \"restarting\" notice.\r\n //\r\n // The SUPERVISOR keeps the header, not the worker: it prints immediately\r\n // (so a slow boot is not silent), it prints once for the whole session\r\n // (restarts are already announced as restarts), and it is the process the\r\n // user actually started.\r\n if (!isRespawnedWorker()) {\r\n displayExecutingCommand(command.name);\r\n }\r\n\r\n if (command.commandPreAction) {\r\n await command.commandPreAction(data);\r\n }\r\n\r\n // load preloaders\r\n if (command.commandPreload) {\r\n try {\r\n await this.loadPreloaders(command);\r\n } catch (error) {\r\n // A preload failure (a bad import in a config file, a connector that\r\n // throws on boot, a missing module export) happens BEFORE the command's\r\n // action runs — there is no run loop to recover into, so it is ALWAYS\r\n // fatal, persistent command (dev/start) or not. Surface the real cause\r\n // and the offending file, then exit. This MUST stay outside the\r\n // command-execute try/catch below: letting a preload rejection escape\r\n // turned `warlock dev` into a silent hang — the unhandled rejection slid\r\n // past while the already-started loader worker thread kept the process\r\n // alive, freezing just after the banner with no error printed.\r\n displayBootError(command.name, error as Error);\r\n process.exit(1);\r\n }\r\n }\r\n\r\n this.captureFatalAsyncErrors(command);\r\n\r\n try {\r\n await command.execute(data);\r\n\r\n // Persistent commands (dev/start) hand control to their run loop here.\r\n // There is no success banner to print and nothing to exit for.\r\n if (command.isPersistent) return \"running\";\r\n\r\n // Yield one full event-loop turn before declaring success, and AWAIT that\r\n // yield. Node flags an unhandled rejection only after the current turn's\r\n // microtasks drain, so reporting success synchronously here fired FIRST\r\n // and erased the failure — a `build` whose async work rejected exited 0\r\n // having emitted nothing.\r\n //\r\n // The await is the load-bearing part, not the yield. Scheduling this as a\r\n // bare `setImmediate` let `execute()` RESOLVE while the verdict was still\r\n // pending: the callback outlived the call it belonged to, so the green\r\n // check could surface after the caller had already moved on — in a later\r\n // command, or after a rejection reported in the interim, which the guard\r\n // below cannot see because by then it reads the state of a run that has\r\n // already ended. Awaiting binds the banner to THIS invocation: `execute`\r\n // does not resolve until the verdict it is about to report has settled.\r\n //\r\n // `setImmediate` runs in the same loop iteration's check phase, so this\r\n // cannot hang on a lingering handle the way dropping the exit would; it\r\n // only lets `captureFatalAsyncErrors` win the race when there is one.\r\n await new Promise<void>((resolve) => setImmediate(resolve));\r\n\r\n // The yield is what gives a floating rejection the chance to be reported;\r\n // this is what acts on it. A green check printed after that error would\r\n // contradict it, and the exit that follows would reset `process.exitCode`\r\n // to 0 — re-erasing the very failure this whole path exists to surface.\r\n if (this.fatalAsyncErrorReported) return \"failed\";\r\n\r\n displayCommandSuccess(command.name, Date.now() - startTime);\r\n\r\n return \"succeeded\";\r\n } catch (error) {\r\n displayCommandError(command.name, error as Error);\r\n // Persistent commands (dev/start) own their startup-failure exits;\r\n // by the time an error reaches here they've already entered their\r\n // run loop, so it's a runtime error we log but don't crash on —\r\n // HMR or process supervisors can recover. Non-persistent\r\n // one-shot commands always exit on failure.\r\n if (!command.isPersistent) {\r\n process.exit(1);\r\n }\r\n\r\n return \"failed\";\r\n }\r\n }\r\n\r\n /**\r\n * Fail loudly on async errors that escape the command's own promise.\r\n *\r\n * `execute` awaits `command.execute(data)`, but work the command merely\r\n * *started* settles outside that await — `@mongez/events` `trigger` /\r\n * `triggerAll` are synchronous and do not await their listeners, so an async\r\n * listener that throws leaves a floating rejection behind. Node reports it at\r\n * the end of the turn, by which point the CLI had already exited 0: the\r\n * failure was erased and the command looked green.\r\n *\r\n * Persistent commands (dev/start) are exempt from the exit for the same\r\n * reason as the catch block in `execute`: by the time they reach this point\r\n * they own a run loop that is expected to survive runtime errors so HMR or a\r\n * supervisor can recover. They still print — silence is what made this class\r\n * of failure invisible in the first place.\r\n */\r\n protected fatalAsyncErrorReported = false;\r\n\r\n protected captureFatalAsyncErrors(command: CLICommand) {\r\n const onFatalAsyncError = (reason: unknown) => {\r\n // A rejection reason is not necessarily an Error (`Promise.reject(\"x\")`),\r\n // and a crash handler must never itself throw.\r\n const error = reason instanceof Error ? reason : new Error(String(reason));\r\n\r\n // Set before printing, so the deferred success banner is suppressed even\r\n // if displaying the error yields.\r\n this.fatalAsyncErrorReported = true;\r\n\r\n displayCommandError(command.name, error);\r\n\r\n if (!command.isPersistent) {\r\n exitAfterFlush(1);\r\n }\r\n };\r\n\r\n process.on(\"unhandledRejection\", onFatalAsyncError);\r\n process.on(\"uncaughtException\", onFatalAsyncError);\r\n }\r\n\r\n /**\r\n * Load preloaders\r\n */\r\n protected async loadPreloaders(command: CLICommand) {\r\n const preloaders = command.commandPreload || {};\r\n\r\n if (preloaders.runtimeStrategy) {\r\n Application.setRuntimeStrategy(preloaders.runtimeStrategy);\r\n }\r\n\r\n // Override, not a default: a command that declares an environment gets it\r\n // regardless of what the shell exported. Then read it back — `process.env`\r\n // is process-global and anything may have written to it, so a value that\r\n // did not take must fail here, loudly, rather than surface later as a\r\n // production React bundle under `warlock dev`.\r\n if (preloaders.environment) {\r\n Application.setEnvironment(preloaders.environment);\r\n\r\n if (process.env.NODE_ENV !== preloaders.environment) {\r\n throw new Error(\r\n `Command \"${command.name}\" set NODE_ENV to \"${preloaders.environment}\", but it read back as ` +\r\n `\"${process.env.NODE_ENV}\". The environment did not take, and every decision made ` +\r\n `downstream of it — env file selection, bundler dep optimisation — would be wrong.`,\r\n );\r\n }\r\n }\r\n\r\n // Env BEFORE config, unconditionally, for every command.\r\n //\r\n // `warlock.config.ts` is a module whose body runs on import, and projects\r\n // legitimately call `env(\"BUILD_OUT\", \"dist\")` in it. Loading config first\r\n // evaluated that body against an empty env store, so every such call\r\n // silently returned its default — under `build` and `start`, which never\r\n // loaded env at all, and under `dev` too, because config was loaded first\r\n // regardless. The ordering is load-bearing in the other direction as well:\r\n // `loadEnv()` reads `process.env.NODE_ENV` at call time to choose\r\n // `.env.<NODE_ENV>` over `.env`, so it must follow `setEnvironment` above.\r\n await loadEnvironmentFiles();\r\n\r\n await warlockConfigManager.load();\r\n\r\n if (preloaders.config || preloaders.bootstrap || preloaders.prestart) {\r\n await filesOrchestrator.init();\r\n }\r\n\r\n // `preloaders.env` is gone: env is loaded above for every command, so the\r\n // branch that used to serve it could only re-parse the same files and\r\n // re-override `process.env` a second time.\r\n if (preloaders.bootstrap) {\r\n await bootstrap();\r\n\r\n if (await fileExistsAsync(appPath(\"bootstrap.ts\"))) {\r\n await filesOrchestrator.load(\"src/app/bootstrap.ts\");\r\n }\r\n }\r\n\r\n // Load configuration files\r\n if (preloaders.config) {\r\n await loadConfigFiles(preloaders.config);\r\n }\r\n\r\n if (preloaders.prestart) {\r\n if (await fileExistsAsync(appPath(\"prestart.ts\"))) {\r\n await filesOrchestrator.load(\"src/app/prestart.ts\");\r\n }\r\n }\r\n\r\n // Initialize connectors.\r\n // `connectors: true` only starts the early phase here — late-phase\r\n // connectors (http, socket) start after the command's action loads\r\n // app code. Explicit lists bypass phase splitting (caller knows\r\n // exactly which connectors they want).\r\n if (preloaders.connectors) {\r\n // REGISTER before starting — the non-bundled counterpart of section 2.5\r\n // of the generated production entry, and the \"dev preloader\" path\r\n // `register-configured-connectors.ts` documents.\r\n //\r\n // Without this, `warlock.config.ts > connectors` drove the BUILD only:\r\n // `warlock build` read the array to drain each connector's contribution,\r\n // but under `warlock dev` nothing ever registered it, so an app had to\r\n // call `connectorsManager.register(...)` from its own `main.ts` to get a\r\n // connector running. Apps that did BOTH then registered the same\r\n // connector twice in production — once here from the baked config, once\r\n // from their own app code — and it booted twice, which surfaced as\r\n // `Route name \"...\" is already taken` while installing page routes.\r\n //\r\n // The array is omitted deliberately, so the function reads the config\r\n // loaded above; `expectedNames` is omitted too, because drift is a\r\n // build↔runtime relationship and dev has no build to drift from.\r\n //\r\n // The idempotency this function provides is against ITSELF — a second\r\n // call skips names the first registered. It does NOT protect against an\r\n // app calling `connectorsManager.register()` directly, which pushes\r\n // unconditionally, and which runs AFTER this point in both modes. So a\r\n // connector belongs in the config array or in app code, never both:\r\n // listing it in both is the duplicate this comment's route-name error\r\n // describes.\r\n registerConfiguredConnectors();\r\n\r\n // Fail fast on a busy port BEFORE the early-phase connectors (database,\r\n // cache, ...) connect. `http` itself is a LATE-phase connector, so its\r\n // own preflight is not reached until after those early connectors are\r\n // up and every app module has been imported — 7-13s of work discarded\r\n // before anyone learns the port was taken. This is the dev-preload\r\n // counterpart of the check the generated production entry runs at the\r\n // same point in its own boot sequence (`production-builder.ts`, step\r\n // 2.6) — a cheap bind-and-release probe, not a connector reorder.\r\n await preflightConfiguredHttpPort();\r\n\r\n if (preloaders.connectors === true) {\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\r\n } else {\r\n await connectorsManager.start(preloaders.connectors);\r\n }\r\n }\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cli-commands.utils.mjs","names":[],"sources":["../../../../../../../core/src/cli/cli-commands.utils.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { Environment } from \"../utils\";\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\n\nexport function isMatchingCommandName(commandName: string, targetingCommandName: string) {\n return commandName.split(\" \")[0] === targetingCommandName;\n}\n\n/**\n * Display the Warlock.js version banner\n */\nexport async function displayWarlockVersionInTerminal() {\n const version = await getWarlockVersion();\n console.log(`⚡ ${colors.bold(\"Warlock.js\")} ${colors.greenBright(`v${version}`)}`);\n}\n\ntype StartBannerOptions = {\n environment: Environment;\n};\n\nfunction getTextColorMethod(environment: Environment) {\n switch (environment) {\n case \"development\":\n return colors.yellowBright;\n case \"production\":\n return colors.greenBright;\n case \"test\":\n return colors.blueBright;\n default:\n return colors.whiteBright;\n }\n}\n\n/**\n * Display CLI startup banner\n */\nexport async function displayStartupBanner({ environment }: StartBannerOptions) {\n const version = await getWarlockVersion();\n const textColorMethod = getTextColorMethod(environment);\n console.log(` ⚡ ${colors.bold(textColorMethod(\"Warlock.js\"))} ${colors.dim(`v${version}`)}`);\n console.log();\n}\n\ntype ProductionReadyBannerOptions = {\n bootDurationMs?: number;\n};\n\n/**\n * Announce that the production server is up — printed on **stdout**, and only\n * after the child process reported a completed boot.\n *\n * Stdout is the channel a supervisor or CI gate greps to decide the service is\n * healthy, so nothing optimistic may be written to it: this function is the\n * single place allowed to say \"started\", and it is called from exactly one\n * place, the `warlock:ready` handler in the `start` command.\n */\nexport async function displayProductionReadyBanner({\n bootDurationMs,\n}: ProductionReadyBannerOptions = {}) {\n const version = await getWarlockVersion();\n const duration = bootDurationMs ? colors.dim(` in ${bootDurationMs}ms`) : \"\";\n\n console.log(\n ` ⚡ ${colors.bold(colors.greenBright(\"Warlock.js\"))} ${colors.dim(`v${version}`)} ${colors.green(\"✔\")} production server started${duration}`,\n );\n console.log();\n}\n\n/**\n * Report that the production server died before it ever served anything.\n *\n * Written to **stderr** only. `warlock start` inherits both child streams, and\n * production launchers commonly merge them (`2>&1`), so mirroring this block to\n * stdout prints every line twice. The command's non-zero exit code is the\n * machine-readable failure signal; stdout remains reserved for the ready\n * banner.\n *\n * `causeWasCaptured` gates the \"the cause is printed above\" line. The\n * supervisor cannot always guarantee the child wrote anything before it\n * died — an import that throws before the logger configures its channels,\n * a bundle with no console channel at all — and claiming a cause is above\n * when nothing was ever captured sends the developer looking for output\n * that doesn't exist, which is worse than admitting the gap.\n */\nexport function displayProductionStartFailure(\n exitCode: number,\n causeWasCaptured: boolean,\n) {\n const causeLine = causeWasCaptured\n ? ` ${colors.dim(\"the cause is printed above, in the application's own output\")}`\n : ` ${colors.dim(\"no output was captured from the application process — its cause did not reach this terminal\")}`;\n\n const lines = [\n \"\",\n ` ${colors.red(\"✖\")} ${colors.bold(\"warlock start\")} failed — the server never finished booting`,\n ` ${colors.dim(`the application process exited with code ${exitCode}`)}`,\n causeLine,\n \"\",\n ];\n\n for (const line of lines) {\n console.error(line);\n }\n}\n\n/**\n * Note that a running child has not reported readiness yet.\n *\n * Emitted on **stderr** only. The process may be perfectly healthy — an older\n * bundle has no readiness signal at all — so saying anything on stdout would be\n * the very false-green this channel exists to prevent.\n *\n * The wording offers the likely causes rather than asserting one. It cannot\n * distinguish an old bundle from a slow boot from a boot that is about to fail,\n * and an earlier version claimed \"the bundle predates readiness reporting\" —\n * which was simply wrong when a current bundle was mid-crash, and sent the\n * reader after the wrong thing.\n */\nexport function displayMissingReadinessNotice(waitedMs: number) {\n console.error();\n console.error(\n ` ${colors.yellow(\"!\")} still running after ${Math.round(waitedMs / 1000)}s with no readiness signal`,\n );\n console.error(` ${colors.dim(\"either the boot is still in progress, or this bundle was built\")}`);\n console.error(\n ` ${colors.dim(\"before readiness reporting — re-run\")} ${colors.cyan(\"warlock build\")} ${colors.dim(\"if the banner never appears\")}`,\n );\n console.error();\n}\n\n/**\n * Display command execution header\n */\nexport function displayExecutingCommand(commandName: string) {\n console.log(` ${colors.cyan(\"›\")} Running ${colors.bold(colors.white(commandName))}...`);\n console.log();\n}\n\n/**\n * Display command not found error with optional suggestions\n */\nexport function displayCommandNotFound(commandName: string, suggestions?: string[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Command ${colors.magenta(commandName)} not found`);\n\n if (suggestions && suggestions.length > 0) {\n console.log();\n console.log(` ${colors.yellow(\"Did you mean?\")}`);\n suggestions.forEach((suggestion) => {\n console.log(` ${colors.cyan(\"→\")} ${colors.white(suggestion)}`);\n });\n }\n\n console.log();\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display missing command error\n */\nexport function displayMissingCommand() {\n console.log();\n console.log(` ${colors.red(\"✖\")} No command specified`);\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display command success message.\n *\n * Written to STDERR, not stdout. This banner is printed around EVERY command,\n * including ones whose stdout is a machine payload a script is meant to parse —\n * `warlock routes --json` is the clearest case. On stdout it produced this,\n * which is not JSON and cannot be piped anywhere:\n *\n * ```\n * []\n *\n * ✔ routes completed successfully (696ms)\n * ```\n *\n * stdout belongs to the command's output; status chrome belongs to stderr,\n * where it stays visible to a person in a terminal and out of a pipe.\n */\nexport function displayCommandSuccess(commandName: string, durationMs?: number) {\n const duration = durationMs ? colors.dim(` (${durationMs}ms)`) : \"\";\n console.error();\n console.error(\n ` ${colors.green(\"✔\")} ${colors.bold(commandName)} completed successfully${duration}`,\n );\n console.error();\n}\n\n/**\n * Display command error message.\n *\n * On stderr for the same reason as {@link displayCommandSuccess}, and one\n * stronger: a failure interleaved into a machine payload corrupts the payload\n * AND hides the failure from anyone reading stderr for it.\n */\nexport function displayCommandError(commandName: string, error: Error) {\n console.error();\n console.error(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed`);\n console.error(` ${colors.dim(error.message)}`);\n console.error();\n}\n\n/**\n * Display a FATAL boot/preload failure and exit information.\n *\n * Preload failures — a bad import in a config file, a connector that throws\n * on startup, a missing module export — happen BEFORE the command's run loop\n * exists, so there is nothing to recover into: they are always fatal. Unlike\n * `displayCommandError`, this prints the full stack (not just `error.message`)\n * because the stack names the exact file + line of the offending import, which\n * is the single most useful clue when a config file pulls in a broken module.\n *\n * Surfacing this loudly is the difference between a clear error and a silent\n * hang: an unhandled preload rejection used to escape while the already-started\n * loader worker thread kept the process alive, leaving `warlock dev` frozen\n * just after the banner with no message at all.\n *\n * @example\n * // SyntaxError: The requested module '@warlock.js/cascade' does not\n * // provide an export named 'belongsTo'\n * // at src/app/.../permission.model.ts:2\n */\nexport function displayBootError(commandName: string, error: Error) {\n console.log();\n console.log(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed to start`);\n console.log(` ${colors.red(error.message)}`);\n if (error.stack) {\n console.log();\n console.log(colors.dim(error.stack));\n }\n console.log();\n}\n\n/**\n * Display missing required options error\n */\nexport function displayMissingOptions(options: { name: string; text: string }[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Missing required options:`);\n options.forEach((opt) => {\n console.log(` ${colors.yellow(opt.text)} ${colors.dim(`(--${opt.name})`)}`);\n });\n console.log();\n}\n\n/**\n * Command info for help display\n */\nexport type HelpCommandInfo = {\n name: string;\n alias?: string;\n description?: string;\n source: \"framework\" | \"plugin\" | \"project\";\n};\n\n/**\n * Display global help with all commands grouped by source\n */\nexport async function displayHelp(commands: HelpCommandInfo[]) {\n const version = await getWarlockVersion();\n\n console.log();\n console.log(\n ` ⚡ ${colors.bold(colors.yellowBright(\"Warlock.js\"))} CLI ${colors.dim(`v${version}`)}`,\n );\n console.log();\n console.log(\n ` ${colors.bold(\"Usage:\")} ${colors.cyan(\"warlock\")} ${colors.dim(\"<command>\")} ${colors.dim(\"[options]\")}`,\n );\n console.log();\n\n // Group by source\n const grouped: Record<string, HelpCommandInfo[]> = {\n framework: [],\n plugin: [],\n project: [],\n };\n\n commands.forEach((cmd) => {\n grouped[cmd.source]?.push(cmd);\n });\n\n // Display each group\n const groupLabels: Record<string, string> = {\n framework: \"Framework Commands\",\n plugin: \"Plugin Commands\",\n project: \"Project Commands\",\n };\n\n for (const [source, cmds] of Object.entries(grouped)) {\n if (cmds.length === 0) continue;\n\n console.log(` ${colors.bold(colors.white(groupLabels[source]))}`);\n console.log();\n\n // Find max name length for alignment\n const maxLen = Math.max(...cmds.map((c) => c.name.length + (c.alias ? c.alias.length + 4 : 0)));\n\n cmds.forEach((cmd) => {\n const aliasStr = cmd.alias ? colors.dim(` (${cmd.alias})`) : \"\";\n const nameWithAlias = cmd.name + (cmd.alias ? ` (${cmd.alias})` : \"\");\n const padding = \" \".repeat(maxLen - nameWithAlias.length + 2);\n // const desc = cmd.description || colors.dim(\"No description\");\n const desc = cmd.description || \"\";\n console.log(` ${colors.cyan(cmd.name)}${aliasStr}${padding}${desc}`);\n });\n console.log();\n }\n\n // Display global flags\n console.log(` ${colors.bold(colors.white(\"Global Flags\"))}`);\n console.log();\n\n const globalFlags = [\n { flag: \"--help, -h\", description: \"Show help for a command\" },\n { flag: \"--version, -v\", description: \"Show Warlock version\" },\n { flag: \"--no-cache\", description: \"Force reload without cache\" },\n { flag: \"--warm-cache\", description: \"Pre-cache all project commands\" },\n ];\n\n const maxFlagLen = Math.max(...globalFlags.map((f) => f.flag.length));\n\n globalFlags.forEach(({ flag, description }) => {\n const padding = \" \".repeat(maxFlagLen - flag.length + 2);\n console.log(` ${colors.yellow(flag)}${padding}${description}`);\n });\n console.log();\n\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock <command> --help\")} ${colors.dim(\"for command-specific help\")}`,\n );\n console.log();\n}\n\n/**\n * Display help for a specific command\n */\nexport function displayCommandHelp(command: {\n name: string;\n alias?: string;\n description?: string;\n options?: { name: string; text: string; description?: string; required?: boolean }[];\n}) {\n console.log();\n console.log(\n ` ${colors.bold(colors.cyan(command.name))}${command.alias ? colors.dim(` (${command.alias})`) : \"\"}`,\n );\n\n if (command.description) {\n console.log(` ${command.description}`);\n }\n console.log();\n\n if (command.options && command.options.length > 0) {\n console.log(` ${colors.bold(\"Options:\")}`);\n console.log();\n\n const maxLen = Math.max(...command.options.map((o) => o.text.length));\n\n command.options.forEach((opt) => {\n const padding = \" \".repeat(maxLen - opt.text.length + 2);\n const required = opt.required ? colors.red(\" (required)\") : \"\";\n const desc = opt.description || \"\";\n console.log(` ${colors.green(opt.text)}${padding}${desc}${required}`);\n });\n console.log();\n } else {\n console.log(` ${colors.dim(\"No options available\")}`);\n console.log();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-commands.utils.mjs","names":[],"sources":["../../../../../../../core/src/cli/cli-commands.utils.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { Environment } from \"../utils\";\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\n\nexport function isMatchingCommandName(commandName: string, targetingCommandName: string) {\n return commandName.split(\" \")[0] === targetingCommandName;\n}\n\n/**\n * Display the Warlock.js version banner\n */\nexport async function displayWarlockVersionInTerminal() {\n const version = await getWarlockVersion();\n console.log(`⚡ ${colors.bold(\"Warlock.js\")} ${colors.greenBright(`v${version}`)}`);\n}\n\ntype StartBannerOptions = {\n environment: Environment;\n};\n\nfunction getTextColorMethod(environment: Environment) {\n switch (environment) {\n case \"development\":\n return colors.yellowBright;\n case \"production\":\n return colors.greenBright;\n case \"test\":\n return colors.blueBright;\n default:\n return colors.whiteBright;\n }\n}\n\n/**\n * Display CLI startup banner\n */\nexport async function displayStartupBanner({ environment }: StartBannerOptions) {\n const version = await getWarlockVersion();\n const textColorMethod = getTextColorMethod(environment);\n console.log(` ⚡ ${colors.bold(textColorMethod(\"Warlock.js\"))} ${colors.dim(`v${version}`)}`);\n console.log();\n}\n\ntype ProductionReadyBannerOptions = {\n bootDurationMs?: number;\n};\n\n/**\n * Announce that the production server is up — printed on **stdout**, and only\n * after the child process reported a completed boot.\n *\n * Stdout is the channel a supervisor or CI gate greps to decide the service is\n * healthy, so nothing optimistic may be written to it: this function is the\n * single place allowed to say \"started\", and it is called from exactly one\n * place, the `warlock:ready` handler in the `start` command.\n */\nexport async function displayProductionReadyBanner({\n bootDurationMs,\n}: ProductionReadyBannerOptions = {}) {\n const version = await getWarlockVersion();\n const duration = bootDurationMs ? colors.dim(` in ${bootDurationMs}ms`) : \"\";\n\n console.log(\n ` ⚡ ${colors.bold(colors.greenBright(\"Warlock.js\"))} ${colors.dim(`v${version}`)} ${colors.green(\"✔\")} production server started${duration}`,\n );\n console.log();\n}\n\n/**\n * Report that the production server died before it ever served anything.\n *\n * Written to **stderr** only. `warlock start` inherits both child streams, and\n * production launchers commonly merge them (`2>&1`), so mirroring this block to\n * stdout prints every line twice. The command's non-zero exit code is the\n * machine-readable failure signal; stdout remains reserved for the ready\n * banner.\n *\n * `causeWasCaptured` gates the \"the cause is printed above\" line. The\n * supervisor cannot always guarantee the child wrote anything before it\n * died — an import that throws before the logger configures its channels,\n * a bundle with no console channel at all — and claiming a cause is above\n * when nothing was ever captured sends the developer looking for output\n * that doesn't exist, which is worse than admitting the gap.\n */\nexport function displayProductionStartFailure(exitCode: number, causeWasCaptured: boolean) {\n const causeLine = causeWasCaptured\n ? ` ${colors.dim(\"the cause is printed above, in the application's own output\")}`\n : ` ${colors.dim(\"no output was captured from the application process — its cause did not reach this terminal\")}`;\n\n const lines = [\n \"\",\n ` ${colors.red(\"✖\")} ${colors.bold(\"warlock start\")} failed — the server never finished booting`,\n ` ${colors.dim(`the application process exited with code ${exitCode}`)}`,\n causeLine,\n \"\",\n ];\n\n for (const line of lines) {\n console.error(line);\n }\n}\n\n/**\n * Note that a running child has not reported readiness yet.\n *\n * Emitted on **stderr** only. The process may be perfectly healthy — an older\n * bundle has no readiness signal at all — so saying anything on stdout would be\n * the very false-green this channel exists to prevent.\n *\n * The wording offers the likely causes rather than asserting one. It cannot\n * distinguish an old bundle from a slow boot from a boot that is about to fail,\n * and an earlier version claimed \"the bundle predates readiness reporting\" —\n * which was simply wrong when a current bundle was mid-crash, and sent the\n * reader after the wrong thing.\n */\nexport function displayMissingReadinessNotice(waitedMs: number) {\n console.error();\n console.error(\n ` ${colors.yellow(\"!\")} still running after ${Math.round(waitedMs / 1000)}s with no readiness signal`,\n );\n console.error(\n ` ${colors.dim(\"either the boot is still in progress, or this bundle was built\")}`,\n );\n console.error(\n ` ${colors.dim(\"before readiness reporting — re-run\")} ${colors.cyan(\"warlock build\")} ${colors.dim(\"if the banner never appears\")}`,\n );\n console.error();\n}\n\n/**\n * Display command execution header\n */\nexport function displayExecutingCommand(commandName: string) {\n console.log(` ${colors.cyan(\"›\")} Running ${colors.bold(colors.white(commandName))}...`);\n console.log();\n}\n\n/**\n * Display command not found error with optional suggestions\n */\nexport function displayCommandNotFound(commandName: string, suggestions?: string[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Command ${colors.magenta(commandName)} not found`);\n\n if (suggestions && suggestions.length > 0) {\n console.log();\n console.log(` ${colors.yellow(\"Did you mean?\")}`);\n suggestions.forEach((suggestion) => {\n console.log(` ${colors.cyan(\"→\")} ${colors.white(suggestion)}`);\n });\n }\n\n console.log();\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display missing command error\n */\nexport function displayMissingCommand() {\n console.log();\n console.log(` ${colors.red(\"✖\")} No command specified`);\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock --help\")} ${colors.dim(\"to see available commands\")}`,\n );\n console.log();\n}\n\n/**\n * Display command success message.\n *\n * Written to STDERR, not stdout. This banner is printed around EVERY command,\n * including ones whose stdout is a machine payload a script is meant to parse —\n * `warlock routes --json` is the clearest case. On stdout it produced this,\n * which is not JSON and cannot be piped anywhere:\n *\n * ```\n * []\n *\n * ✔ routes completed successfully (696ms)\n * ```\n *\n * stdout belongs to the command's output; status chrome belongs to stderr,\n * where it stays visible to a person in a terminal and out of a pipe.\n */\nexport function displayCommandSuccess(commandName: string, durationMs?: number) {\n const duration = durationMs ? colors.dim(` (${durationMs}ms)`) : \"\";\n console.error();\n console.error(\n ` ${colors.green(\"✔\")} ${colors.bold(commandName)} completed successfully${duration}`,\n );\n console.error();\n}\n\n/**\n * Display command error message.\n *\n * On stderr for the same reason as {@link displayCommandSuccess}, and one\n * stronger: a failure interleaved into a machine payload corrupts the payload\n * AND hides the failure from anyone reading stderr for it.\n */\nexport function displayCommandError(commandName: string, error: Error) {\n console.error();\n console.error(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed`);\n console.error(` ${colors.dim(error.message)}`);\n console.error();\n}\n\n/**\n * Display a FATAL boot/preload failure and exit information.\n *\n * Preload failures — a bad import in a config file, a connector that throws\n * on startup, a missing module export — happen BEFORE the command's run loop\n * exists, so there is nothing to recover into: they are always fatal. Unlike\n * `displayCommandError`, this prints the full stack (not just `error.message`)\n * because the stack names the exact file + line of the offending import, which\n * is the single most useful clue when a config file pulls in a broken module.\n *\n * Surfacing this loudly is the difference between a clear error and a silent\n * hang: an unhandled preload rejection used to escape while the already-started\n * loader worker thread kept the process alive, leaving `warlock dev` frozen\n * just after the banner with no message at all.\n *\n * @example\n * // SyntaxError: The requested module '@warlock.js/cascade' does not\n * // provide an export named 'belongsTo'\n * // at src/app/.../permission.model.ts:2\n */\nexport function displayBootError(commandName: string, error: Error) {\n console.log();\n console.log(` ${colors.red(\"✖\")} ${colors.bold(commandName)} failed to start`);\n console.log(` ${colors.red(error.message)}`);\n if (error.stack) {\n console.log();\n console.log(colors.dim(error.stack));\n }\n console.log();\n}\n\n/**\n * Display missing required options error\n */\nexport function displayMissingOptions(options: { name: string; text: string }[]) {\n console.log();\n console.log(` ${colors.red(\"✖\")} Missing required options:`);\n options.forEach((opt) => {\n console.log(` ${colors.yellow(opt.text)} ${colors.dim(`(--${opt.name})`)}`);\n });\n console.log();\n}\n\n/**\n * Command info for help display\n */\nexport type HelpCommandInfo = {\n name: string;\n alias?: string;\n description?: string;\n source: \"framework\" | \"plugin\" | \"project\";\n};\n\n/**\n * Display global help with all commands grouped by source\n */\nexport async function displayHelp(commands: HelpCommandInfo[]) {\n const version = await getWarlockVersion();\n\n console.log();\n console.log(\n ` ⚡ ${colors.bold(colors.yellowBright(\"Warlock.js\"))} CLI ${colors.dim(`v${version}`)}`,\n );\n console.log();\n console.log(\n ` ${colors.bold(\"Usage:\")} ${colors.cyan(\"warlock\")} ${colors.dim(\"<command>\")} ${colors.dim(\"[options]\")}`,\n );\n console.log();\n\n // Group by source\n const grouped: Record<string, HelpCommandInfo[]> = {\n framework: [],\n plugin: [],\n project: [],\n };\n\n commands.forEach((cmd) => {\n grouped[cmd.source]?.push(cmd);\n });\n\n // Display each group\n const groupLabels: Record<string, string> = {\n framework: \"Framework Commands\",\n plugin: \"Plugin Commands\",\n project: \"Project Commands\",\n };\n\n for (const [source, cmds] of Object.entries(grouped)) {\n if (cmds.length === 0) continue;\n\n console.log(` ${colors.bold(colors.white(groupLabels[source]))}`);\n console.log();\n\n // Find max name length for alignment\n const maxLen = Math.max(...cmds.map((c) => c.name.length + (c.alias ? c.alias.length + 4 : 0)));\n\n cmds.forEach((cmd) => {\n const aliasStr = cmd.alias ? colors.dim(` (${cmd.alias})`) : \"\";\n const nameWithAlias = cmd.name + (cmd.alias ? ` (${cmd.alias})` : \"\");\n const padding = \" \".repeat(maxLen - nameWithAlias.length + 2);\n // const desc = cmd.description || colors.dim(\"No description\");\n const desc = cmd.description || \"\";\n console.log(` ${colors.cyan(cmd.name)}${aliasStr}${padding}${desc}`);\n });\n console.log();\n }\n\n // Display global flags\n console.log(` ${colors.bold(colors.white(\"Global Flags\"))}`);\n console.log();\n\n const globalFlags = [\n { flag: \"--help, -h\", description: \"Show help for a command\" },\n { flag: \"--version, -v\", description: \"Show Warlock version\" },\n { flag: \"--no-cache\", description: \"Force reload without cache\" },\n { flag: \"--warm-cache\", description: \"Pre-cache all project commands\" },\n ];\n\n const maxFlagLen = Math.max(...globalFlags.map((f) => f.flag.length));\n\n globalFlags.forEach(({ flag, description }) => {\n const padding = \" \".repeat(maxFlagLen - flag.length + 2);\n console.log(` ${colors.yellow(flag)}${padding}${description}`);\n });\n console.log();\n\n console.log(\n ` ${colors.dim(\"Run\")} ${colors.cyan(\"warlock <command> --help\")} ${colors.dim(\"for command-specific help\")}`,\n );\n console.log();\n}\n\n/**\n * Display help for a specific command\n */\nexport function displayCommandHelp(command: {\n name: string;\n alias?: string;\n description?: string;\n options?: { name: string; text: string; description?: string; required?: boolean }[];\n}) {\n console.log();\n console.log(\n ` ${colors.bold(colors.cyan(command.name))}${command.alias ? colors.dim(` (${command.alias})`) : \"\"}`,\n );\n\n if (command.description) {\n console.log(` ${command.description}`);\n }\n console.log();\n\n if (command.options && command.options.length > 0) {\n console.log(` ${colors.bold(\"Options:\")}`);\n console.log();\n\n const maxLen = Math.max(...command.options.map((o) => o.text.length));\n\n command.options.forEach((opt) => {\n const padding = \" \".repeat(maxLen - opt.text.length + 2);\n const required = opt.required ? colors.red(\" (required)\") : \"\";\n const desc = opt.description || \"\";\n console.log(` ${colors.green(opt.text)}${padding}${desc}${required}`);\n });\n console.log();\n } else {\n console.log(` ${colors.dim(\"No options available\")}`);\n console.log();\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"optional-peers.check.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/optional-peers.check.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { config } from \"../../../../config/config-getter\";\nimport type { CheckStatus, DoctorCheck } from \"../check.types\";\n\n/**\n * A package this project's own configuration requires, and what asked for it.\n */\ntype RequiredPeer = {\n /** The npm package name to probe via `require.resolve`. */\n package: string;\n\n /** The config that asked for it, quoted back to the user verbatim. */\n because: string;\n};\n\n/**\n * Resolve from the project's working directory, not from core's own\n * `node_modules`, so the probe reflects what the *consuming app* has installed.\n */\nconst projectRequire = createRequire(`${process.cwd()}/package.json`);\n\n/**\n * Whether a package can be resolved from the project. A failed resolve (the\n * normal \"not installed\" case) is swallowed and reported as missing.\n */\nfunction isInstalled(packageName: string): boolean {\n try {\n projectRequire.resolve(packageName);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * A non-empty string, or `undefined`. Config values arrive from `env(...)` and\n * an unset variable yields `undefined` — but a `.env` line with nothing after\n * the `=` yields `\"\"`, which is \"not configured\" just as much.\n */\nfunction text(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\n/**\n * The database driver's package. `driver` is a closed union in cascade\n * (`mongodb | postgres | mysql`), so an unrecognised value means the config is\n * wrong in a way this check is not the right one to report.\n */\nfunction databasePeer(): RequiredPeer | undefined {\n const driver = text(config.get(\"database.driver\"));\n\n const packageName =\n driver === \"mongodb\"\n ? \"mongodb\"\n : driver === \"postgres\"\n ? \"pg\"\n : driver === \"mysql\"\n ? \"mysql2\"\n : undefined;\n\n return packageName ? { package: packageName, because: `database.driver = \"${driver}\"` } : undefined;\n}\n\n/**\n * The selected cache driver. Only the DEFAULT matters: `cache.drivers` is a\n * menu the scaffold fills in for every driver the framework ships, and warning\n * about a menu entry nobody selected is the false positive this whole check was\n * rewritten to remove.\n */\nfunction cachePeer(): RequiredPeer | undefined {\n const selected = text(config.get(\"cache.default\"));\n\n if (!selected || !/redis/i.test(selected)) return undefined;\n\n return { package: \"redis\", because: `cache.default = \"${selected}\"` };\n}\n\n/**\n * The selected storage driver, read through its canonical `driver` field\n * (`local` / `s3` / `r2` / `spaces`) rather than the key the app happened to\n * name it, so a driver called \"cdn\" is still recognised as S3.\n */\nfunction storagePeer(): RequiredPeer | undefined {\n const selected = text(config.get(\"storage.default\"));\n\n if (!selected) return undefined;\n\n const driver = text(config.get(`storage.drivers.${selected}.driver`));\n\n if (!driver || driver === \"local\") return undefined;\n\n return {\n package: \"@aws-sdk/client-s3\",\n because: `storage.default = \"${selected}\" (driver \"${driver}\")`,\n };\n}\n\n/**\n * The mail transport. A `mail` config with no host and no driver is the\n * scaffold's placeholder — every field is an unset env var — so it asks for\n * nothing and this stays quiet.\n */\nfunction mailPeer(): RequiredPeer | undefined {\n const driver = text(config.get(\"mail.driver\"));\n\n if (driver === \"ses\") {\n return { package: \"@aws-sdk/client-sesv2\", because: `mail.driver = \"ses\"` };\n }\n\n const host = text(config.get(\"mail.host\"));\n\n if (!host) return undefined;\n\n return { package: \"nodemailer\", because: `mail.host = \"${host}\"` };\n}\n\n/**\n * Realtime sockets. The connector no-ops without a `socket` config, so the\n * presence of that config is exactly the request for the peer.\n */\nfunction socketPeer(): RequiredPeer | undefined {\n if (!config.get(\"socket\")) return undefined;\n\n return { package: \"socket.io\", because: \"a socket config is present\" };\n}\n\n/**\n * Every peer THIS project's configuration asks for.\n */\nfunction requiredPeers(): RequiredPeer[] {\n return [databasePeer(), cachePeer(), storagePeer(), mailPeer(), socketPeer()].filter(\n (peer): peer is RequiredPeer => Boolean(peer),\n );\n}\n\n/**\n * Checks that every optional peer the project's own configuration selects is\n * actually installed.\n *\n * NEEDS NO BOOTED APP — it reads config and resolves module paths. It runs after\n * the boot pass only because config is loaded by then.\n *\n * WHAT THIS REPLACED, AND WHY: the previous version probed nine packages and\n * warned about every one that was absent. Every one of them is an OPTIONAL\n * peer — a fresh app installs none of them by design — so the check warned, at\n * length, about the framework's own default state. \"No MySQL driver installed\"\n * is not a diagnosis for someone who never wanted MySQL; it is noise that\n * teaches the reader to skim past the line where a real finding will one day\n * appear.\n *\n * Inverted, it becomes a genuine one: a project whose config selects a driver\n * whose package is missing is broken — it will throw the first time that\n * feature is used — so a missing peer here is a `fail`, not a warning.\n */\nexport const optionalPeersCheck: DoctorCheck = {\n name: \"optional-peers\",\n run: () => {\n const required = requiredPeers();\n\n if (required.length === 0) {\n return undefined;\n }\n\n const missing = required.filter((peer) => !isInstalled(peer.package));\n\n const status: CheckStatus = missing.length > 0 ? \"fail\" : \"ok\";\n\n if (missing.length > 0) {\n return {\n name: \"optional-peers\",\n status,\n detail:\n `${missing.length} configured feature(s) have no driver installed:\\n` +\n missing.map((peer) => ` - ${peer.package} — required by ${peer.because}`).join(\"\\n\"),\n };\n }\n\n return {\n name: \"optional-peers\",\n status,\n detail: `every configured driver is installed (${required\n .map((peer) => peer.package)\n .join(\", \")})`,\n };\n },\n};\n"],"mappings":";;;;;;;;AAmBA,MAAM,iBAAiB,cAAc,GAAG,QAAQ,IAAI,EAAE,cAAc;;;;;AAMpE,SAAS,YAAY,aAA8B;CACjD,IAAI;EACF,eAAe,QAAQ,WAAW;EAClC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;AAOA,SAAS,KAAK,OAAoC;CAChD,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;;;;;;AAOA,SAAS,eAAyC;CAChD,MAAM,SAAS,KAAK,OAAO,IAAI,iBAAiB,CAAC;CAEjD,MAAM,cACJ,WAAW,YACP,YACA,WAAW,aACT,OACA,WAAW,UACT,WACA;CAEV,OAAO,cAAc;EAAE,SAAS;EAAa,SAAS,sBAAsB,OAAO;CAAG,IAAI;AAC5F;;;;;;;AAQA,SAAS,YAAsC;CAC7C,MAAM,WAAW,KAAK,OAAO,IAAI,eAAe,CAAC;CAEjD,IAAI,CAAC,YAAY,CAAC,SAAS,KAAK,QAAQ,GAAG,OAAO;CAElD,OAAO;EAAE,SAAS;EAAS,SAAS,oBAAoB,SAAS;CAAG;AACtE;;;;;;AAOA,SAAS,cAAwC;CAC/C,MAAM,WAAW,KAAK,OAAO,IAAI,iBAAiB,CAAC;CAEnD,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,SAAS,KAAK,OAAO,IAAI,mBAAmB,SAAS,QAAQ,CAAC;CAEpE,IAAI,CAAC,UAAU,WAAW,SAAS,OAAO;CAE1C,OAAO;EACL,SAAS;EACT,SAAS,sBAAsB,SAAS,aAAa,OAAO;CAC9D;AACF;;;;;;AAOA,SAAS,WAAqC;CAG5C,IAFe,KAAK,OAAO,IAAI,aAAa,CAEnC,MAAM,OACb,OAAO;EAAE,SAAS;EAAyB,SAAS;CAAsB;CAG5E,MAAM,OAAO,KAAK,OAAO,IAAI,WAAW,CAAC;CAEzC,IAAI,CAAC,MAAM,OAAO;CAElB,OAAO;EAAE,SAAS;EAAc,SAAS,gBAAgB,KAAK;CAAG;AACnE;;;;;AAMA,SAAS,aAAuC;CAC9C,IAAI,CAAC,OAAO,IAAI,QAAQ,GAAG,OAAO;CAElC,OAAO;EAAE,SAAS;EAAa,SAAS;CAA6B;AACvE;;;;AAKA,SAAS,gBAAgC;CACvC,OAAO;EAAC,aAAa;EAAG,UAAU;EAAG,YAAY;EAAG,SAAS;EAAG,WAAW;CAAC,CAAC,CAAC,QAC3E,SAA+B,QAAQ,IAAI,CAC9C;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,qBAAkC;CAC7C,MAAM;CACN,WAAW;EACT,MAAM,WAAW,cAAc;EAE/B,IAAI,SAAS,WAAW,GACtB;EAGF,MAAM,UAAU,SAAS,QAAQ,SAAS,CAAC,YAAY,KAAK,OAAO,CAAC;EAEpE,MAAM,SAAsB,QAAQ,SAAS,IAAI,SAAS;EAE1D,IAAI,QAAQ,SAAS,GACnB,OAAO;GACL,MAAM;GACN;GACA,QACE,GAAG,QAAQ,OAAO,sDAClB,QAAQ,KAAK,SAAS,OAAO,KAAK,QAAQ,iBAAiB,KAAK,SAAS,CAAC,CAAC,KAAK,IAAI;EACxF;EAGF,OAAO;GACL,MAAM;GACN;GACA,QAAQ,yCAAyC,SAC9C,KAAK,SAAS,KAAK,OAAO,CAAC,CAC3B,KAAK,IAAI,EAAE;EAChB;CACF;AACF"}
1
+ {"version":3,"file":"optional-peers.check.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/optional-peers.check.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { config } from \"../../../../config/config-getter\";\nimport type { CheckStatus, DoctorCheck } from \"../check.types\";\n\n/**\n * A package this project's own configuration requires, and what asked for it.\n */\ntype RequiredPeer = {\n /** The npm package name to probe via `require.resolve`. */\n package: string;\n\n /** The config that asked for it, quoted back to the user verbatim. */\n because: string;\n};\n\n/**\n * Resolve from the project's working directory, not from core's own\n * `node_modules`, so the probe reflects what the *consuming app* has installed.\n */\nconst projectRequire = createRequire(`${process.cwd()}/package.json`);\n\n/**\n * Whether a package can be resolved from the project. A failed resolve (the\n * normal \"not installed\" case) is swallowed and reported as missing.\n */\nfunction isInstalled(packageName: string): boolean {\n try {\n projectRequire.resolve(packageName);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * A non-empty string, or `undefined`. Config values arrive from `env(...)` and\n * an unset variable yields `undefined` — but a `.env` line with nothing after\n * the `=` yields `\"\"`, which is \"not configured\" just as much.\n */\nfunction text(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\n/**\n * The database driver's package. `driver` is a closed union in cascade\n * (`mongodb | postgres | mysql`), so an unrecognised value means the config is\n * wrong in a way this check is not the right one to report.\n */\nfunction databasePeer(): RequiredPeer | undefined {\n const driver = text(config.get(\"database.driver\"));\n\n const packageName =\n driver === \"mongodb\"\n ? \"mongodb\"\n : driver === \"postgres\"\n ? \"pg\"\n : driver === \"mysql\"\n ? \"mysql2\"\n : undefined;\n\n return packageName\n ? { package: packageName, because: `database.driver = \"${driver}\"` }\n : undefined;\n}\n\n/**\n * The selected cache driver. Only the DEFAULT matters: `cache.drivers` is a\n * menu the scaffold fills in for every driver the framework ships, and warning\n * about a menu entry nobody selected is the false positive this whole check was\n * rewritten to remove.\n */\nfunction cachePeer(): RequiredPeer | undefined {\n const selected = text(config.get(\"cache.default\"));\n\n if (!selected || !/redis/i.test(selected)) return undefined;\n\n return { package: \"redis\", because: `cache.default = \"${selected}\"` };\n}\n\n/**\n * The selected storage driver, read through its canonical `driver` field\n * (`local` / `s3` / `r2` / `spaces`) rather than the key the app happened to\n * name it, so a driver called \"cdn\" is still recognised as S3.\n */\nfunction storagePeer(): RequiredPeer | undefined {\n const selected = text(config.get(\"storage.default\"));\n\n if (!selected) return undefined;\n\n const driver = text(config.get(`storage.drivers.${selected}.driver`));\n\n if (!driver || driver === \"local\") return undefined;\n\n return {\n package: \"@aws-sdk/client-s3\",\n because: `storage.default = \"${selected}\" (driver \"${driver}\")`,\n };\n}\n\n/**\n * The mail transport. A `mail` config with no host and no driver is the\n * scaffold's placeholder — every field is an unset env var — so it asks for\n * nothing and this stays quiet.\n */\nfunction mailPeer(): RequiredPeer | undefined {\n const driver = text(config.get(\"mail.driver\"));\n\n if (driver === \"ses\") {\n return { package: \"@aws-sdk/client-sesv2\", because: `mail.driver = \"ses\"` };\n }\n\n const host = text(config.get(\"mail.host\"));\n\n if (!host) return undefined;\n\n return { package: \"nodemailer\", because: `mail.host = \"${host}\"` };\n}\n\n/**\n * Realtime sockets. The connector no-ops without a `socket` config, so the\n * presence of that config is exactly the request for the peer.\n */\nfunction socketPeer(): RequiredPeer | undefined {\n if (!config.get(\"socket\")) return undefined;\n\n return { package: \"socket.io\", because: \"a socket config is present\" };\n}\n\n/**\n * Every peer THIS project's configuration asks for.\n */\nfunction requiredPeers(): RequiredPeer[] {\n return [databasePeer(), cachePeer(), storagePeer(), mailPeer(), socketPeer()].filter(\n (peer): peer is RequiredPeer => Boolean(peer),\n );\n}\n\n/**\n * Checks that every optional peer the project's own configuration selects is\n * actually installed.\n *\n * NEEDS NO BOOTED APP — it reads config and resolves module paths. It runs after\n * the boot pass only because config is loaded by then.\n *\n * WHAT THIS REPLACED, AND WHY: the previous version probed nine packages and\n * warned about every one that was absent. Every one of them is an OPTIONAL\n * peer — a fresh app installs none of them by design — so the check warned, at\n * length, about the framework's own default state. \"No MySQL driver installed\"\n * is not a diagnosis for someone who never wanted MySQL; it is noise that\n * teaches the reader to skim past the line where a real finding will one day\n * appear.\n *\n * Inverted, it becomes a genuine one: a project whose config selects a driver\n * whose package is missing is broken — it will throw the first time that\n * feature is used — so a missing peer here is a `fail`, not a warning.\n */\nexport const optionalPeersCheck: DoctorCheck = {\n name: \"optional-peers\",\n run: () => {\n const required = requiredPeers();\n\n if (required.length === 0) {\n return undefined;\n }\n\n const missing = required.filter((peer) => !isInstalled(peer.package));\n\n const status: CheckStatus = missing.length > 0 ? \"fail\" : \"ok\";\n\n if (missing.length > 0) {\n return {\n name: \"optional-peers\",\n status,\n detail:\n `${missing.length} configured feature(s) have no driver installed:\\n` +\n missing.map((peer) => ` - ${peer.package} — required by ${peer.because}`).join(\"\\n\"),\n };\n }\n\n return {\n name: \"optional-peers\",\n status,\n detail: `every configured driver is installed (${required\n .map((peer) => peer.package)\n .join(\", \")})`,\n };\n },\n};\n"],"mappings":";;;;;;;;AAmBA,MAAM,iBAAiB,cAAc,GAAG,QAAQ,IAAI,EAAE,cAAc;;;;;AAMpE,SAAS,YAAY,aAA8B;CACjD,IAAI;EACF,eAAe,QAAQ,WAAW;EAClC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;;;AAOA,SAAS,KAAK,OAAoC;CAChD,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;;;;;;AAOA,SAAS,eAAyC;CAChD,MAAM,SAAS,KAAK,OAAO,IAAI,iBAAiB,CAAC;CAEjD,MAAM,cACJ,WAAW,YACP,YACA,WAAW,aACT,OACA,WAAW,UACT,WACA;CAEV,OAAO,cACH;EAAE,SAAS;EAAa,SAAS,sBAAsB,OAAO;CAAG,IACjE;AACN;;;;;;;AAQA,SAAS,YAAsC;CAC7C,MAAM,WAAW,KAAK,OAAO,IAAI,eAAe,CAAC;CAEjD,IAAI,CAAC,YAAY,CAAC,SAAS,KAAK,QAAQ,GAAG,OAAO;CAElD,OAAO;EAAE,SAAS;EAAS,SAAS,oBAAoB,SAAS;CAAG;AACtE;;;;;;AAOA,SAAS,cAAwC;CAC/C,MAAM,WAAW,KAAK,OAAO,IAAI,iBAAiB,CAAC;CAEnD,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,SAAS,KAAK,OAAO,IAAI,mBAAmB,SAAS,QAAQ,CAAC;CAEpE,IAAI,CAAC,UAAU,WAAW,SAAS,OAAO;CAE1C,OAAO;EACL,SAAS;EACT,SAAS,sBAAsB,SAAS,aAAa,OAAO;CAC9D;AACF;;;;;;AAOA,SAAS,WAAqC;CAG5C,IAFe,KAAK,OAAO,IAAI,aAAa,CAEnC,MAAM,OACb,OAAO;EAAE,SAAS;EAAyB,SAAS;CAAsB;CAG5E,MAAM,OAAO,KAAK,OAAO,IAAI,WAAW,CAAC;CAEzC,IAAI,CAAC,MAAM,OAAO;CAElB,OAAO;EAAE,SAAS;EAAc,SAAS,gBAAgB,KAAK;CAAG;AACnE;;;;;AAMA,SAAS,aAAuC;CAC9C,IAAI,CAAC,OAAO,IAAI,QAAQ,GAAG,OAAO;CAElC,OAAO;EAAE,SAAS;EAAa,SAAS;CAA6B;AACvE;;;;AAKA,SAAS,gBAAgC;CACvC,OAAO;EAAC,aAAa;EAAG,UAAU;EAAG,YAAY;EAAG,SAAS;EAAG,WAAW;CAAC,CAAC,CAAC,QAC3E,SAA+B,QAAQ,IAAI,CAC9C;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,qBAAkC;CAC7C,MAAM;CACN,WAAW;EACT,MAAM,WAAW,cAAc;EAE/B,IAAI,SAAS,WAAW,GACtB;EAGF,MAAM,UAAU,SAAS,QAAQ,SAAS,CAAC,YAAY,KAAK,OAAO,CAAC;EAEpE,MAAM,SAAsB,QAAQ,SAAS,IAAI,SAAS;EAE1D,IAAI,QAAQ,SAAS,GACnB,OAAO;GACL,MAAM;GACN;GACA,QACE,GAAG,QAAQ,OAAO,sDAClB,QAAQ,KAAK,SAAS,OAAO,KAAK,QAAQ,iBAAiB,KAAK,SAAS,CAAC,CAAC,KAAK,IAAI;EACxF;EAGF,OAAO;GACL,MAAM;GACN;GACA,QAAQ,yCAAyC,SAC9C,KAAK,SAAS,KAAK,OAAO,CAAC,CAC3B,KAAK,IAAI,EAAE;EAChB;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"release-hygiene.check.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/release-hygiene.check.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { rootPath } from \"../../../../utils/paths\";\nimport type { DoctorCheck } from \"../check.types\";\n\n/**\n * A `## x.y.z` changelog heading with an optional ` - YYYY-MM-DD` suffix.\n * Mirrors the shape parsed by the tier-0 release-hygiene unit guard; inlined\n * here because that guard lives in the test tree and is not importable from\n * `src`.\n */\ntype ChangelogHeading = {\n version: string;\n date?: string;\n};\n\n/**\n * The parts of `package.json` this check reads.\n */\ntype ProjectManifest = {\n version?: unknown;\n private?: unknown;\n name?: unknown;\n};\n\n/**\n * Parse the top-most `## x.y.z[ - YYYY-MM-DD]` heading out of a CHANGELOG body.\n * Returns `undefined` when no version heading is present.\n */\nfunction parseTopHeading(changelog: string): ChangelogHeading | undefined {\n const match = changelog.match(\n /^##\\s+(\\d+)\\.(\\d+)\\.(\\d+)(?:\\s*[-–]\\s*(\\d{4}-\\d{2}-\\d{2}))?\\s*$/m,\n );\n\n if (!match) {\n return undefined;\n }\n\n const [, major, minor, patch, date] = match;\n\n return {\n version: `${major}.${minor}.${patch}`,\n date,\n };\n}\n\n/**\n * Whether this project is something anyone could `npm publish`.\n *\n * THE GATE, and the reason it exists: a changelog is a promise to the people\n * who install your package. An application has no such people. Running this\n * check on every project projected the framework monorepo's own release\n * practice onto users building an app, who were told — on every single run,\n * forever — that their private application was missing a CHANGELOG.md it had no\n * reason to have. `\"private\": true` is exactly the field npm itself reads to\n * refuse a publish, so it is the honest test of whether the concern applies.\n */\nfunction isPublishable(manifest: ProjectManifest): boolean {\n return manifest.private !== true && typeof manifest.name === \"string\";\n}\n\n/**\n * Checks that a PUBLISHABLE package's `package.json` version matches the\n * top-most `## x.y.z` heading in `CHANGELOG.md` — the same version↔changelog\n * invariant the release-hygiene unit guard enforces, surfaced as a pre-release\n * doctor check.\n *\n * NEEDS NO BOOTED APP — it reads two files from the project root.\n *\n * Verdicts:\n * - `\"private\": true`, or no package name → not applicable, no line at all;\n * - no `CHANGELOG.md` → `warn` (recommended for a published package, not\n * mandatory);\n * - no parseable heading → `warn`;\n * - heading version ≠ package.json version → `fail`;\n * - otherwise → `ok`.\n */\nexport const releaseHygieneCheck: DoctorCheck = {\n name: \"release-hygiene\",\n run: () => {\n const packageJsonPath = rootPath(\"package.json\");\n const changelogPath = rootPath(\"CHANGELOG.md\");\n\n const manifest: ProjectManifest = JSON.parse(readFileSync(packageJsonPath, \"utf8\"));\n\n if (!isPublishable(manifest)) {\n return undefined;\n }\n\n const pkgVersion = manifest.version;\n\n if (typeof pkgVersion !== \"string\") {\n return {\n name: \"release-hygiene\",\n status: \"fail\",\n detail: \"package.json is publishable but has no string version field\",\n };\n }\n\n if (!existsSync(changelogPath)) {\n return {\n name: \"release-hygiene\",\n status: \"warn\",\n detail: `no CHANGELOG.md at project root (package.json is ${pkgVersion})`,\n };\n }\n\n const changelog = readFileSync(changelogPath, \"utf8\");\n const heading = parseTopHeading(changelog);\n\n if (!heading) {\n return {\n name: \"release-hygiene\",\n status: \"warn\",\n detail: \"CHANGELOG.md has no parseable `## x.y.z` heading\",\n };\n }\n\n if (heading.version !== pkgVersion) {\n return {\n name: \"release-hygiene\",\n status: \"fail\",\n detail: `package.json ${pkgVersion} ≠ top CHANGELOG heading ${heading.version}`,\n };\n }\n\n return {\n name: \"release-hygiene\",\n status: \"ok\",\n detail: `package.json and CHANGELOG agree on ${pkgVersion}`,\n };\n },\n};\n"],"mappings":";;;;;;;;AA4BA,SAAS,gBAAgB,WAAiD;CACxE,MAAM,QAAQ,UAAU,MACtB,kEACF;CAEA,IAAI,CAAC,OACH;CAGF,MAAM,GAAG,OAAO,OAAO,OAAO,QAAQ;CAEtC,OAAO;EACL,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG;EAC9B;CACF;AACF;;;;;;;;;;;;AAaA,SAAS,cAAc,UAAoC;CACzD,OAAO,SAAS,YAAY,QAAQ,OAAO,SAAS,SAAS;AAC/D;;;;;;;;;;;;;;;;;AAkBA,MAAa,sBAAmC;CAC9C,MAAM;CACN,WAAW;EACT,MAAM,kBAAkB,SAAS,cAAc;EAC/C,MAAM,gBAAgB,SAAS,cAAc;EAE7C,MAAM,WAA4B,KAAK,MAAM,aAAa,iBAAiB,MAAM,CAAC;EAElF,IAAI,CAAC,cAAc,QAAQ,GACzB;EAGF,MAAM,aAAa,SAAS;EAE5B,IAAI,OAAO,eAAe,UACxB,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;EACV;EAGF,IAAI,CAAC,WAAW,aAAa,GAC3B,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,oDAAoD,WAAW;EACzE;EAIF,MAAM,UAAU,gBADE,aAAa,eAAe,MACN,CAAC;EAEzC,IAAI,CAAC,SACH,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;EACV;EAGF,IAAI,QAAQ,YAAY,YACtB,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,gBAAgB,WAAW,2BAA2B,QAAQ;EACxE;EAGF,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,uCAAuC;EACjD;CACF;AACF"}
1
+ {"version":3,"file":"release-hygiene.check.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/release-hygiene.check.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { rootPath } from \"../../../../utils/paths\";\nimport type { DoctorCheck } from \"../check.types\";\n\n/**\n * A `## x.y.z` changelog heading with an optional ` - YYYY-MM-DD` suffix.\n * Mirrors the shape parsed by the tier-0 release-hygiene unit guard; inlined\n * here because that guard lives in the test tree and is not importable from\n * `src`.\n */\ntype ChangelogHeading = {\n version: string;\n date?: string;\n};\n\n/**\n * The parts of `package.json` this check reads.\n */\ntype ProjectManifest = {\n version?: unknown;\n private?: unknown;\n name?: unknown;\n};\n\n/**\n * Parse the top-most `## x.y.z[ - YYYY-MM-DD]` heading out of a CHANGELOG body.\n * Returns `undefined` when no version heading is present.\n */\nfunction parseTopHeading(changelog: string): ChangelogHeading | undefined {\n const match = changelog.match(/^##\\s+(\\d+)\\.(\\d+)\\.(\\d+)(?:\\s*[-–]\\s*(\\d{4}-\\d{2}-\\d{2}))?\\s*$/m);\n\n if (!match) {\n return undefined;\n }\n\n const [, major, minor, patch, date] = match;\n\n return {\n version: `${major}.${minor}.${patch}`,\n date,\n };\n}\n\n/**\n * Whether this project is something anyone could `npm publish`.\n *\n * THE GATE, and the reason it exists: a changelog is a promise to the people\n * who install your package. An application has no such people. Running this\n * check on every project projected the framework monorepo's own release\n * practice onto users building an app, who were told — on every single run,\n * forever — that their private application was missing a CHANGELOG.md it had no\n * reason to have. `\"private\": true` is exactly the field npm itself reads to\n * refuse a publish, so it is the honest test of whether the concern applies.\n */\nfunction isPublishable(manifest: ProjectManifest): boolean {\n return manifest.private !== true && typeof manifest.name === \"string\";\n}\n\n/**\n * Checks that a PUBLISHABLE package's `package.json` version matches the\n * top-most `## x.y.z` heading in `CHANGELOG.md` — the same version↔changelog\n * invariant the release-hygiene unit guard enforces, surfaced as a pre-release\n * doctor check.\n *\n * NEEDS NO BOOTED APP — it reads two files from the project root.\n *\n * Verdicts:\n * - `\"private\": true`, or no package name → not applicable, no line at all;\n * - no `CHANGELOG.md` → `warn` (recommended for a published package, not\n * mandatory);\n * - no parseable heading → `warn`;\n * - heading version ≠ package.json version → `fail`;\n * - otherwise → `ok`.\n */\nexport const releaseHygieneCheck: DoctorCheck = {\n name: \"release-hygiene\",\n run: () => {\n const packageJsonPath = rootPath(\"package.json\");\n const changelogPath = rootPath(\"CHANGELOG.md\");\n\n const manifest: ProjectManifest = JSON.parse(readFileSync(packageJsonPath, \"utf8\"));\n\n if (!isPublishable(manifest)) {\n return undefined;\n }\n\n const pkgVersion = manifest.version;\n\n if (typeof pkgVersion !== \"string\") {\n return {\n name: \"release-hygiene\",\n status: \"fail\",\n detail: \"package.json is publishable but has no string version field\",\n };\n }\n\n if (!existsSync(changelogPath)) {\n return {\n name: \"release-hygiene\",\n status: \"warn\",\n detail: `no CHANGELOG.md at project root (package.json is ${pkgVersion})`,\n };\n }\n\n const changelog = readFileSync(changelogPath, \"utf8\");\n const heading = parseTopHeading(changelog);\n\n if (!heading) {\n return {\n name: \"release-hygiene\",\n status: \"warn\",\n detail: \"CHANGELOG.md has no parseable `## x.y.z` heading\",\n };\n }\n\n if (heading.version !== pkgVersion) {\n return {\n name: \"release-hygiene\",\n status: \"fail\",\n detail: `package.json ${pkgVersion} ≠ top CHANGELOG heading ${heading.version}`,\n };\n }\n\n return {\n name: \"release-hygiene\",\n status: \"ok\",\n detail: `package.json and CHANGELOG agree on ${pkgVersion}`,\n };\n },\n};\n"],"mappings":";;;;;;;;AA4BA,SAAS,gBAAgB,WAAiD;CACxE,MAAM,QAAQ,UAAU,MAAM,kEAAkE;CAEhG,IAAI,CAAC,OACH;CAGF,MAAM,GAAG,OAAO,OAAO,OAAO,QAAQ;CAEtC,OAAO;EACL,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG;EAC9B;CACF;AACF;;;;;;;;;;;;AAaA,SAAS,cAAc,UAAoC;CACzD,OAAO,SAAS,YAAY,QAAQ,OAAO,SAAS,SAAS;AAC/D;;;;;;;;;;;;;;;;;AAkBA,MAAa,sBAAmC;CAC9C,MAAM;CACN,WAAW;EACT,MAAM,kBAAkB,SAAS,cAAc;EAC/C,MAAM,gBAAgB,SAAS,cAAc;EAE7C,MAAM,WAA4B,KAAK,MAAM,aAAa,iBAAiB,MAAM,CAAC;EAElF,IAAI,CAAC,cAAc,QAAQ,GACzB;EAGF,MAAM,aAAa,SAAS;EAE5B,IAAI,OAAO,eAAe,UACxB,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;EACV;EAGF,IAAI,CAAC,WAAW,aAAa,GAC3B,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,oDAAoD,WAAW;EACzE;EAIF,MAAM,UAAU,gBADE,aAAa,eAAe,MACN,CAAC;EAEzC,IAAI,CAAC,SACH,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;EACV;EAGF,IAAI,QAAQ,YAAY,YACtB,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,gBAAgB,WAAW,2BAA2B,QAAQ;EACxE;EAGF,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,uCAAuC;EACjD;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"column-dsl-parser.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/generate/generators/column-dsl-parser.ts"],"sourcesContent":["export type ParsedColumn = {\n name: string;\n helper: string; // e.g. \"text\", \"boolCol\", \"decimal\"\n modifiers: string[]; // e.g. [\".nullable()\", \".unsigned()\"]\n};\n\nconst typeMapping: Record<string, string> = {\n text: \"text\",\n string: \"string\",\n integer: \"integer\",\n int: \"integer\",\n bigInteger: \"bigInteger\",\n bigInt: \"bigInteger\",\n boolean: \"boolCol\",\n bool: \"boolCol\",\n uuid: \"uuid\",\n timestamp: \"timestamp\",\n date: \"date\",\n json: \"json\",\n object: \"objectCol\",\n decimal: \"decimal\",\n float: \"float\",\n enum: \"enumCol\",\n set: \"setCol\",\n blob: \"blobCol\",\n binary: \"blobCol\",\n};\n\n/**\n * Parses a subset DSL for column schemas:\n * \"phone:text:nullable,price:decimal:notNullable:unsigned\"\n */\nexport function parseColumnDsl(input: string): ParsedColumn[] {\n if (!input) return [];\n\n const columns = input.split(\",\").map((c) => c.trim()).filter(Boolean);\n \n return columns.map((colStr) => {\n const parts = colStr.split(\":\").map((p) => p.trim());\n const name = parts[0];\n const rawType = parts[1] || \"string\";\n const helper = typeMapping[rawType] || rawType; // fallback to raw string if not mapped\n\n const modifiers: string[] = [];\n for (let i = 2; i < parts.length; i++) {\n let modifier = parts[i];\n // Basic modifiers that translates to builder calls\n modifiers.push(`.${modifier}()`);\n }\n\n return {\n name,\n helper,\n modifiers,\n };\n });\n}\n"],"mappings":";AAMA,MAAM,cAAsC;CAC1C,MAAM;CACN,QAAQ;CACR,SAAS;CACT,KAAK;CACL,YAAY;CACZ,QAAQ;CACR,SAAS;CACT,MAAM;CACN,MAAM;CACN,WAAW;CACX,MAAM;CACN,MAAM;CACN,QAAQ;CACR,SAAS;CACT,OAAO;CACP,MAAM;CACN,KAAK;CACL,MAAM;CACN,QAAQ;AACV;;;;;AAMA,SAAgB,eAAe,OAA+B;CAC5D,IAAI,CAAC,OAAO,OAAO,CAAC;CAIpB,OAFgB,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,OAEhD,CAAC,CAAC,KAAK,WAAW;EAC7B,MAAM,QAAQ,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC;EACnD,MAAM,OAAO,MAAM;EACnB,MAAM,UAAU,MAAM,MAAM;EAC5B,MAAM,SAAS,YAAY,YAAY;EAEvC,MAAM,YAAsB,CAAC;EAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,IAAI,WAAW,MAAM;GAErB,UAAU,KAAK,IAAI,SAAS,GAAG;EACjC;EAEA,OAAO;GACL;GACA;GACA;EACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"column-dsl-parser.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/generate/generators/column-dsl-parser.ts"],"sourcesContent":["export type ParsedColumn = {\n name: string;\n helper: string; // e.g. \"text\", \"boolCol\", \"decimal\"\n modifiers: string[]; // e.g. [\".nullable()\", \".unsigned()\"]\n};\n\nconst typeMapping: Record<string, string> = {\n text: \"text\",\n string: \"string\",\n integer: \"integer\",\n int: \"integer\",\n bigInteger: \"bigInteger\",\n bigInt: \"bigInteger\",\n boolean: \"boolCol\",\n bool: \"boolCol\",\n uuid: \"uuid\",\n timestamp: \"timestamp\",\n date: \"date\",\n json: \"json\",\n object: \"objectCol\",\n decimal: \"decimal\",\n float: \"float\",\n enum: \"enumCol\",\n set: \"setCol\",\n blob: \"blobCol\",\n binary: \"blobCol\",\n};\n\n/**\n * Parses a subset DSL for column schemas:\n * \"phone:text:nullable,price:decimal:notNullable:unsigned\"\n */\nexport function parseColumnDsl(input: string): ParsedColumn[] {\n if (!input) return [];\n\n const columns = input\n .split(\",\")\n .map((c) => c.trim())\n .filter(Boolean);\n\n return columns.map((colStr) => {\n const parts = colStr.split(\":\").map((p) => p.trim());\n const name = parts[0];\n const rawType = parts[1] || \"string\";\n const helper = typeMapping[rawType] || rawType; // fallback to raw string if not mapped\n\n const modifiers: string[] = [];\n for (let i = 2; i < parts.length; i++) {\n let modifier = parts[i];\n // Basic modifiers that translates to builder calls\n modifiers.push(`.${modifier}()`);\n }\n\n return {\n name,\n helper,\n modifiers,\n };\n });\n}\n"],"mappings":";AAMA,MAAM,cAAsC;CAC1C,MAAM;CACN,QAAQ;CACR,SAAS;CACT,KAAK;CACL,YAAY;CACZ,QAAQ;CACR,SAAS;CACT,MAAM;CACN,MAAM;CACN,WAAW;CACX,MAAM;CACN,MAAM;CACN,QAAQ;CACR,SAAS;CACT,OAAO;CACP,MAAM;CACN,KAAK;CACL,MAAM;CACN,QAAQ;AACV;;;;;AAMA,SAAgB,eAAe,OAA+B;CAC5D,IAAI,CAAC,OAAO,OAAO,CAAC;CAOpB,OALgB,MACb,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,OAAO,OAEG,CAAC,CAAC,KAAK,WAAW;EAC7B,MAAM,QAAQ,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC;EACnD,MAAM,OAAO,MAAM;EACnB,MAAM,UAAU,MAAM,MAAM;EAC5B,MAAM,SAAS,YAAY,YAAY;EAEvC,MAAM,YAAsB,CAAC;EAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,IAAI,WAAW,MAAM;GAErB,UAAU,KAAK,IAAI,SAAS,GAAG;EACjC;EAEA,OAAO;GACL;GACA;GACA;EACF;CACF,CAAC;AACH"}