@ultimat3/cli 23.0.0 → 25.0.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 (296) hide show
  1. package/CLAUDE.md +28 -13
  2. package/README.md +32 -7
  3. package/package.json +29 -29
  4. package/src/admin-catalog-keys.ts +2 -2
  5. package/src/affected.ts +30 -36
  6. package/src/api-registration.ts +89 -0
  7. package/src/app-artifacts.ts +1 -1
  8. package/src/app-auth.ts +4 -27
  9. package/src/app-boundaries.ts +17 -4
  10. package/src/app-config-load.ts +51 -0
  11. package/src/app-env.ts +26 -25
  12. package/src/app-load.ts +38 -14
  13. package/src/app-manifest.ts +4 -10
  14. package/src/app-mcp.ts +31 -46
  15. package/src/app-name.ts +52 -0
  16. package/src/app-permissions-borrowed.ts +123 -0
  17. package/src/app-permissions-site.ts +48 -0
  18. package/src/app-permissions.ts +7 -17
  19. package/src/app-root.ts +9 -5
  20. package/src/bin.ts +3 -7
  21. package/src/browser-launcher.ts +4 -2
  22. package/src/budgets-charged.ts +60 -0
  23. package/src/budgets-scan.ts +76 -0
  24. package/src/budgets-unmeasured.ts +91 -0
  25. package/src/budgets.ts +117 -154
  26. package/src/cdp-shot-driver.ts +20 -1
  27. package/src/cdp-shot-watch.ts +35 -31
  28. package/src/ci-log.ts +0 -0
  29. package/src/cmd-auth-spec.ts +16 -0
  30. package/src/cmd-auth.ts +65 -0
  31. package/src/cmd-build-spec.ts +7 -1
  32. package/src/cmd-build.ts +151 -22
  33. package/src/cmd-db-backfill.ts +26 -13
  34. package/src/cmd-db-branch.ts +2 -2
  35. package/src/cmd-db.ts +10 -6
  36. package/src/cmd-deploy-compose.ts +94 -0
  37. package/src/cmd-deploy-drain.ts +48 -0
  38. package/src/cmd-deploy-helm.ts +7 -6
  39. package/src/cmd-deploy-start-first.ts +178 -0
  40. package/src/cmd-deploy.ts +158 -36
  41. package/src/cmd-dev.ts +18 -356
  42. package/src/cmd-doctor-spec.ts +2 -9
  43. package/src/cmd-doctor.ts +57 -80
  44. package/src/cmd-env.ts +55 -16
  45. package/src/cmd-generate.ts +89 -32
  46. package/src/cmd-help.ts +19 -6
  47. package/src/cmd-i18n.ts +21 -17
  48. package/src/cmd-jobs-spec.ts +7 -22
  49. package/src/cmd-jobs.ts +33 -140
  50. package/src/cmd-manifest-spec.ts +1 -1
  51. package/src/cmd-manifest.ts +6 -25
  52. package/src/cmd-mcp.ts +71 -22
  53. package/src/cmd-new.ts +36 -8
  54. package/src/cmd-planned.ts +24 -8
  55. package/src/cmd-routes.ts +15 -5
  56. package/src/cmd-secrets.ts +47 -6
  57. package/src/cmd-shot-matrix.ts +22 -7
  58. package/src/cmd-shot-spec.ts +10 -2
  59. package/src/cmd-shot.ts +46 -41
  60. package/src/cmd-tasks.ts +2 -2
  61. package/src/cmd-test-spec.ts +1 -7
  62. package/src/cmd-test.ts +14 -30
  63. package/src/cmd-verify.ts +4 -4
  64. package/src/coverage-lcov.ts +4 -2
  65. package/src/db-backfill.ts +2 -2
  66. package/src/db-replay-engine.ts +4 -10
  67. package/src/db-subscribes.ts +27 -16
  68. package/src/dev-boot.ts +290 -0
  69. package/src/dev-child-watch.ts +63 -12
  70. package/src/dev-dashboard-guard.ts +76 -0
  71. package/src/dev-dashboard.ts +56 -30
  72. package/src/dev-lock.ts +100 -31
  73. package/src/dev-rebuild.ts +80 -0
  74. package/src/dev-restart-retire.ts +46 -0
  75. package/src/dev-route-table.ts +30 -8
  76. package/src/dev-supervisor.ts +114 -22
  77. package/src/dev-traces.ts +53 -20
  78. package/src/dispatch.ts +17 -13
  79. package/src/doctor-app-url.ts +39 -0
  80. package/src/doctor-auth.ts +95 -0
  81. package/src/doctor-offline.ts +6 -5
  82. package/src/document-options.ts +7 -0
  83. package/src/drift-replica-identity.ts +42 -0
  84. package/src/drift.ts +29 -6
  85. package/src/error-catalog.ts +6 -9
  86. package/src/error-codes.ts +29 -3
  87. package/src/error-page-csp.ts +14 -15
  88. package/src/error-unthrown.ts +29 -11
  89. package/src/errors.ts +54 -83
  90. package/src/exec.ts +8 -0
  91. package/src/favicon.ts +2 -2
  92. package/src/fix-command.ts +9 -1
  93. package/src/framework-env.ts +148 -0
  94. package/src/framework-schema-apply.ts +78 -0
  95. package/src/framework-schema-stamp.ts +96 -0
  96. package/src/framework-schema.ts +25 -3
  97. package/src/generate-catalog-locales.ts +79 -0
  98. package/src/generate-files.ts +4 -3
  99. package/src/generate-kinds.ts +18 -0
  100. package/src/generate-plural.ts +50 -0
  101. package/src/generate-shadow.ts +95 -0
  102. package/src/generate-write.ts +47 -13
  103. package/src/gh-target.ts +12 -7
  104. package/src/hold.ts +56 -1
  105. package/src/i18n-audit.ts +25 -9
  106. package/src/i18n-index.ts +5 -6
  107. package/src/i18n-registration.ts +7 -9
  108. package/src/import-scan.ts +28 -3
  109. package/src/index.ts +2 -6
  110. package/src/island-bundle.ts +57 -79
  111. package/src/island-capture.ts +1 -1
  112. package/src/island-duplicate-refusal.ts +57 -0
  113. package/src/island-duplicates.ts +221 -0
  114. package/src/island-entry-missing.ts +64 -0
  115. package/src/island-harness-route.ts +20 -4
  116. package/src/island-harness-script.ts +23 -2
  117. package/src/island-identity.ts +17 -14
  118. package/src/island-package-dedupe.ts +150 -0
  119. package/src/island-realtime.ts +36 -16
  120. package/src/island-routes.ts +2 -2
  121. package/src/island-runtime.ts +241 -0
  122. package/src/island-sources.ts +105 -0
  123. package/src/island-states-load.ts +6 -0
  124. package/src/island-store.ts +90 -6
  125. package/src/jobs-json.ts +0 -26
  126. package/src/jobs-report.ts +36 -20
  127. package/src/local-cli-handoff.ts +90 -0
  128. package/src/loopback-host.ts +13 -0
  129. package/src/manifest-staleness.ts +62 -0
  130. package/src/mcp-errors.ts +20 -4
  131. package/src/mcp-host.ts +22 -14
  132. package/src/mcp-test-run.ts +38 -0
  133. package/src/mcp-ui-inspect.ts +3 -2
  134. package/src/mcp-ui-interact.ts +3 -2
  135. package/src/mcp-ui.ts +8 -7
  136. package/src/measure-scope.ts +15 -10
  137. package/src/messages.ts +29 -16
  138. package/src/metrics-endpoint.ts +28 -3
  139. package/src/migration-extensions.ts +1 -1
  140. package/src/otlp-export.ts +2 -2
  141. package/src/output.ts +24 -0
  142. package/src/page-navigation.ts +7 -23
  143. package/src/page-speculation.ts +19 -31
  144. package/src/parse.ts +0 -8
  145. package/src/permission-grants.ts +3 -3
  146. package/src/posix-path.ts +30 -0
  147. package/src/prerender-locales.ts +6 -4
  148. package/src/prerender-out.ts +34 -7
  149. package/src/prerender-report.ts +58 -0
  150. package/src/prerender-served.ts +52 -0
  151. package/src/prerender.ts +71 -103
  152. package/src/pwa-artifacts.ts +33 -94
  153. package/src/pwa-manifest-locales.ts +9 -9
  154. package/src/registry.ts +2 -0
  155. package/src/retry-memo.ts +1 -1
  156. package/src/revalidated-response.ts +4 -3
  157. package/src/role-realtime.ts +7 -1
  158. package/src/role-replicator.ts +124 -29
  159. package/src/role-start.ts +69 -85
  160. package/src/role-sync.ts +119 -25
  161. package/src/root-env.ts +157 -28
  162. package/src/route-islands.ts +69 -0
  163. package/src/runtime-admin.ts +4 -1
  164. package/src/runtime-assets.ts +26 -9
  165. package/src/runtime-bindings.ts +27 -7
  166. package/src/runtime-cache.ts +46 -55
  167. package/src/runtime-hooks.ts +2 -1
  168. package/src/runtime-idempotency-scope.ts +23 -0
  169. package/src/runtime-isr-outcome.ts +100 -0
  170. package/src/runtime-isr.ts +36 -0
  171. package/src/runtime-jobs.ts +76 -41
  172. package/src/runtime-live-feed.ts +1 -1
  173. package/src/runtime-mail.ts +26 -0
  174. package/src/runtime-mfa-warning.ts +29 -0
  175. package/src/runtime-notify-retention.ts +10 -46
  176. package/src/runtime-overrides.ts +4 -4
  177. package/src/runtime-purge.ts +38 -16
  178. package/src/runtime-queue.ts +43 -22
  179. package/src/runtime-realtime.ts +11 -45
  180. package/src/runtime-render.ts +136 -114
  181. package/src/runtime-replica.ts +3 -8
  182. package/src/runtime-services.ts +49 -26
  183. package/src/runtime-storage-upload.ts +2 -2
  184. package/src/runtime-storage.ts +13 -8
  185. package/src/schema-diff.ts +16 -0
  186. package/src/schema-drift.ts +45 -15
  187. package/src/secrets-rotation.ts +8 -9
  188. package/src/seo-routes.ts +80 -9
  189. package/src/serve-boot.ts +51 -12
  190. package/src/serve-drain.ts +11 -33
  191. package/src/serve-env.ts +9 -6
  192. package/src/serve-retire.ts +95 -0
  193. package/src/serve-web.ts +31 -15
  194. package/src/serve.ts +27 -10
  195. package/src/server-barrels.ts +6 -3
  196. package/src/shot-browser.ts +1 -1
  197. package/src/shot-locale.ts +6 -33
  198. package/src/shot-settle.ts +3 -3
  199. package/src/shot-theme.ts +1 -1
  200. package/src/shot-viewport.ts +152 -0
  201. package/src/signal-shred.ts +26 -12
  202. package/src/site-asset-routes.ts +2 -2
  203. package/src/site-assets.ts +3 -5
  204. package/src/site-config.ts +9 -48
  205. package/src/site-seo.ts +7 -5
  206. package/src/sitemap-lastmod.ts +5 -0
  207. package/src/solid-loader.ts +12 -3
  208. package/src/source-files.ts +19 -3
  209. package/src/static-report.ts +5 -0
  210. package/src/stored-object-headers.ts +43 -0
  211. package/src/style-routes.ts +2 -2
  212. package/src/sw-artifacts.ts +8 -8
  213. package/src/sw-precache-plan.ts +34 -4
  214. package/src/sync-authenticator.ts +3 -3
  215. package/src/sync-url.ts +3 -3
  216. package/src/tasks-facts.ts +3 -29
  217. package/src/templates/action.ts +7 -4
  218. package/src/templates/backfill.ts +4 -4
  219. package/src/templates/entity.ts +2 -2
  220. package/src/templates/github/ci.yml.ts +28 -18
  221. package/src/templates/github/image.yml.ts +92 -0
  222. package/src/templates/github/runner.ts +18 -0
  223. package/src/templates/guard-bare-error.ts +16 -2
  224. package/src/templates/guard-unzoned-date.ts +56 -6
  225. package/src/templates/index.ts +1 -1
  226. package/src/templates/job-body-test.ts +2 -2
  227. package/src/templates/job.ts +6 -6
  228. package/src/templates/naming.ts +28 -0
  229. package/src/templates/query.ts +9 -8
  230. package/src/templates/resource-page.ts +9 -7
  231. package/src/templates/resource-service.ts +2 -2
  232. package/src/templates/route.ts +26 -14
  233. package/src/templates/scaffold-api.ts +2 -3
  234. package/src/templates/scaffold-app.ts +8 -3
  235. package/src/templates/scaffold-auth.ts +205 -70
  236. package/src/templates/scaffold-bin.ts +122 -0
  237. package/src/templates/scaffold-claude-commands.ts +11 -11
  238. package/src/templates/scaffold-container-compose.ts +10 -1
  239. package/src/templates/scaffold-container.ts +16 -8
  240. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  241. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  242. package/src/templates/scaffold-dashboard-example.ts +112 -172
  243. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  244. package/src/templates/scaffold-dashboard.ts +9 -5
  245. package/src/templates/scaffold-db-client.ts +4 -2
  246. package/src/templates/scaffold-db-package.ts +2 -2
  247. package/src/templates/scaffold-docs.ts +20 -77
  248. package/src/templates/scaffold-domain-package.ts +0 -4
  249. package/src/templates/scaffold-entries.ts +4 -3
  250. package/src/templates/scaffold-env.ts +6 -3
  251. package/src/templates/scaffold-gitattributes.ts +59 -0
  252. package/src/templates/scaffold-helm-fences.ts +76 -0
  253. package/src/templates/scaffold-helm-templates.ts +100 -19
  254. package/src/templates/scaffold-helm.ts +62 -5
  255. package/src/templates/scaffold-http.ts +1 -1
  256. package/src/templates/scaffold-i18n.ts +15 -2
  257. package/src/templates/scaffold-icon.ts +2 -2
  258. package/src/templates/scaffold-repo.ts +27 -16
  259. package/src/templates/scaffold-shell.ts +55 -37
  260. package/src/test-counts.ts +2 -6
  261. package/src/test-ignores.ts +25 -0
  262. package/src/test-only-shipped.ts +193 -0
  263. package/src/test-passes.ts +0 -15
  264. package/src/test-select.ts +7 -15
  265. package/src/test-shards.ts +40 -95
  266. package/src/test-workers.ts +26 -4
  267. package/src/theme-boot.ts +8 -33
  268. package/src/theme-brand.ts +66 -0
  269. package/src/trusted-hops-error.ts +17 -0
  270. package/src/trusted-hops.ts +49 -0
  271. package/src/unscanned-admin.ts +2 -2
  272. package/src/unthrown-codes.ts +26 -0
  273. package/src/verify-checks.ts +29 -48
  274. package/src/verify-coverage-run.ts +3 -2
  275. package/src/verify-deadline.ts +207 -33
  276. package/src/verify-merge.ts +108 -43
  277. package/src/verify-part-step.ts +98 -0
  278. package/src/verify-role-load-probe.ts +12 -2
  279. package/src/verify-role-load.ts +9 -4
  280. package/src/verify-run.ts +32 -8
  281. package/src/verify-shard.ts +13 -2
  282. package/src/verify-stalled.ts +152 -0
  283. package/src/verify-step.ts +2 -0
  284. package/src/verify-test-run.ts +3 -2
  285. package/src/verify-tests.ts +5 -3
  286. package/src/worker-bundle.ts +2 -2
  287. package/src/workspace-checks.ts +53 -7
  288. package/src/workspace-graph.ts +3 -2
  289. package/src/write-line.ts +82 -7
  290. package/src/browser-launcher-fake-html.ts +0 -80
  291. package/src/browser-launcher-fake.ts +0 -168
  292. package/src/build-errors.ts +0 -54
  293. package/src/jobs-drain.ts +0 -163
  294. package/src/scaffold-typecheck.ts +0 -227
  295. package/src/thrown-by.ts +0 -24
  296. package/src/verify-errors.ts +0 -58
