@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
@@ -0,0 +1,74 @@
1
+ // Single responsibility: the browser half of client error reporting — the text of
2
+ // `/x-client-errors.js`. Two window listeners, one observer on the island failure marker, the caps,
3
+ // and a same-origin `fetch`. Generated as a string so `hydrate.ts` pays no byte for an app that
4
+ // has the feature off; the route that receives it is `client-errors-routes.ts`.
5
+
6
+ import { CLIENT_BUILD_META } from '@ultimat3/core';
7
+ import { ISLAND_FAILED_ATTRIBUTE, NAVIGATED_EVENT } from '@ultimat3/render';
8
+
9
+ /** Named by every `app/` document of an app that turned `clientErrors.enabled` on. */
10
+ export const CLIENT_ERRORS_SCRIPT_PATH = '/x-client-errors.js';
11
+
12
+ /** Where a report is POSTed. Beside `/_storage`: a framework route no page can shadow. */
13
+ export const CLIENT_ERRORS_PATH = '/_client-errors';
14
+
15
+ /** Reports one document may send. A loop that throws per frame costs five requests, not 60/s. */
16
+ export const CLIENT_ERRORS_MAX_REPORTS = 5;
17
+
18
+ /** Per-field caps, shared by the sender (which cuts) and the route (which refuses past them). */
19
+ export const CLIENT_ERRORS_LIMITS = Object.freeze({
20
+ message: 1_000,
21
+ stack: 8_000,
22
+ path: 2_048,
23
+ island: 128,
24
+ build: 128,
25
+ });
26
+
27
+ /**
28
+ * The script. Rules it keeps, each one a line below:
29
+ * - an `error` counts only when its `filename` is on this origin, which drops an extension's, a
30
+ * CDN's and the opaque `Script error.` a cross-origin script raises; a `Script error.` with no
31
+ * stack is dropped wherever it claims to come from;
32
+ * - a rejection whose stack names only other origins is someone else's;
33
+ * - an island whose `mount()` rejected never reaches `unhandledrejection` (the hydration runtime
34
+ * catches it), so the `data-x-failed` marker is watched instead, including one set before this
35
+ * script ran;
36
+ * - at most five per document and one per distinct message, the path with no query string, the
37
+ * build id the DOCUMENT was rendered by (`data-build` on this script's own tag), and `fetch`
38
+ * with `keepalive` so a report survives the tab closing;
39
+ * - every cap is per DOCUMENT: the client router runs this script once per tab and swaps pages
40
+ * under it, so `NAVIGATED_EVENT` resets the count and the dedupe and rescans the new body; the
41
+ * dedupe key carries the path; the build is read at send time from the swapped-in
42
+ * `x-ultimate-build` meta (the tag's `data-build` when there is none); an island ELEMENT is
43
+ * reported once, so one the router carried across (`data-x-persist`) is not re-reported;
44
+ * - it never throws and never reports itself: every listener is wrapped, the rejection of its
45
+ * own `fetch` is handled, and a failure raised while sending is not re-entered.
46
+ */
47
+ export function clientErrorsScript(): string {
48
+ const limits = CLIENT_ERRORS_LIMITS;
49
+ return `(function(){try{
50
+ var d=document,o=location.origin,n=0,seen={},busy=0,A='${ISLAND_FAILED_ATTRIBUTE}';
51
+ var s=d.currentScript,b0=s&&s.getAttribute('data-build')||'';
52
+ function build(){var m=d.querySelector&&d.querySelector('meta[name="${CLIENT_BUILD_META}"]');return m&&m.getAttribute('content')||b0}
53
+ function cut(v,m){return typeof v==='string'?v.slice(0,m):''}
54
+ function send(k,m,st,i){if(busy||n>=${CLIENT_ERRORS_MAX_REPORTS})return;busy=1;try{
55
+ m=cut(m,${limits.message});var key=location.pathname+'|'+k+'|'+(i||'')+'|'+m;if(seen[key])return;seen[key]=1;n++;
56
+ var body={kind:k,message:m,path:cut(location.pathname,${limits.path}),build:cut(build(),${limits.build})};
57
+ st=cut(st,${limits.stack});if(st)body.stack=st;if(i)body.island=cut(i,${limits.island});
58
+ var p=fetch('${CLIENT_ERRORS_PATH}',{method:'POST',keepalive:true,credentials:'same-origin',
59
+ headers:{'content-type':'application/json'},body:JSON.stringify(body)});
60
+ if(p&&typeof p.then==='function')p.then(null,function(){})}catch(x){}finally{busy=0}}
61
+ window.addEventListener('error',function(e){try{var f=e&&e.filename;
62
+ if(typeof f!=='string'||f.indexOf(o+'/')!==0)return;var st=e.error&&e.error.stack;
63
+ if(e.message==='Script error.'&&!st)return;send('error',e.message,st)}catch(x){}});
64
+ window.addEventListener('unhandledrejection',function(e){try{var r=e&&e.reason;
65
+ var st=r&&typeof r.stack==='string'?r.stack:'';if(st&&st.indexOf(o+'/')<0)return;
66
+ send('rejection',r&&typeof r.message==='string'?r.message:String(r),st)}catch(x){}});
67
+ function isl(el){if(el&&el.getAttribute&&!el.__xr&&el.getAttribute(A)!==null){el.__xr=1;send('island',el.getAttribute(A),'',el.getAttribute('data-x-island'))}}
68
+ if(typeof MutationObserver==='function')new MutationObserver(function(ms){for(var i=0;i<ms.length;i++){try{isl(ms[i].target)}catch(x){}}})
69
+ .observe(d.documentElement,{subtree:true,attributes:true,attributeFilter:[A]});
70
+ function scan(){var f=d.querySelectorAll('['+A+']');for(var j=0;j<f.length;j++)isl(f[j])}scan();
71
+ d.addEventListener('${NAVIGATED_EVENT}',function(){try{n=0;seen={};scan()}catch(x){}});
72
+ }catch(x){}})();
73
+ `;
74
+ }
@@ -0,0 +1,197 @@
1
+ // Single responsibility: the server half of real-user timings — `GET /x-client-vitals.js` and
2
+ // `POST /_client-vitals`, which turns one beacon into histogram samples labelled by route PATTERN
3
+ // and metric. Anonymous, stored nowhere but the metric: no actor, no path, no row. A framework
4
+ // route like `/_client-errors`, never an action: it must not appear in OpenAPI or as an MCP tool.
5
+
6
+ import type { ClientVitalsConfig, ClientVitalsSurface } from '@ultimat3/core';
7
+ import { histogram, isJsonObject, proveSameOrigin, readWithinLimit } from '@ultimat3/core';
8
+ import type { RequestContext, Route, UltimateRequest } from '@ultimat3/http';
9
+ import { NO_STORE, selfOrigin } from '@ultimat3/http';
10
+ import { routeEntries } from '@ultimat3/render';
11
+ import type { ClientErrorPage } from './client-errors-routes';
12
+ import {
13
+ CLIENT_VITAL_NAMES,
14
+ CLIENT_VITALS_PATH,
15
+ CLIENT_VITALS_SCRIPT_PATH,
16
+ type ClientVitalName,
17
+ clientVitalsHead,
18
+ clientVitalsScript,
19
+ } from './client-vitals-script';
20
+ import { revalidatedResponse } from './revalidated-response';
21
+
22
+ /** A beacon is one path and five numbers; 4 KiB is that with room, and nothing more. */
23
+ export const CLIENT_VITALS_MAX_BODY_BYTES = 4 * 1024;
24
+
25
+ /** One beacon per page view: sixty a minute per address is a reader opening a page a second. */
26
+ export const CLIENT_VITALS_PER_MINUTE = 60;
27
+
28
+ /** A timing past a minute is a backgrounded tab's clock, not what a user waited for. */
29
+ const MAX_MS = 60_000;
30
+ /** CLS is unitless; real pages sit under 1, and a value past 10 is a measurement gone wrong. */
31
+ const MAX_CLS = 10;
32
+
33
+ /** A path, never a URL and never a query: the same screen `/_client-errors` applies. */
34
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: the class REFUSES them, by design.
35
+ const PATH = /^\/[^?#\s\x00-\x1f\x7f]{0,2047}$/;
36
+
37
+ const NAMES: ReadonlySet<string> = new Set(CLIENT_VITAL_NAMES);
38
+ const isVitalName = (name: string): name is ClientVitalName => NAMES.has(name);
39
+
40
+ /**
41
+ * Two instruments, declared once. Labels are `route` (a pattern from the route table) and `metric`
42
+ * (one of four), so the series count is the page count times four, whatever the traffic sends.
43
+ */
44
+ const seconds = histogram('web_vital_seconds', {
45
+ unit: 's',
46
+ description: 'Real-user page timings (LCP, FCP, INP, TTFB), by route pattern and metric',
47
+ bounds: [0.05, 0.1, 0.2, 0.3, 0.5, 0.8, 1, 1.8, 2.5, 3, 4, 6, 10, 20, 60],
48
+ });
49
+ const shift = histogram('web_vital_cls', {
50
+ unit: '1',
51
+ description: 'Real-user cumulative layout shift, by route pattern',
52
+ bounds: [0.01, 0.05, 0.1, 0.15, 0.25, 0.5, 1, 2],
53
+ });
54
+
55
+ export interface ClientVitalsRouteOptions {
56
+ readonly pages?: () => readonly ClientErrorPage[];
57
+ }
58
+
59
+ /** The most specific page on a listed surface the path names — its PATTERN, else nothing. */
60
+ function patternOf(
61
+ pages: readonly ClientErrorPage[],
62
+ surfaces: readonly ClientVitalsSurface[],
63
+ path: string,
64
+ ): string | undefined {
65
+ let best: ClientErrorPage | undefined;
66
+ for (const page of pages) {
67
+ if (!surfaces.some((surface) => surface === page.surface)) continue;
68
+ if (!page.pattern.regex.test(path)) continue;
69
+ if (best === undefined || page.pattern.specificity > best.pattern.specificity) best = page;
70
+ }
71
+ return best?.path;
72
+ }
73
+
74
+ /** Same-origin evidence, or the beacon is dropped: a cross-site page must not skew the numbers. */
75
+ function sameOrigin(request: UltimateRequest, ctx: RequestContext): boolean {
76
+ return proveSameOrigin({
77
+ selfOrigins: [selfOrigin(ctx.url, ctx.https)],
78
+ origin: request.header('origin'),
79
+ secFetchSite: request.header('sec-fetch-site') || null,
80
+ listed: () => false,
81
+ listName: "the app's own origin",
82
+ }).ok;
83
+ }
84
+
85
+ /** The beacon's `{ path, vitals }`, or `undefined` — never a reason: a probe learns nothing. */
86
+ async function beaconOf(request: UltimateRequest) {
87
+ const read = await readWithinLimit(request.raw.body, CLIENT_VITALS_MAX_BODY_BYTES);
88
+ if ('over' in read) return undefined;
89
+ let body: unknown;
90
+ try {
91
+ body = JSON.parse(new TextDecoder().decode(read.bytes));
92
+ } catch {
93
+ return undefined;
94
+ }
95
+ if (!isJsonObject(body) || typeof body['path'] !== 'string') return undefined;
96
+ const vitals = body['vitals'];
97
+ if (!isJsonObject(vitals) || !PATH.test(body['path'])) return undefined;
98
+ return { path: body['path'], vitals };
99
+ }
100
+
101
+ /** Records every known, in-range timing; drops the rest one by one. */
102
+ function record(route: string, vitals: Readonly<Record<string, unknown>>): void {
103
+ for (const [name, value] of Object.entries(vitals)) {
104
+ if (!isVitalName(name) || typeof value !== 'number' || !Number.isFinite(value)) continue;
105
+ if (value < 0) continue;
106
+ if (name === 'cls') {
107
+ if (value <= MAX_CLS) shift.record(value, { route });
108
+ } else if (value <= MAX_MS) {
109
+ seconds.record(value / 1000, { route, metric: name });
110
+ }
111
+ }
112
+ }
113
+
114
+ export function clientVitalsRoutes(
115
+ config: ClientVitalsConfig,
116
+ options: ClientVitalsRouteOptions = {},
117
+ ): readonly Route[] {
118
+ const pages = options.pages ?? routeEntries;
119
+ const source = clientVitalsScript(config.sampleRate);
120
+ const answered = (): Response => new Response(null, { status: 204 });
121
+ return [
122
+ {
123
+ method: 'GET',
124
+ path: CLIENT_VITALS_SCRIPT_PATH,
125
+ meta: { name: 'clientVitals.script', auth: 'public' },
126
+ // Revalidated, as `/x-client-errors.js` is: a changed rate reaches a returning tab.
127
+ handler: (request) => revalidatedResponse(request, source, 'text/javascript; charset=utf-8'),
128
+ },
129
+ {
130
+ method: 'POST',
131
+ path: CLIENT_VITALS_PATH,
132
+ meta: {
133
+ name: 'clientVitals.beacon',
134
+ // Anonymous by design: a timing names no one, so no sign-in stands in front of it.
135
+ auth: 'public',
136
+ cache: NO_STORE,
137
+ // Per address, on the app's own rate-limit store — the stage `/_client-errors` uses.
138
+ rateLimit: 'clientVitals.beacon',
139
+ rateLimitBucket: {
140
+ capacity: CLIENT_VITALS_PER_MINUTE,
141
+ refillPerSecond: CLIENT_VITALS_PER_MINUTE / 60,
142
+ },
143
+ },
144
+ // Every outcome is the same 204: a refusal that said why would teach a probe the shape.
145
+ handler: async (request, ctx) => {
146
+ if (!sameOrigin(request, ctx)) return answered();
147
+ const beacon = await beaconOf(request);
148
+ if (beacon === undefined) return answered();
149
+ const route = patternOf(pages(), config.surfaces, beacon.path);
150
+ if (route !== undefined) record(route, beacon.vitals);
151
+ return answered();
152
+ },
153
+ },
154
+ ];
155
+ }
156
+
157
+ export interface ClientVitalsSurfaceMount {
158
+ readonly routes: readonly Route[];
159
+ /** For `DocumentOptions.appHead`, `''` when `app` is not listed. */
160
+ readonly appHead: string;
161
+ /** For the `site/` document head, `''` unless `site` is listed — the static path opts in. */
162
+ readonly siteHead: string;
163
+ }
164
+
165
+ /** What a boot mounts for `clientErrors.vitals`: nothing at all unless enabled. */
166
+ export function clientVitalsSurface(
167
+ config: ClientVitalsConfig | undefined,
168
+ options: ClientVitalsRouteOptions = {},
169
+ ): ClientVitalsSurfaceMount | undefined {
170
+ if (config?.enabled !== true) return undefined;
171
+ const head = (surface: ClientVitalsSurface): string =>
172
+ config.surfaces.includes(surface) ? clientVitalsHead() : '';
173
+ return {
174
+ routes: clientVitalsRoutes(config, options),
175
+ appHead: head('app'),
176
+ siteHead: head('site'),
177
+ };
178
+ }
179
+
180
+ /**
181
+ * The document heads the browser scripts compose into — for `DocumentOptions`, spread. The one
182
+ * place `x dev` and the container combine them, so the two cannot name different scripts. A key
183
+ * is absent rather than `''` when nothing rides it. `zone` (`time.detect`, `zone-boot.ts`) is an
184
+ * `app/` tag only: a `site/` page is a file, and a file cannot answer in the visitor's zone.
185
+ */
186
+ export function clientScriptHeads(
187
+ errors: { readonly head: string } | undefined,
188
+ vitals: ClientVitalsSurfaceMount | undefined,
189
+ zone?: { readonly head: string } | undefined,
190
+ ): { readonly appHead?: string; readonly siteHead?: string } {
191
+ const appHead = (zone?.head ?? '') + (errors?.head ?? '') + (vitals?.appHead ?? '');
192
+ const siteHead = vitals?.siteHead ?? '';
193
+ return {
194
+ ...(appHead === '' ? {} : { appHead }),
195
+ ...(siteHead === '' ? {} : { siteHead }),
196
+ };
197
+ }
@@ -0,0 +1,56 @@
1
+ // Single responsibility: the browser half of real-user timings — the text of `/x-client-vitals.js`.
2
+ // `PerformanceObserver` for LCP, CLS, INP and FCP, the navigation entry for TTFB, and ONE
3
+ // `sendBeacon` when the page is hidden. Generated as a string so a document with the feature off
4
+ // carries no byte of it; the route that receives the beacon is `client-vitals-route.ts`.
5
+
6
+ import { finiteOption } from '@ultimat3/core';
7
+
8
+ /** Named by every document on a surface `clientErrors.vitals.surfaces` lists. */
9
+ export const CLIENT_VITALS_SCRIPT_PATH = '/x-client-vitals.js';
10
+
11
+ /** Where the beacon lands. Beside `/_client-errors`: a framework route no page can shadow. */
12
+ export const CLIENT_VITALS_PATH = '/_client-vitals';
13
+
14
+ /** The timings a beacon may carry. CLOSED: the route drops any other name. */
15
+ export const CLIENT_VITAL_NAMES = ['lcp', 'fcp', 'inp', 'ttfb', 'cls'] as const;
16
+
17
+ export type ClientVitalName = (typeof CLIENT_VITAL_NAMES)[number];
18
+
19
+ /**
20
+ * The script. Rules it keeps, each one a line below:
21
+ * - sampled ONCE per page view, here, against `rate`: a page view that rolled out observes nothing;
22
+ * - every timing is milliseconds from navigation start; CLS is the largest session window (gaps
23
+ * under 1 s, windows capped at 5 s), counting no shift that followed input; INP is the slowest
24
+ * interaction the view saw, which is the metric's own reading under 50 interactions;
25
+ * - one beacon per page view, on the first `visibilitychange` to hidden (or `pagehide`), naming the
26
+ * path the view LOADED at — the client router swaps the path under a tab, and a timing taken on
27
+ * one page must not be filed under the next;
28
+ * - a `text/plain` blob: `sendBeacon` with a JSON content type is a preflighted request some
29
+ * browsers drop silently, and the route parses the body whatever it was labelled;
30
+ * - it never throws: an observer type this browser lacks is skipped, and every callback is wrapped.
31
+ */
32
+ export function clientVitalsScript(sampleRate: number): string {
33
+ // Interpolated into script text: a number `config.ts` already screened, screened again here
34
+ // because a `NaN` would print as an identifier and sample every view or none.
35
+ const rate = finiteOption('clientVitalsScript', 'sampleRate', sampleRate);
36
+ return `(function(){try{
37
+ if(!(Math.random()<${rate})||typeof PerformanceObserver!=='function')return;
38
+ var v={},p=location.pathname,sent=0,cs=0,cw=0,cf=0,cl=0;
39
+ function ob(t,f,o){try{new PerformanceObserver(function(l){try{l.getEntries().forEach(f)}catch(x){}}).observe(o||{type:t,buffered:true})}catch(x){}}
40
+ ob('largest-contentful-paint',function(e){v.lcp=e.startTime});
41
+ ob('paint',function(e){if(e.name==='first-contentful-paint')v.fcp=e.startTime});
42
+ ob('event',function(e){if(e.interactionId&&(!(v.inp>=e.duration)))v.inp=e.duration},{type:'event',buffered:true,durationThreshold:16});
43
+ ob('layout-shift',function(e){if(e.hadRecentInput)return;var t=e.startTime;
44
+ if(cw&&t-cl<1000&&t-cf<5000){cw+=e.value}else{cw=e.value;cf=t}cl=t;if(cw>cs)cs=cw;v.cls=cs});
45
+ try{var n=performance.getEntriesByType('navigation')[0];if(n&&n.responseStart>0)v.ttfb=n.responseStart}catch(x){}
46
+ function send(){try{if(sent)return;sent=1;
47
+ if(navigator.sendBeacon)navigator.sendBeacon('${CLIENT_VITALS_PATH}',new Blob([JSON.stringify({path:p,vitals:v})],{type:'text/plain'}))}catch(x){}}
48
+ document.addEventListener('visibilitychange',function(){if(document.visibilityState==='hidden')send()});
49
+ addEventListener('pagehide',send);
50
+ }catch(x){}})();
51
+ `;
52
+ }
53
+
54
+ /** The tag a document on a listed surface carries. */
55
+ export const clientVitalsHead = (): string =>
56
+ `<script src="${CLIENT_VITALS_SCRIPT_PATH}" defer></script>`;
@@ -7,6 +7,7 @@
7
7
  // `workspace-graph.ts` — the `cmd-jobs.ts` / `jobs-report.ts` split, repeated.
8
8
 
9
9
  import { affectedScope } from './affected';
10
+ import { appTestScope } from './affected-tests';
10
11
  import { affectedSpec } from './cmd-affected-spec';
11
12
  import type { CliCommand, CommandContext } from './command';
12
13
  import { msg } from './messages';
@@ -31,6 +32,8 @@ export const AFFECTED_MESSAGE_KEYS = [
31
32
  'cli.affected.rootWide',
32
33
  'cli.affected.dirty',
33
34
  'cli.test.affected.none',
35
+ 'cli.affected.tests',
36
+ 'cli.affected.testsAll',
34
37
  ] as const;
35
38
 
36
39
  export const affectedCommand: CliCommand = {
@@ -46,6 +49,7 @@ export const affectedCommand: CliCommand = {
46
49
  command: 'affected',
47
50
  });
48
51
  const params = { base: selection.base, changed: plan.changed.length };
52
+ const tests = await appTestScope(ctx.cwd, root, plan.changed);
49
53
  const table = renderTable(
50
54
  HEADER,
51
55
  plan.workspaces.map((workspace) => [workspace.name, workspace.dir]),
@@ -62,6 +66,16 @@ export const affectedCommand: CliCommand = {
62
66
  dir: workspace.dir,
63
67
  })),
