@ultimat3/cli 29.3.2 → 30.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 (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +13 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +28 -8
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +42 -138
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +17 -27
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
package/src/dev-lock.ts CHANGED
@@ -17,7 +17,9 @@
17
17
  import { linkSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { stringField, UltimateError } from '@ultimat3/core';
20
- import { exec, type Runner } from './exec';
20
+ import { reusedPid, startedOf } from './dev-lock-identity';
21
+ import { type PortHolder, portHolder } from './dev-lock-port-holder';
22
+ import { stealStale } from './dev-lock-steal';
21
23
  import { portPairAfter } from './flag-number';
22
24
  import { quoteArg } from './shell-quote';
23
25
 
@@ -30,8 +32,16 @@ export interface DevLock {
30
32
  readonly url: string;
31
33
  /** ISO 8601. Only ever printed — a stale lock is decided by the pid, never by the clock. */
32
34
  readonly startedAt: string;
35
+ /** The holder's OS start time (`dev-lock-identity.ts`): a reused pid does not match it. */
36
+ readonly started?: string;
33
37
  }
34
38
 
39
+ export { netstatListener, portHolder, tasklistImage } from './dev-lock-port-holder';
40
+ // Re-exported: the port refusal's holder, split out at the file ceiling.
41
+ export type { PortHolder };
42
+ // Re-exported: `cmd-stop.ts` asks the same question before it signals.
43
+ export { reusedPid };
44
+
35
45
  export const lockPath = (stateDir: string): string => join(stateDir, DEV_LOCK_FILE);
36
46
 
37
47
  /**
@@ -47,6 +57,7 @@ export const parseLock = (raw: string): DevLock | null => {
47
57
  port: value.port,
48
58
  url: typeof value.url === 'string' ? value.url : `http://localhost:${value.port}`,
49
59
  startedAt: typeof value.startedAt === 'string' ? value.startedAt : '',
60
+ ...(typeof value.started === 'string' ? { started: value.started } : {}),
50
61
  };
51
62
  } catch {
52
63
  return null;
@@ -115,7 +126,8 @@ export class DevAlreadyRunningError extends UltimateError {
115
126
  super({
116
127
  code: 'X_DEV_ALREADY_RUNNING',
117
128
  cause: `pid ${input.lock.pid} is already running x dev on ${input.lock.url} and holds ${input.stateDir}${single}`,
118
- fix: `use the one already running at ${input.lock.url}, or stop it: kill ${input.lock.pid}`,
129
+ // `x stop`, not `kill <pid>`: a kill returns before the drain ends and the next boot refuses.
130
+ fix: `use the one already running at ${input.lock.url}, or stop it: x stop --json`,
119
131
  meta: { pid: input.lock.pid, port: input.lock.port, stateDir: input.stateDir },
120
132
  });
121
133
  }
@@ -162,82 +174,6 @@ export class DevStateUnwritableError extends UltimateError {
162
174
  }
163
175
  }
164
176
 
165
- /** Whatever is listening, as far as the OS will say. Both fields absent when it will not say. */
166
- export interface PortHolder {
167
- readonly pid?: number;
168
- /** The process name, e.g. `bun`. Enough to recognise "that is my other project". */
169
- readonly command?: string;
170
- }
171
-
172
- /**
173
- * Who holds the port. `ss` first because it is present on every Linux box the framework targets and
174
- * needs no elevation for your own processes; `lsof` is the macOS answer; `netstat -ano` plus
175
- * `tasklist` the Windows one.
176
- *
177
- * Through `exec.ts`, like every other subprocess the CLI runs, so a test injects a fake `Runner`
178
- * rather than racing a real socket.
179
- *
180
- * EVERY PROBE IS CAUGHT SEPARATELY. `exec` refuses a missing program with `X_CLI_UNEXPECTED`, and
181
- * letting that escape would mean a box without `ss` gets the CLI's catch-all instead of
182
- * `X_PORT_IN_USE` — the exact substitution this module exists to end. A missing `ss` must fall
183
- * through to `lsof`, and a box with none still gets the refusal, just without a pid in it.
184
- */
185
- export const portHolder = async (port: number, runner: Runner = exec): Promise<PortHolder> => {
186
- const probe = async (command: readonly string[]): Promise<string> => {
187
- try {
188
- const result = await runner(command, { cwd: process.cwd() });
189
- return result.stdout;
190
- } catch {
191
- return '';
192
- }
193
- };
194
-
195
- const parsed = /users:\(\("([^"]+)",pid=(\d+)/.exec(
196
- await probe(['ss', '-lptnH', `sport = :${port}`]),
197
- );
198
- const name = parsed?.[1];
199
- if (parsed !== null && name !== undefined) return { command: name, pid: Number(parsed[2]) };
200
-
201
- const lines = (
202
- await probe(['lsof', '-nP', `-iTCP:${String(port)}`, '-sTCP:LISTEN', '-Fpc'])
203
- ).split('\n');
204
- const pid = lines.find((line) => line.startsWith('p'))?.slice(1);
205
- const command = lines.find((line) => line.startsWith('c'))?.slice(1);
206
- if (pid !== undefined && /^\d+$/.test(pid)) {
207
- return command === undefined ? { pid: Number(pid) } : { command, pid: Number(pid) };
208
- }
209
-
210
- // Windows has neither: `netstat -ano` names the listening pid, `tasklist` its image.
211
- const listener = netstatListener(await probe(['netstat', '-ano', '-p', 'TCP']), port);
212
- if (listener === undefined) return {};
213
- const image = tasklistImage(
214
- await probe(['tasklist', '/FI', `PID eq ${String(listener)}`, '/FO', 'CSV', '/NH']),
215
- );
216
- return image === undefined ? { pid: listener } : { command: image, pid: listener };
217
- };
218
-
219
- /**
220
- * The pid `netstat -ano` reports LISTENING on `port`, any local address: `TCP 127.0.0.1:3000
221
- * 0.0.0.0:0 LISTENING 4242`, and `[::1]:3000` for IPv6. The state column is the word Windows
222
- * prints in English on every locale this was read on; an unrecognised line is skipped.
223
- */
224
- export function netstatListener(text: string, port: number): number | undefined {
225
- for (const line of text.split(/\r?\n/)) {
226
- const [proto, local, , state, pid] = line.trim().split(/\s+/);
227
- if (proto !== 'TCP' || state !== 'LISTENING' || pid === undefined || !/^\d+$/.test(pid)) {
228
- continue;
229
- }
230
- if (local?.endsWith(`:${String(port)}`) === true) return Number(pid);
231
- }
232
- return undefined;
233
- }
234
-
235
- /** The image name off `tasklist /FO CSV /NH`'s one row — `"bun.exe","4242",…` — or `undefined`. */
236
- export function tasklistImage(text: string): string | undefined {
237
- const name = /^"([^"]+)","\d+"/m.exec(text.trim())?.[1];
238
- return name === undefined || name === '' ? undefined : name;
239
- }
240
-
241
177
  /**
242
178
  * The port is bound by something that is not us. Named separately from the lock case because the
243
179
  * remedy is different: a stranger's port is worked around with `--port`, while a second `x dev` on
@@ -333,11 +269,23 @@ function claimExclusive(stateDir: string, path: string, lock: DevLock): boolean
333
269
  }
334
270
  }
335
271
 
272
+ /**
273
+ * The lock's bytes right now, or `null` when there is no file to read. Total: a rival's steal may
274
+ * remove the file between `exists()` and the read, and that is "absent", never a bare `ENOENT`.
275
+ */
276
+ const readRaw = async (path: string): Promise<string | null> => {
277
+ const file = Bun.file(path);
278
+ try {
279
+ return (await file.exists()) ? await file.text() : null;
280
+ } catch {
281
+ return null;
282
+ }
283
+ };
284
+
336
285
  /** Whatever is on disk right now, or `null` if it is absent or half-written. */
