@warlock.js/core 5.0.1 → 5.1.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 (120) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/esm/cli/cli-commands.manager.mjs +36 -1
  3. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  4. package/esm/cli/commands/dev-server.command.mjs +1 -1
  5. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +207 -0
  6. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -0
  7. package/esm/cli/commands/doctor/checks/config.check.mjs +5 -2
  8. package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
  9. package/esm/cli/commands/doctor/checks/connectors.check.mjs +38 -12
  10. package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
  11. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +8 -4
  12. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
  13. package/esm/cli/commands/doctor/checks/health.check.mjs +77 -11
  14. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  15. package/esm/cli/commands/doctor/checks/index.mjs +5 -0
  16. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  17. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +116 -53
  18. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +27 -9
  20. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
  21. package/esm/cli/commands/doctor/checks/routes.check.mjs +51 -12
  22. package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
  23. package/esm/cli/commands/doctor/doctor-command.action.mjs +16 -6
  24. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  25. package/esm/cli/commands/doctor/run-checks.mjs +15 -6
  26. package/esm/cli/commands/doctor/run-checks.mjs.map +1 -1
  27. package/esm/cli/commands/doctor.command.mjs +17 -9
  28. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  29. package/esm/connectors/describe-server-address.mjs +89 -6
  30. package/esm/connectors/describe-server-address.mjs.map +1 -1
  31. package/esm/connectors/http-connector.d.mts +18 -0
  32. package/esm/connectors/http-connector.d.mts.map +1 -1
  33. package/esm/connectors/http-connector.mjs +36 -3
  34. package/esm/connectors/http-connector.mjs.map +1 -1
  35. package/esm/dev-server/dev-logger.mjs +1 -4
  36. package/esm/dev-server/dev-logger.mjs.map +1 -1
  37. package/esm/dev-server/development-server.mjs +3 -7
  38. package/esm/dev-server/development-server.mjs.map +1 -1
  39. package/esm/dev-server/module-loader.mjs +1 -1
  40. package/esm/dev-server/ready-block.mjs +62 -0
  41. package/esm/dev-server/ready-block.mjs.map +1 -0
  42. package/esm/generations/add-command.action.mjs +2 -575
  43. package/esm/generations/add-command.action.mjs.map +1 -1
  44. package/esm/generations/features/access.feature.mjs +76 -0
  45. package/esm/generations/features/access.feature.mjs.map +1 -0
  46. package/esm/generations/features/ai-anthropic.feature.mjs +10 -0
  47. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -0
  48. package/esm/generations/features/ai-bedrock.feature.mjs +10 -0
  49. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -0
  50. package/esm/generations/features/ai-google.feature.mjs +10 -0
  51. package/esm/generations/features/ai-google.feature.mjs.map +1 -0
  52. package/esm/generations/features/ai-ollama.feature.mjs +10 -0
  53. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -0
  54. package/esm/generations/features/ai-openai.feature.mjs +10 -0
  55. package/esm/generations/features/ai-openai.feature.mjs.map +1 -0
  56. package/esm/generations/features/ai-panoptic.feature.mjs +13 -0
  57. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -0
  58. package/esm/generations/features/ai-tools.feature.mjs +13 -0
  59. package/esm/generations/features/ai-tools.feature.mjs.map +1 -0
  60. package/esm/generations/features/ai-workspace.feature.mjs +13 -0
  61. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -0
  62. package/esm/generations/features/ai.feature.mjs +15 -0
  63. package/esm/generations/features/ai.feature.mjs.map +1 -0
  64. package/esm/generations/features/herald.feature.mjs +19 -0
  65. package/esm/generations/features/herald.feature.mjs.map +1 -0
  66. package/esm/generations/features/image.feature.mjs +9 -0
  67. package/esm/generations/features/image.feature.mjs.map +1 -0
  68. package/esm/generations/features/index.mjs +72 -0
  69. package/esm/generations/features/index.mjs.map +1 -0
  70. package/esm/generations/features/mail.feature.mjs +10 -0
  71. package/esm/generations/features/mail.feature.mjs.map +1 -0
  72. package/esm/generations/features/mongodb.feature.mjs +9 -0
  73. package/esm/generations/features/mongodb.feature.mjs.map +1 -0
  74. package/esm/generations/features/mysql.feature.mjs +9 -0
  75. package/esm/generations/features/mysql.feature.mjs.map +1 -0
  76. package/esm/generations/features/notifications.feature.mjs +43 -0
  77. package/esm/generations/features/notifications.feature.mjs.map +1 -0
  78. package/esm/generations/features/postgres.feature.mjs +9 -0
  79. package/esm/generations/features/postgres.feature.mjs.map +1 -0
  80. package/esm/generations/features/react-email.feature.mjs +70 -0
  81. package/esm/generations/features/react-email.feature.mjs.map +1 -0
  82. package/esm/generations/features/react.feature.mjs +16 -0
  83. package/esm/generations/features/react.feature.mjs.map +1 -0
  84. package/esm/generations/features/redis.feature.mjs +9 -0
  85. package/esm/generations/features/redis.feature.mjs.map +1 -0
  86. package/esm/generations/features/s3.feature.mjs +13 -0
  87. package/esm/generations/features/s3.feature.mjs.map +1 -0
  88. package/esm/generations/features/scheduler.feature.mjs +9 -0
  89. package/esm/generations/features/scheduler.feature.mjs.map +1 -0
  90. package/esm/generations/features/ses.feature.mjs +9 -0
  91. package/esm/generations/features/ses.feature.mjs.map +1 -0
  92. package/esm/generations/features/shadcn.feature.mjs +397 -0
  93. package/esm/generations/features/shadcn.feature.mjs.map +1 -0
  94. package/esm/generations/features/shared/link-ai-package-import.mjs +37 -0
  95. package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -0
  96. package/esm/generations/features/shared/migration-timestamp.mjs +17 -0
  97. package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -0
  98. package/esm/generations/features/socket.feature.mjs +15 -0
  99. package/esm/generations/features/socket.feature.mjs.map +1 -0
  100. package/esm/generations/features/tailwind.feature.mjs +174 -0
  101. package/esm/generations/features/tailwind.feature.mjs.map +1 -0
  102. package/esm/generations/features/test.feature.mjs +96 -0
  103. package/esm/generations/features/test.feature.mjs.map +1 -0
  104. package/esm/generations/features/web.feature.mjs +179 -0
  105. package/esm/generations/features/web.feature.mjs.map +1 -0
  106. package/esm/generations/stubs.mjs +95 -8
  107. package/esm/generations/stubs.mjs.map +1 -1
  108. package/esm/http/index.d.mts +1 -0
  109. package/esm/http/index.mjs +1 -0
  110. package/esm/http/ready-report.d.mts +56 -0
  111. package/esm/http/ready-report.d.mts.map +1 -0
  112. package/esm/http/ready-report.mjs +24 -0
  113. package/esm/http/ready-report.mjs.map +1 -0
  114. package/esm/index.d.mts +2 -1
  115. package/esm/index.mjs +2 -1
  116. package/llms-full.txt +322 -1
  117. package/llms.txt +2 -0
  118. package/package.json +12 -12
  119. package/skills/use-request-locals/SKILL.md +24 -1
  120. package/skills/warlock-conventions/SKILL.md +5 -1
@@ -1 +1 @@
1
- {"version":3,"file":"routes.check.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/routes.check.ts"],"sourcesContent":["import { router } from \"../../../../router/router\";\nimport type { DoctorCheck } from \"../check.types\";\n\n/**\n * Reports the number of registered routes. A booted app with zero routes\n * almost always means a route module failed to register silently, so this\n * warns (rather than passing) when the route table is empty.\n *\n * Read-only: introspects the router via `routeCount()` only.\n */\nexport const routesCheck: DoctorCheck = {\n name: \"routes\",\n run: () => {\n const count = router.routeCount();\n\n if (count === 0) {\n return {\n name: \"routes\",\n status: \"warn\",\n detail: \"0 routes registereddid a route module fail to load?\",\n };\n }\n\n return {\n name: \"routes\",\n status: \"ok\",\n detail: `${count} registered`,\n };\n },\n};\n"],"mappings":";;;;;;;;;;AAUA,MAAa,cAA2B;CACtC,MAAM;CACN,WAAW;EACT,MAAM,QAAQ,OAAO,WAAW;EAEhC,IAAI,UAAU,GACZ,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;EACV;EAGF,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,GAAG,MAAM;EACnB;CACF;AACF"}
1
+ {"version":3,"file":"routes.check.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/doctor/checks/routes.check.ts"],"sourcesContent":["import type { DoctorBootContext, DoctorCheck } from \"../check.types\";\n\n/**\n * Describe the failed modules as an indented block, one per line, cause first.\n * The file that failed is the only thing that makes this actionable, so it is\n * never summarised away.\n */\nfunction describeFailures(context: DoctorBootContext): string {\n return context.moduleFailures\n .map((failure) => ` - ${failure.file}: ${failure.message}`)\n .join(\"\\n\");\n}\n\n/**\n * Reports the route surface the app will actually serve.\n *\n * NEEDS A BOOTED APP — and now gets one. Routes exist only as the side effect of\n * importing the module that declares them, so a doctor that did not import app\n * code reported `0 routes` for an app the dev server served fourteen routes\n * for. The boot pass imports the same modules, in the same order, and boots\n * (never starts) the same connectors, so the number printed here is the number\n * `warlock dev` prints for the same project — the two are read off the same\n * router after the same registrations.\n *\n * Verdicts:\n * - a module failed to import → `fail`, naming every file and its cause. This\n * is the finding doctor exists for: a route file that throws on import is\n * invisible at runtime, it just 404s;\n * - route modules on disk but nothing registered → `fail` (they loaded and\n * registered nothing, which is not a state a working app is in);\n * - no route modules at all → `ok`, stated plainly. A worker or CLI-only app is\n * a legitimate shape and must not be nagged;\n * - otherwise → `ok` with the count, split into what app code registered and\n * what connectors added.\n */\nexport const routesCheck: DoctorCheck = {\n name: \"routes\",\n run: (context) => {\n if (context.moduleFailures.length > 0) {\n const count = context.moduleFailures.length;\n\n return {\n name: \"routes\",\n status: \"fail\",\n detail:\n `${count} app module(s) failed to load — the routes they declare will 404:\\n` +\n describeFailures(context),\n };\n }\n\n if (!context.booted) {\n return {\n name: \"routes\",\n status: \"fail\",\n detail: \"app modules could not be discovered — nothing to report on\",\n };\n }\n\n if (context.totalRoutes === 0) {\n if (context.routeModules === 0) {\n return {\n name: \"routes\",\n status: \"ok\",\n detail: \"no route modules found (src/app/*/routes.ts) this app serves no HTTP routes\",\n };\n }\n\n return {\n name: \"routes\",\n status: \"fail\",\n detail:\n `${context.routeModules} route module(s) loaded without error but registered 0 routes — ` +\n \"every request will 404\",\n };\n }\n\n const fromConnectors = context.totalRoutes - context.appRoutes;\n const breakdown =\n fromConnectors > 0\n ? `${context.appRoutes} from ${context.routeModules} route module(s), ` +\n `${fromConnectors} from connectors`\n : `from ${context.routeModules} route module(s)`;\n\n return {\n name: \"routes\",\n status: \"ok\",\n detail: `${context.totalRoutes} registered (${breakdown})`,\n };\n },\n};\n"],"mappings":";;;;;;AAOA,SAAS,iBAAiB,SAAoC;CAC5D,OAAO,QAAQ,eACZ,KAAK,YAAY,OAAO,QAAQ,KAAK,IAAI,QAAQ,SAAS,CAAC,CAC3D,KAAK,IAAI;AACd;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,cAA2B;CACtC,MAAM;CACN,MAAM,YAAY;EAChB,IAAI,QAAQ,eAAe,SAAS,GAGlC,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QACE,GANU,QAAQ,eAAe,OAMxB,uEACT,iBAAiB,OAAO;EAC5B;EAGF,IAAI,CAAC,QAAQ,QACX,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;EACV;EAGF,IAAI,QAAQ,gBAAgB,GAAG;GAC7B,IAAI,QAAQ,iBAAiB,GAC3B,OAAO;IACL,MAAM;IACN,QAAQ;IACR,QAAQ;GACV;GAGF,OAAO;IACL,MAAM;IACN,QAAQ;IACR,QACE,GAAG,QAAQ,aAAa;GAE5B;EACF;EAEA,MAAM,iBAAiB,QAAQ,cAAc,QAAQ;EACrD,MAAM,YACJ,iBAAiB,IACb,GAAG,QAAQ,UAAU,QAAQ,QAAQ,aAAa,oBAC/C,eAAe,oBAClB,QAAQ,QAAQ,aAAa;EAEnC,OAAO;GACL,MAAM;GACN,QAAQ;GACR,QAAQ,GAAG,QAAQ,YAAY,eAAe,UAAU;EAC1D;CACF;AACF"}
@@ -1,24 +1,34 @@
1
+ import { bootForDiagnostics } from "./boot-for-diagnostics.mjs";
1
2
  import { defaultDoctorChecks } from "./checks/index.mjs";