package/src/cmd-dev.ts CHANGED
@@ -1,371 +1,32 @@
1
- // `x dev` — the app, booted. Every role in one Bun process, embedded Postgres/events/storage
2
- // started for real, the app's own modules loaded into the framework's registries, and the route
3
- // table those registries hold served over HTTP. `@ultimat3/admin`'s `/_x` dashboard is mounted
4
- // alongside it — mounted, never re-implemented — so an agent can introspect the running app.
1
+ // `x dev` — the app, booted (`dev-boot.ts`), reported and held. Every role in one Bun process,
2
+ // embedded Postgres/events/storage started for real, and `@ultimat3/admin`'s `/_x` dashboard
3
+ // mounted alongside the app — mounted, never re-implemented — so an agent can introspect it.
5
4
  // No Docker, no env setup: an unset variable means the embedded default.
6
5
 
7
6
  import { join } from 'node:path';
8
- import { devShellStyle } from '@ultimat3/admin/dev';
9
- import type { Role } from '@ultimat3/core';
10
- import { configureTelemetry, METRICS_PATH, noopExporter } from '@ultimat3/core';
11
- import { setStatementObserver } from '@ultimat3/db';
12
- import type { OverlayNotice, RequestContext } from '@ultimat3/http';
13
- import { asCtx } from '@ultimat3/http';
14
- import type { Manifest } from '@ultimat3/manifest';
7
+ import { METRICS_PATH } from '@ultimat3/core';
15
8
  import { MANIFEST_FILENAME } from '@ultimat3/manifest';