64
68
  paths: plan.workspaces.map((workspace) => workspace.dir),
69
+ ...(tests === undefined
70
+ ? {}
71
+ : {
72
+ tests: {
73
+ app: tests.app,
74
+ all: tests.all,
75
+ forcing: [...tests.forcing],
76
+ files: [...tests.tests],
77
+ },
78
+ }),
65
79
  };
66
80
  return {
67
81
  // An empty answer is a fact, not a failure: a `.md`-only diff genuinely re-checks nothing,
@@ -76,6 +90,15 @@ export const affectedCommand: CliCommand = {
76
90
  : msg('cli.affected.count', { ...params, count: plan.workspaces.length }),
77
91
  lines: [
78
92
  ...(selection.dirty ? [msg('cli.affected.dirty')] : []),
93
+ ...(tests === undefined
94
+ ? []
95
+ : [
96
+ tests.all
97
+ ? msg('cli.affected.testsAll', {
98
+ files: `${tests.forcing.slice(0, 3).join(', ')}${tests.forcing.length > 3 ? ` (+${tests.forcing.length - 3})` : ''}`,
99
+ })
100
+ : msg('cli.affected.tests', { count: tests.tests.length }),
101
+ ]),
79
102
  ...(plan.rootWide.length === 0
80
103
  ? []
81
104
  : [msg('cli.affected.rootWide', { files: plan.rootWide.join(', ') })]),
@@ -8,7 +8,7 @@ export const buildSpec: CommandSpec = {
8
8
  summary:
9
9
  'build a container image, a single binary, a prerendered static site, or the store an image boots from',
10
10
  usage:
11
- 'x build --target docker|binary|static|prebuilt [--tag name] [--out path] [--platform bun-target] [--no-preflight] [--json]',
11
+ 'x build --target docker|binary|static|prebuilt [--tag name] [--out path] [--platform bun-target] [--no-preflight] [--no-capture] [--json]',
12
12
  requiresApp: true,
13
13
  flags: [
14
14
  {
@@ -32,5 +32,11 @@ export const buildSpec: CommandSpec = {
32
32
  summary:
33
33
  '--no-preflight skips the six static gate steps (typecheck, lint, boundaries, filesize, package-shape, errors) — only when `x verify` runs right after, as in `x build --no-preflight && x verify`',
34
34
  },
35
+ {
36
+ name: 'capture',
37
+ type: 'boolean',
38
+ summary:
39
+ "static target only: --no-capture stamps every og.capture card's og:image tags and photographs none, so no browser is needed — what `x verify`'s own build passes; a deployable build takes the pictures",
40
+ },
35
41
  ],
36
42
  };
package/src/cmd-build.ts CHANGED
@@ -6,7 +6,9 @@ import { existsSync } from 'node:fs';
6
6
  import { join, resolve } from 'node:path';
7
7
  import { ERROR_DOCS_URL, frameworkVersion, UltimateError, VERSION_DEFINE } from '@ultimat3/core';
8
8
  import { requireAppRoot } from './app-root';
9
+ import { BUILD_CAPTURE_ENV } from './build-capture-env';
9
10
  import { buildSpec } from './cmd-build-spec';
11
+ import { DEFAULT_IMAGE } from './cmd-deploy-compose';
10
12
  import { runVerify } from './cmd-verify';
11
13
  import type { CliCommand, CommandContext } from './command';
12
14
  import { externalArgs } from './compile-externals';
@@ -199,8 +201,19 @@ export function binaryArgs(
199
201
  ];
200
202
  }
201
203
 
204
+ /** The module `staticArgs` preloads: the render loader, ahead of the script's whole graph. */
205
+ export const PRERENDER_PRELOAD = join(import.meta.dir, 'prerender-preload.ts');
206
+
202
207
  export function staticArgs(root: string, out: string): readonly string[] {
203
- return ['bun', 'run', join(root, BUILD_ENTRY.static), '--out', out];
208
+ return [
209
+ 'bun',
210
+ 'run',
211
+ '--preload',
212
+ PRERENDER_PRELOAD,
213
+ join(root, BUILD_ENTRY.static),
214
+ '--out',
215
+ out,
216
+ ];
204
217
  }
205
218
 
206
219
  export function argsFor(
@@ -264,7 +277,14 @@ export function buildResult(input: {
264
277
  {
265
278
  code: 'X_BUILD_FAILED',
266
279
  cause: `${input.command.join(' ')} exited ${result.code}`,
267
- fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
280
+ // Static is the build `x verify`'s budgets step runs (plan 101, 21): `x verify` would be
281
+ // the loop the reader came from, and this command's `data.output` is the builder's own.
282
+ fix:
283
+ target === 'docker'
284
+ ? 'x doctor --json && docker info'
285
+ : target === 'static'
286
+ ? 'x build --target static --json'
287
+ : 'x verify --json',
268
288
  docs: ERROR_DOCS_URL,
269
289
  },
270
290
  ],
@@ -285,7 +305,7 @@ export function buildResult(input: {
285
305
  }
286
306
 
287
307
  /** The flags only some targets read. */
288
- const TARGET_SCOPED_FLAGS = ['tag', 'out', 'platform', 'preflight'] as const;
308
+ const TARGET_SCOPED_FLAGS = ['tag', 'out', 'platform', 'preflight', 'capture'] as const;
289
309
 
290
310
  type TargetScopedFlag = (typeof TARGET_SCOPED_FLAGS)[number];
291
311
 
@@ -293,7 +313,7 @@ type TargetScopedFlag = (typeof TARGET_SCOPED_FLAGS)[number];
293
313
  const TARGET_FLAGS: ReadonlyMap<BuildTarget, readonly TargetScopedFlag[]> = new Map([
294
314
  ['docker', ['tag', 'preflight']],
295
315
  ['binary', ['out', 'platform', 'preflight']],
296
- ['static', ['out', 'preflight']],
316
+ ['static', ['out', 'preflight', 'capture']],
297
317
  // The boot reads ONE place, and the image holds no devDependencies to run a gate with.
298
318
  ['prebuilt', []],
299
319
  ]);
@@ -304,6 +324,7 @@ const UNREAD_BECAUSE: Readonly<Record<TargetScopedFlag, string>> = {
304
324
  out: 'the docker target writes an image, not a path',
305
325
  platform: 'only the binary target compiles an executable for a platform',
306
326
  preflight: 'the prebuilt target runs no gate: `x verify` ran before `docker build` was called',
327
+ capture: 'only the static target photographs social cards',
307
328
  };
308
329
 
309
330
  /**
@@ -377,9 +398,9 @@ export const buildCommand: CliCommand = {
377
398
  const platform = readPlatform(flagString(ctx.args, 'platform'));
378
399
  if (target === 'prebuilt') return buildPrebuilt(root);
379
400
 
380
- // Run static verify steps before building — unless the caller is a gate that runs the same
381
- // six steps itself right after (`bin/check`: `x build --no-preflight && x verify`), where the
382
- // preflight was the same ~17 s of typecheck/lint/scans paid twice per run. The default stays
401
+ // Run static verify steps before building — unless the caller is the gate itself, which has
402
+ // run those six steps already (`x verify`'s budgets step spawns `x build --no-preflight`), where
403
+ // the preflight was the same ~17 s of typecheck/lint/scans paid twice per run. The default stays
383
404
  // safe: a bare `x build` never ships an artifact from a tree that does not typecheck.
384
405
  const preflight = ctx.args.flags.get('preflight') !== false;
385
406
  if (preflight) {
@@ -405,7 +426,7 @@ export const buildCommand: CliCommand = {
405
426
  ? join(root, '.x', target === 'static' ? 'static' : 'app')
406
427
  : resolve(ctx.cwd, outFlag);
407
428
  const out = target === 'binary' ? binaryOutfile(requested, platform) : requested;
408
- const tag = flagString(ctx.args, 'tag') ?? 'ultimate-app:dev';
429
+ const tag = flagString(ctx.args, 'tag') ?? DEFAULT_IMAGE;
409
430
  // The docker target stamps the manifest's build id into the image, so no role derives one at
410
431
  // boot. The island chunks and stylesheets are the image build's own (`--target prebuilt`).
411
432
  const buildId =
@@ -420,7 +441,12 @@ export const buildCommand: CliCommand = {
420
441
  // Removed BEFORE the builder runs, so a build that writes no inventory can never be reported
421
442
  // with the last one's: a stale emitted list is worse than none, because it reads as this run's.
422
443
  if (target === 'static') await removeStaticReport(root);
423
- const result = await ctx.runner(command, { cwd: root });
444
+ // `--no-capture` reaches the app's own `prerender.ts` through the env (`build-capture-env.ts`).
445
+ const capture = ctx.args.flags.get('capture') !== false;
446
+ const result = await ctx.runner(command, {
447
+ cwd: root,
448
+ ...(capture ? {} : { env: { [BUILD_CAPTURE_ENV]: 'off' } }),
449
+ });
424
450
  // `prerenderSite` writes it; an app whose `apps/web/prerender.ts` does not call that writes no
425
451
  // `.x/build-stats.json` either, and `x verify`'s `budgets` step already reds that app with
426
452
  // `X_BUDGET_UNMEASURED` — so the absence needs no second code here.
@@ -12,30 +12,66 @@ export const TAIL_LINES = 40;
12
12
  */
13
13
  export const CI_EVENTS = ['push', 'pull_request', 'workflow_dispatch'] as const;
14
14
 
15
+ /** `runs` is the triage `x ci` has always been; `tested` asks whether a push may reuse a PR run. */
16
+ export const CI_SUBCOMMANDS = ['runs', 'tested'] as const;
17
+
18
+ /** How old the green PR run `tested` reuses may be: the runner image and the registry move. */
19
+ export const TESTED_MAX_AGE = '24h';
20
+
21
+ /** The workflow file whose run `tested` reads, when `--workflow` names none. */
22
+ export const TESTED_WORKFLOW = 'ci.yml';
23
+
15
24
  export const ciSpec: CommandSpec = {
16
25
  name: 'ci',
17
- summary: 'the workflow runs for this branch, and the findings inside the failed steps log',
26
+ summary:
27
+ 'the workflow runs for this branch and the findings inside the failed steps log; tested: whether a push reuses a green PR run',
18
28
  usage:
19
- 'x ci [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--workflow <name>] [--event <push|pull_request|workflow_dispatch>] [--json]',
29
+ 'x ci [runs] [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--workflow <name>] [--event <push|pull_request|workflow_dispatch>] [--json] · x ci tested [--max-age 24h] [--repo owner/name] [--workflow ci.yml] [--json]',
30
+ subcommands: CI_SUBCOMMANDS,
31
+ // The bare `x ci` is the triage it always was; `tested` is asked for by name.
32
+ defaultSubcommand: 'runs',
20
33
  flags: [
21
34
  { name: 'repo', type: 'string', summary: 'owner/name; the checkout own remote by default' },
22
- { name: 'branch', type: 'string', summary: 'branch to read runs for; this one by default' },
23
- { name: 'run', type: 'string', summary: 'one run id, instead of this branch latest' },
35
+ {
36
+ name: 'branch',
37
+ type: 'string',
38
+ summary: 'branch to read runs for; this one by default',
39
+ subcommands: ['runs'],
40
+ },
41
+ {
42
+ name: 'run',
43
+ type: 'string',
44
+ summary: 'one run id, instead of this branch latest',
45
+ subcommands: ['runs'],
46
+ },
24
47
  {
25
48
  name: 'tail',
26
49
  type: 'string',
27
50
  summary: `log lines kept per failed job (default ${TAIL_LINES})`,
51
+ subcommands: ['runs'],
52
+ },
53
+ {
54
+ name: 'full',
55
+ type: 'boolean',
56
+ summary: 'the whole failed-step log, not the tail',
57
+ subcommands: ['runs'],
28
58
  },
29
- { name: 'full', type: 'boolean', summary: 'the whole failed-step log, not the tail' },
30
59
  {
31
60
  name: 'workflow',
32
61
  type: 'string',
33
- summary: 'only this workflow name or filename (e.g. ci.yml)',
62
+ summary: `the workflow name or filename to read (e.g. ci.yml); for tested, the one whose run counts (default ${TESTED_WORKFLOW})`,
34
63
  },
35
64
  {
36
65
  name: 'event',
37
66
  type: 'string',
38
67
  summary: 'only runs of this event (push, pull_request, workflow_dispatch)',
68
+ subcommands: ['runs'],
69
+ },
70
+ {
71
+ name: 'max-age',
72
+ type: 'string',
73
+ summary: `tested: the oldest PR run still reused (default ${TESTED_MAX_AGE})`,
74
+ subcommands: ['tested'],
39
75
  },
40
76
  ],
41
77
  };