2
3
  import { printReport } from "./format-report.mjs";
3
4
  import { runChecks } from "./run-checks.mjs";
4
5
 
5
6
  //#region ../core/src/cli/commands/doctor/doctor-command.action.ts
6
7
  /**
7
- * Action behind `warlock doctor`. Runs the default read-only diagnostic
8
- * checks, prints the grouped pass/warn/fail report, and — when any check
9
- * failed exits the process with a non-zero code so the command signals
10
- * failure to scripts/CI.
8
+ * Action behind `warlock doctor`.
9
+ *
10
+ * Two steps, in this order and for a reason: ONE boot pass, then every check
11
+ * against what that pass observed. The checks never boot anything themselves,
12
+ * so the report cannot contain two lines that inspected the app at different
13
+ * moments and contradicted each other — which is exactly what it used to
14
+ * contain (`routes: 0 registered` beside `health: /health + /ready
15
+ * registered`, in the same run, on an app serving fourteen routes).
16
+ *
17
+ * The pass boots every connector and starts none, so no database, cache or
18
+ * socket connection is opened and no port is bound. See
19
+ * `boot-for-diagnostics.ts`.
11
20
  *
12
21
  * A failing check is REPORTED (a line in the report + a non-zero exit), never
13
22
  * thrown: the runner converts any thrown check into a `fail` result first.
14
23
  *
15
24
  * On success this simply returns; the CLI manager then prints its own success
16
- * banner and exits zero.
25
+ * banner and exits zero — which also disposes of the handles the boot pass
26
+ * left open (a Fastify instance, a Vite dev server for `web` apps).
17
27
  *
18
28
  * @param _data Parsed CLI args (unused — doctor takes no positional args).
19
29
  */
20
30
  async function doctorCommandAction(_data) {
21
- const report = await runChecks(defaultDoctorChecks);
31
+ const report = await runChecks(defaultDoctorChecks, await bootForDiagnostics());
22
32
  printReport(report);
23
33
  if (report.hasFailures) process.exit(report.exitCode);
24
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-command.action.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/doctor/doctor-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\r\nimport { defaultDoctorChecks } from \"./checks\";\r\nimport { printReport } from \"./format-report\";\r\nimport { runChecks } from \"./run-checks\";\r\n\r\n/**\r\n * Action behind `warlock doctor`. Runs the default read-only diagnostic\r\n * checks, prints the grouped pass/warn/fail report, and — when any check\r\n * failed exits the process with a non-zero code so the command signals\r\n * failure to scripts/CI.\r\n *\r\n * A failing check is REPORTED (a line in the report + a non-zero exit), never\r\n * thrown: the runner converts any thrown check into a `fail` result first.\r\n *\r\n * On success this simply returns; the CLI manager then prints its own success\r\n * banner and exits zero.\r\n *\r\n * @param _data Parsed CLI args (unused — doctor takes no positional args).\r\n */\r\nexport async function doctorCommandAction(_data: CommandActionData): Promise<void> {\r\n const report = await runChecks(defaultDoctorChecks);\r\n\r\n printReport(report);\r\n\r\n if (report.hasFailures) {\r\n process.exit(report.exitCode);\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,eAAsB,oBAAoB,OAAyC;CACjF,MAAM,SAAS,MAAM,UAAU,mBAAmB;CAElD,YAAY,MAAM;CAElB,IAAI,OAAO,aACT,QAAQ,KAAK,OAAO,QAAQ;AAEhC"}
1
+ {"version":3,"file":"doctor-command.action.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/doctor/doctor-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\nimport { bootForDiagnostics } from \"./boot-for-diagnostics\";\nimport { defaultDoctorChecks } from \"./checks\";\nimport { printReport } from \"./format-report\";\nimport { runChecks } from \"./run-checks\";\n\n/**\n * Action behind `warlock doctor`.\n *\n * Two steps, in this order and for a reason: ONE boot pass, then every check\n * against what that pass observed. The checks never boot anything themselves,\n * so the report cannot contain two lines that inspected the app at different\n * moments and contradicted each other which is exactly what it used to\n * contain (`routes: 0 registered` beside `health: /health + /ready\n * registered`, in the same run, on an app serving fourteen routes).\n *\n * The pass boots every connector and starts none, so no database, cache or\n * socket connection is opened and no port is bound. See\n * `boot-for-diagnostics.ts`.\n *\n * A failing check is REPORTED (a line in the report + a non-zero exit), never\n * thrown: the runner converts any thrown check into a `fail` result first.\n *\n * On success this simply returns; the CLI manager then prints its own success\n * banner and exits zero — which also disposes of the handles the boot pass\n * left open (a Fastify instance, a Vite dev server for `web` apps).\n *\n * @param _data Parsed CLI args (unused — doctor takes no positional args).\n */\nexport async function doctorCommandAction(_data: CommandActionData): Promise<void> {\n const context = await bootForDiagnostics();\n\n const report = await runChecks(defaultDoctorChecks, context);\n\n printReport(report);\n\n if (report.hasFailures) {\n process.exit(report.exitCode);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,eAAsB,oBAAoB,OAAyC;CAGjF,MAAM,SAAS,MAAM,UAAU,qBAAqB,MAF9B,mBAAmB,CAEkB;CAE3D,YAAY,MAAM;CAElB,IAAI,OAAO,aACT,QAAQ,KAAK,OAAO,QAAQ;AAEhC"}
@@ -4,10 +4,13 @@
4
4
  * `fail` carrying the error message, never re-thrown. This is what keeps
5
5
  * `doctor` itself crash-proof — a broken probe degrades to a failed check
6
6
  * instead of taking the whole command down.
7
+ *
8
+ * A check that returns `undefined` does not apply to this project; the caller
9
+ * drops it from the report rather than printing a line about nothing.
7
10
  */