16
- import { createIsrController } from '@ultimat3/render/server';
17
- import { loadSignInPath } from './app-auth';
18
- import { appManifest } from './app-manifest';
19
- import type { StalePin } from './app-reload-graph';
20
- import { enableReloadTracking, takeStalePins } from './app-reload-graph';
21
9
  import { requireAppRoot } from './app-root';
22
- import { loadAppRuntime } from './app-runtime';
23
10
  import { devSpec } from './cmd-dev-spec';
24
11
  import type { CliCommand, CommandContext } from './command';
25
- import type { DevDashboardInput, DevStatus } from './dev-dashboard';
26
- import { devPanels } from './dev-dashboard';
27
- import { declareDevEnvironment, needsDevEnvironmentDeclaration } from './dev-environment';
12
+ import { startDev } from './dev-boot';
28
13
  import { clearLock, preflight, writeLock } from './dev-lock';
29
- import { createStatementLedger } from './dev-n-plus-one';
30
14
  import { devPortFor } from './dev-port';
31
- import { coalesceReloads } from './dev-reload';
32
- import { devRouteTable } from './dev-route-table';
33
- import { childRestart, restartFinding } from './dev-supervisor';
34
- import { createTraceRecorder } from './dev-traces';
35
- import { watchTree } from './dev-watch-tree';
15
+ import { childRestart } from './dev-supervisor';
36
16
  import { holdUntilShutdown } from './hold';
