@ultimat3/cli 29.4.0 → 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 +11 -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 +9 -0
  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 +41 -81
  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 +7 -1
  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,182 @@
1
+ // The app's own `POST /mcp`, as `x new` writes it: `apps/web/mcp.ts` — the file and export the
2
+ // mount reads (`app-mcp.ts`) — over the `@<app>/mcp` catalog, and the token resolver that makes it a
3
+ // route. Mounted and REFUSING: no bearer, or one nobody issued, is a 401 before a tool is read. The
4
+ // development token works under `x dev` alone, and its test says so in every other environment.
5
+
6
+ import type { GeneratedFile, NameSet } from './naming';
7
+
8
+ const endpoint = (
9
+ app: NameSet,
10
+ ): string => `// The app's own MCP endpoint: \`POST /mcp\`, mounted by \`x dev\` and by the web role, which find
11
+ // this file by its path and its \`mcp\` export. The tools are \`@${app.kebab}/mcp\`'s catalog; who may
12
+ // call them is \`resolveAgentToken\`. Delete this file to unmount it — the boot then says
13
+ // X_MCP_APP_UNMOUNTED — or set \`ai: { mcp: { expose: false } }\` in app.config.ts.
14
+ //
15
+ // Built when the boot scan reaches this file, after \`apps/*/api\` registered its actions: a tool
16
+ // declared later in a running \`x dev\` is on \`POST /mcp\` from its next restart.
17
+ import { appMcp } from '@${app.kebab}/mcp';
18
+ import { resolveAgentToken } from './app/auth/agent-token';
19
+
20
+ export const mcp = appMcp({ resolveToken: resolveAgentToken });
21
+ `;
22
+
23
+ const endpointTest =
24
+ (): string => `// The endpoint is mounted and refusing: a route at /mcp, and a 401 for a request that carries no
25
+ // token or one this process does not honour — before any tool, or the body, is read.
26
+ import { expect, unitTest } from '@ultimat3/testing';
27
+ import { DEV_AGENT_TOKEN_PREFIX } from './app/auth/agent-token';
28
+ import { mcp } from './mcp';
29
+
30
+ const post = (headers: Record<string, string>) =>
31
+ new Request('http://localhost/mcp', { method: 'POST', headers, body: '{}' });
32
+
33
+ unitTest('POST /mcp is mounted, and answers 401 without a token', async () => {
34
+ const route = mcp.route;
35
+ if (route === undefined) return expect.unreachable('apps/web/mcp.ts built no route');
36
+ expect(route.path).toBe('/mcp');
37
+ const response = await route.handle(post({}));
38
+ expect(response.status).toBe(401);
39
+ // The development token is refused outside \`x dev\`: \`bun test\` is not development.
40
+ const dev = await route.handle(post({ authorization: \`Bearer \${DEV_AGENT_TOKEN_PREFIX}admin\` }));
41
+ expect(dev.status).toBe(401);
42
+ });
43
+ `;
44
+
45
+ const agentToken =
46
+ (): string => `// Who an MCP caller is, for \`apps/web/mcp.ts\`: an API key issued through \`@ultimat3/auth\` —
47
+ // \`issueApiKey\`, stored in the framework's \`x_api_keys\` — resolved to an agent actor, in every
48
+ // environment. Under \`x dev\` alone, \`dev-agent.<role>\` is also an agent holding that declared role,
49
+ // so a local client can call the app's tools before the app issues keys. It FAILS CLOSED: a process
50
+ // naming no environment is production, where that token is refused like any unknown one.
51
+ import { type Auth, apiKeyResolver } from '@ultimat3/auth';
52
+ import { type Actor, agentActor, peerAddressClass, tryResolveEnvironment } from '@ultimat3/core';
53
+ import { DEMO_ORG_ID } from '../../shared/demo-org';
54
+ import { roles } from '../../shared/roles';
55
+ import { appAuth } from './auth';
56
+
57
+ /** \`Authorization: Bearer dev-agent.admin\` under \`x dev\`; refused everywhere else. */
58
+ export const DEV_AGENT_TOKEN_PREFIX = 'dev-agent.';
59
+
60
+ /** What \`defineAppMcp\`'s \`resolveToken\` answers: an agent, and the scopes its connection holds. */
61
+ export interface AgentCaller {
62
+ readonly actor: Actor;
63
+ readonly scopes: ReadonlySet<string>;
64
+ }
65
+
66
+ /** The development agent a token names, or \`null\` for a role \`shared/roles.ts\` never declared. */
67
+ export const devAgentFor = (token: string): AgentCaller | null => {
68
+ const role = token.slice(DEV_AGENT_TOKEN_PREFIX.length);
69
+ // \`Object.hasOwn\`, never \`in\`: a token saying \`constructor\` names no role.
70
+ if (!Object.hasOwn(roles, role)) return null;
71
+ return {
72
+ actor: agentActor({ id: 'dev-agent', orgId: DEMO_ORG_ID, roles: [role] }),
73
+ scopes: new Set(),
74
+ };
75
+ };
76
+
77
+ export interface AgentTokenOptions {
78
+ readonly env: Readonly<Record<string, string | undefined>>;
79
+ /** The app's \`Auth\`, asked for when a key is presented, never at import. */
80
+ readonly auth: () => Auth;
81
+ }
82
+
83
+ /** How the request arrived: the two facts \`defineAppMcp\` hands every resolver that this one reads. */
84
+ export interface CallerFacts {
85
+ /** The \`Origin\` header: a browser sends one on every POST, a local MCP client none. */
86
+ readonly origin: string | null;
87
+ /** The caller's address as the host resolved it. */
88
+ readonly address: string | null;
89
+ }
90
+
91
+ /**
92
+ * The development token is honoured only from this machine and never from a browser: a page that
93
+ * rebinds its name to 127.0.0.1 reaches \`x dev\` over loopback, but it sends \`Origin\`.
94
+ */
95
+ const localClient = (facts: CallerFacts | undefined): boolean =>
96
+ facts !== undefined &&
97
+ facts.origin === null &&
98
+ facts.address !== null &&
99
+ peerAddressClass(facts.address) === 'loopback';
100
+
101
+ /** The resolver, over an environment and an \`Auth\` — so its test chooses both. */
102
+ export const agentTokenResolver = (options: AgentTokenOptions) => {
103
+ const keys = apiKeyResolver(() => options.auth().adapter);
104
+ return async (token: string, facts?: CallerFacts): Promise<AgentCaller | null> => {
105
+ if (!token.startsWith(DEV_AGENT_TOKEN_PREFIX)) return await keys(token);
106
+ const environment = tryResolveEnvironment({ env: options.env, fallback: 'production' });
107
+ return environment === 'development' && localClient(facts) ? devAgentFor(token) : null;
108
+ };
109
+ };
110
+
111
+ export const resolveAgentToken = agentTokenResolver({ env: process.env, auth: appAuth });
112
+ `;
113
+
114
+ const agentTokenTest =
115
+ (): string => `// An issued key is an agent of its org everywhere; the development token is an agent under
116
+ // \`x dev\` and nothing anywhere else — including a process that names no environment at all.
117
+ import { defineAuth, issueApiKey, memoryAuthAdapter } from '@ultimat3/auth';
118
+ import { expect, unitTest } from '@ultimat3/testing';
119
+ import { DEMO_ORG_ID } from '../../shared/demo-org';
120
+ import { agentTokenResolver, DEV_AGENT_TOKEN_PREFIX, resolveAgentToken } from './agent-token';
121
+
122
+ /** In memory: the key store is the package's, and this test is about the wiring around it. */
123
+ const memoryAuth = () => {
124
+ const auth = defineAuth({ adapter: memoryAuthAdapter() });
125
+ return () => auth;
126
+ };
127
+
128
+ unitTest('an issued API key is an agent of its org, in every environment', async () => {
129
+ const auth = memoryAuth();
130
+ const issued = issueApiKey({ env: 'dev', scopes: [], orgId: DEMO_ORG_ID });
131
+ await auth().adapter.putApiKey(issued.record);
132
+ for (const ULTIMATE_ENV of ['development', 'production']) {
133
+ const caller = await agentTokenResolver({ env: { ULTIMATE_ENV }, auth })(issued.plaintext);
134
+ expect(caller?.actor.kind).toBe('agent');
135
+ expect(caller?.actor.orgId).toBe(DEMO_ORG_ID);
136
+ }
137
+ // One character off the secret is nobody.
138
+ const resolve = agentTokenResolver({ env: {}, auth });
139
+ expect(await resolve(\`\${issued.plaintext}x\`)).toBeNull();
140
+ });
141
+
142
+ /** A local, non-browser client: what \`x dev\`'s own MCP clients are. */
143
+ const LOCAL = { origin: null, address: '127.0.0.1' };
144
+
145
+ unitTest('dev-agent.<role> is that declared role under x dev, and nobody elsewhere', async () => {
146
+ const auth = memoryAuth();
147
+ const dev = agentTokenResolver({ env: { ULTIMATE_ENV: 'development' }, auth });
148
+ const admin = \`\${DEV_AGENT_TOKEN_PREFIX}admin\`;
149
+ expect((await dev(admin, LOCAL))?.actor.roles).toEqual(['admin']);
150
+ expect(await dev(\`\${DEV_AGENT_TOKEN_PREFIX}nobody\`, LOCAL)).toBeNull();
151
+ expect(await dev(\`\${DEV_AGENT_TOKEN_PREFIX}constructor\`, LOCAL)).toBeNull();
152
+ for (const env of [{ ULTIMATE_ENV: 'production' }, { ULTIMATE_ENV: 'staging' }, {}]) {
153
+ expect(await agentTokenResolver({ env, auth })(admin, LOCAL)).toBeNull();
154
+ }
155
+ });
156
+
157
+ unitTest('the development token is refused from a browser or from off this machine', async () => {
158
+ // A hostile page that rebinds its name to 127.0.0.1 reaches \`x dev\` from a loopback socket,
159
+ // but a browser sends Origin on every POST; another machine on the network has no loopback address.
160
+ const dev = agentTokenResolver({ env: { ULTIMATE_ENV: 'development' }, auth: memoryAuth() });
161
+ const admin = \`\${DEV_AGENT_TOKEN_PREFIX}admin\`;
162
+ expect(await dev(admin, { origin: 'http://rebound.test:3000', address: '127.0.0.1' })).toBeNull();
163
+ expect(await dev(admin, { origin: 'null', address: '::1' })).toBeNull();
164
+ expect(await dev(admin, { origin: null, address: '192.168.1.20' })).toBeNull();
165
+ expect(await dev(admin, { origin: null, address: null })).toBeNull();
166
+ expect(await dev(admin)).toBeNull();
167
+ expect((await dev(admin, { origin: null, address: '::1' }))?.actor.kind).toBe('agent');
168
+ });
169
+
170
+ unitTest('the installed resolver refuses the development token under bun test', async () => {
171
+ // \`bun test\` sets NODE_ENV=test: not development, so no key store is ever asked.
172
+ expect(await resolveAgentToken(\`\${DEV_AGENT_TOKEN_PREFIX}admin\`)).toBeNull();
173
+ });
174
+ `;
175
+
176
+ /** `apps/web/mcp.ts`, the resolver it mounts with, and the tests that pin both refusing. */
177
+ export const mcpEndpointFiles = (app: NameSet): readonly GeneratedFile[] => [
178
+ { path: 'apps/web/mcp.ts', contents: endpoint(app) },
179
+ { path: 'apps/web/mcp.test.ts', contents: endpointTest() },
180
+ { path: 'apps/web/app/auth/agent-token.ts', contents: agentToken() },
181
+ { path: 'apps/web/app/auth/agent-token.test.ts', contents: agentTokenTest() },
182
+ ];
@@ -0,0 +1,31 @@
1
+ // `.mcp.json`: the one MCP client config `x new` writes, in the format several clients read. It
2
+ // starts `x mcp serve` over stdio — the dev tools — through the SCOPED package, because a client may
3
+ // spawn the command outside the installed app, and there `bunx x` fetches an unrelated npm package.
4
+ // A client that wants another file is told how in `wiki/MCP-And-AI.md`, never generated for.
5
+
6
+ import type { GeneratedFile, NameSet } from './naming';
7
+
8
+ export const MCP_JSON_PATH = '.mcp.json';
9
+
10
+ const COMMAND = 'bunx';
11
+ const ARGS = ['@ultimat3/cli', 'mcp', 'serve', '--transport', 'stdio'] as const;
12
+
13
+ /** The line the file encodes, as a shell would run it — what the docs and the smoke both quote. */
14
+ export const MCP_SERVE_COMMAND = [COMMAND, ...ARGS].join(' ');
15
+
16
+ /** The file itself, Biome-formatted: one server, keyed by the app's name. */
17
+ export function mcpJsonFile(app: NameSet): GeneratedFile {
18
+ const args = ARGS.map((arg) => `"${arg}"`).join(', ');
19
+ return {
20
+ path: MCP_JSON_PATH,
21
+ contents: `{
22
+ "mcpServers": {
23
+ "${app.kebab}": {
24
+ "command": "${COMMAND}",
25
+ "args": [${args}]
26
+ }
27
+ }
28
+ }
29
+ `,
30
+ };
31
+ }
@@ -1,85 +1,88 @@
1
1
  // The generated app's `packages/mcp`: the app's own MCP surface, projected straight from the