337
286
  const readLock = async (path: string): Promise<DevLock | null> => {
338
- const file = Bun.file(path);
339
- if (!(await file.exists())) return null;
340
- return parseLock(await file.text());
287
+ const raw = await readRaw(path);
288
+ return raw === null ? null : parseLock(raw);
341
289
  };
342
290
 
343
291
  export interface PreflightInput {
@@ -363,6 +311,62 @@ export interface PreflightResult {
363
311
  release(): void;
364
312
  }
365
313
 
314
+ /** Rounds of claim-or-clear before the lock is reported unreadable, and the wait on a busy steal. */
315
+ const CLAIM_ROUNDS = 200;
316
+ const STEAL_BUSY_MS = 5;
317
+
318
+ /**
319
+ * The claim, clearing a stale lock on the way: `true` when one was cleared. Each round claims, or
320
+ * reads the holder and refuses a live one, or clears a stale one under `dev.lock.steal`
321
+ * (`stealStale`) and goes round again — so a lock that changed hands mid-round is judged afresh,
322
+ * never deleted by path. A rival's fresh claim then reads as live and is refused; an unlink after
323
+ * an unguarded read deleted it, and two boots shared one `.x/pgdata`.
324
+ */
325
+ async function claimOrClearStale(
326
+ input: PreflightInput,
327
+ path: string,
328
+ mine: DevLock,
329
+ alive: (pid: number) => boolean,
330
+ ): Promise<boolean> {
331
+ let clearedStale = false;
332
+ let held: DevLock | null = null;
333
+ for (let round = 0; round < CLAIM_ROUNDS; round += 1) {
334
+ if (claimExclusive(input.stateDir, path, mine)) return clearedStale;
335
+ const raw = await readRaw(path);
336
+ const now = raw === null ? null : parseLock(raw);
337
+ // `held` keeps the last identity read: a half-written file parses as `null` for microseconds.
338
+ held = now ?? held;
339
+ if (now !== null && alive(now.pid) && !(await reusedPid(now))) {
340
+ throw new DevAlreadyRunningError({
341
+ lock: now,
342
+ stateDir: input.stateDir,
343
+ ...(input.embeddedDb === undefined ? {} : { embeddedDb: input.embeddedDb }),
344
+ });
345
+ }
346
+ // Stale: a hard kill, or a file nothing here wrote. No bytes read (a directory at the lock's
347
+ // path, say) leaves nothing to compare, so nothing is removed and the claim decides alone.
348
+ clearedStale = true;
349
+ if (raw === null) continue;
350
+ const outcome = stealStale({
351
+ path,
352
+ judged: raw,
353
+ claim: (guard) => claimExclusive(input.stateDir, guard, mine),
354
+ alive: isProcessAlive,
355
+ });
356
+ if (outcome === 'busy') await Bun.sleep(STEAL_BUSY_MS);
357
+ }
358
+ // NEVER `mine`, which is what it fell back to: with an unparseable lock that could not be
359
+ // cleared, both reads answered `null` and the refusal named THIS pid as the holder — `kill
360
+ // <self>` as the remedy for a file nobody could read.
361
+ const holder = (await readLock(path)) ?? held;
362
+ if (holder === null) throw new DevLockUnreadableError({ path, stateDir: input.stateDir });
363
+ throw new DevAlreadyRunningError({
364
+ lock: holder,
365
+ stateDir: input.stateDir,
366
+ ...(input.embeddedDb === undefined ? {} : { embeddedDb: input.embeddedDb }),
367
+ });
368
+ }
369
+
366
370
  /**
367
371
  * Run before anything boots, and it CLAIMS: it returns holding the directory, never having merely
368
372
  * looked at it.
@@ -374,8 +378,8 @@ export interface PreflightResult {
374
378
  * records. The claim closes the window it names.
375
379
  *
376
380
  * The stale path still exists and is still normal (a hard kill leaves a lock behind): the file is
377
- * removed and the claim retried once. A retry that ALSO loses is another boot that claimed the
378
- * cleared slot in that instant, which is a refusal and not a third boot.
381
+ * removed (`claimOrClearStale`) and the claim retried. A retry that ALSO loses is another boot
382
+ * that claimed the cleared slot in that instant, which is a refusal and not a third boot.
379
383
  */
380
384
  export const preflight = async (input: PreflightInput): Promise<PreflightResult> => {
381
385
  const alive = input.alive ?? isProcessAlive;
@@ -396,43 +400,9 @@ export const preflight = async (input: PreflightInput): Promise<PreflightResult>
396
400
  // is what the refusal prints, and it is the address the boot is about to ask for.
397
401
  url: `http://${input.hostname}:${input.port}`,
398
402
  startedAt: new Date().toISOString(),
403
+ ...(await startedOf(process.pid)),
399
404
  };
400
- let clearedStale = false;
401
-
402
- if (!claimExclusive(input.stateDir, path, mine)) {
403
- const held = await readLock(path);
404
- if (held !== null && alive(held.pid)) {
405
- throw new DevAlreadyRunningError({
406
- lock: held,
407
- stateDir: input.stateDir,
408
- ...(input.embeddedDb === undefined ? {} : { embeddedDb: input.embeddedDb }),
409
- });
410
- }
411
- // Stale: a hard kill, or a file nothing here wrote. Removing it is what makes the next boot
412
- // work, and `unlinkSync` because a lock that outlives its own cleanup is the bug being fixed.
413
- try {
414
- unlinkSync(path);
415
- } catch {
416
- // Already gone, or not ours to remove. The claim below is what decides.
417
- }
418
- clearedStale = true;
419
- if (!claimExclusive(input.stateDir, path, mine)) {
420
- // Lost the race for the slot we just cleared. `held` is the best identity available — a
421
- // half-written file parses as `null` for microseconds — and refusing on a stale pid beats
422
- // the alternative, which is two processes writing one single-writer data directory.
423
- //
424
- // NEVER `mine`, which is what it fell back to: with an unparseable lock and a failed
425
- // `unlinkSync`, both reads answer `null` and the refusal named THIS pid as the holder —
426
- // `kill <self>` as the remedy for a file nobody could read.
427
- const holder = (await readLock(path)) ?? held;
428
- if (holder === null) throw new DevLockUnreadableError({ path, stateDir: input.stateDir });
429
- throw new DevAlreadyRunningError({
430
- lock: holder,
431
- stateDir: input.stateDir,
432
- ...(input.embeddedDb === undefined ? {} : { embeddedDb: input.embeddedDb }),
433
- });
434
- }
435
- }
405
+ const clearedStale = await claimOrClearStale(input, path, mine, alive);
436
406
 
437
407
  if (bound(input.port, input.hostname)) {
438
408
  // Released before the throw: the remedy this refusal prints is `x dev --port <n>`, and a claim
@@ -455,11 +425,13 @@ export const preflight = async (input: PreflightInput): Promise<PreflightResult>
455
425
  * with the address the server actually bound, which is the one field the preflight could not know.
456
426
  */
457
427
  export const writeLock = async (stateDir: string, lock: DevLock): Promise<void> => {
428
+ const stamped: DevLock =
429
+ lock.started === undefined ? { ...lock, ...(await startedOf(lock.pid)) } : lock;
458
430
  // Staged and renamed over the claim, never rewritten in place: a truncate-then-write leaves an
459
431
  // empty lock for an instant, and a preflight reading it then would clear a live claim as stale.
460
432
  const path = lockPath(stateDir);
461
433
  const staged = stagingPath(path);
462
- writeFileSync(staged, lockText(lock));
434
+ writeFileSync(staged, lockText(stamped));
463
435
  renameSync(staged, path);
464
436
  };
465
437
 
@@ -0,0 +1,51 @@
1
+ // The `/_x` mail panel's templates source: every registered mail, its declared `preview` rendered
2
+ // in each of the app's locales by `@ultimat3/mail`'s `previewMail` — the same render
3
+ // `x mail preview` prints. Dates in a preview render in UTC, named: there is no ambient zone.
4
+
5
+ import type { MailTemplateFact, MailTemplatePreviewFact } from '@ultimat3/admin/dev';
6
+ import { InternalError, renderFixShellArg, renderThrowable, stringField } from '@ultimat3/core';
7
+ import { mailFor, previewMail, registeredMailIds } from '@ultimat3/mail';
8
+
9
+ /** The zone a preview's dates render in: a sample has no reader, so no reader's zone. */
10
+ const PREVIEW_ZONE = 'UTC';
11
+
12
+ export function mailTemplateFacts(locales: readonly string[]): readonly MailTemplateFact[] {
13
+ return registeredMailIds().map((id) => {
14
+ const hasPreview = mailFor(id)?.preview !== undefined;
15
+ return {
16
+ id,
17
+ hasPreview,
18
+ previews: hasPreview ? locales.map((locale) => previewIn(id, locale)) : [],
19
+ };
20
+ });
21
+ }
22
+
23
+ /**
24
+ * A preview function's data is parsed at each render, so one locale's can be refused while the
25
+ * others render: that refusal is this cell's, shown with its fix, never the whole panel's.
26
+ */
27
+ function previewIn(id: string, locale: string): MailTemplatePreviewFact {
28
+ try {
29
+ const preview = previewMail(id, { locale, tz: PREVIEW_ZONE });
30
+ return {
31
+ locale,
32
+ ok: true,
33
+ subject: preview.subject,
34
+ html: preview.html,
35
+ text: preview.text,
36
+ missing: preview.misses.map((miss) => miss.key),
37
+ };
38
+ } catch (error) {
39
+ return {
40
+ locale,
41
+ ok: false,
42
+ error: {
43
+ code: stringField(error, 'code') ?? InternalError.code,
44
+ cause: stringField(error, 'cause') ?? renderThrowable(error),
45
+ fix:
46
+ stringField(error, 'fix') ??
47
+ `x mail preview ${renderFixShellArg(id, '<mail-id>')} --locale ${renderFixShellArg(locale, '<locale>')} --json`,
48
+ },
49
+ };
50
+ }
51
+ }
@@ -3,6 +3,7 @@
3
3
  // supervisor as a restart. Coalesced, so a tick arriving mid-build is one trailing rebuild.
4
4
 
5
5
  import type { Manifest } from '@ultimat3/manifest';
6
+ import { beginDeviceSocketGeneration } from '@ultimat3/realtime/server';
6
7
  import type { IsrController } from '@ultimat3/render/server';
7
8
  import { appManifest } from './app-manifest';
8
9
  import type { StalePin } from './app-reload-graph';
@@ -42,17 +43,36 @@ export interface DevRebuildOptions {
42
43
  readonly onRestart?: (pins: readonly StalePin[]) => void;
43
44
  }
44
45
 
45
- /** The watcher's `onChange`: rebuild `state` for the saved `file`, never rejecting. */
46
+ /**
47
+ * A save that declared a `deviceSocket()` path the listener did not bind: Bun's websocket paths
48
+ * are fixed when it starts, so only a new process serves it — `restartFinding`'s code and fix.
49
+ */
50
+ export function unservedDeviceFinding(path: string, file: string): Finding {
51
+ return {
52
+ code: 'X_DEV_RESTART_REQUIRED',
53
+ cause: `deviceSocket('${path}') was declared after x dev bound its listener, which serves only the device paths it booted with`,
54
+ fix: 'x stop && x dev # the listener binds its websocket paths once, at boot',
55
+ at: file,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * The watcher's `onChange`: rebuild `state` for the saved `file`, never rejecting. `refreshDevices`
61
+ * is the running roles' (`RunningRoles.refreshDevices`): the rescan is a new device generation, so
62
+ * a re-evaluated `deviceSocket()` replaces its path's old value instead of colliding with it.
63
+ */
46
64
  export function devRebuilder(
47
65
  options: DevRebuildOptions,
48
66
  state: DevState,
49
67
  isr: IsrController,
68
+ refreshDevices: () => Promise<readonly string[]>,
50
69
  ): ReloadTrigger {
51
70
  // One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
52
71
  // trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
53
72
  return coalesceReloads(
54
73
  async (file) => {
55
74
  const started = performance.now();
75
+ beginDeviceSocketGeneration();
56
76
  const [{ manifest, findings }, islands] = await Promise.all([
57
77
  appManifest(options.root),
58
78
  buildIslands(options.root),
@@ -60,14 +80,18 @@ export function devRebuilder(
60
80
  state.manifest = manifest;
61
81
  state.appFindings = findings;
62
82
  state.islands = islands;
83
+ const unserved = await refreshDevices();
63
84
  for (const path of isr.store().paths()) isr.store().delete(path);
64
85
  state.reloads += 1;
65
86
  // Read on EVERY rebuild, so a pin one save reached is never reported against the next save.
66
87
  const pins = takeStalePins();
88
+ const [device] = unserved;
67
89
  state.reloadFinding =
68
90
  pins.length > 0 && options.onRestart === undefined
69
91
  ? restartFinding(options.root, pins)
70
- : undefined;
92
+ : device === undefined
93
+ ? undefined
94
+ : unservedDeviceFinding(device, file);
71
95
  if (pins.length > 0 && options.onRestart !== undefined) options.onRestart(pins);
72
96
  else options.onReload?.(file, Math.round(performance.now() - started));
73
97
  },
@@ -11,7 +11,10 @@ import type { IsrController } from '@ultimat3/render/server';
11
11
  import type { ImageTransformDriver } from '@ultimat3/seo';
12
12
  import type { Storage } from '@ultimat3/storage';
13
13
  import { apiMountRoutes, apiRoutes, pagePostRoutes } from './api-routes';
14
+ import { loadAppConfig } from './app-config-load';
14
15
  import { mountAppMcp } from './app-mcp';
16
+ import { clientErrorsSurface } from './client-errors-routes';
17
+ import { clientScriptHeads, clientVitalsSurface } from './client-vitals-route';
15
18
  import type { DevDashboardInput } from './dev-dashboard';
16
19
  import { devDashboardRoutes } from './dev-dashboard';
17
20
  import { errorPageStyleBodies } from './error-page-csp';
@@ -37,6 +40,7 @@ import { serviceWorkerRoutes } from './sw-routes';
37
40
  import type { ThemeBoot } from './theme-boot';
38
41
  import { loadThemeMode, themeBoot } from './theme-boot';
39
42
  import { loadThemeBrand } from './theme-brand';
43
+ import { type ZoneBoot, zoneBoot } from './zone-boot';
40
44
 
41
45
  export interface DevRouteTableInput {
42
46
  readonly root: string;
@@ -71,6 +75,8 @@ export interface DevRouteTable {
71
75
  readonly theme: ThemeBoot;
72
76
  /** The speculation rules, admitted the same way — `undefined` when no document carries any. */
73
77
  readonly speculation: PageSpeculation | undefined;
78
+ /** The zone script (`time.detect`), admitted the same way — `undefined` when the app left it off. */
79
+ readonly zone: ZoneBoot | undefined;
74
80
  /**
75
81
  * Every inline `<style>` body these routes put in a document: the app's error pages' and its
76
82
  * brand's (`theme-brand.ts`) — what `startRoles({ inlineStyles })` hashes into `style-src`.
@@ -138,6 +144,13 @@ export async function devRouteTable(input: DevRouteTableInput): Promise<DevRoute
138
144
  const mcpMount = await mountAppMcp(input.root);
139
145
  // `serve-boot.ts`'s answer: the app's declared disks when it has any, read per request.
140
146
  const served = servedStorage(input.storage);
147
+ // `clientErrors.enabled` or nothing: off, the route is a 404 and no document names the script.
148
+ const appConfig = await loadAppConfig(input.root);
149
+ const reporting = appConfig?.clientErrors;
150
+ const zone = zoneBoot(appConfig?.time);
151
+ const clientErrors = clientErrorsSurface(reporting, input.buildId);
152
+ // Real-user timings: their own switch (`clientErrors.vitals.enabled`), the same door.
153
+ const vitals = clientVitalsSurface(reporting?.vitals);
141
154
  const routes: readonly Route[] = [
142
155
  ...devDashboardRoutes(input.dashboard),
143
156
  // The same API table the container serves: a read that answers here and 404s in production
@@ -156,6 +169,8 @@ export async function devRouteTable(input: DevRouteTableInput): Promise<DevRoute
156
169
  ...(pwa === undefined ? {} : { pwa }),
157
170
  }),
158
171
  ...storageRoutes({ storage: served }),
172
+ ...(clientErrors?.routes ?? []),
173
+ ...(vitals?.routes ?? []),
159
174
  // The chunks the documents below name. Mounted before the app's routes for the reason
160
175
  // `/icons` and `/media` are: a page route must not be able to shadow an asset URL.
161
176
  ...islandRoutes(() => input.islands()),
@@ -205,6 +220,7 @@ export async function devRouteTable(input: DevRouteTableInput): Promise<DevRoute
205
220
  ...(pwa === undefined
206
221
  ? {}
207
222
  : { pwaHead: (locale: string) => pwa.headFor(locale) + (serviceWorker?.head ?? '') }),
223
+ ...clientScriptHeads(clientErrors, vitals, zone),
208
224
  }),
209
225
  ];
210
226
 
@@ -212,6 +228,7 @@ export async function devRouteTable(input: DevRouteTableInput): Promise<DevRoute
212
228
  routes,
213
229
  theme,
214
230
  speculation,
231
+ zone,
215
232
  inlineStyles,
216
233
  mcpPath: mcpMount.path,
217
234
  mcpPaths: mcpMount.paths,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { StalePin } from './app-reload-graph';
9
9
  import { devSpec } from './cmd-dev-spec';
10
- import type { DevChildGone, DevDrainMessage } from './dev-child-watch';
10
+ import type { DevChildLeaving, DevDrainMessage } from './dev-child-watch';
11
11
  import {
12
12
  DEV_SUPERVISOR_PID_ENV,
13
13
  devHardExitMs,
@@ -267,7 +267,7 @@ export function childRestart(
267
267
  if (env[DEV_CHILD_ENV] !== '1') {
268
268
  return { options: {}, exit: () => undefined, adopt: () => undefined };
269
269
  }
270
- let leaving: 'restart' | DevChildGone | undefined;
270
+ let leaving: 'restart' | DevChildLeaving | undefined;
271
271
  let worker: RetiringWorker | null = null;
272
272
  let restarting = false;
273
273
  const watched =
@@ -279,6 +279,9 @@ export function childRestart(
279
279
  return {
280
280
  options: {
281
281
  onRestart: (pins) => {
282
+ // Already stopping for a signal (`x stop`, Ctrl-C) or a gone supervisor or root: a save
283
+ // landing during that drain must not respawn a child its operator asked to end.
284
+ if (leaving !== undefined && leaving !== 'restart') return;
282
285
  writeErrorLine(msg('cli.dev.restart', { reason: restartReason(root, pins) }));
283
286
  leaving ??= 'restart';
284
287
  watched?.stopping(DEV_RESTART_EXIT_CODE);
@@ -47,14 +47,17 @@ export interface WatchTree {
47
47
  const DEFAULT_DEBOUNCE_MS = 30;
48
48
 
49
49
  /**
50
- * Every directory under `root` an event could be a source change in, `''` first. Ignored
51
- * directories are not descended, so a `node_modules` tree costs one `readdir` and nothing else.
52
- * A symlinked directory is deliberately not followed: `Dirent.isDirectory()` is false for one, and
53
- * a workspace symlink pointing back into the checkout would otherwise be walked twice.
50
+ * Every directory at or under `from` (`''`, the app root, by default) an event could be a source
51
+ * change in, `from` first, every path root-relative. Ignored directories are not descended, so a
52
+ * `node_modules` tree costs one `readdir` and nothing else. A symlinked directory is deliberately
53
+ * not followed: `Dirent.isDirectory()` is false for one, and a workspace symlink pointing back into
54
+ * the checkout would otherwise be walked twice. Root-relative because the ignore set is the ROOT's
55
+ * `.gitignore`: a walk rooted at a new directory asked an anchored `/build` about `build`, and hid
56
+ * `apps/web/build/` when `apps/web` appeared after boot.
54
57
  */
55
- export function admittedDirectories(root: string, ignore: DevIgnore): readonly string[] {
56
- const admitted: string[] = [''];
57
- const pending: string[] = [''];
58
+ export function admittedDirectories(root: string, ignore: DevIgnore, from = ''): readonly string[] {
59
+ const admitted: string[] = [from];
60
+ const pending: string[] = [from];
58
61
  for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
59
62
  for (const entry of childrenOf(join(root, next))) {
60
63
  if (!entry.isDirectory()) continue;
@@ -156,9 +159,7 @@ export function watchTree(options: WatchTreeOptions): WatchTree {
156
159
 
157
160
  /** The admitted directories at or under `directory`, discovered rather than assumed. */
158
161
  const subtree = (directory: string): readonly string[] =>
159
- admittedDirectories(join(root, directory), ignore).map((found) =>
160
- found === '' ? directory : `${directory}/${found}`,
161
- );
162
+ admittedDirectories(root, ignore, directory);
162
163
 
163
164
  /** The whole set, re-derived: a directory the author just ignored gives its descriptor back. */
164
165
  const reconcile = (): void => {
package/src/dev-watch.ts CHANGED
@@ -30,7 +30,7 @@ import { pathSegments } from './path-segments';
30
30
  * - `node_modules` — an install, not an edit. `x dev` cannot reload for a dependency change: the
31
31
  * modules are already in this process's cache.
32
32
  * - `.personal` — an app's uncommitted local state, written by scripts while the dev server runs.
33
- * - `.claude` — agent scratch, session logs and worktrees. ai-maxxing keeps two entire copies of
33
+ * - `.claude` — agent scratch, session logs and worktrees. One adopter keeps two entire copies of
34
34
  * the app under `.claude/worktrees/`, so a second agent's edit rebuilt the first agent's islands.
35
35
  *
36
36
  * `dist` and `coverage` were here and are deliberately not: both are ordinary build output that
package/src/dispatch.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  // whether it is printed as text or JSON and what the process exits with. Errors take the same
3
3
  // path as results, so a failure is machine-readable exactly like a success.
4
4
 
5
- import { isAbsolute, resolve } from 'node:path';
5
+ // why: a synchronous bin probe at the one place a result is rendered; Bun has no path helpers.
6
+ import { existsSync } from 'node:fs';
7
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
6
8
  import { setLogStream } from '@ultimat3/core';
7
9
  import { requireAppRoot, requireBunVersion } from './app-root';
8
10
  import { createHelpCommand } from './cmd-help';
@@ -13,6 +15,7 @@ import type { Runner } from './exec';
13
15
  import { exec } from './exec';
14
16
  import type { CommandResult } from './output';
15
17
  import { exitCodeFor, findingFrom, render } from './output';
18
+ import type { ReaderOptions } from './output-findings';
16
19
  import type { ParsedArgs } from './parse';
17
20
  import { parseArgs, wantsJson } from './parse';
18
21
  import { commandFor, SPECS } from './registry';
@@ -56,11 +59,29 @@ export const sinkFor = (
56
59
  ): ((line: string) => void) =>
57
60
  result.stream === 'stderr' ? (options.writeError ?? options.write) : options.write;
58
61
 
62
+ /** `x` where Bun links it: on POSIX, and the shim it writes on Windows. */
63
+ const LOCAL_BINS = ['x', 'x.exe'];
64
+
59
65
  const resolveCwd = (cwd: string, flag: string | undefined): string => {
60
66
  if (flag === undefined) return cwd;
61
67
  return isAbsolute(flag) ? flag : resolve(cwd, flag);
62
68
  };
63
69
 
70
+ /**
71
+ * Whether this reader's fixes should name `bunx x`. Only where it resolves to the app's own CLI: with
72
+ * no local bin, `bunx x` fetches whatever npm calls `x`, and a global `x` is what ran anyway. An
73
+ * entry point with its own name (`invocation`, `create-ultimate`) already names its runner.
74
+ */
75
+ const readerFor = (cwd: string, invocation: string | undefined): ReaderOptions => {
76
+ if (invocation !== undefined) return {};
77
+ for (let dir = cwd; ; dir = dirname(dir)) {
78
+ if (LOCAL_BINS.some((bin) => existsSync(join(dir, 'node_modules', '.bin', bin)))) {
79
+ return { bunxRunner: true };
80
+ }
81
+ if (dirname(dir) === dir) return {};
82
+ }
83
+ };
84
+
64
85
  const errorResult = (command: string, error: unknown): CommandResult => ({
65
86
  ok: false,
66
87
  command,
@@ -74,13 +95,14 @@ const errorResult = (command: string, error: unknown): CommandResult => ({
74
95
  * end without terminating the test runner.
75
96
  */
76
97
  export async function dispatch(options: DispatchOptions): Promise<number> {
98
+ const earlyReader = readerFor(options.cwd, options.invocation);
77
99
  // Its own branch, ahead of the parse: an unsupported Bun is a fact about the environment and
78
100
  // outranks anything argv says, including the planned pre-empt below.
79
101
  try {
80
102
  requireBunVersion(options.bunVersion);
81
103
  } catch (error) {
82
104
  const failure = errorResult('x', error);
83
- sinkFor(failure, options)(render(failure, wantsJson(options.argv)));
105
+ sinkFor(failure, options)(render(failure, wantsJson(options.argv), false, earlyReader));
84
106
  return 1;
85
107
  }
86
108
 
@@ -99,7 +121,7 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
99
121
  // `wantsJson`, not `includes('--json')`: a typo'd flag or a typo'd command is exactly the case
100
122
  // an agent hits while always passing `-j`, and the short form rendered prose it then parsed.
101
123
  const result = errorResult(planned?.name ?? 'x', failure);
102
- sinkFor(result, options)(render(result, wantsJson(options.argv)));
124
+ sinkFor(result, options)(render(result, wantsJson(options.argv), false, earlyReader));
103
125
  return 1;
104
126
  }
105
127
 
@@ -112,7 +134,7 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
112
134
  known: SPECS.map((spec) => spec.name),
113
135
  }),
114
136
  );
115
- sinkFor(result, options)(render(result, args.json));
137
+ sinkFor(result, options)(render(result, args.json, false, earlyReader));
116
138
  return 1;
117
139
  }
118
140
 
@@ -134,6 +156,7 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
134
156
  options.env,
135
157
  await rootEnvChanges({ cwd, processCwd: options.cwd, env: options.env }),
136
158
  );
159
+ const reader = readerFor(cwd, options.invocation);
137
160
  const ctx: CommandContext = {
138
161
  args: helpArgs,
139
162
  cwd,
@@ -164,7 +187,7 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
164
187
  // command, which declares nothing: usage for a command must be readable from anywhere.
165
188
  if (target.spec.requiresApp === true) requireAppRoot(target.spec.name, ctx.cwd);
166
189
  const result = await target.run(ctx);
167
- sinkFor(result, options)(render(result, args.json, args.flags.get('verbose') === true));
190
+ sinkFor(result, options)(render(result, args.json, args.flags.get('verbose') === true, reader));
168
191
  // `x dev` and `x mcp serve --transport http` are still listening here: report first, so the
169
192
  // url is on stdout the moment it is reachable, then stay in the process until the drain that
170
193
  // stops them. Without this the exit code below is what takes the server down.
@@ -172,7 +195,7 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
172
195
  return exitCodeFor(result);
173
196
  } catch (error) {
174
197
  const result = errorResult(args.command, error);
175
- sinkFor(result, options)(render(result, args.json));
198
+ sinkFor(result, options)(render(result, args.json, false, reader));
176
199
  return 1;
177
200
  }
178
201
  }