37
- import type { IslandBundle } from './island-bundle';
38
- import { buildIslands } from './island-bundle';
39
- import { FRAME_STYLE } from './island-harness';
40
17
  import { msg } from './messages';
41
- import type { CommandResult, Finding } from './output';
42
- import { findingFrom } from './output';
18
+ import type { CommandResult } from './output';
43
19
  import { flagString } from './parse';
44
- import type { RunningRoles } from './role-start';
45
- import { DEV_BINDING, DEV_ROLES, selectRoles, startRoles } from './role-start';
46
- import type { DevServices } from './runtime-bindings';
47
- import {
48
- describeServices,
49
- reportedUrls,
50
- resolveServices,
51
- withRealtimeEvents,
52
- } from './runtime-bindings';
20
+ import { DEV_BINDING, selectRoles } from './role-start';
21
+ import { describeServices, reportedUrls, resolveServices } from './runtime-bindings';
53
22
  import { liveFeedLabel } from './runtime-live-feed';
54
- import type { RuntimeOverrides } from './runtime-overrides';
55
- import { replicaOverrides } from './runtime-replica';
56
- import type { RunningServices } from './runtime-services';
57
- import { cdnLabel, describeCdn, describeMail, mailLabel, startServices } from './runtime-services';
58
- import { metricsPortFor, releaseBoot } from './serve';
59
- import { loopFacts, loopFinding, loopNotice } from './statement-loop';
23
+ import { cdnLabel, describeCdn, describeMail, mailLabel } from './runtime-services';
24
+ import { writeLine } from './write-line';
60
25
 