8
- async function runOne(check) {
11
+ async function runOne(check, context) {
9
12
  try {
10
- return await check.run();
13
+ return await check.run(context);
11
14
  } catch (error) {
12
15
  const message = error instanceof Error ? error.message : String(error);
13
16
  return {
@@ -20,16 +23,22 @@ async function runOne(check) {
20
23
  /**
21
24
  * Run every check and aggregate the results into a {@link DoctorReport}.
22
25
  *
23
- * Checks run in registration order. Each is isolated via {@link runOne}, so one
24
- * throwing check cannot abort the others. The exit code is `1` if any check
26
+ * Checks run in registration order against ONE shared boot context, so the
27
+ * report can never contain two checks that disagree about the app because they
28
+ * inspected it at different moments. Each is isolated via {@link runOne}, so
29
+ * one throwing check cannot abort the others. The exit code is `1` if any check
25
30
  * resolved to `fail`, otherwise `0` — warnings never fail the command.
26
31
  *
27
32
  * @param checks The diagnostic probes to execute.
33
+ * @param context What the boot pass observed.
28
34
  * @returns The aggregated report (results + per-status counts + exit code).
29
35
  */
30
- async function runChecks(checks) {
36
+ async function runChecks(checks, context) {
31
37
  const results = [];
32
- for (const check of checks) results.push(await runOne(check));
38
+ for (const check of checks) {
39
+ const result = await runOne(check, context);
40
+ if (result) results.push(result);
41
+ }
33
42
  const summary = {
34
43
  ok: 0,
35
44
  warn: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"run-checks.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/doctor/run-checks.ts"],"sourcesContent":["import type { CheckResult, CheckStatus, DoctorCheck, DoctorReport } from \"./check.types\";\n\n/**\n * Run one check defensively: a check that throws (or rejects) is recorded as a\n * `fail` carrying the error message, never re-thrown. This is what keeps\n * `doctor` itself crash-proof — a broken probe degrades to a failed check\n * instead of taking the whole command down.\n */\nasync function runOne(check: DoctorCheck): Promise<CheckResult> {\n try {\n return await check.run();\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n name: check.name,\n status: \"fail\",\n detail: `check threw: ${message}`,\n };\n }\n}\n\n/**\n * Run every check and aggregate the results into a {@link DoctorReport}.\n *\n * Checks run in registration order. Each is isolated via {@link runOne}, so one\n * throwing check cannot abort the others. The exit code is `1` if any check\n * resolved to `fail`, otherwise `0` — warnings never fail the command.\n *\n * @param checks The diagnostic probes to execute.\n * @returns The aggregated report (results + per-status counts + exit code).\n */\nexport async function runChecks(checks: DoctorCheck[]): Promise<DoctorReport> {\n const results: CheckResult[] = [];\n\n for (const check of checks) {\n results.push(await runOne(check));\n }\n\n const summary: Record<CheckStatus, number> = { ok: 0, warn: 0, fail: 0 };\n\n for (const result of results) {\n summary[result.status] += 1;\n }\n\n const hasFailures = summary.fail > 0;\n\n return {\n results,\n summary,\n hasFailures,\n exitCode: hasFailures ? 1 : 0,\n };\n}\n"],"mappings":";;;;;;;AAQA,eAAe,OAAO,OAA0C;CAC9D,IAAI;EACF,OAAO,MAAM,MAAM,IAAI;CACzB,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAErE,OAAO;GACL,MAAM,MAAM;GACZ,QAAQ;GACR,QAAQ,gBAAgB;EAC1B;CACF;AACF;;;;;;;;;;;AAYA,eAAsB,UAAU,QAA8C;CAC5E,MAAM,UAAyB,CAAC;CAEhC,KAAK,MAAM,SAAS,QAClB,QAAQ,KAAK,MAAM,OAAO,KAAK,CAAC;CAGlC,MAAM,UAAuC;EAAE,IAAI;EAAG,MAAM;EAAG,MAAM;CAAE;CAEvE,KAAK,MAAM,UAAU,SACnB,QAAQ,OAAO,WAAW;CAG5B,MAAM,cAAc,QAAQ,OAAO;CAEnC,OAAO;EACL;EACA;EACA;EACA,UAAU,cAAc,IAAI;CAC9B;AACF"}
1
+ {"version":3,"file":"run-checks.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/doctor/run-checks.ts"],"sourcesContent":["import type {\n CheckResult,\n CheckStatus,\n DoctorBootContext,\n DoctorCheck,\n DoctorReport,\n} from \"./check.types\";\n\n/**\n * Run one check defensively: a check that throws (or rejects) is recorded as a\n * `fail` carrying the error message, never re-thrown. This is what keeps\n * `doctor` itself crash-proof — a broken probe degrades to a failed check\n * instead of taking the whole command down.\n *\n * A check that returns `undefined` does not apply to this project; the caller\n * drops it from the report rather than printing a line about nothing.\n */\nasync function runOne(\n check: DoctorCheck,\n context: DoctorBootContext,\n): Promise<CheckResult | undefined> {\n try {\n return await check.run(context);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n name: check.name,\n status: \"fail\",\n detail: `check threw: ${message}`,\n };\n }\n}\n\n/**\n * Run every check and aggregate the results into a {@link DoctorReport}.\n *\n * Checks run in registration order against ONE shared boot context, so the\n * report can never contain two checks that disagree about the app because they\n * inspected it at different moments. Each is isolated via {@link runOne}, so\n * one throwing check cannot abort the others. The exit code is `1` if any check\n * resolved to `fail`, otherwise `0` — warnings never fail the command.\n *\n * @param checks The diagnostic probes to execute.\n * @param context What the boot pass observed.\n * @returns The aggregated report (results + per-status counts + exit code).\n */\nexport async function runChecks(\n checks: DoctorCheck[],\n context: DoctorBootContext,\n): Promise<DoctorReport> {\n const results: CheckResult[] = [];\n\n for (const check of checks) {\n const result = await runOne(check, context);\n\n // `undefined` is \"not applicable to this project\" — omitted entirely.\n if (result) {\n results.push(result);\n }\n }\n\n const summary: Record<CheckStatus, number> = { ok: 0, warn: 0, fail: 0 };\n\n for (const result of results) {\n summary[result.status] += 1;\n }\n\n const hasFailures = summary.fail > 0;\n\n return {\n results,\n summary,\n hasFailures,\n exitCode: hasFailures ? 1 : 0,\n };\n}\n"],"mappings":";;;;;;;;;;AAiBA,eAAe,OACb,OACA,SACkC;CAClC,IAAI;EACF,OAAO,MAAM,MAAM,IAAI,OAAO;CAChC,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAErE,OAAO;GACL,MAAM,MAAM;GACZ,QAAQ;GACR,QAAQ,gBAAgB;EAC1B;CACF;AACF;;;;;;;;;;;;;;AAeA,eAAsB,UACpB,QACA,SACuB;CACvB,MAAM,UAAyB,CAAC;CAEhC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,MAAM,OAAO,OAAO,OAAO;EAG1C,IAAI,QACF,QAAQ,KAAK,MAAM;CAEvB;CAEA,MAAM,UAAuC;EAAE,IAAI;EAAG,MAAM;EAAG,MAAM;CAAE;CAEvE,KAAK,MAAM,UAAU,SACnB,QAAQ,OAAO,WAAW;CAG5B,MAAM,cAAc,QAAQ,OAAO;CAEnC,OAAO;EACL;EACA;EACA;EACA,UAAU,cAAc,IAAI;CAC9B;AACF"}
@@ -3,21 +3,29 @@ import { doctorCommandAction } from "./doctor/doctor-command.action.mjs";
3
3
 
4
4
  //#region ../core/src/cli/commands/doctor.command.ts
5
5
  /**
6
- * `warlock doctor` — run a set of READ-ONLY diagnostic checks (routes, config,
7
- * connectors, optional peers, health endpoints, release hygiene) and print a
8
- * grouped pass/warn/fail report. Exits non-zero if any check fails.
6
+ * `warlock doctor` — run a set of diagnostic checks against the real app and
7
+ * print a grouped pass/warn/fail report. Exits non-zero if any check fails.
9
8
  *
10
- * Preload plan: load every config file and bootstrap app code so routes and
11
- * connectors are registered for introspection, but DELIBERATELY start no
12
- * connectors doctor must not open database/cache/socket connections. The
13
- * connectors check therefore reports the registered set and which (if any) are
14
- * already active without forcing them up.
9
+ * Preload plan: config + env + framework bootstrap, and NO connectors. The
10
+ * `connectors` preloader would START them (open the database, dial the cache,
11
+ * bind the port); doctor's own boot pass BOOTS them instead — the phase where
12
+ * routes and endpoints register which is what gives the checks a real app to
13
+ * read without opening a single connection. See
14
+ * `doctor/boot-for-diagnostics.ts` for that seam.
15
+ *
16
+ * `runtimeStrategy: "development"` is declared, not defaulted: connectors branch
17
+ * on it (`web` serves pages from a manifest in production and from Vite in
18
+ * development, `http` picks its router scan the same way), and a diagnostic
19
+ * command that left the strategy unset would take the production branch by
20
+ * accident — refusing to boot a perfectly healthy app because no build manifest
21
+ * exists. Doctor reads a working tree, so it says so.
15
22
  */
16
23
  const doctorCommand = command({
17
24
  name: "doctor",
18
- description: "Run read-only health checks (routes, config, connectors, optional peers, health endpoints, release hygiene) and print a pass/warn/fail report",
25
+ description: "Boot the app read-only (no connections, no port) and report on its routes, config, connectors, drivers and health endpoints",
19
26
  action: doctorCommandAction,
20
27
  preload: {
28
+ runtimeStrategy: "development",
21
29
  config: true,
22
30
  env: true,
23
31
  bootstrap: true
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/doctor.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\r\nimport { doctorCommandAction } from \"./doctor/doctor-command.action\";\r\n\r\n/**\r\n * `warlock doctor` — run a set of READ-ONLY diagnostic checks (routes, config,\r\n * connectors, optional peers, health endpoints, release hygiene) and print a\r\n * grouped pass/warn/fail report. Exits non-zero if any check fails.\r\n *\r\n * Preload plan: load every config file and bootstrap app code so routes and\r\n * connectors are registered for introspection, but DELIBERATELY start no\r\n * connectorsdoctor must not open database/cache/socket connections. The\r\n * connectors check therefore reports the registered set and which (if any) are\r\n * already active without forcing them up.\r\n */\r\nexport const doctorCommand = command({\r\n name: \"doctor\",\r\n description:\r\n \"Run read-only health checks (routes, config, connectors, optional peers, health endpoints, release hygiene) and print a pass/warn/fail report\",\r\n action: doctorCommandAction,\r\n preload: {\r\n config: true,\r\n env: true,\r\n bootstrap: true,\r\n },\r\n});\r\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAa,gBAAgB,QAAQ;CACnC,MAAM;CACN,aACE;CACF,QAAQ;CACR,SAAS;EACP,QAAQ;EACR,KAAK;EACL,WAAW;CACb;AACF,CAAC"}
1
+ {"version":3,"file":"doctor.command.mjs","names":[],"sources":["../../../../../../../../core/src/cli/commands/doctor.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { doctorCommandAction } from \"./doctor/doctor-command.action\";\n\n/**\n * `warlock doctor` — run a set of diagnostic checks against the real app and\n * print a grouped pass/warn/fail report. Exits non-zero if any check fails.\n *\n * Preload plan: config + env + framework bootstrap, and NO connectors. The\n * `connectors` preloader would START them (open the database, dial the cache,\n * bind the port); doctor's own boot pass BOOTS them instead — the phase where\n * routes and endpoints register which is what gives the checks a real app to\n * read without opening a single connection. See\n * `doctor/boot-for-diagnostics.ts` for that seam.\n *\n * `runtimeStrategy: \"development\"` is declared, not defaulted: connectors branch\n * on it (`web` serves pages from a manifest in production and from Vite in\n * development, `http` picks its router scan the same way), and a diagnostic\n * command that left the strategy unset would take the production branch by\n * accident refusing to boot a perfectly healthy app because no build manifest\n * exists. Doctor reads a working tree, so it says so.\n */\nexport const doctorCommand = command({\n name: \"doctor\",\n description:\n \"Boot the app read-only (no connections, no port) and report on its routes, config, connectors, drivers and health endpoints\",\n action: doctorCommandAction,\n preload: {\n runtimeStrategy: \"development\",\n config: true,\n env: true,\n bootstrap: true,\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,gBAAgB,QAAQ;CACnC,MAAM;CACN,aACE;CACF,QAAQ;CACR,SAAS;EACP,iBAAiB;EACjB,QAAQ;EACR,KAAK;EACL,WAAW;CACb;AACF,CAAC"}
@@ -27,6 +27,21 @@
27
27
  * **loopback on a port nothing is listening on**, because someone will paste it
28
28
  * into a browser and conclude the server is broken. A non-loopback base URL is
29
29
  * a deployment address, reported plainly and never flagged.
30
+ *
31
+ * ## Why the address bound and the address printed are not the same string
32
+ *
33
+ * `listen()` resolves `localhost` and hands back whichever address it actually
34
+ * bound — on a dual-stack machine that is `http://[::1]:2030`. That literal is
35
+ * correct and useless: most terminals will not linkify a bracketed IPv6 host,
36
+ * and on a first run it reads as broken output. So {@link toDisplayUrl} maps
37
+ * the *loopback and wildcard* spellings onto `localhost`, which resolves back
38
+ * to the very socket that was bound.
39
+ *
40
+ * This is a DISPLAY transform and nothing else — `host` still reaches
41
+ * `listen()` untouched (`http-connector.ts`), so the set of interfaces the
42
+ * server accepts on is byte-for-byte what it was. A genuine routable IPv6
43
+ * address is left exactly as it is: there is no synonym for it, and inventing
44
+ * one would be the same class of lie as printing `app.baseUrl` was.
30
45
  */
31
46
  /** Hosts that all mean "this machine" for the purpose of comparing addresses. */
32
47
  const LOOPBACK_HOSTS = new Set([
@@ -37,6 +52,60 @@ const LOOPBACK_HOSTS = new Set([
37
52
  "0.0.0.0",
38
53
  ""
39
54
  ]);
55
+ /**
56
+ * Hosts for which `localhost` is a truthful, reachable synonym.
57
+ *
58
+ * The two loopback literals are exact synonyms. The two wildcards (`::`,
59
+ * `0.0.0.0`) are not synonyms — they are strictly wider — but every one of them
60
+ * *includes* loopback, so `localhost` always reaches the server. Callers that
61
+ * need to tell the reader the bind is wider than loopback should say so
62
+ * separately; see `dev-server/ready-block.ts`.
63
+ *
64
+ * The bracketed spellings are the ones that actually occur: WHATWG `hostname`
65
+ * KEEPS the brackets for an IPv6 host (`new URL("http://[::1]:1").hostname ===
66
+ * "[::1]"`). The bare forms are listed too so a caller that hands over a host
67
+ * rather than a URL is not silently missed.
68
+ */
69
+ const DISPLAY_AS_LOCALHOST = new Set([
70
+ "127.0.0.1",
71
+ "[::1]",
72
+ "::1",
73
+ "0.0.0.0",
74
+ "[::]",
75
+ "::"
76
+ ]);
77
+ /** The wildcard spellings — a bind wider than loopback. */
78
+ const WILDCARD_HOSTS = new Set([
79
+ "0.0.0.0",
80
+ "[::]",
81
+ "::"
82
+ ]);
83
+ /**
84
+ * Whether the given bound address listens on more than loopback.
85
+ *
86
+ * Used to add "(all interfaces)" next to a URL that has been rewritten to
87
+ * `localhost`, so the rewrite never hides a wider bind than the reader thinks.
88
+ */
89
+ function isWildcardBind(boundAddress) {
90
+ const url = parseUrl(boundAddress);
91
+ if (!url) return false;
92
+ return WILDCARD_HOSTS.has(url.hostname);
93
+ }
94
+ /**
95
+ * The address as it should be SHOWN to a human. See the module doc above.
96
+ *
97
+ * Anything unparseable is returned untouched: a best-effort prettifier must
98
+ * never be the reason a developer is shown something other than what the
99
+ * server reported.
100
+ */
101
+ function toDisplayUrl(boundAddress) {
102
+ if (!boundAddress) return "";
103
+ const url = parseUrl(boundAddress);
104
+ if (!url) return boundAddress;
105
+ if (!DISPLAY_AS_LOCALHOST.has(url.hostname)) return boundAddress;
106
+ url.hostname = "localhost";
107
+ return url.href.replace(/\/$/, "");
108
+ }
40
109
  function parseUrl(value) {
41
110
  if (typeof value !== "string" || value.length === 0) return void 0;
42
111
  try {
@@ -56,16 +125,30 @@ function effectivePort(url) {
56
125
  function describeServerAddress(boundAddress, baseUrl) {
57
126
  const bound = parseUrl(boundAddress);
58
127
  const configured = parseUrl(baseUrl);
59
- if (!bound) return { ready: `Server ready at ${configured?.href ?? boundAddress ?? baseUrl}` };
60
- const ready = `Server ready at ${boundAddress}`;
61
- if (!configured) return { ready };
128
+ if (!bound) {
129
+ const fallback = String(configured?.href ?? boundAddress ?? baseUrl ?? "");
130
+ return {
131
+ ready: `Server ready at ${fallback}`,
132
+ url: fallback,
133
+ boundAddress,
134
+ wildcardBind: false
135
+ };
136
+ }
137
+ const url = toDisplayUrl(boundAddress);
138
+ const base = {
139
+ ready: `Server ready at ${url}`,
140
+ url,
141
+ boundAddress,
142
+ wildcardBind: isWildcardBind(boundAddress)
143
+ };
144
+ if (!configured) return base;
62
145
  if (!isLoopback(configured)) return {
63
- ready,
146
+ ...base,
64
147
  publicUrl: `Public URL (app.baseUrl): ${baseUrl}`
65
148
  };
66
- if (effectivePort(configured) === effectivePort(bound)) return { ready };
149
+ if (effectivePort(configured) === effectivePort(bound)) return base;
67
150
  return {
68
- ready,
151
+ ...base,
69
152
  warning: `app.baseUrl is ${baseUrl}, but the server bound port ${effectivePort(bound)}. Nothing is listening on port ${effectivePort(configured)} — check BASE_URL and PORT, including anything exported in your shell.`
70
153
  };
71
154
  }
@@ -1 +1 @@
1
- {"version":3,"file":"describe-server-address.mjs","names":[],"sources":["../../../../../../../core/src/connectors/describe-server-address.ts"],"sourcesContent":["/**\n * What to say once the HTTP server has bound.\n *\n * ## Why this is not one template string\n *\n * `http-connector.ts` used to bind `httpConfig.port` and then announce\n * `config.get(\"app.baseUrl\")` — a different value, read from a different\n * environment variable (`PORT` vs `BASE_URL`), with nothing checking the two\n * agreed. On 2026-08-24 a server listening on 3000 printed\n * `Server ready at http://localhost:41900`. **Following the line the server\n * itself printed gave connection refused, on a server that was working.** The\n * reference app carried the rule as a comment in `.env` — \"must agree with the\n * port in BASE_URL above\" — and a rule a comment enforces is not enforced.\n *\n * So the ready line now states the address actually bound. `app.baseUrl` is a\n * separate claim, and it is reported separately.\n *\n * ## Why a disagreement is not fatal\n *\n * A reverse-proxied deployment legitimately binds `0.0.0.0:3000` and publishes\n * `https://example.com`. Refusing to boot on a mismatch would break the correct\n * case, and warning on it would fire on every correct production boot — a\n * warning that always fires is one nobody reads.\n *\n * The mismatch worth a warning is the ACTIONABLE one: a base URL that points at\n * **loopback on a port nothing is listening on**, because someone will paste it\n * into a browser and conclude the server is broken. A non-loopback base URL is\n * a deployment address, reported plainly and never flagged.\n */\n\n/** Hosts that all mean \"this machine\" for the purpose of comparing addresses. */\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\", \"0.0.0.0\", \"\"]);\n\nexport type ServerAddressReport = {\n /** Where the server is genuinely reachable. Always present. */\n ready: string;\n /** The configured public address, when it is a deployment URL rather than a local one. */\n publicUrl?: string;\n /** Set only when `app.baseUrl` points at loopback on a port nothing bound. */\n warning?: string;\n};\n\nfunction parseUrl(value: unknown): URL | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n\n try {\n return new URL(value);\n } catch {\n // An unparseable base URL is a separate problem, and it is NOT evidence of\n // a port mismatch — reporting one from it would be a guess.\n return undefined;\n }\n}\n\nfunction isLoopback(url: URL): boolean {\n return LOOPBACK_HOSTS.has(url.hostname);\n}\n\n/** `:3000` explicitly, or the protocol's default when the URL omits it. */\nfunction effectivePort(url: URL): string {\n if (url.port) return url.port;\n\n return url.protocol === \"https:\" ? \"443\" : \"80\";\n}\n\nexport function describeServerAddress(\n boundAddress: string | undefined,\n baseUrl: unknown,\n): ServerAddressReport {\n const bound = parseUrl(boundAddress);\n const configured = parseUrl(baseUrl);\n\n // No bound address to report means the caller could not obtain one; the\n // configured URL is then the only thing we know, and saying nothing would be\n // worse than saying the one thing we have.\n if (!bound) {\n return { ready: `Server ready at ${configured?.href ?? boundAddress ?? baseUrl}` };\n }\n\n const ready = `Server ready at ${boundAddress}`;\n\n if (!configured) return { ready };\n\n if (!isLoopback(configured)) {\n return { ready, publicUrl: `Public URL (app.baseUrl): ${baseUrl}` };\n }\n\n if (effectivePort(configured) === effectivePort(bound)) {\n return { ready };\n }\n\n return {\n ready,\n warning:\n `app.baseUrl is ${baseUrl}, but the server bound port ${effectivePort(bound)}. ` +\n `Nothing is listening on port ${effectivePort(configured)} — ` +\n `check BASE_URL and PORT, including anything exported in your shell.`,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;CAAS;CAAW;AAAE,CAAC;AAWxF,SAAS,SAAS,OAAiC;CACjD,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG,OAAO;CAE5D,IAAI;EACF,OAAO,IAAI,IAAI,KAAK;CACtB,QAAQ;EAGN;CACF;AACF;AAEA,SAAS,WAAW,KAAmB;CACrC,OAAO,eAAe,IAAI,IAAI,QAAQ;AACxC;;AAGA,SAAS,cAAc,KAAkB;CACvC,IAAI,IAAI,MAAM,OAAO,IAAI;CAEzB,OAAO,IAAI,aAAa,WAAW,QAAQ;AAC7C;AAEA,SAAgB,sBACd,cACA,SACqB;CACrB,MAAM,QAAQ,SAAS,YAAY;CACnC,MAAM,aAAa,SAAS,OAAO;CAKnC,IAAI,CAAC,OACH,OAAO,EAAE,OAAO,mBAAmB,YAAY,QAAQ,gBAAgB,UAAU;CAGnF,MAAM,QAAQ,mBAAmB;CAEjC,IAAI,CAAC,YAAY,OAAO,EAAE,MAAM;CAEhC,IAAI,CAAC,WAAW,UAAU,GACxB,OAAO;EAAE;EAAO,WAAW,6BAA6B;CAAU;CAGpE,IAAI,cAAc,UAAU,MAAM,cAAc,KAAK,GACnD,OAAO,EAAE,MAAM;CAGjB,OAAO;EACL;EACA,SACE,kBAAkB,QAAQ,8BAA8B,cAAc,KAAK,EAAE,iCAC7C,cAAc,UAAU,EAAE;CAE9D;AACF"}
1
+ {"version":3,"file":"describe-server-address.mjs","names":[],"sources":["../../../../../../../core/src/connectors/describe-server-address.ts"],"sourcesContent":["/**\n * What to say once the HTTP server has bound.\n *\n * ## Why this is not one template string\n *\n * `http-connector.ts` used to bind `httpConfig.port` and then announce\n * `config.get(\"app.baseUrl\")` — a different value, read from a different\n * environment variable (`PORT` vs `BASE_URL`), with nothing checking the two\n * agreed. On 2026-08-24 a server listening on 3000 printed\n * `Server ready at http://localhost:41900`. **Following the line the server\n * itself printed gave connection refused, on a server that was working.** The\n * reference app carried the rule as a comment in `.env` — \"must agree with the\n * port in BASE_URL above\" — and a rule a comment enforces is not enforced.\n *\n * So the ready line now states the address actually bound. `app.baseUrl` is a\n * separate claim, and it is reported separately.\n *\n * ## Why a disagreement is not fatal\n *\n * A reverse-proxied deployment legitimately binds `0.0.0.0:3000` and publishes\n * `https://example.com`. Refusing to boot on a mismatch would break the correct\n * case, and warning on it would fire on every correct production boot — a\n * warning that always fires is one nobody reads.\n *\n * The mismatch worth a warning is the ACTIONABLE one: a base URL that points at\n * **loopback on a port nothing is listening on**, because someone will paste it\n * into a browser and conclude the server is broken. A non-loopback base URL is\n * a deployment address, reported plainly and never flagged.\n *\n * ## Why the address bound and the address printed are not the same string\n *\n * `listen()` resolves `localhost` and hands back whichever address it actually\n * bound — on a dual-stack machine that is `http://[::1]:2030`. That literal is\n * correct and useless: most terminals will not linkify a bracketed IPv6 host,\n * and on a first run it reads as broken output. So {@link toDisplayUrl} maps\n * the *loopback and wildcard* spellings onto `localhost`, which resolves back\n * to the very socket that was bound.\n *\n * This is a DISPLAY transform and nothing else — `host` still reaches\n * `listen()` untouched (`http-connector.ts`), so the set of interfaces the\n * server accepts on is byte-for-byte what it was. A genuine routable IPv6\n * address is left exactly as it is: there is no synonym for it, and inventing\n * one would be the same class of lie as printing `app.baseUrl` was.\n */\n\n/** Hosts that all mean \"this machine\" for the purpose of comparing addresses. */\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\", \"0.0.0.0\", \"\"]);\n\n/**\n * Hosts for which `localhost` is a truthful, reachable synonym.\n *\n * The two loopback literals are exact synonyms. The two wildcards (`::`,\n * `0.0.0.0`) are not synonyms — they are strictly wider — but every one of them\n * *includes* loopback, so `localhost` always reaches the server. Callers that\n * need to tell the reader the bind is wider than loopback should say so\n * separately; see `dev-server/ready-block.ts`.\n *\n * The bracketed spellings are the ones that actually occur: WHATWG `hostname`\n * KEEPS the brackets for an IPv6 host (`new URL(\"http://[::1]:1\").hostname ===\n * \"[::1]\"`). The bare forms are listed too so a caller that hands over a host\n * rather than a URL is not silently missed.\n */\nconst DISPLAY_AS_LOCALHOST = new Set([\"127.0.0.1\", \"[::1]\", \"::1\", \"0.0.0.0\", \"[::]\", \"::\"]);\n\n/** The wildcard spellings — a bind wider than loopback. */\nconst WILDCARD_HOSTS = new Set([\"0.0.0.0\", \"[::]\", \"::\"]);\n\n/**\n * Whether the given bound address listens on more than loopback.\n *\n * Used to add \"(all interfaces)\" next to a URL that has been rewritten to\n * `localhost`, so the rewrite never hides a wider bind than the reader thinks.\n */\nexport function isWildcardBind(boundAddress: string | undefined): boolean {\n const url = parseUrl(boundAddress);\n\n if (!url) return false;\n\n return WILDCARD_HOSTS.has(url.hostname);\n}\n\n/**\n * The address as it should be SHOWN to a human. See the module doc above.\n *\n * Anything unparseable is returned untouched: a best-effort prettifier must\n * never be the reason a developer is shown something other than what the\n * server reported.\n */\nexport function toDisplayUrl(boundAddress: string | undefined): string {\n if (!boundAddress) return \"\";\n\n const url = parseUrl(boundAddress);\n\n if (!url) return boundAddress;\n\n if (!DISPLAY_AS_LOCALHOST.has(url.hostname)) return boundAddress;\n\n url.hostname = \"localhost\";\n\n // `URL.href` re-appends a trailing slash to a bare origin, which is noise in\n // a status line and is not what `listen()` returned.\n return url.href.replace(/\\/$/, \"\");\n}\n\nexport type ServerAddressReport = {\n /** Where the server is genuinely reachable, spelled for a human. Always present. */\n ready: string;\n /**\n * The same address as a bare URL, ready to paste into a browser — the value\n * `ready` embeds. Callers that render their own layout (the dev ready block)\n * use this instead of re-deriving it.\n */\n url: string;\n /**\n * Exactly what `listen()` returned, untransformed. Kept so a caller that\n * needs to reason about the BIND (which interfaces, which family) reads the\n * fact rather than the presentation of it.\n */\n boundAddress?: string;\n /** Whether the bind covers more than loopback (`0.0.0.0` / `::`). */\n wildcardBind: boolean;\n /** The configured public address, when it is a deployment URL rather than a local one. */\n publicUrl?: string;\n /** Set only when `app.baseUrl` points at loopback on a port nothing bound. */\n warning?: string;\n};\n\nfunction parseUrl(value: unknown): URL | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n\n try {\n return new URL(value);\n } catch {\n // An unparseable base URL is a separate problem, and it is NOT evidence of\n // a port mismatch — reporting one from it would be a guess.\n return undefined;\n }\n}\n\nfunction isLoopback(url: URL): boolean {\n return LOOPBACK_HOSTS.has(url.hostname);\n}\n\n/** `:3000` explicitly, or the protocol's default when the URL omits it. */\nfunction effectivePort(url: URL): string {\n if (url.port) return url.port;\n\n return url.protocol === \"https:\" ? \"443\" : \"80\";\n}\n\nexport function describeServerAddress(\n boundAddress: string | undefined,\n baseUrl: unknown,\n): ServerAddressReport {\n const bound = parseUrl(boundAddress);\n const configured = parseUrl(baseUrl);\n\n // No bound address to report means the caller could not obtain one; the\n // configured URL is then the only thing we know, and saying nothing would be\n // worse than saying the one thing we have.\n if (!bound) {\n const fallback = String(configured?.href ?? boundAddress ?? baseUrl ?? \"\");\n\n return {\n ready: `Server ready at ${fallback}`,\n url: fallback,\n boundAddress,\n wildcardBind: false,\n };\n }\n\n // The URL a human is shown. `boundAddress` (the literal `listen()` returned)\n // travels on the report untouched — see the module doc on why these differ.\n const url = toDisplayUrl(boundAddress);\n const base = {\n ready: `Server ready at ${url}`,\n url,\n boundAddress,\n wildcardBind: isWildcardBind(boundAddress),\n };\n\n if (!configured) return base;\n\n if (!isLoopback(configured)) {\n return { ...base, publicUrl: `Public URL (app.baseUrl): ${baseUrl}` };\n }\n\n if (effectivePort(configured) === effectivePort(bound)) {\n return base;\n }\n\n return {\n ...base,\n warning:\n `app.baseUrl is ${baseUrl}, but the server bound port ${effectivePort(bound)}. ` +\n `Nothing is listening on port ${effectivePort(configured)} — ` +\n `check BASE_URL and PORT, including anything exported in your shell.`,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;CAAS;CAAW;AAAE,CAAC;;;;;;;;;;;;;;;AAgBxF,MAAM,uBAAuB,IAAI,IAAI;CAAC;CAAa;CAAS;CAAO;CAAW;CAAQ;AAAI,CAAC;;AAG3F,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAW;CAAQ;AAAI,CAAC;;;;;;;AAQxD,SAAgB,eAAe,cAA2C;CACxE,MAAM,MAAM,SAAS,YAAY;CAEjC,IAAI,CAAC,KAAK,OAAO;CAEjB,OAAO,eAAe,IAAI,IAAI,QAAQ;AACxC;;;;;;;;AASA,SAAgB,aAAa,cAA0C;CACrE,IAAI,CAAC,cAAc,OAAO;CAE1B,MAAM,MAAM,SAAS,YAAY;CAEjC,IAAI,CAAC,KAAK,OAAO;CAEjB,IAAI,CAAC,qBAAqB,IAAI,IAAI,QAAQ,GAAG,OAAO;CAEpD,IAAI,WAAW;CAIf,OAAO,IAAI,KAAK,QAAQ,OAAO,EAAE;AACnC;AAyBA,SAAS,SAAS,OAAiC;CACjD,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG,OAAO;CAE5D,IAAI;EACF,OAAO,IAAI,IAAI,KAAK;CACtB,QAAQ;EAGN;CACF;AACF;AAEA,SAAS,WAAW,KAAmB;CACrC,OAAO,eAAe,IAAI,IAAI,QAAQ;AACxC;;AAGA,SAAS,cAAc,KAAkB;CACvC,IAAI,IAAI,MAAM,OAAO,IAAI;CAEzB,OAAO,IAAI,aAAa,WAAW,QAAQ;AAC7C;AAEA,SAAgB,sBACd,cACA,SACqB;CACrB,MAAM,QAAQ,SAAS,YAAY;CACnC,MAAM,aAAa,SAAS,OAAO;CAKnC,IAAI,CAAC,OAAO;EACV,MAAM,WAAW,OAAO,YAAY,QAAQ,gBAAgB,WAAW,EAAE;EAEzE,OAAO;GACL,OAAO,mBAAmB;GAC1B,KAAK;GACL;GACA,cAAc;EAChB;CACF;CAIA,MAAM,MAAM,aAAa,YAAY;CACrC,MAAM,OAAO;EACX,OAAO,mBAAmB;EAC1B;EACA;EACA,cAAc,eAAe,YAAY;CAC3C;CAEA,IAAI,CAAC,YAAY,OAAO;CAExB,IAAI,CAAC,WAAW,UAAU,GACxB,OAAO;EAAE,GAAG;EAAM,WAAW,6BAA6B;CAAU;CAGtE,IAAI,cAAc,UAAU,MAAM,cAAc,KAAK,GACnD,OAAO;CAGT,OAAO;EACL,GAAG;EACH,SACE,kBAAkB,QAAQ,8BAA8B,cAAc,KAAK,EAAE,iCAC7C,cAAc,UAAU,EAAE;CAE9D;AACF"}
@@ -21,6 +21,24 @@ declare class HttpConnector extends BaseConnector {
21
21
  * Fastify Server instance
22
22
  */
23
23
  protected http?: FastifyInstance;
24
+ /**
25
+ * Whether someone downstream will render the combined ready block.
26
+ *
27
+ * Only the dev server does (`dev-server/ready-block.ts`), and only it can:
28
+ * the block reports facts — boot duration, the web surface — that this
29
+ * connector does not own and must not wait for. When it will, this connector
30
+ * records and stays quiet; otherwise it narrates as it always has.
31
+ *
32
+ * Keyed on the runtime strategy rather than on `NODE_ENV`, because that is
33
+ * what actually distinguishes "a dev server is driving this boot" from "an
34
+ * app happens to be running with development config".
35
+ *
36
+ * `!isBooted` is the second half and it is not a detail. The block summarises
37
+ * a BOOT. A `restart()` after boot — an edited `src/config/http.ts` — is an
38
+ * EVENT, and there is no block coming to carry it, so suppressing the lines
39
+ * there would rebind the port in total silence.
40
+ */
41
+ protected get rendersReadyBlock(): boolean;
24
42
  /**
25
43
  * Boot the connector — construction only (create Fastify, register
26
44
  * plugins, populate container). Route scanning is deferred to
@@ -1 +1 @@
1
- {"version":3,"file":"http-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"mappings":";;;;;;;AAsCA;;cAAa,aAAA,SAAsB,aAAA;EAAA,SACjB,IAAA;EAAA,SACA,QAAA,GAAQ,iBAAA,CAAA,IAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,IAAA;EAmBb;;;;;EAAA,mBAZE,YAAA;EAV2B;;;EAAA,UAepC,IAAA,GAAO,eAAA;EAbO;;;;;EAoBX,IAAA,IAAI,OAAA;EAPP;;;;;;;EAAA,QA0CF,oBAAA;EA2FgB;;;;;EA3DX,KAAA,IAAS,OAAA;EAgGqB;;;;;EArC9B,OAAA,IAAW,OAAA;;;;EASX,QAAA,IAAY,OAAA;;;;;;EA4BlB,aAAA,CAAc,YAAA;AAAA"}
1
+ {"version":3,"file":"http-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"mappings":";;;;;;;AAuCA;;cAAa,aAAA,SAAsB,aAAA;EAAA,SACjB,IAAA;EAAA,SACA,QAAA,GAAQ,iBAAA,CAAA,IAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,IAAA;EAwCb;;;;;EAAA,mBAjCE,YAAA;EAV2B;;;EAAA,UAepC,IAAA,GAAO,eAAA;EAbO;;;;;;;;;;;;;;;;;EAAA,cAgCV,iBAAA;EA6MP;;;AAAoC;;EApM9B,IAAA,IAAI,OAAA;;;;;;;;UAmCT,oBAAA;;;;;;EAgCK,KAAA,IAAS,OAAA;;;;;;EA4FT,OAAA,IAAW,OAAA;;;;EASX,QAAA,IAAY,OAAA;;;;;;EA4BlB,aAAA,CAAc,YAAA;AAAA"}
@@ -6,6 +6,7 @@ import { registerHttpPlugins } from "../http/plugins.mjs";
6
6
  import { closeServerWithTimeout, getHttpServer, startHttpServer } from "../http/server.mjs";
7
7
  import "../application/index.mjs";
8
8
  import { health } from "../http/health.mjs";
9
+ import { setHttpReadyReport } from "../http/ready-report.mjs";
9
10
  import { ConnectorLifecyclePhase, ConnectorPriority } from "./types.mjs";
10
11
  import { BaseConnector } from "./base-connector.mjs";
11
12
  import { describeServerAddress } from "./describe-server-address.mjs";
@@ -40,6 +41,26 @@ var HttpConnector = class extends BaseConnector {
40
41
  this.watchedFiles = ["src/config/http.ts", "src/config/http.tsx"];
41
42
  }
42
43
  /**
44
+ * Whether someone downstream will render the combined ready block.
45
+ *
46
+ * Only the dev server does (`dev-server/ready-block.ts`), and only it can:
47
+ * the block reports facts — boot duration, the web surface — that this
48
+ * connector does not own and must not wait for. When it will, this connector
49
+ * records and stays quiet; otherwise it narrates as it always has.
50
+ *
51
+ * Keyed on the runtime strategy rather than on `NODE_ENV`, because that is
52
+ * what actually distinguishes "a dev server is driving this boot" from "an
53
+ * app happens to be running with development config".
54
+ *
55
+ * `!isBooted` is the second half and it is not a detail. The block summarises
56
+ * a BOOT. A `restart()` after boot — an edited `src/config/http.ts` — is an
57
+ * EVENT, and there is no block coming to carry it, so suppressing the lines
58
+ * there would rebind the port in total silence.
59
+ */
60
+ get rendersReadyBlock() {
61
+ return Application.runtimeStrategy === "development" && !Application.isBooted;
62
+ }
63
+ /**
43
64
  * Boot the connector — construction only (create Fastify, register
44
65
  * plugins, populate container). Route scanning is deferred to
45
66
  * `start()` so it reads the router after app code has registered.
@@ -85,10 +106,12 @@ var HttpConnector = class extends BaseConnector {
85
106
  async start() {
86
107
  const httpConfig = config.get("http");
87
108
  if (!httpConfig || !this.http) return;
109
+ const deferToReadyBlock = this.rendersReadyBlock;
88
110
  if (Application.runtimeStrategy === "development") router.scanDevServer(this.http);
89
111
  else router.scan(this.http);
90
112
  health.addRoutesRegisteredCheck(() => router.routeCount());
91
- log.info("http", "routes", `${router.routeCount()} route(s) registered`);
113
+ const routeCount = router.routeCount();
114
+ if (!deferToReadyBlock) log.info("http", "routes", `${routeCount} route(s) registered`);
92
115
  try {
93
116
  const boundAddress = await this.http.listen({
94
117
  port: httpConfig.port,
@@ -96,8 +119,18 @@ var HttpConnector = class extends BaseConnector {
96
119
  });
97
120
  Application.setServedPort(httpConfig.port);
98
121
  const address = describeServerAddress(boundAddress, config.get("app.baseUrl"));
99
- log.success(`http`, "connection", address.ready);
100
- if (address.publicUrl) log.info(`http`, "connection", address.publicUrl);
122
+ setHttpReadyReport({
123
+ boundAddress: address.boundAddress,
124
+ url: address.url,
125
+ wildcardBind: address.wildcardBind,
126
+ port: httpConfig.port,
127
+ routeCount,
128
+ publicUrl: address.publicUrl
129
+ });
130
+ if (!deferToReadyBlock) {
131
+ log.success(`http`, "connection", address.ready);
132
+ if (address.publicUrl) log.info(`http`, "connection", address.publicUrl);
133
+ }
101
134
  if (address.warning) log.warn(`http`, "connection", address.warning);
102
135
  } catch (error) {
103
136
  await log.fatal("http", "connection", error);
@@ -1 +1 @@
1
- {"version":3,"file":"http-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport { colors } from \"@mongez/copper\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { Application } from \"../application\";\r\nimport { health } from \"../http/health\";\r\nimport { registerHttpPlugins } from \"../http/plugins\";\r\nimport { closeServerWithTimeout, FastifyInstance, getHttpServer, startHttpServer } from \"../http/server\";\r\nimport { router } from \"../router/router\";\r\nimport { Environment } from \"../utils\";\r\nimport { setBaseUrl } from \"../utils/urls\";\r\nimport { container } from \"./../container\";\r\nimport { BaseConnector } from \"./base-connector\";\r\nimport { describeServerAddress } from \"./describe-server-address\";\r\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\r\n\r\nfunction environmentColor(environment: Environment) {\r\n switch (environment) {\r\n case \"development\":\r\n return colors.magentaBright(environment);\r\n case \"test\":\r\n return colors.yellowBright(environment);\r\n case \"production\":\r\n return colors.greenBright(environment);\r\n default:\r\n return colors.white(environment);\r\n }\r\n}\r\n\r\n/**\r\n * Default time to wait for in-flight requests to drain on shutdown before the\r\n * HTTP server is force-closed. Override via `http.gracefulShutdown.timeout`.\r\n */\r\nconst DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\r\n\r\n/**\r\n * HTTP Connector\r\n * Manages HTTP server (Fastify) lifecycle\r\n */\r\nexport class HttpConnector extends BaseConnector {\r\n public readonly name = \"http\";\r\n public readonly priority = ConnectorPriority.HTTP;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\r\n\r\n /**\r\n * Files that trigger HTTP server restart\r\n * Note: routes.ts changes will be handled by HMR with wildcard routing\r\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\r\n */\r\n protected readonly watchedFiles = [\"src/config/http.ts\", \"src/config/http.tsx\"];\r\n\r\n /**\r\n * Fastify Server instance\r\n */\r\n protected http?: FastifyInstance;\r\n\r\n /**\r\n * Boot the connector — construction only (create Fastify, register\r\n * plugins, populate container). Route scanning is deferred to\r\n * `start()` so it reads the router after app code has registered.\r\n */\r\n public async boot() {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig) return;\r\n\r\n const port = httpConfig.port;\r\n log.info(\r\n `http`,\r\n \"connection\",\r\n `Starting http server on port ${port} in ${environmentColor(Application.environment)} mode`,\r\n );\r\n\r\n this.http = startHttpServer(httpConfig.serverOptions);\r\n\r\n container.set(\"http.server\", this.http);\r\n\r\n await registerHttpPlugins(this.http);\r\n\r\n const baseUrl = config.get(\"app.baseUrl\");\r\n\r\n // update base url\r\n setBaseUrl(baseUrl);\r\n\r\n // Liveness/readiness endpoints — registered on Fastify before start() scans\r\n // the app router, so they exist by the time the server listens.\r\n this.registerHealthRoutes();\r\n }\r\n\r\n /**\r\n * Register the built-in liveness (`/health`) and readiness (`/ready`)\r\n * endpoints directly on the Fastify instance — infra routes, kept off the app\r\n * router so they're immune to HMR and never collide with route scanning. Opt\r\n * out via `http.health.enabled = false`; rename via `http.health.path` /\r\n * `http.health.readinessPath`.\r\n */\r\n private registerHealthRoutes(): void {\r\n if (!this.http) {\r\n return;\r\n }\r\n\r\n const healthConfig = config.get(\"http.health\");\r\n\r\n if (healthConfig?.enabled === false) {\r\n return;\r\n }\r\n\r\n const livenessPath = healthConfig?.path ?? \"/health\";\r\n const readinessPath = healthConfig?.readinessPath ?? \"/ready\";\r\n\r\n this.http.get(livenessPath, async (_request: any, reply: any) => {\r\n const result = health.liveness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n\r\n this.http.get(readinessPath, async (_request: any, reply: any) => {\r\n const result = await health.readiness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n }\r\n\r\n /**\r\n * Initialize HTTP server — bind app-registered routes to Fastify\r\n * then listen. Scanning here (not in `boot`) lets HTTP boot before\r\n * app code without losing routes.\r\n */\r\n public async start(): Promise<void> {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig || !this.http) return;\r\n\r\n if (Application.runtimeStrategy === \"development\") {\r\n router.scanDevServer(this.http);\r\n } else {\r\n router.scan(this.http);\r\n }\r\n\r\n // Surface the route table as a readiness signal + a boot log, so an empty\r\n // or partial route surface (e.g. a route module that failed to register) is\r\n // detectable instead of silently 404ing. `addRoutesRegisteredCheck` is\r\n // keyed by name, so re-running on restart is idempotent.\r\n health.addRoutesRegisteredCheck(() => router.routeCount());\r\n log.info(\"http\", \"routes\", `${router.routeCount()} route(s) registered`);\r\n\r\n try {\r\n // `listen()` RESOLVES with the address it actually bound — which is the\r\n // only address worth announcing. See `describe-server-address.ts` for why\r\n // announcing `app.baseUrl` here instead was a defect rather than a\r\n // shortcut.\r\n const boundAddress = await this.http.listen({\r\n port: httpConfig.port,\r\n host: httpConfig.host || \"localhost\",\r\n });\r\n\r\n Application.setServedPort(httpConfig.port);\r\n\r\n const address = describeServerAddress(boundAddress, config.get(\"app.baseUrl\"));\r\n\r\n log.success(`http`, \"connection\", address.ready);\r\n\r\n if (address.publicUrl) {\r\n log.info(`http`, \"connection\", address.publicUrl);\r\n }\r\n\r\n if (address.warning) {\r\n log.warn(`http`, \"connection\", address.warning);\r\n }\r\n } catch (error) {\r\n // A failed listen()/port-bind at boot means the app can't serve — fatal.\r\n // `log.fatal` reaches Sentry/file; `await log.flush()` drains buffered and\r\n // async channels before the process exits.\r\n await log.fatal(\"http\", \"connection\", error);\r\n await log.flush();\r\n\r\n process.exit(1); // stop the process, exit with error\r\n }\r\n\r\n this.active = true;\r\n }\r\n\r\n /**\r\n * Restart — needs a fresh Fastify instance since `start()` now\r\n * re-runs `router.scan()`, and re-scanning the same Fastify would\r\n * register duplicate route handlers.\r\n */\r\n public async restart(): Promise<void> {\r\n await this.shutdown();\r\n await this.boot();\r\n await this.start();\r\n }\r\n\r\n /**\r\n * Shutdown HTTP server\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) {\r\n return;\r\n }\r\n\r\n const server = getHttpServer();\r\n\r\n if (server) {\r\n const timeout = config.get(\"http.gracefulShutdown.timeout\", DEFAULT_SHUTDOWN_TIMEOUT);\r\n const drained = await closeServerWithTimeout(server, timeout);\r\n\r\n if (!drained) {\r\n log.warn(\r\n \"http\",\r\n \"shutdown\",\r\n `In-flight requests did not drain within ${timeout}ms; closing anyway`,\r\n );\r\n }\r\n }\r\n\r\n this.active = false;\r\n }\r\n\r\n /**\r\n * Override shouldRestart to handle routes.ts specially\r\n * routes.ts changes should NOT restart the server (use HMR instead)\r\n * Now receives config file paths directly from layer-executor\r\n */\r\n public shouldRestart(changedFiles: string[]): boolean {\r\n // Only restart for config changes, not routes\r\n return changedFiles.some((file) => {\r\n const relativePath = file.replace(/\\\\/g, \"/\");\r\n return this.watchedFiles.includes(relativePath);\r\n });\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"http-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport { colors } from \"@mongez/copper\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { Application } from \"../application\";\r\nimport { health } from \"../http/health\";\r\nimport { registerHttpPlugins } from \"../http/plugins\";\r\nimport { setHttpReadyReport } from \"../http/ready-report\";\r\nimport { closeServerWithTimeout, FastifyInstance, getHttpServer, startHttpServer } from \"../http/server\";\r\nimport { router } from \"../router/router\";\r\nimport { Environment } from \"../utils\";\r\nimport { setBaseUrl } from \"../utils/urls\";\r\nimport { container } from \"./../container\";\r\nimport { BaseConnector } from \"./base-connector\";\r\nimport { describeServerAddress } from \"./describe-server-address\";\r\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\r\n\r\nfunction environmentColor(environment: Environment) {\r\n switch (environment) {\r\n case \"development\":\r\n return colors.magentaBright(environment);\r\n case \"test\":\r\n return colors.yellowBright(environment);\r\n case \"production\":\r\n return colors.greenBright(environment);\r\n default:\r\n return colors.white(environment);\r\n }\r\n}\r\n\r\n/**\r\n * Default time to wait for in-flight requests to drain on shutdown before the\r\n * HTTP server is force-closed. Override via `http.gracefulShutdown.timeout`.\r\n */\r\nconst DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\r\n\r\n/**\r\n * HTTP Connector\r\n * Manages HTTP server (Fastify) lifecycle\r\n */\r\nexport class HttpConnector extends BaseConnector {\r\n public readonly name = \"http\";\r\n public readonly priority = ConnectorPriority.HTTP;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\r\n\r\n /**\r\n * Files that trigger HTTP server restart\r\n * Note: routes.ts changes will be handled by HMR with wildcard routing\r\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\r\n */\r\n protected readonly watchedFiles = [\"src/config/http.ts\", \"src/config/http.tsx\"];\r\n\r\n /**\r\n * Fastify Server instance\r\n */\r\n protected http?: FastifyInstance;\r\n\r\n /**\r\n * Whether someone downstream will render the combined ready block.\r\n *\r\n * Only the dev server does (`dev-server/ready-block.ts`), and only it can:\r\n * the block reports facts — boot duration, the web surface — that this\r\n * connector does not own and must not wait for. When it will, this connector\r\n * records and stays quiet; otherwise it narrates as it always has.\r\n *\r\n * Keyed on the runtime strategy rather than on `NODE_ENV`, because that is\r\n * what actually distinguishes \"a dev server is driving this boot\" from \"an\r\n * app happens to be running with development config\".\r\n *\r\n * `!isBooted` is the second half and it is not a detail. The block summarises\r\n * a BOOT. A `restart()` after boot — an edited `src/config/http.ts` — is an\r\n * EVENT, and there is no block coming to carry it, so suppressing the lines\r\n * there would rebind the port in total silence.\r\n */\r\n protected get rendersReadyBlock(): boolean {\r\n return Application.runtimeStrategy === \"development\" && !Application.isBooted;\r\n }\r\n\r\n /**\r\n * Boot the connector — construction only (create Fastify, register\r\n * plugins, populate container). Route scanning is deferred to\r\n * `start()` so it reads the router after app code has registered.\r\n */\r\n public async boot() {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig) return;\r\n\r\n const port = httpConfig.port;\r\n log.info(\r\n `http`,\r\n \"connection\",\r\n `Starting http server on port ${port} in ${environmentColor(Application.environment)} mode`,\r\n );\r\n\r\n this.http = startHttpServer(httpConfig.serverOptions);\r\n\r\n container.set(\"http.server\", this.http);\r\n\r\n await registerHttpPlugins(this.http);\r\n\r\n const baseUrl = config.get(\"app.baseUrl\");\r\n\r\n // update base url\r\n setBaseUrl(baseUrl);\r\n\r\n // Liveness/readiness endpoints — registered on Fastify before start() scans\r\n // the app router, so they exist by the time the server listens.\r\n this.registerHealthRoutes();\r\n }\r\n\r\n /**\r\n * Register the built-in liveness (`/health`) and readiness (`/ready`)\r\n * endpoints directly on the Fastify instance — infra routes, kept off the app\r\n * router so they're immune to HMR and never collide with route scanning. Opt\r\n * out via `http.health.enabled = false`; rename via `http.health.path` /\r\n * `http.health.readinessPath`.\r\n */\r\n private registerHealthRoutes(): void {\r\n if (!this.http) {\r\n return;\r\n }\r\n\r\n const healthConfig = config.get(\"http.health\");\r\n\r\n if (healthConfig?.enabled === false) {\r\n return;\r\n }\r\n\r\n const livenessPath = healthConfig?.path ?? \"/health\";\r\n const readinessPath = healthConfig?.readinessPath ?? \"/ready\";\r\n\r\n this.http.get(livenessPath, async (_request: any, reply: any) => {\r\n const result = health.liveness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n\r\n this.http.get(readinessPath, async (_request: any, reply: any) => {\r\n const result = await health.readiness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n }\r\n\r\n /**\r\n * Initialize HTTP server — bind app-registered routes to Fastify\r\n * then listen. Scanning here (not in `boot`) lets HTTP boot before\r\n * app code without losing routes.\r\n */\r\n public async start(): Promise<void> {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig || !this.http) return;\r\n\r\n // Read once. `Application.isBooted` flips during the same boot this method\r\n // belongs to, and a predicate that answered differently before and after\r\n // `listen()` would print half a report.\r\n const deferToReadyBlock = this.rendersReadyBlock;\r\n\r\n if (Application.runtimeStrategy === \"development\") {\r\n router.scanDevServer(this.http);\r\n } else {\r\n router.scan(this.http);\r\n }\r\n\r\n // Surface the route table as a readiness signal + a boot log, so an empty\r\n // or partial route surface (e.g. a route module that failed to register) is\r\n // detectable instead of silently 404ing. `addRoutesRegisteredCheck` is\r\n // keyed by name, so re-running on restart is idempotent.\r\n health.addRoutesRegisteredCheck(() => router.routeCount());\r\n\r\n const routeCount = router.routeCount();\r\n\r\n // In development this fact is one line of the single ready block rendered\r\n // after boot completes (`dev-server/ready-block.ts`); printing it here as\r\n // well would put the same number on screen twice, in two formats. Outside\r\n // development the line is load-bearing — a supervisor or CI log greps it —\r\n // so it stays exactly as it was.\r\n if (!deferToReadyBlock) {\r\n log.info(\"http\", \"routes\", `${routeCount} route(s) registered`);\r\n }\r\n\r\n try {\r\n // `listen()` RESOLVES with the address it actually bound — which is the\r\n // only address worth announcing. See `describe-server-address.ts` for why\r\n // announcing `app.baseUrl` here instead was a defect rather than a\r\n // shortcut.\r\n const boundAddress = await this.http.listen({\r\n port: httpConfig.port,\r\n host: httpConfig.host || \"localhost\",\r\n });\r\n\r\n Application.setServedPort(httpConfig.port);\r\n\r\n const address = describeServerAddress(boundAddress, config.get(\"app.baseUrl\"));\r\n\r\n // Recorded unconditionally: the ready block reads it in development, and\r\n // in every other mode it is simply the one place that knows what was\r\n // bound, which is worth having whether or not anything renders it.\r\n setHttpReadyReport({\r\n boundAddress: address.boundAddress,\r\n url: address.url,\r\n wildcardBind: address.wildcardBind,\r\n port: httpConfig.port,\r\n routeCount,\r\n publicUrl: address.publicUrl,\r\n });\r\n\r\n if (!deferToReadyBlock) {\r\n log.success(`http`, \"connection\", address.ready);\r\n\r\n if (address.publicUrl) {\r\n log.info(`http`, \"connection\", address.publicUrl);\r\n }\r\n }\r\n\r\n // NEVER conditional on the block. A base URL pointing at a port nothing\r\n // is listening on is the single most misleading state this connector can\r\n // be in, and a warning a status block swallows is a warning nobody reads\r\n // — so it is logged where it lands ABOVE the block, in both modes.\r\n if (address.warning) {\r\n log.warn(`http`, \"connection\", address.warning);\r\n }\r\n } catch (error) {\r\n // A failed listen()/port-bind at boot means the app can't serve — fatal.\r\n // `log.fatal` reaches Sentry/file; `await log.flush()` drains buffered and\r\n // async channels before the process exits.\r\n await log.fatal(\"http\", \"connection\", error);\r\n await log.flush();\r\n\r\n process.exit(1); // stop the process, exit with error\r\n }\r\n\r\n this.active = true;\r\n }\r\n\r\n /**\r\n * Restart — needs a fresh Fastify instance since `start()` now\r\n * re-runs `router.scan()`, and re-scanning the same Fastify would\r\n * register duplicate route handlers.\r\n */\r\n public async restart(): Promise<void> {\r\n await this.shutdown();\r\n await this.boot();\r\n await this.start();\r\n }\r\n\r\n /**\r\n * Shutdown HTTP server\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) {\r\n return;\r\n }\r\n\r\n const server = getHttpServer();\r\n\r\n if (server) {\r\n const timeout = config.get(\"http.gracefulShutdown.timeout\", DEFAULT_SHUTDOWN_TIMEOUT);\r\n const drained = await closeServerWithTimeout(server, timeout);\r\n\r\n if (!drained) {\r\n log.warn(\r\n \"http\",\r\n \"shutdown\",\r\n `In-flight requests did not drain within ${timeout}ms; closing anyway`,\r\n );\r\n }\r\n }\r\n\r\n this.active = false;\r\n }\r\n\r\n /**\r\n * Override shouldRestart to handle routes.ts specially\r\n * routes.ts changes should NOT restart the server (use HMR instead)\r\n * Now receives config file paths directly from layer-executor\r\n */\r\n public shouldRestart(changedFiles: string[]): boolean {\r\n // Only restart for config changes, not routes\r\n return changedFiles.some((file) => {\r\n const relativePath = file.replace(/\\\\/g, \"/\");\r\n return this.watchedFiles.includes(relativePath);\r\n });\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -70,9 +70,6 @@ function devLogHMR(file, dependents) {
70
70
  const depInfo = dependents ? colors.dim(` +${dependents} module${dependents > 1 ? "s" : ""}`) : "";
71
71
  console.log(`${timestamp()} 🔥 ${colors.green("hmr update")} ${colors.dim(relativePath)}${depInfo}`);
72
72
  }
73
- function devLogReady(message) {
74
- console.log(`\n${timestamp()} ${colors.green("➜")} ${colors.bold(message)}`);
75
- }
76
73
  function devLogSection(title) {
77
74
  console.log(`\n${timestamp()} ${colors.bold(colors.cyan(title))}`);
78
75
  }
@@ -106,5 +103,5 @@ function formatModuleNotFoundError(error, suggestions) {
106
103
  const devServeLog = devLog;
107
104
 
108
105
  //#endregion
109
- export { devLogDim, devLogError, devLogHMR, devLogInfo, devLogReady, devLogSection, devLogSuccess, devLogWarn, devServeLog, formatModuleNotFoundError };
106
+ export { devLogDim, devLogError, devLogHMR, devLogInfo, devLogSection, devLogSuccess, devLogWarn, devServeLog, formatModuleNotFoundError };
110
107
  //# sourceMappingURL=dev-logger.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-logger.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/dev-logger.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport dayjs from \"dayjs\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Dev server logger — Vite-style formatting helpers.\n */\n\nfunction timestamp(): string {\n return colors.dim(`${dayjs().format(\"HH:mm:ss A\")}`);\n}\n\nexport function devLog(message: string) {\n console.log(`${timestamp()} ${message}`);\n}\n\nexport function devLogSuccess(message: string) {\n console.log(`${timestamp()} ${colors.green(\"✓\")} ${colors.green(message)}`);\n}\n\n/**\n * Colourise a stack trace so the eye lands on *your* code.\n *\n * - Error header (`ReferenceError: x is not defined`) → bold red.\n * - Frames in project `src/` → highlighted: a green `›` pointer, yellow\n * function name, cyan relative path, dim `:line:col`. These are almost\n * always where the bug is.\n * - Framework (`@warlock.js`), `node_modules`, and `node:` internal frames\n * → dimmed and relativised. Still there for context, just out of the way.\n *\n * Source maps are enabled in dev, so the paths/lines here are already the\n * original `.ts` locations — this only changes how they're painted.\n */\nexport function formatErrorStack(stack: string): string {\n const frame = /^(\\s*)at (.+?) \\((.+):(\\d+):(\\d+)\\)$/;\n const bareFrame = /^(\\s*)at (.+):(\\d+):(\\d+)$/;\n let headerDone = false;\n\n return stack\n .split(\"\\n\")\n .map(line => {\n const withFn = line.match(frame);\n const bare = line.match(bareFrame);\n\n if (!withFn && !bare) {\n // Header line(s) before the first frame.\n const painted = headerDone ? colors.dim(line) : colors.bold(colors.red(line));\n return painted;\n }\n\n headerDone = true;\n\n const fn = withFn ? withFn[2] : \"\";\n const file = withFn ? withFn[3] : bare![2];\n const lineNo = withFn ? withFn[4] : bare![3];\n const col = withFn ? withFn[5] : bare![4];\n\n const isNodeInternal = file.startsWith(\"node:\");\n const isDep = file.includes(\"node_modules\");\n const isFramework = /[\\\\/]@warlock\\.js[\\\\/]/.test(file);\n const isUserCode = !isNodeInternal && !isDep && !isFramework;\n\n const rel = isNodeInternal ? file : Path.toRelative(file);\n const loc = `:${lineNo}:${col}`;\n\n if (isUserCode) {\n return (\n ` ${colors.green(\"›\")} ${colors.dim(\"at\")} ` +\n `${colors.yellow(fn || \"<anonymous>\")} ` +\n `${colors.cyan(rel)}${colors.dim(loc)}`\n );\n }\n\n const label = fn ? `at ${fn} ${rel}${loc}` : `at ${rel}${loc}`;\n return ` ${colors.dim(label)}`;\n })\n .join(\"\\n\");\n}\n\nexport function devLogError(message: string, error?: any) {\n console.log(`${timestamp()} ${colors.red(\"✗\")} ${colors.red(message)}`);\n if (error?.stack) console.log(formatErrorStack(error.stack));\n}\n\nexport function devLogWarn(message: string) {\n console.log(`${timestamp()} ${colors.yellow(\"⚠\")} ${colors.yellow(message)}`);\n}\n\nexport function devLogInfo(message: string) {\n console.log(`${timestamp()} ${colors.cyan(message)}`);\n}\n\nexport function devLogDim(message: string) {\n console.log(`${timestamp()} ${colors.dim(message)}`);\n}\n\nexport function devLogHMR(file: string, dependents?: number) {\n const relativePath = Path.toRelative(file);\n const depInfo = dependents\n ? colors.dim(` +${dependents} module${dependents > 1 ? \"s\" : \"\"}`)\n : \"\";\n console.log(\n `${timestamp()} 🔥 ${colors.green(\"hmr update\")} ${colors.dim(relativePath)}${depInfo}`,\n );\n}\n\nexport function devLogConfig(file: string, connectors?: string[]) {\n const relativePath = Path.toRelative(file);\n const connectorInfo =\n connectors && connectors.length > 0 ? colors.dim(` → restarting ${connectors.join(\", \")}`) : \"\";\n console.log(\n `${timestamp()} ${colors.cyan(\"config reload\")} ${colors.dim(relativePath)}${connectorInfo}`,\n );\n}\n\nexport function devLogReady(message: string) {\n console.log(`\\n${timestamp()} ${colors.green(\"➜\")} ${colors.bold(message)}`);\n}\n\nexport function devLogSection(title: string) {\n console.log(`\\n${timestamp()} ${colors.bold(colors.cyan(title))}`);\n}\n\n/**\n * Format ERR_MODULE_NOT_FOUND so the displayed paths are relative to the\n * project root. The loader hook keeps source paths in the URL so we only\n * need to strip the absolute prefix — no cache-path translation any more.\n */\nexport function formatModuleNotFoundError(error: Error, suggestions?: string[]): string {\n const match = error.message.match(/Cannot find module '([^']+)' imported from '([^']+)'/);\n if (!match) return error.message;\n\n const [, modulePath, importerPath] = match;\n const lines: string[] = [\n \"\",\n `${colors.red(\"❌ MODULE NOT FOUND\")}`,\n \"\",\n `${colors.dim(\"Cannot find:\")} ${colors.cyan(Path.toRelative(modulePath))}`,\n \"\",\n `${colors.dim(\"Imported by:\")}`,\n ` ${colors.yellow(\"→\")} ${colors.white(Path.toRelative(importerPath))}`,\n ];\n\n if (suggestions && suggestions.length > 0) {\n lines.push(\"\");\n lines.push(`${colors.dim(\"Did you mean?\")}`);\n suggestions.forEach(s => lines.push(` ${colors.cyan(\"→\")} ${colors.green(s)}`));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/** @deprecated alias retained for older callers. Use `devLog` directly. */\nexport const devServeLog = devLog;\n"],"mappings":"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"}
1
+ {"version":3,"file":"dev-logger.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/dev-logger.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport dayjs from \"dayjs\";\nimport { Path } from \"../utils/normalized-path\";\n\n/**\n * Dev server logger — Vite-style formatting helpers.\n */\n\nfunction timestamp(): string {\n return colors.dim(`${dayjs().format(\"HH:mm:ss A\")}`);\n}\n\nexport function devLog(message: string) {\n console.log(`${timestamp()} ${message}`);\n}\n\nexport function devLogSuccess(message: string) {\n console.log(`${timestamp()} ${colors.green(\"✓\")} ${colors.green(message)}`);\n}\n\n/**\n * Colourise a stack trace so the eye lands on *your* code.\n *\n * - Error header (`ReferenceError: x is not defined`) → bold red.\n * - Frames in project `src/` → highlighted: a green `›` pointer, yellow\n * function name, cyan relative path, dim `:line:col`. These are almost\n * always where the bug is.\n * - Framework (`@warlock.js`), `node_modules`, and `node:` internal frames\n * → dimmed and relativised. Still there for context, just out of the way.\n *\n * Source maps are enabled in dev, so the paths/lines here are already the\n * original `.ts` locations — this only changes how they're painted.\n */\nexport function formatErrorStack(stack: string): string {\n const frame = /^(\\s*)at (.+?) \\((.+):(\\d+):(\\d+)\\)$/;\n const bareFrame = /^(\\s*)at (.+):(\\d+):(\\d+)$/;\n let headerDone = false;\n\n return stack\n .split(\"\\n\")\n .map(line => {\n const withFn = line.match(frame);\n const bare = line.match(bareFrame);\n\n if (!withFn && !bare) {\n // Header line(s) before the first frame.\n const painted = headerDone ? colors.dim(line) : colors.bold(colors.red(line));\n return painted;\n }\n\n headerDone = true;\n\n const fn = withFn ? withFn[2] : \"\";\n const file = withFn ? withFn[3] : bare![2];\n const lineNo = withFn ? withFn[4] : bare![3];\n const col = withFn ? withFn[5] : bare![4];\n\n const isNodeInternal = file.startsWith(\"node:\");\n const isDep = file.includes(\"node_modules\");\n const isFramework = /[\\\\/]@warlock\\.js[\\\\/]/.test(file);\n const isUserCode = !isNodeInternal && !isDep && !isFramework;\n\n const rel = isNodeInternal ? file : Path.toRelative(file);\n const loc = `:${lineNo}:${col}`;\n\n if (isUserCode) {\n return (\n ` ${colors.green(\"›\")} ${colors.dim(\"at\")} ` +\n `${colors.yellow(fn || \"<anonymous>\")} ` +\n `${colors.cyan(rel)}${colors.dim(loc)}`\n );\n }\n\n const label = fn ? `at ${fn} ${rel}${loc}` : `at ${rel}${loc}`;\n return ` ${colors.dim(label)}`;\n })\n .join(\"\\n\");\n}\n\nexport function devLogError(message: string, error?: any) {\n console.log(`${timestamp()} ${colors.red(\"✗\")} ${colors.red(message)}`);\n if (error?.stack) console.log(formatErrorStack(error.stack));\n}\n\nexport function devLogWarn(message: string) {\n console.log(`${timestamp()} ${colors.yellow(\"⚠\")} ${colors.yellow(message)}`);\n}\n\nexport function devLogInfo(message: string) {\n console.log(`${timestamp()} ${colors.cyan(message)}`);\n}\n\nexport function devLogDim(message: string) {\n console.log(`${timestamp()} ${colors.dim(message)}`);\n}\n\nexport function devLogHMR(file: string, dependents?: number) {\n const relativePath = Path.toRelative(file);\n const depInfo = dependents\n ? colors.dim(` +${dependents} module${dependents > 1 ? \"s\" : \"\"}`)\n : \"\";\n console.log(\n `${timestamp()} 🔥 ${colors.green(\"hmr update\")} ${colors.dim(relativePath)}${depInfo}`,\n );\n}\n\nexport function devLogConfig(file: string, connectors?: string[]) {\n const relativePath = Path.toRelative(file);\n const connectorInfo =\n connectors && connectors.length > 0 ? colors.dim(` → restarting ${connectors.join(\", \")}`) : \"\";\n console.log(\n `${timestamp()} ${colors.cyan(\"config reload\")} ${colors.dim(relativePath)}${connectorInfo}`,\n );\n}\n\nexport function devLogReady(message: string) {\n console.log(`\\n${timestamp()} ${colors.green(\"➜\")} ${colors.bold(message)}`);\n}\n\nexport function devLogSection(title: string) {\n console.log(`\\n${timestamp()} ${colors.bold(colors.cyan(title))}`);\n}\n\n/**\n * Format ERR_MODULE_NOT_FOUND so the displayed paths are relative to the\n * project root. The loader hook keeps source paths in the URL so we only\n * need to strip the absolute prefix — no cache-path translation any more.\n */\nexport function formatModuleNotFoundError(error: Error, suggestions?: string[]): string {\n const match = error.message.match(/Cannot find module '([^']+)' imported from '([^']+)'/);\n if (!match) return error.message;\n\n const [, modulePath, importerPath] = match;\n const lines: string[] = [\n \"\",\n `${colors.red(\"❌ MODULE NOT FOUND\")}`,\n \"\",\n `${colors.dim(\"Cannot find:\")} ${colors.cyan(Path.toRelative(modulePath))}`,\n \"\",\n `${colors.dim(\"Imported by:\")}`,\n ` ${colors.yellow(\"→\")} ${colors.white(Path.toRelative(importerPath))}`,\n ];\n\n if (suggestions && suggestions.length > 0) {\n lines.push(\"\");\n lines.push(`${colors.dim(\"Did you mean?\")}`);\n suggestions.forEach(s => lines.push(` ${colors.cyan(\"→\")} ${colors.green(s)}`));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/** @deprecated alias retained for older callers. Use `devLog` directly. */\nexport const devServeLog = devLog;\n"],"mappings":"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"}
@@ -4,12 +4,13 @@ import { ConnectorLifecyclePhase } from "../connectors/types.mjs";
4
4
  import { connectorsManager } from "../connectors/connectors-manager.mjs";
5
5
  import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
6
6
  import "../warlock-config/index.mjs";
7
- import { devLogInfo, devLogReady, devLogSection, devLogWarn, devServeLog } from "./dev-logger.mjs";
7
+ import { devLogInfo, devLogSection, devLogWarn, devServeLog } from "./dev-logger.mjs";
8
8
  import { MANIFEST_PATH } from "./flags.mjs";
9
9
  import { filesOrchestrator } from "./files-orchestrator.mjs";
10
10
  import { restartDevServer } from "./restart-dev-server.mjs";
11
11
  import { devServerShortcuts } from "./shortcuts.mjs";
12
12
  import { LayerExecutor } from "./layer-executor.mjs";
13
+ import { printReadyBlock } from "./ready-block.mjs";
13
14
  import { typeGenerator } from "./type-generator.mjs";
14
15
  import { colors } from "@mongez/copper";
15
16
  import events from "@mongez/events";
@@ -46,12 +47,12 @@ var DevelopmentServer = class {
46
47
  this.layerExecutor = new LayerExecutor(filesOrchestrator.getDependencyGraph(), filesOrchestrator.specialFilesCollector, filesOrchestrator.moduleLoader, (absolutePath) => filesOrchestrator.bumpVersion(absolutePath), () => filesOrchestrator.flushVersionBumps());
47
48
  this.running = true;
48
49
  const duration = performance.now() - startedAt;
49
- devLogReady(`Development Server is ready in ${colors.greenBright(parseDuration(duration))}`);
50
50
  Application.markBooted({
51
51
  environment: Application.environment,
52
52
  runtimeStrategy: Application.runtimeStrategy,
53
53
  bootDurationMs: duration
54
54
  });
55
+ printReadyBlock(duration);
55
56
  const devServerConfig = await warlockConfigManager.get("devServer");
56
57
  const generateTypings = this.options.generateTypings ?? devServerConfig?.generateTypings ?? true;
57
58
  const healthCheckers = this.options.healthCheckers ?? devServerConfig?.healthCheckers ?? true;
@@ -129,11 +130,6 @@ function isEnvPath(path) {
129
130
  const basename = path.split("/").pop() ?? path;
130
131
  return basename === ".env" || basename.startsWith(".env.");
131
132
  }
132
- function parseDuration(ms) {
133
- if (ms < 1e3) return `${ms.toFixed(2)}ms`;
134
- if (ms > 6e4) return `${(ms / 6e4).toFixed(2)}m`;
135
- return `${(ms / 1e3).toFixed(2)}s`;
136
- }
137
133
 
138
134
  //#endregion
139
135
  export { DevelopmentServer };