2
2
  // action registry rather than re-listed, plus the test that pins every projected tool describing
3
- // itself.
3
+ // itself — and the `apps/web/mcp.ts` endpoint that mounts it (`scaffold-mcp-endpoint.ts`).
4
4
 
5
- import { sortedImports } from './imports';
6
5
  import type { GeneratedFile, NameSet } from './naming';
7
- import { packageShapeFiles } from './scaffold-package-shape';
6
+ import { mcpEndpointFiles } from './scaffold-mcp-endpoint';
7
+ import { packageShapeFiles, workspacePackageJson } from './scaffold-package-shape';
8
8
 
9
9
  const DESCRIPTION = "The app's own MCP tools";
10
10
 
11
- /**
12
- * Writes its own manifest, for the reason `scaffold-i18n.ts` does: `workspacePackageJson` is the
13
- * dependency-free shape, and this package has a real dependency — `src/index.ts` below imports the
14
- * app's actions out of `apps/web/api`, because `registerActions` has to see them.
15
- *
16
- * The edge points AT the app, which is unusual and correct. `apps/web` is a workspace like any
17
- * other; reversing it would put the tool catalog upstream of the actions it projects. Declared
18
- * rather than left to the root tsconfig's `paths`: an undeclared edge resolves for `tsc` and for
19
- * nothing else — not for `bun --filter` ordering, not for any tool asking what a change affects
20
- * (`X_WORKSPACE_DEP_UNDECLARED`).
21
- *
22
- * `"0.0.0"` and not `workspace:*`: it is the version `apps/web` really carries, which is what
23
- * `checkLockstep` compares a sibling pin against, and it is the one spelling every other manifest
24
- * `x new` writes already uses.
25
- */
26
- const mcpPackage = (app: NameSet): string => `{
27
- "name": "@${app.kebab}/mcp",
28
- "version": "0.0.0",
29
- "private": true,
30
- "type": "module",
31
- "description": "${DESCRIPTION}",
32
- "exports": {
33
- ".": "./src/index.ts"
34
- },
35
- "scripts": {
36
- "typecheck": "tsc --noEmit -p ../../tsconfig.json"
37
- },
38
- "dependencies": {
39
- "@${app.kebab}/web": "0.0.0"
40
- }
41
- }
42
- `;
43
-
44
11
  const mcpIndex = (
45
12
  app: NameSet,
46
13
  ): string => `// The app's own MCP tools. Every action with mcp.expose is already a tool; add app-specific
47
14
  // read-only helpers here. Authorization is the action's policy, unchanged.
48
- ${sortedImports([
49
- `import * as api from '@${app.kebab}/web/api/health';`,
50
- `import { registerActions } from '@ultimat3/action';`,
51
- `import { defineAppMcp } from '@ultimat3/mcp';`,
52
- ])}
15
+ //
16
+ // A FUNCTION, and it imports no app (X_BOUNDARY_PACKAGE_TO_APP): \`defineAppMcp\` reads the registry
17
+ // where it is called, and \`runRole()\` and \`loadApp()\` both fill that registry before anything
18
+ // asks for the catalog. A module-scope call froze whatever had registered by import time.
19
+ import { defineAppMcp, type McpRequestFacts, type ResolvedToken } from '@ultimat3/mcp';
53
20
 
54
- // Names come from export names, so the registry agrees with the module the app already wrote.
55
- registerActions(api);
21
+ /** Who a bearer token is, or \`null\` for nobody — answered 401. */
22
+ export type TokenResolver = (
23
+ token: string,
24
+ facts: McpRequestFacts,
25
+ ) => Promise<ResolvedToken | null> | ResolvedToken | null;
26
+
27
+ export interface AppMcpOptions {
28
+ /** \`apps/web/mcp.ts\` passes the app's resolver; without one there is no route. */
29
+ readonly resolveToken?: TokenResolver | undefined;
30
+ }
56
31
 
57
32
  // \`include: 'exposed'\` projects straight from the registry. Re-listing the actions here would
58
33
  // copy \`mcp: { expose: true }\` into a second place, and the copy goes stale in silence.
59
- export const mcp = defineAppMcp({
60
- name: '${app.kebab}',
61
- include: 'exposed',
34
+ export const appMcp = (options: AppMcpOptions = {}) =>
35
+ defineAppMcp({
36
+ name: '${app.kebab}',
37
+ include: 'exposed',
38
+ ...(options.resolveToken === undefined ? {} : { resolveToken: options.resolveToken }),
39
+ });
40
+ `;
41
+
42
+ const mcpUnitTest =
43
+ (): string => `// The catalog is built where it is called, from whatever has registered by then: this file loads no
44
+ // app, so what it pins is the shape. The tools themselves are the contract test's, next door.
45
+ import { expect, unitTest } from '@ultimat3/testing';
46
+ import { appMcp } from './index';
47
+
48
+ unitTest('the catalog is in process until a token resolver is given', () => {
49
+ // No \`resolveToken\` means no route: a mount with no bearer check is the failure this rules out.
50
+ expect(appMcp().route).toBeUndefined();
51
+ });
52
+
53
+ unitTest('given one, it is a route at /mcp', () => {
54
+ expect(appMcp({ resolveToken: () => null }).route?.path).toBe('/mcp');
62
55
  });
63
56
  `;