61
- const _DEFAULT_PORT = 3000;
62
-
63
- export interface DevServer {
64
- readonly url: string;
65
- readonly services: DevServices;
66
- readonly roles: readonly Role[];
67
- /**
68
- * `BUILD_ID` when stamped — the id every response carries. Otherwise the manifest as it stands
69
- * now, so a reload that registers a new route moves it.
70
- */
71
- readonly buildId: string;
72
- /**
73
- * Modules that would not import, primitives that would not register, reloads that would not
74
- * build — and the statement loops this process has counted so far, which is what puts an N+1 in
75
- * `x dev`'s own output and in `--json` without a channel of its own.
76
- */
77
- readonly findings: readonly Finding[];
78
- readonly running: RunningRoles;
79
- readonly runtime: RunningServices;
80
- /** Panel keys `/_x` mounted, in tab order. Reported so `--json` names what is reachable. */
81
- readonly panels: readonly string[];
82
- /** `POST <path>` of the app's default MCP endpoint, or `null` when nothing was mounted. */
83
- readonly mcp: string | null;
84
- /** `POST <path>` of every mounted MCP endpoint, default first — `[]` when none was. */
85
- readonly mcpPaths: readonly string[];
86
- stop(): Promise<void>;
87
- }
88
-
89
- interface DevState {
90
- manifest: Manifest;
91
- reloads: number;
92
- /** A save that will not build. Replaced on every attempt, so a fixed file clears it. */
93
- reloadFinding: Finding | undefined;
94
- /**
95
- * Modules that would not import and primitives that would not register, as of the LAST scan —
96
- * the boot's, then each rebuild's. A page saved with a syntax error is a finding here until the
97
- * save that fixes it, and the boot's list alone would never show it.
98
- */
99
- appFindings: readonly Finding[];
100
- /**
101
- * The client entries, rebuilt on the same tick as the manifest. An island is the one module this
102
- * process never imports, so a fresh `Bun.build` is the whole of its reload. The route module
103
- * beside it is re-imported by that same scan (`app-load.ts`) — the two are one generation, or
104
- * a save serves a new island under an old page, which is what it did until 2026-09-07.
105
- */
106
- islands: IslandBundle;
107
- }
108
-
109
- export interface StartDevOptions {
110
- readonly root: string;
111
- readonly port: number;
112
- readonly env: Readonly<Record<string, string | undefined>>;
113
- readonly roles?: readonly Role[];
114
- readonly onReload?: (file: string, durationMs: number) => void;
115
- /**
116
- * A save reached a module that defines a primitive, so this process cannot serve it
117
- * (`takeStalePins`). Given, it is called INSTEAD of `onReload` — `x dev`'s child drains and exits
118
- * for its supervisor to boot a fresh one. Absent, the save is `X_DEV_RESTART_REQUIRED` on `/_x`.
119
- */
120
- readonly onRestart?: (pins: readonly StalePin[]) => void;
121
- }
122
-
123
- /**
124
- * The environment `devDashboard` refuses to mount in. Spread conditionally rather than passed as
125
- * `undefined`: `exactOptionalPropertyTypes` makes "absent" and "explicitly undefined" different
126
- * answers, and only the absent one lets the dashboard read the process environment itself.
127
- */
128
- const envOf = (env: StartDevOptions['env']): { env?: string } => {
129
- const value = env['NODE_ENV'] ?? env['X_ENV'];
130
- return value === undefined ? {} : { env: value };
131
- };
132
-
133
- /**
134
- * Boot order is the production order: services first, then the app's modules (importing them IS
135
- * the registration), then the roles that serve what those modules registered. A module that
136
- * fails to import becomes a finding rather than a dead process — the point of the dev loop is to
137
- * still be reachable while something is broken.
138
- */
139
- export async function startDev(options: StartDevOptions): Promise<DevServer> {
140
- // EVERY boot: a scratch server (`x shot`, `ui.shot`) boots here too, and without this a
141
- // fail-closed dev actor installs nothing — the picture is of a 401. Idempotent.
142
- declareDevEnvironment(options.env);
143
- // Before the first scan: the watcher's rescans evict a saved module AND everything importing it,
144
- // and that needs the import graph recorded as each module is first loaded (`app-reload-graph.ts`).
145
- enableReloadTracking(true);
146
- // The declaration lands on `process.env`; the env this boot passes on must carry it too, or every
147
- // service resolved from `options.env` still reads no `ULTIMATE_ENV`.
148
- const env = needsDevEnvironmentDeclaration(options.env)
149
- ? { ...options.env, ULTIMATE_ENV: 'development' }
150
- : options.env;
151
- const resolved = resolveServices(options.root, env);
152
- // The app's `apps/<app>/runtime.ts`, read ONCE and handed to `startServices` exactly as
153
- // `serve.ts`'s `withAppRuntime` does — so the disks (`storage`), the queue, the mail driver and
154
- // every other override a deployment declares are the ones development runs too. Until 22.4 this
155
- // boot passed nothing here and built its own embedded disk, so an app's `/_storage` routes and its
156
- // actions wrote to two different places in `x dev` and to one in production.
157
- const appRuntime = await loadAppRuntime(options.root);
158
- const runtime: RunningServices = await startServices(resolved, env, appRuntime);
159
- // The events binding the boot line reports is the bus the runtime chose, read off it.
160
- const services = withRealtimeEvents(resolved, env, runtime.realtime);
161
- // `serve.ts`'s `releaseBoot` shape: everything acquired from here on is released, newest first,
162
- // if the boot throws — a failed `x dev` left PGlite holding `.x/pgdata` for the retry to meet.
163
- const acquired: (() => void | Promise<void>)[] = [() => runtime.stop()];
164
- try {
165
- return await bootDev(options, services, runtime, acquired, appRuntime);
166
- } catch (error) {
167
- await releaseBoot(acquired);
168
- throw error;
169
- }
170
- }
171
-
172
- async function bootDev(
173
- options: StartDevOptions,
174
- services: ReturnType<typeof resolveServices>,
175
- runtime: RunningServices,
176
- acquired: (() => void | Promise<void>)[],
177
- appRuntime: RuntimeOverrides | undefined,
178
- ): Promise<DevServer> {
179
- // Installed before the app loads, so a span opened during registration is already recorded.
180
- // Tracing is always on in the framework and free until an exporter is configured; `x dev` is
181
- // what configures one, which is the whole reason `/_x/timeline` has anything to draw.
182
- const traces = createTraceRecorder();
183
- configureTelemetry({ exporter: traces.exporter });
184
- acquired.push(() => {
185
- configureTelemetry({ exporter: noopExporter });
186
- traces.reset();
187
- });
188
- // Installed at the same moment and for the same reason: an observer is the single switch that
189
- // turns statement instrumentation on at all (`@ultimat3/db`'s `observe.ts`), so the timeline's
190
- // SQL rows and the repeat counts arrive together rather than through two toggles. `serve.ts`
191
- // installs neither — a production process pays the one `undefined` branch the seam costs
192
- // uninstalled, and nothing more (axiom 6).
193
- const statements = createStatementLedger();
194
- setStatementObserver(statements.observer);
195
- acquired.push(() => {
196
- setStatementObserver(undefined);
197
- statements.reset();
198
- });
199
- // ONE load at boot, the same call the rebuild below makes: the manifest and the findings are
200
- // two projections of one scan. Until 2026-09-07 this was `loadApp` for the findings and then
201
- // `appManifest` — which loads again — for the manifest, so a save landing between the two put
202
- // `/_x`'s findings and its manifest on different registration states.
203
- const app = await appManifest(options.root);
204
- const state: DevState = {
205
- manifest: app.manifest,
206
- reloads: 0,
207
- reloadFinding: undefined,
208
- appFindings: app.findings,
209
- islands: await buildIslands(options.root),
210
- };
211
- // The manifest's build id is a content hash of every fact below it, so a dev document's
212
- // `x-ultimate-build` header names the exact shape the client was served against. Pinned at
213
- // boot on purpose: the header is handed to the HTTP config and the render modes once, and a
214
- // reload cannot re-pin it — `state.manifest.buildId` is what `/_x` and `--json` report, so a
215
- // divergence between the two is visible rather than silent, and a restart closes it.
216
- // `BUILD_ID` wins when set — `serve.ts`'s rule, so an e2e `deploy.newBuild()` can restart `x dev`
217
- // as a new build with the same sources.
218
- // Stamped, it is ALSO what `server.buildId` answers below: the process serves no other build.
219
- const rawStamp = options.env['BUILD_ID'];
220
- const stamped = rawStamp !== undefined && rawStamp !== '' ? rawStamp : undefined;
221
- const buildId = stamped ?? state.manifest.buildId;
222
-
223
- let server: DevServer;
224
- // Read at request time, never captured at boot: `/_x/services` must report the reload counter
225
- // and the findings as they are now, not as they were when the route table was built.
226
- const dashboard: DevDashboardInput = {
227
- root: options.root,
228
- runtime,
229
- status: (): DevStatus => ({
230
- url: server.url,
231
- services: server.services,
232
- roles: server.roles,
233
- findings: server.findings,
234
- reloads: state.reloads,
235
- }),
236
- traces,
237
- statements,
238
- ...envOf(options.env),
239
- };
240
- const panels = devPanels(dashboard).map((panel) => panel.key);
241
-
242
- // `x dev`'s own, so a reload can empty it: an `isr` page's first render was otherwise served for
243
- // its whole ttl after every save, whatever the modules behind it now said.
244
- const isr = createIsrController({ buildId });
245
- const { routes, theme, speculation, errorStyles, mcpPath, mcpPaths } = await devRouteTable({
246
- isr,
247
- root: options.root,
248
- env: options.env,
249
- buildId,
250
- storage: runtime.storage,
251
- dashboard,
252
- islands: () => state.islands,
253
- realtime: runtime.realtime,
254
- rateLimitStore: appRuntime?.rateLimitStore ?? runtime.rateLimitStore,
255
- appRoutes: appRuntime?.routes,
256
- });
257
-
258
- // The app's `apps/<app>/runtime.ts`, composed exactly as `runRole` composes a caller's
259
- // `runtime`: the replica scope in front, the app's own middleware behind it. Before this the
260
- // first argument was `undefined` here and an app's middleware reached no development process.
261
- const replicaOverride = replicaOverrides(appRuntime, services.db, options.env);
262
- const running = await startRoles({
263
- roles: options.roles ?? DEV_ROLES,
264
- port: options.port,
265
- // `serve.ts`'s expression, called rather than restated: `METRICS_PORT` was read in the
266
- // container and ignored here, so the scrape port an operator moved was the one port `x dev`
267
- // could not move — and the second `x dev` on a box died binding the hardcoded 9090.
268
- metricsPort: metricsPortFor(options.env, options.port),
269
- buildId,
270
- runtime,
271
- routes,
272
- env: options.env,
273
- // Read from `app.config.ts` rather than threaded through `DevOptions`: `x dev` and `serve.ts`
274
- // must not be able to disagree about where the app's sign-in page is.
275
- signInPath: await loadSignInPath(options.root),
276
- // The same seam `serve.ts` passes: the app's own error page is a FILE under this root.
277
- root: options.root,
278
- // The `/_x` shell, the harness's frame, and the app's own error pages — the inline bodies this
279
- // process serves; the app's surfaces are content-hashed files `'self'` admits.
280
- inlineStyles: [await devShellStyle(), FRAME_STYLE, ...errorStyles],
281
- inlineScripts: [theme.cspSource, ...(speculation === undefined ? [] : [speculation.cspSource])],
282
- // The overlay renders this request's own loops under the error it is already showing.
283
- // `serve.ts` boots through the same `startRoles` and passes nothing (axiom 6).
284
- devNotices: (ctx: RequestContext): readonly OverlayNotice[] =>
285
- statements.repeatsFor(asCtx(ctx)).map(loopFacts).map(loopNotice),
286
- // The read-replica scope, opened per request. Absent for every app that names no
287
- // `DATABASE_REPLICA_URL` — which is every embedded boot by construction, since PGlite has no
288
- // standby — so this key does not exist on a homework app's boot at all.
289
- ...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
290
- });
291
- acquired.push(() => running.stop());
292
-
293
- // One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
294
- // trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
295
- const rebuild = coalesceReloads(
296
- async (file) => {
297
- const started = performance.now();
298
- const [{ manifest, findings }, islands] = await Promise.all([
299
- appManifest(options.root),
300
- buildIslands(options.root),
301
- ]);
302
- state.manifest = manifest;
303
- state.appFindings = findings;
304
- state.islands = islands;
305
- for (const path of isr.store().paths()) isr.store().delete(path);
306
- state.reloads += 1;
307
- // Read on EVERY rebuild, so a pin one save reached is never reported against the next save.
308
- const pins = takeStalePins();
309
- state.reloadFinding =
310
- pins.length > 0 && options.onRestart === undefined
311
- ? restartFinding(options.root, pins)
312
- : undefined;
313
- if (pins.length > 0 && options.onRestart !== undefined) options.onRestart(pins);
314
- else options.onReload?.(file, Math.round(performance.now() - started));
315
- },
316
- // Same rule as a module that will not import: a save the manifest cannot be rebuilt from is
317
- // a finding on `/_x`, never an unhandled rejection that takes the dev server down.
318
- (error: unknown, file: string) => {
319
- state.reloadFinding = { ...findingFrom(error), at: file };
320
- },
321
- );
322
- // Watched one directory at a time, so an ignored one costs no descriptor at all — `dev-watch.ts`
323
- // decides which, from the app's own `.gitignore`. A recursive watch on the root registered one
324
- // inotify descriptor per directory in the tree, `.git/`, `node_modules/` and the `.x/` this
325
- // process writes to included, and filtered the events afterwards.
326
- const watcher = watchTree({ root: options.root, onChange: rebuild });
327
-
328
- server = {
329
- url: running.url ?? `http://localhost:${options.port}`,
330
- services,
331
- roles: running.roles,
332
- mcp: mcpPath,
333
- mcpPaths,
334
- get buildId(): string {
335
- return stamped ?? state.manifest.buildId;
336
- },
337
- // A getter, not a snapshot: `/_x` and `--json` must show the reload that just failed and the
338
- // loop the last request tripped, not the findings as they were when the route table was built.
339
- // The loops come last and carry their request id, so a boot report reads as a boot report and
340
- // a diagnostic that arrived a minute later reads as one too.
341
- get findings(): readonly Finding[] {
342
- const loops = statements.repeats().map(loopFacts).map(loopFinding);
343
- return state.reloadFinding === undefined
344
- ? [...state.appFindings, ...loops]
345
- : [...state.appFindings, state.reloadFinding, ...loops];
346
- },
347
- running,
348
- runtime,
349
- panels,
350
- async stop() {
351
- watcher.close();
352
- await running.stop();
353
- await runtime.stop();
354
- // Released after the roles: a span opened by an in-flight request still has an exporter to
355
- // end into. `configureTelemetry` merges, so handing back the noop is how it is uninstalled —
356
- // leaving it in place would keep every span of the next `startDev` in this process's buffer.
357
- configureTelemetry({ exporter: noopExporter });
358
- traces.reset();
359
- // Released with the exporter, after the roles, for the same reason: a statement still in
360
- // flight is observed by the ledger that counted the rest of its request. Leaving it
361
- // installed would keep every statement of the next `startDev` in this process's counts —
362
- // and, worse, keep instrumentation on in a process that is no longer a dev server.
363
- setStatementObserver(undefined);
364
- statements.reset();
365
- },
366
- };
367
- return server;
368
- }
26
+ // The boot moved to `dev-boot.ts`; its names stay importable from here, where every caller and the
27
+ // package barrel already reach them.
28
+ export type { DevServer, StartDevOptions } from './dev-boot';
29
+ export { startDev } from './dev-boot';
369
30
 
370
31
  export const devCommand: CliCommand = {
371
32
  spec: devSpec,
@@ -399,8 +60,7 @@ export const devCommand: CliCommand = {
399
60
  roles,
400
61
  env: ctx.env,
401
62
  onReload: (file, durationMs) => {
402
- if (!ctx.args.json)
403
- process.stdout.write(`${msg('cli.dev.hmr', { file, ms: durationMs })}\n`);
63
+ if (!ctx.args.json) writeLine(msg('cli.dev.hmr', { file, ms: durationMs }));
404
64
  },
405
65
  // Supervised, a save this process cannot serve is a drain and an exit for a fresh child.
406
66
  ...restart.options,
@@ -408,6 +68,8 @@ export const devCommand: CliCommand = {
408
68
  release();
409
69
  throw error;
410
70
  });
71
+ // A restart retires this worker before it drains, so a save never cancels a running job.
72
+ restart.adopt(server.running.worker);
411
73
  const result: CommandResult = {
412
74
  ok: server.findings.length === 0,
413
75
  command: 'dev',
@@ -3,19 +3,12 @@
3
3
 
4
4
  import type { CommandSpec } from './parse';
5
5
 
6
- /** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
7
- export const DEFAULT_DOCTOR_PORT = 3000;
8
-
9
6
  export const doctorSpec: CommandSpec = {
10
7
  name: 'doctor',
11
8
  summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
12
9
  usage: 'x doctor [--port 3000] [--json]',
13
10
  flags: [
14
- {
15
- name: 'port',
16
- type: 'string',
17
- summary: 'port to test',
18
- default: String(DEFAULT_DOCTOR_PORT),
19
- },
11
+ // No `default`: it would be indistinguishable from a typed value, and `PORT` could never win.
12
+ { name: 'port', type: 'string', summary: 'port to test (default: PORT, then 3000)' },
20
13
  ],
21
14
  };