64
57
 
65
58
  const mcpTest =
66
- (): string => `// The app exposes its actions as MCP tools, and each tool carries the action's own policy. An
67
- // agent reaching a tool that authorises differently is the failure this rules out.
68
- import { expect, unitTest } from '@ultimat3/testing';
69
- import { mcp } from './index';
59
+ (): string => `// The app exposes its actions as MCP tools, and each tool carries the action's own policy. The
60
+ // catalog reads a registry, so the honest proof fills it the way the process does: \`loadApp\` scans
61
+ // \`apps/*\` exactly as \`runRole()\` does. Booting the app is the fixture, so the timeout moves.
62
+ import { resolve } from 'node:path';
63
+ import { loadApp } from '@ultimat3/cli';
64
+ import { beforeAll, contractTest, expect } from '@ultimat3/testing';
65
+ import { appMcp } from './index';
66
+
67
+ beforeAll(async () => {
68
+ await loadApp(resolve(import.meta.dir, '../../..'));
69
+ }, 30_000);
70
70
 
71
- unitTest('the app exposes its actions as MCP tools', () => {
72
- expect(mcp.tools.length).toBeGreaterThan(0);
71
+ contractTest('the app exposes its actions as MCP tools', () => {
72
+ const tools = appMcp().tools;
73
+ expect(tools.length).toBeGreaterThan(0);
73
74
  // Every projected tool must describe itself: an agent picks a tool by its description. Assert
74
75
  // on the value, not its length — a failure then prints the empty description, not "0 > 0".
75
- for (const tool of mcp.tools) expect(tool.description).not.toBe('');
76
+ for (const tool of tools) expect(tool.description).not.toBe('');
76
77
  });
77
78
  `;
78
79
 
79
- /** Every file the `packages/mcp` workspace ships, in the order `x new` writes them. */
80
+ /** Every file the `packages/mcp` workspace ships, then the endpoint that mounts it. */
80
81
  export const mcpPackageFiles = (app: NameSet): readonly GeneratedFile[] => [
81
- { path: 'packages/mcp/package.json', contents: mcpPackage(app) },
82
+ { path: 'packages/mcp/package.json', contents: workspacePackageJson(app, 'mcp', DESCRIPTION) },
82
83
  ...packageShapeFiles(app, 'mcp', DESCRIPTION),
83
84
  { path: 'packages/mcp/src/index.ts', contents: mcpIndex(app) },
84
- { path: 'packages/mcp/src/index.test.ts', contents: mcpTest() },
85
+ { path: 'packages/mcp/src/index.test.ts', contents: mcpUnitTest() },
86
+ { path: 'packages/mcp/src/index.contract.test.ts', contents: mcpTest() },
87
+ ...mcpEndpointFiles(app),
85
88
  ];
@@ -74,7 +74,6 @@ const rootPackage = (app: NameSet, version: string): string => `{
74
74
  "scripts": {
75
75
  "setup": "${binScript(BIN_SCRIPTS.setup)}",
76
76
  "dev": "x dev",
77
- "check": "${binScript(BIN_SCRIPTS.check)}",
78
77
  "verify": "x verify",
79
78
  "typecheck": "tsc -p . --pretty",
80
79
  "lint": "biome check .",
@@ -208,7 +207,8 @@ export const config = defineConfig({
208
207
  // Locales are declared by the catalogs (\`defineCatalogs\` in \`packages/i18n/src/index.ts\`); no
209
208
  // zone or currency default exists — every format call names its zone, every \`Money\` its currency.
210
209
  // Env KEYS, never the value: the same image deploys to every environment. The database is
211
- // configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
210
+ // configured from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`. Off by default,
211
+ // \`database: { rowSecurity: true }\` adds the database's own tenant guard under the repository's.
212
212
  // The tiers ARE the cache selection: add 'redis' to build the shared rung, which reads
213
213
  // \`REDIS_URL\` and refuses the boot when it is unset.
214
214
  cache: { tiers: ['request-memo', 'lru'] },
@@ -340,6 +340,8 @@ const SCAFFOLD_FLOOR: readonly VerifyStepName[] = [
340
340
  'errors',
341
341
  'unit',
342
342
  'contract',
343
+ // Every app has an `app.config.ts` and primitives to run: `x new` writes `apps/web/exercise.ts`.
344
+ 'exercise',
343
345
  'eval',
344
346
  'drift',
345
347
  'budgets',
@@ -387,6 +389,8 @@ const verifyFloor = (example: boolean): string =>
387
389
  funcs: COVERAGE_BAR,
388
390
  exclude: SCAFFOLD_COVERAGE_EXCLUDE,
389
391
  },
392
+ // The rolling rail judges from the first migration: a new app has nothing to grandfather.
393
+ rollingSince: '0',
390
394
  },
391
395
  null,
392
396
  2,
@@ -433,6 +437,7 @@ packages/*/dist/
433
437
  *.tsbuildinfo
434
438
  .env
435
439
  .env.*.local
440
+ .env.production
436
441
  /coverage/
437
442
  playwright-report/
438
443
  test-results/
@@ -14,7 +14,9 @@ const SHELL_DIR = 'apps/web/shared';
14
14
  const navType = (example: boolean): string => (example ? "'dashboard' | 'posts'" : "'dashboard'");
15
15
 
16
16
  const postsItem = (example: boolean): string =>
17
- example ? `\n {item('posts', '/posts', t('shell.nav.posts'), iconList)}` : '';
17
+ example
18
+ ? `\n { label: t('shell.nav.posts'), href: '/posts', icon: <Icon glyph={iconList} size="sm" /> },`
19
+ : '';
18
20
 
19
21
  // Plain strings for the framework lines, never template literals: the workspace-dependency scanner
20
22
  // blanks a string's contents but not a nested template's, so a template here would bill the CLI
@@ -25,14 +27,16 @@ const shell = (
25
27
  ): string => `// The app's frame: brand mark, the sidebar with one link per destination, an environment pill,
26
28
  // the page's own actions at the end of the bar, and a footer — composed over the catalog's
27
29
  // \`AppShell\`, so the skip link, the landmarks and the phone layout are the framework's and not
28
- // this file's. \`site/page.tsx\` does NOT use it: the landing page is a 0kb document and this is the
29
- // signed-in product's chrome. Every page under \`app/\` does.
30
+ // this file's. The destinations are ONE list, drawn twice: \`NavList\` in the sidebar, \`BottomNav\`
31
+ // along a phone's bottom edge; each marks the current page by its href, with no script.
32
+ // \`site/page.tsx\` does NOT use it: the landing page is a 0kb document and this is the signed-in
33
+ // product's chrome. Every page under \`app/\` does.
30
34
  //
31
35
  // \`useT()\`, not \`t\` from @ultimat3/i18n — see apps/web/site/page.tsx for why.
32
36
  ${sortedImports([
33
37
  `import { useT } from '@${app.kebab}/i18n';`,
34
38
  "import { tryResolveEnvironment } from '@ultimat3/core';",
35
- "import { AppShell, Icon, type IconGlyph, Link } from '@ultimat3/ui';",
39
+ "import { AppShell, BottomNav, Icon, Link, NavList } from '@ultimat3/ui';",
36
40
  "import { iconHexagon } from '@ultimat3/ui/icons/hexagon';",
37
41
  "import { iconLayoutDashboard } from '@ultimat3/ui/icons/layout-dashboard';",
38
42
  ...(example ? ["import { iconList } from '@ultimat3/ui/icons/list';"] : []),
@@ -43,6 +47,11 @@ import styles from './shell.module.scss';
43
47
  /** The routes the sidebar knows about. A page names itself so its link can carry aria-current. */
44
48
  export type ShellNav = ${navType(example)};
45
49
 
50
+ /** Where each destination lives — what \`NavList\` and \`BottomNav\` match the current page against. */
51
+ const HREFS: Readonly<Record<ShellNav, string>> = {
52
+ dashboard: '/dashboard',${example ? "\n posts: '/posts'," : ''}
53
+ };
54
+
46
55
  export interface ShellProps {
47
56
  readonly nav?: ShellNav | undefined;
48
57
  /** Controls that belong to the page, rendered at the end of the header bar — a theme toggle. */
@@ -56,20 +65,15 @@ export function Shell(props: ShellProps): JSX.Element {
56
65
  // neither ULTIMATE_ENV nor NODE_ENV names one, and the pill is absent with it.
57
66
  const environment = tryResolveEnvironment();
58
67
 
59
- const item = (id: ShellNav, href: string, label: string, glyph: IconGlyph): JSX.Element => (
60
- <li>
61
- <Link
62
- class={styles.navLink}
63
- href={href}
64
- tone="inherit"
65
- underline="none"
66
- aria-current={props.nav === id ? 'page' : false}
67
- >
68
- <Icon glyph={glyph} size="sm" class={styles.navIcon} />
69
- <span>{label}</span>
70
- </Link>
71
- </li>
72
- );
68
+ const destinations = [
69
+ {
70
+ label: t('shell.nav.dashboard'),
71
+ href: '/dashboard',
72
+ icon: <Icon glyph={iconLayoutDashboard} size="sm" />,
73
+ },${postsItem(example)}
74
+ ];
75
+ // No page named means nothing is current: an href no destination covers.
76
+ const currentHref = props.nav === undefined ? '' : HREFS[props.nav];
73
77
 
74
78
  return (
75
79
  <div class={styles.doc}>
@@ -97,11 +101,8 @@ export function Shell(props: ShellProps): JSX.Element {
97
101
  </div>
98
102
  </div>
99
103
  }
100
- sidebar={
101
- <ul class={styles.nav}>
102
- {item('dashboard', '/dashboard', t('shell.nav.dashboard'), iconLayoutDashboard)}${postsItem(example)}
103
- </ul>
104
- }
104
+ sidebar={<NavList items={destinations} currentHref={currentHref} />}
105
+ bottomNav={<BottomNav items={destinations} currentHref={currentHref} />}
105
106
  footer={<span>{t('shell.footer')}</span>}
106
107
  >
107
108
  {props.children}
@@ -207,55 +208,6 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
207
208
  opacity: 0.35;
208
209
  }
209
210
  }
210
-
211
- .nav {
212
- display: flex;
213
- // A row on a phone — stacked links cost a fifth of the first screen — a rail from md up.
214
- flex-direction: row;
215
- gap: tokens.space(1);
216
- margin: 0;
217
- padding: 0;
218
- overflow-x: auto;
219
- list-style: none;
220
-
221
- @include tokens.respond-to(md) {
222
- flex-direction: column;
223
- }
224
- }
225
-
226
- .navLink {
227
- display: flex;
228
- align-items: center;
229
- gap: tokens.space(3);
230
- padding: tokens.space(2) tokens.space(3);
231
- border-radius: tokens.radius('md');
232
- color: tokens.role('fg-muted');
233
- font-size: tokens.text('sm');
234
- transition:
235
- color tokens.duration('fast') tokens.easing('out'),
236
- background-color tokens.duration('fast') tokens.easing('out');
237
-
238
- &:hover {
239
- color: tokens.role('fg-strong');
240
- background: tokens.role('surface');
241
- }
242
-
243
- &[aria-current='page'] {
244
- color: tokens.role('fg-strong');
245
- background: tokens.role('surface-raised');
246
- box-shadow: inset 0 0 0 1px tokens.role('line', 0.7);
247
-
248
- .navIcon {
249
- color: tokens.role('accent');
250
- }
251
- }
252
- }
253
-
254
- .navIcon {
255
- flex: none;
256
- color: tokens.role('fg-muted');
257
- transition: color tokens.duration('fast') tokens.easing('out');
258
- }
259
211
  `;
260
212
 
261
213
  const shellTest =
@@ -267,10 +219,13 @@ import { Shell } from './shell';
267
219
 
268
220
  unitTest('the shell marks the current destination and keeps the page in <main>', async () => {
269
221
  const view = await renderView(Shell, { nav: 'dashboard', children: 'the page body' });
270
- expect(view.html).toMatch(/<a href="\\/dashboard" aria-current="page"/);
271
- // Landmarks come from AppShell: one banner, one navigation, one main, one contentinfo.
222
+ // One current link per list — the sidebar's and the bottom bar's — and both are the dashboard.
223
+ const current = [...view.html.matchAll(/<a[^>]*aria-current="page"[^>]*>/g)].map((m) => m[0]);
224
+ expect(current).toHaveLength(2);
225
+ for (const link of current) expect(link).toContain('href="/dashboard"');
226
+ // Landmarks come from AppShell: one banner, two navigations (sidebar, bottom bar), one main.
272
227
  expect(view.html.match(/<main\\b/g)).toHaveLength(1);
273
- expect(view.html.match(/<nav\\b/g)).toHaveLength(1);
228
+ expect(view.html.match(/<nav\\b/g)).toHaveLength(2);
274
229
  expect(view.html).toMatch(/<main[^>]*>the page body<\\/main>/);
275
230
  });
276
231
 
@@ -295,6 +250,7 @@ import {
295
250
  createContext,
296
251
  createEffect,
297
252
  createMemo,
253
+ createRoot,
298
254
  createSignal,
299
255
  onCleanup,
300
256
  useContext,
@@ -309,14 +265,24 @@ export interface ThemeToggleIslandProps {
309
265
  /**
310
266
  * The one export the hydration runtime calls. \`setSolidRuntime\` comes FIRST and is not optional:
311
267
  * \`@ultimat3/ui\` imports types from solid-js and never a runtime, so the reactive graph a
312
- * component reaches is the one an entry registers. Six NAMED imports, never a namespace: a
268
+ * component reaches is the one an entry registers. Seven NAMED imports, never a namespace: a
313
269
  * namespace object keeps every export of solid-js alive in the chunk. The shell is cleared before
314
270
  * \`render\`, which APPENDS — the server's copy would otherwise stay on screen beside the live one.
315
271
  */
316
- export function mount(el: HTMLElement, props: ThemeToggleIslandProps): void {
317
- setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });
272
+ export function mount(el: HTMLElement, props: ThemeToggleIslandProps): () => void {
273
+ setSolidRuntime({
274
+ createContext,
275
+ useContext,
276
+ createSignal,
277
+ createMemo,
278
+ createEffect,
279
+ onCleanup,
280
+ createRoot,
281
+ });
318
282
  el.textContent = '';
319
- render(
283
+ // Solid's disposer, returned: the client router calls it when it navigates away, and a mount
284
+ // that returns nothing is X_ISLAND_DISPOSER_MISSING.
285
+ return render(
320
286
  () => (
321
287
  <UiProvider locale={props.locale}>
322
288
  <ThemeToggle mode="toggle" />
@@ -1,9 +1,10 @@
1
1
  // The backing services a new app's suites run against: `docker/docker-compose.test.yml` and the
2
2
  // one env file that points the suites at it. In RAM, durability off, silent — the framework's own
3
3
  // `docker/docker-compose.test.yml`, cut to what an app has. Ports sit apart from the dev stack's
4
- // so both can run at once; `ci.yml` starts this file before `bun run check`.
4
+ // so both can run at once; `ci.yml` starts this file before `bun run verify`.
5
5
 
6
6
  import type { GeneratedFile, NameSet } from './naming';
7
+ import { S3_GATEWAY_IMAGE } from './pinned-images';
7
8
 
8
9
  export const TEST_COMPOSE_PATH = 'docker/docker-compose.test.yml';
9
10
  export const TEST_ENV_PATH = 'docker/test-services.env';
@@ -17,7 +18,7 @@ const composeTest = (
17
18
  #
18
19
  # docker compose -f ${TEST_COMPOSE_PATH} up -d --wait
19
20
  # set -a; . ${TEST_ENV_PATH}; set +a
20
- # bun run check
21
+ # bun run verify
21
22
  #
22
23
  # \`--wait\` returns once every healthcheck passes. CI runs the same lines (.github/workflows/ci.yml).
23
24
  # None of it is required: with no TEST_* variable set the unit suite runs on the in-memory driver,
@@ -41,7 +42,9 @@ x-silent: &silent
41
42
  services:
42
43
  # Postgres 18, the production major. \`initdb -c\` writes each setting into the new cluster;
43
44
  # \`--no-sync\` is initdb's own fsync of a cluster on a tmpfs. Postgres 18 images keep the cluster
44
- # under /var/lib/postgresql/18, so the tmpfs is the parent.
45
+ # under /var/lib/postgresql/18, so the tmpfs is the parent. After the logging: no JIT and no
46
+ # autovacuum for millisecond queries, room for every parallel test process, and a small WAL —
47
+ # it is RAM here.
45
48
  postgres:
46
49
  <<: *silent
47
50
  image: postgres:18-alpine
@@ -62,6 +65,11 @@ services:
62
65
  -c log_checkpoints=off
63
66
  -c log_connections=off
64
67
  -c log_disconnections=off
68
+ -c jit=off
69
+ -c autovacuum=off
70
+ -c max_connections=400
71
+ -c min_wal_size=32MB
72
+ -c max_wal_size=256MB
65
73
  tmpfs: [/var/lib/postgresql]
66
74
  ports: ['127.0.0.1:${TEST_PORTS.postgres}:5432']
67
75
  healthcheck:
@@ -74,7 +82,7 @@ services:
74
82
  redis:
75
83
  <<: *silent
76
84
  image: redis:8-alpine
77
- command: ['redis-server', '--save', '', '--appendonly', 'no', '--loglevel', 'warning']
85
+ command: ['redis-server', '--save', '', '--appendonly', 'no', '--loglevel', 'nothing']
78
86
  tmpfs: [/data]
79
87
  ports: ['127.0.0.1:${TEST_PORTS.redis}:6379']
80
88
  healthcheck:
@@ -99,7 +107,7 @@ services:
99
107
  # A directory under /data is a bucket, so the tmpfs mounted AT the bucket's path is the bootstrap.
100
108
  s3:
101
109
  <<: *silent
102
- image: versity/versitygw:v1.8.0@sha256:30292fc2eeacc67a36993b01f7a7a5e3361a19cced0e80c1d71cfa2a4b0a2499
110
+ image: ${S3_GATEWAY_IMAGE}
103
111
  environment:
104
112
  VGW_BACKEND: posix
105
113
  VGW_BACKEND_ARG: /data