@ultimat3/cli 29.4.0 → 30.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +1314 -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,69 @@
1
+ // `x docs Upgrading#28x--2900-entry-by-entry` — a wiki topic, answered with its WHOLE section. A
2
+ // search hit carries a capped excerpt; an agent moving one major needs every entry of that major,
3
+ // and the 386 kB page it lives on is the thing it must not have to read.
4
+
5
+ // why: Bun ships no path module; the page is joined onto the wiki directory the CLI resolved.
6
+ import { join } from 'node:path';
7
+ import { wikiSection } from '@ultimat3/manifest';
8
+ import type { WikiSource } from './docs-sources';
9
+ import { msg } from './messages';
10
+ import type { CommandResult } from './output';
11
+
12
+ /**
13
+ * `Upgrading#anchor` or `wiki.Upgrading#anchor`. The page is a bare file stem — no `/`, no `.` —
14
+ * so the topic can only ever name a file directly inside the wiki directory.
15
+ */
16
+ const WIKI_TOPIC = /^(?:wiki\.)?([A-Za-z0-9][A-Za-z0-9-]*)#([^\s#/]+)$/;
17
+
18
+ export interface WikiTopic {
19
+ readonly page: string;
20
+ readonly anchor: string;
21
+ }
22
+
23
+ export function parseWikiTopic(query: string): WikiTopic | undefined {
24
+ const match = WIKI_TOPIC.exec(query);
25
+ if (match === null) return undefined;
26
+ return { page: match[1] ?? '', anchor: match[2] ?? '' };
27
+ }
28
+
29
+ /**
30
+ * The section, or `undefined` when the page or the anchor is not in this install — the caller then
31
+ * answers as for any question, so a stale anchor still ends in the nearest topics.
32
+ */
33
+ export async function sectionResult(
34
+ wiki: WikiSource,
35
+ topic: WikiTopic,
36
+ query: string,
37
+ ): Promise<CommandResult | undefined> {
38
+ if (topic.page === 'Error-Codes') return undefined;
39
+ const file = Bun.file(join(wiki.dir, `${topic.page}.md`));
40
+ if (!(await file.exists())) return undefined;
41
+ const text = wikiSection(await file.text(), topic.anchor);
42
+ if (text === undefined) return undefined;
43
+ const id = `wiki.${topic.page}#${topic.anchor}`;
44
+ return {
45
+ ok: true,
46
+ command: 'docs',
47
+ summary: msg('cli.docs.found', { count: 1, query }),
48
+ // The page's own prose, verbatim: quoted source, not this command's wording.
49
+ lines: text.split('\n').map((line) => ` ${line}`),
50
+ data: {
51
+ matches: [
52
+ {
53
+ topic: id,
54
+ package: '@ultimat3/cli',
55
+ version: wiki.version,
56
+ kind: 'guide',
57
+ title: text.split('\n')[0]?.replace(/^#+\s*/, '') ?? '',
58
+ text,
59
+ symbols: [],
60
+ source: `${wiki.relative}/${topic.page}.md`,
61
+ matched: [],
62
+ score: 0,
63
+ },
64
+ ],
65
+ suggestions: [],
66
+ query,
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,100 @@
1
+ // What `x docs` reads: every framework package the installed CLI can see, and the wiki its tarball
2
+ // ships. Two scope directories, because Bun's isolated layout puts only an app's DIRECT
3
+ // dependencies under its own `node_modules/@ultimat3`: measured on a fresh install, `x docs` saw one
4
+ // package of 28 and answered "nothing" for `upload a file to s3`.
5
+
6
+ // why: Bun ships no path module and no synchronous existence check for a path.
7
+ import { existsSync, realpathSync } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ import type { DocEntry } from '@ultimat3/manifest';
10
+ import { scanInstalledDocs, scanWikiDocs } from '@ultimat3/manifest';
11
+ import { posixRelative } from './posix-path';
12
+
13
+ /** This CLI's own package directory: `src/` is where every module of it lives, installed or not. */
14
+ export const cliPackageDir = (): string => dirname(import.meta.dir);
15
+
16
+ /**
17
+ * The wiki pages beside this CLI. Installed, `prepack` copied them into the tarball's `wiki/`; in
18
+ * this repository they are the checkout's own `wiki/`, two directories up from `packages/cli`, and
19
+ * nothing is copied. `Home.md` is the page every wiki has, so its absence means no wiki at all.
20
+ */
21
+ export function wikiDirFor(cliDir: string): string | undefined {
22
+ const shipped = join(cliDir, 'wiki');
23
+ if (existsSync(join(shipped, 'Home.md'))) return shipped;
24
+ // Two up is the repository root only when it holds THIS `packages/cli`. Installed, two up from
25
+ // `node_modules/@ultimat3/cli` is `node_modules/`, and an npm package named `wiki` is not ours.
26
+ const root = join(cliDir, '..', '..');
27
+ if (!isSameDir(join(root, 'packages', 'cli'), cliDir)) return undefined;
28
+ const checkout = join(root, 'wiki');
29
+ return existsSync(join(checkout, 'Home.md')) ? checkout : undefined;
30
+ }
31
+
32
+ const isSameDir = (a: string, b: string): boolean => {
33
+ try {
34
+ return realpathSync(a) === realpathSync(b);
35
+ } catch {
36
+ return false;
37
+ }
38
+ };
39
+
40
+ /** Where the shipped wiki is, and the CLI version it shipped in: what a wiki entry is dated by. */
41
+ export interface WikiSource {
42
+ readonly dir: string;
43
+ /** `dir` relative to the CLI package: what a hit's `source` is written against. */
44
+ readonly relative: string;
45
+ readonly version: string;
46
+ }
47
+
48
+ export interface DocsCorpus {
49
+ readonly entries: readonly DocEntry[];
50
+ /** `undefined` when the install ships no wiki — reported by the caller, never silently empty. */
51
+ readonly wiki: WikiSource | undefined;
52
+ }
53
+
54
+ async function versionOf(cliDir: string): Promise<string> {
55
+ const file = Bun.file(join(cliDir, 'package.json'));
56
+ if (!(await file.exists())) return '';
57
+ const parsed: unknown = await file.json().catch(() => undefined);
58
+ const version: unknown =
59
+ typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'version') : undefined;
60
+ return typeof version === 'string' ? version : '';
61
+ }
62
+
63
+ export async function wikiSource(
64
+ cliDir: string = cliPackageDir(),
65
+ ): Promise<WikiSource | undefined> {
66
+ const dir = wikiDirFor(cliDir);
67
+ if (dir === undefined) return undefined;
68
+ // Forward slashes on every OS: `source` is printed and pasted, never handed back to `join`.
69
+ const fromCli = posixRelative(cliDir, dir);
70
+ return { dir, relative: fromCli, version: await versionOf(cliDir) };
71
+ }
72
+
73
+ /**
74
+ * The app's own scope first, then the CLI's: the CLI's store entry holds the CLI and every package
75
+ * it depends on, and the app's adds the ones only the app installed (`ui`, `scraping`). A package
76
+ * in both is read once, from the app's, which is the copy the app runs.
77
+ */
78
+ export async function loadDocs(
79
+ appScope: string,
80
+ cliDir: string = cliPackageDir(),
81
+ ): Promise<DocsCorpus> {
82
+ const seen = new Set<string>();
83
+ const entries: DocEntry[] = [];
84
+ for (const scope of new Set([appScope, dirname(cliDir)])) {
85
+ const scanned = await scanInstalledDocs(scope);
86
+ entries.push(...scanned.filter((entry) => !seen.has(entry.package)));
87
+ for (const entry of scanned) seen.add(entry.package);
88
+ }
89
+ const wiki = await wikiSource(cliDir);
90
+ if (wiki !== undefined) {
91
+ entries.push(
92
+ ...(await scanWikiDocs(wiki.dir, {
93
+ package: '@ultimat3/cli',
94
+ version: wiki.version,
95
+ dir: wiki.relative,
96
+ })),
97
+ );
98
+ }
99
+ return { entries, wiki };
100
+ }
@@ -69,9 +69,18 @@ export function authStorageFindings(fact: AuthStorageFact): readonly Finding[] {
69
69
  * PGlite and takes the single-writer lock the next command needs. A server that does not answer
70
70
  * is `database()`'s finding to make, so every failure here is "nothing to report".
71
71
  */
72
- export async function authStorageProbe(url: string | undefined): Promise<AuthStorageFact> {
72
+ export async function authStorageProbe(
73
+ url: string | undefined,
74
+ env: Readonly<Record<string, string | undefined>> = process.env,
75
+ ): Promise<AuthStorageFact> {
73
76
  if (url === undefined || url.trim() === '') return NO_AUTH_STORAGE_FACT;
74
- const client = postgresClient({ url, applicationName: 'x-doctor' });
77
+ let client: ReturnType<typeof postgresClient>;
78
+ try {
79
+ client = postgresClient({ url, env, applicationName: 'x-doctor' });
80
+ } catch {
81
+ // `poolerProbe`'s finding: a refused `DATABASE_POOLER` or role, said once.
82
+ return NO_AUTH_STORAGE_FACT;
83
+ }
75
84
  try {
76
85
  const present = async (table: string): Promise<boolean> => {
77
86
  const rows = await client.query<{ readonly present: boolean }>(
@@ -0,0 +1,55 @@
1
+ // `x doctor`'s chart listing: the monitoring templates an app's Helm chart predates, with the one
2
+ // command that writes them. A LISTING, never a finding: the chart is the app's, and a chart that
3
+ // declines the operator's objects (or keeps its own) is a decision `x doctor` must not overrule.
4
+
5
+ // why: a synchronous exists check; Bun.file().exists() is async and the probe is not.
6
+ import { existsSync } from 'node:fs';
7
+ // why: Bun exposes no path-join primitive; existsSync takes one already joined.
8
+ import { join } from 'node:path';
9
+ import { CHART_FILE } from './generate-chart-alerts';
10
+ import { msg } from './messages';
11
+ import type { CommandResult, JsonValue } from './output';
12
+ import { names } from './templates';
13
+ import { helmMonitoringFiles } from './templates/scaffold-helm-alerts';
14
+
15
+ /** The command every row names: it writes only the files the chart lacks. */
16
+ export const CHART_ALERTS_ADD = 'x g chart-alerts';
17
+
18
+ /** The rule, pure: which monitoring templates are absent from a chart that exists. */
19
+ export function missingMonitoringTemplates(
20
+ hasChart: boolean,
21
+ exists: (path: string) => boolean,
22
+ ): readonly string[] {
23
+ if (!hasChart) return [];
24
+ // The paths only — they do not depend on the chart's name.
25
+ return helmMonitoringFiles(names('app'))
26
+ .map((file) => file.path)
27
+ .filter((path) => !exists(path));
28
+ }
29
+
30
+ /** The I/O half. An app with no chart deploys some other way, and is listed nothing. */
31
+ export function chartAlertsProbe(root: string): readonly string[] {
32
+ return missingMonitoringTemplates(existsSync(join(root, CHART_FILE)), (path) =>
33
+ existsSync(join(root, path)),
34
+ );
35
+ }
36
+
37
+ /** The listing, on a result `x doctor` already decided: `ok` and the findings stay as they came. */
38
+ export function withChartAlertsListing(
39
+ result: CommandResult,
40
+ missing: readonly string[],
41
+ ): CommandResult {
42
+ if (missing.length === 0) return result;
43
+ const data = typeof result.data === 'object' && result.data !== null ? result.data : {};
44
+ const chartAlerts: JsonValue = { missing: [...missing], add: CHART_ALERTS_ADD };
45
+ return {
46
+ ...result,
47
+ data: { ...(data as Record<string, JsonValue>), chartAlerts },
48
+ lines: [
49
+ ...(result.lines ?? []),
50
+ msg('cli.doctor.chartAlerts', { count: missing.length }),
51
+ ...missing.map((path) => ` ${path}`),
52
+ ` ${CHART_ALERTS_ADD}`,
53
+ ],
54
+ };
55
+ }
@@ -0,0 +1,107 @@
1
+ // `x doctor`'s statement bound: what the session in force actually reads, on demand — the boot's
2
+ // `X_DB_STATEMENT_BOUND_LOST` line, asked without booting. And the one fact a pooler changes about a
3
+ // worker: the wake is off, the poll answers instead. Allowed and cheap, so a line and never a finding.
4
+
5
+ import { type Role, resolveRole } from '@ultimat3/core';
6
+ import type { PoolerMode, StatementBound } from '@ultimat3/db';
7
+ import { DbError, lostStatementBound, postgresClient, statementBound } from '@ultimat3/db';
8
+ import { IDLE_POLL_CEILING_MS } from '@ultimat3/jobs';
9
+ import { msg } from './messages';
10
+ import type { CommandResult, Finding, JsonValue } from './output';
11
+ import { findingFrom } from './output';
12
+
13
+ export interface PoolerFact {
14
+ readonly role: Role | undefined;
15
+ readonly pooler: PoolerMode | undefined;
16
+ /** `undefined` when there was no session to read: no external database, or a refusal. */
17
+ readonly bound: StatementBound | undefined;
18
+ /** The pooler refused this configuration: every other database probe would only repeat it. */
19
+ readonly refused: Finding | undefined;
20
+ }
21
+
22
+ export const NO_POOLER_FACT: PoolerFact = {
23
+ role: undefined,
24
+ pooler: undefined,
25
+ bound: undefined,
26
+ refused: undefined,
27
+ };
28
+
29
+ /**
30
+ * The I/O half, external databases only: `probeDatabase`'s rule. An unreachable server is
31
+ * `database()`'s finding to make, so only a pooler's own refusal is reported from here.
32
+ */
33
+ export async function poolerProbe(
34
+ url: string | undefined,
35
+ env: Readonly<Record<string, string | undefined>>,
36
+ ): Promise<PoolerFact> {
37
+ if (url === undefined || url.trim() === '') return NO_POOLER_FACT;
38
+ let role: Role;
39
+ let client: ReturnType<typeof postgresClient>;
40
+ try {
41
+ role = resolveRole({ env });
42
+ client = postgresClient({ url, role, env, applicationName: 'x-doctor' });
43
+ } catch (error) {
44
+ return { ...NO_POOLER_FACT, refused: findingFrom(error) };
45
+ }
46
+ // The client's answer, so the mode reported is the mode it connected with.
47
+ const { pooler } = client;
48
+ try {
49
+ return { role, pooler, bound: await statementBound(client), refused: undefined };
50
+ } catch (error) {
51
+ const refused =
52
+ error instanceof DbError && error.code === 'X_DB_POOLER_OPTIONS_REFUSED'
53
+ ? findingFrom(error)
54
+ : undefined;
55
+ return { role, pooler, bound: undefined, refused };
56
+ } finally {
57
+ await client.close();
58
+ }
59
+ }
60
+
61
+ /** The rule, pure: a refusal, or a bound that is not the role's. */
62
+ export function poolerFindings(fact: PoolerFact): readonly Finding[] {
63
+ if (fact.refused !== undefined) return [fact.refused];
64
+ if (fact.bound === undefined || fact.role === undefined) return [];
65
+ const lost = lostStatementBound(fact.bound, fact.role, fact.pooler);
66
+ return lost === undefined ? [] : [findingFrom(lost)];
67
+ }
68
+
69
+ const readingOf = (reading: StatementBound['statementTimeout']): JsonValue => ({
70
+ appliedMs: reading.applied,
71
+ expectedMs: reading.expected,
72
+ });
73
+
74
+ /** The report, on a result `x doctor` already decided: `ok` and the findings stay as they came. */
75
+ export function withPoolerReport(result: CommandResult, fact: PoolerFact): CommandResult {
76
+ if (fact.bound === undefined || fact.role === undefined) return result;
77
+ const data = typeof result.data === 'object' && result.data !== null ? result.data : {};
78
+ const lines: string[] = [];
79
+ const wakeOff = fact.role === 'worker' && fact.pooler === 'transaction';
80
+ if (fact.pooler !== undefined) {
81
+ lines.push(
82
+ msg('cli.doctor.poolerBound', {
83
+ pooler: fact.pooler,
84
+ role: fact.role,
85
+ statement: fact.bound.statementTimeout.applied,
86
+ idle: fact.bound.idleInTransaction.applied,
87
+ }),
88
+ );
89
+ }
90
+ if (wakeOff) lines.push(msg('cli.doctor.poolerWakeOff', { ms: IDLE_POLL_CEILING_MS }));
91
+ const statement: JsonValue = {
92
+ role: fact.role,
93
+ pooler: fact.pooler ?? null,
94
+ statementTimeout: readingOf(fact.bound.statementTimeout),
95
+ idleInTransaction: readingOf(fact.bound.idleInTransaction),
96
+ };
97
+ const wake: JsonValue = { off: true, pickupWithinMs: IDLE_POLL_CEILING_MS };
98
+ return {
99
+ ...result,
100
+ data: {
101
+ ...(data as Record<string, JsonValue>),
102
+ statementBound: statement,
103
+ ...(wakeOff ? { wake } : {}),
104
+ },
105
+ lines: [...(result.lines ?? []), ...lines],
106
+ };
107
+ }
@@ -0,0 +1,35 @@
1
+ // `x doctor`'s reminder: an app with no `rollingSince` is not judged by the rolling rail, and one
2
+ // line says which id turns it on. A REMINDER, never a finding: the rail is off because an applied
3
+ // migration cannot gain a marker, and a red `x doctor` over that would be a failure nobody can clear.
4
+
5
+ import { msg } from './messages';
6
+ import { readMigrations } from './migrations';
7
+ import type { CommandResult, JsonValue } from './output';
8
+ import { readVerifyFloor } from './verify-floor';
9
+
10
+ export interface RollingReminder {
11
+ /** The newest migration id: what `rollingSince` is set to, judging only what comes after. */
12
+ readonly newest: string;
13
+ }
14
+
15
+ /** The I/O half. `undefined` when the rail is on, or when an app has no migration to name. */
16
+ export async function rollingSinceProbe(root: string): Promise<RollingReminder | undefined> {
17
+ if ((await readVerifyFloor(root))?.rollingSince !== undefined) return undefined;
18
+ const newest = (await readMigrations(root)).at(-1)?.id;
19
+ return newest === undefined ? undefined : { newest };
20
+ }
21
+
22
+ /** The line, on a result `x doctor` already decided: `ok` and the findings stay as they came. */
23
+ export function withRollingReminder(
24
+ result: CommandResult,
25
+ reminder: RollingReminder | undefined,
26
+ ): CommandResult {
27
+ if (reminder === undefined) return result;
28
+ const data = typeof result.data === 'object' && result.data !== null ? result.data : {};
29
+ const rolling: JsonValue = { unset: true, newest: reminder.newest };
30
+ return {
31
+ ...result,
32
+ data: { ...(data as Record<string, JsonValue>), rolling },
33
+ lines: [...(result.lines ?? []), msg('cli.doctor.rollingSince', { newest: reminder.newest })],
34
+ };
35
+ }
@@ -0,0 +1,40 @@
1
+ // Single responsibility: `x doctor`'s row-security question, asked without booting — with
2
+ // `database.rowSecurity` on, is the role `DATABASE_URL` connects as one Postgres applies policies
3
+ // to. The boot refuses the same role (`X_DB_ROW_SECURITY_BYPASSED`); this says so before a deploy.
4
+
5
+ import { resolveRole } from '@ultimat3/core';
6
+ import { assertRowSecurityBinds, DbError, postgresClient } from '@ultimat3/db';
7
+ import { loadAppConfig } from './app-config-load';
8
+ import type { Finding } from './output';
9
+ import { findingFrom } from './output';
10
+ import { rowSecurityDeclared } from './row-security-boot';
11
+
12
+ /**
13
+ * External databases only: an embedded one serves as `x_app` by construction (`serveAsAppRole`).
14
+ * An unreachable server is `database()`'s finding to make, so only the exemption is reported here.
15
+ * The flag-against-migrations half is `X_DB_ROW_SECURITY_DRIFT`, reported by the drift probe.
16
+ */
17
+ export async function rowSecurityProbe(
18
+ root: string,
19
+ url: string | undefined,
20
+ env: Readonly<Record<string, string | undefined>>,
21
+ ): Promise<readonly Finding[]> {
22
+ if (url === undefined || url.trim() === '') return [];
23
+ if (!rowSecurityDeclared(await loadAppConfig(root))) return [];
24
+ const client = postgresClient({
25
+ url,
26
+ role: resolveRole({ env }),
27
+ env,
28
+ applicationName: 'x-doctor',
29
+ });
30
+ try {
31
+ await assertRowSecurityBinds(client);
32
+ return [];
33
+ } catch (error) {
34
+ return error instanceof DbError && error.code === 'X_DB_ROW_SECURITY_BYPASSED'
35
+ ? [findingFrom(error)]
36
+ : [];
37
+ } finally {
38
+ await client.close();
39
+ }
40
+ }
@@ -85,4 +85,17 @@ export interface DocumentOptions {
85
85
  * app declares no favicon — the browser's own `/favicon.ico` request is then the only one.
86
86
  */
87
87
  readonly faviconHead?: string;
88
+ /**
89
+ * Tags on `app/` documents ONLY — `/x-client-errors.js` (`client-errors-routes.ts`) when
90
+ * `clientErrors.enabled`, and `/x-client-vitals.js` (`client-vitals-route.ts`) when
91
+ * `clientErrors.vitals` lists `app`. A `site/` document never carries them: the static path
92
+ * never pays for the app path (axiom 6).
93
+ */
94
+ readonly appHead?: string;
95
+ /**
96
+ * Tags on `site/` documents ONLY — today `/x-client-vitals.js`, and only when
97
+ * `clientErrors.vitals.surfaces` lists `site`: the static path opts in by name, and the bytes
98
+ * are then charged to its own budget (`prerender-served.ts`'s `vitalsMeasure`).
99
+ */
100
+ readonly siteHead?: string;
88
101
  }
@@ -0,0 +1,25 @@
1
+ // Where `x drill`'s sync client dials: the fleet's `sync` role binds its socket on PORT + 1
2
+ // (`syncPortFor`, the rule `role-sync.ts` listens by), at the node's `/_x/sync`. A fleet with no
3
+ // sync role has no lane, and `sync-resumes` is then red on no evidence.
4
+
5
+ import { PROTOCOL_VERSION } from '@ultimat3/realtime';
6
+ import type { DrillFleet, DrillSyncLane } from '@ultimat3/testing';
7
+ import { startDrillSync } from '@ultimat3/testing';
8
+ import { syncPortFor } from './flag-number';
9
+
10
+ /** The sync node's socket URL for a role handed `origin` as its PORT. */
11
+ export function drillSyncUrl(origin: string): string {
12
+ const url = new URL(origin);
13
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
14
+ url.port = String(syncPortFor(Number(url.port)));
15
+ url.pathname = '/_x/sync';
16
+ return url.href;
17
+ }
18
+
19
+ /** The lane `runDrill` starts with the fault, or none when the fleet runs no sync role. */
20
+ export function drillSyncLane(fleet: DrillFleet): (() => DrillSyncLane) | undefined {
21
+ const sync = fleet.roles.get('sync');
22
+ if (sync === undefined) return undefined;
23
+ const url = drillSyncUrl(sync.origin);
24
+ return () => startDrillSync({ url, protocolVersion: PROTOCOL_VERSION });
25
+ }
@@ -0,0 +1,39 @@
1
+ // What `x drill`'s load sends: the site's parameterless pages, read anonymously, and the write
2
+ // lane — every action an anonymous POST can reach, each request a fresh input drawn as `x exercise`
3
+ // draws one, the app's `exercise.ts` honoured. The writes are how a job reaches the queue over HTTP.
4
+
5
+ import { toRoute } from '@ultimat3/action';
6
+ import type { DrillTarget, DrillWrites } from '@ultimat3/testing';
7
+ import { drillWriteTargets } from '@ultimat3/testing';
8
+ import { loadApp } from './app-load';
9
+ import { readExerciseOverrides } from './exercise-overrides';
10
+ import { plainAppRoutes, routeRows } from './route-table';
11
+
12
+ export interface DrillLoadPlan {
13
+ readonly targets: readonly DrillTarget[];
14
+ readonly skipped: DrillWrites['skipped'];
15
+ }
16
+
17
+ /** The load for the app at `root`; loading it is what fills the registries read here. */
18
+ export async function drillTargets(root: string): Promise<DrillLoadPlan> {
19
+ await loadApp(root);
20
+ // Anonymous pages: a non-2xx is the fault's, never a 401's.
21
+ const reads = routeRows(await plainAppRoutes(root))
22
+ .filter((route) => route.surface === 'site' && !/[[:*]/.test(route.path))
23
+ .map((route): DrillTarget => ({ method: 'GET', path: route.path }));
24
+ const { listActions } = await import('@ultimat3/action');
25
+ const writes = await drillWriteTargets({
26
+ actions: listActions().map((action) => {
27
+ const route = toRoute(action);
28
+ return {
29
+ name: action.name,
30
+ method: route.method,
31
+ path: route.path,
32
+ anonymous: route.meta.auth === 'public',
33
+ input: action.input,
34
+ };
35
+ }),
36
+ overrides: await readExerciseOverrides(root),
37
+ });
38
+ return { targets: [...reads, ...writes.targets], skipped: writes.skipped };
39
+ }
@@ -4,7 +4,7 @@
4
4
  // `packages/i18n` workspace pinned `@ultimat3/i18n@19.0.0` while its root had 19.1.0 registered
5
5
  // every catalog into the workspace's copy, the CLI read the root's, and `x i18n check` answered
6
6
  // `X_CATALOG_UNREGISTERED` with a fix that said to move a `defineCatalogs()` call that was already
7
- // exactly where the fix said to put it (ai-maxxing, 2026-09-05). The same split is the one
7
+ // exactly where the fix said to put it (one adopter, 2026-09-05). The same split is the one
8
8
  // `local-cli.ts` hands over for — a global `x` is a second copy of `@ultimat3/entity` — except this
9
9
  // one lives inside the app's own `node_modules`, where no hand-over can reach it.
10
10
 
@@ -0,0 +1,93 @@
1
+ // Single responsibility: the `.env.production` `x env init --production` writes, as text. Pure:
2
+ // the caller hands in the app's env declaration, its compose file and the randomness, so a test
3
+ // can read every line without a filesystem or a real secret.
4
+
5
+ import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
6
+ import { VAPID_PRIVATE_KEY_ENV, VAPID_PUBLIC_KEY_ENV } from '@ultimat3/pwa';
7
+ import { composeDatabase, composeRequiredVars, DEFAULT_IMAGE } from './cmd-deploy-compose';
8
+ import { type AppSecretFacts, configOwes, FRAMEWORK_SECRETS } from './framework-env';
9
+
10
+ /** The two origins one box serves on: the pages on :3000, the sync socket on :3001. */
11
+ export const LOCAL_ORIGINS: Readonly<Record<string, string>> = {
12
+ APP_URL: 'http://localhost:3000',
13
+ SYNC_URL: 'ws://localhost:3001/_x/sync',
14
+ };
15
+
16
+ export interface ProductionEnvInput {
17
+ readonly schema: EnvSchema;
18
+ /** `docker/docker-compose.prod.yml`'s text, or `undefined` when the app has none. */
19
+ readonly compose: string | undefined;
20
+ readonly facts: AppSecretFacts;
21
+ /** One fresh secret per call: 32 random bytes as hex, `openssl rand -hex 32`'s shape. */
22
+ readonly secret: () => string;
23
+ /** The Web Push pair, minted by the caller only when `facts.push` owes one. */
24
+ readonly vapid?: { readonly publicKey: string; readonly privateKey: string } | undefined;
25
+ }
26
+
27
+ export interface ProductionEnv {
28
+ readonly contents: string;
29
+ /** Every key written with a value, in file order. Names only: the values are secrets. */
30
+ readonly written: readonly string[];
31
+ /** Keys the file must carry and nothing here can know: written empty, for the operator. */
32
+ readonly unfilled: readonly string[];
33
+ }
34
+
35
+ /** Required at boot: no default and no `required: false`. A role-scoped key is required there. */
36
+ export const requiredAtBoot = (decl: EnvVarDecl): boolean =>
37
+ decl.default === undefined && decl.required !== false;
38
+
39
+ /**
40
+ * The file. Order is the reader's: the environment, the image, the database, the origins, then
41
+ * every secret, and last the keys left for the operator, each under the description that says
42
+ * what it is.
43
+ */
44
+ export function productionEnvFile(input: ProductionEnvInput): ProductionEnv {
45
+ const values = new Map<string, string>([
46
+ ['ULTIMATE_ENV', 'production'],
47
+ ['IMAGE', DEFAULT_IMAGE],
48
+ ]);
49
+ const required = input.compose === undefined ? [] : composeRequiredVars(input.compose);
50
+ const db = composeDatabase(input.compose);
51
+ if (db !== undefined) {
52
+ const password = input.secret();
53
+ values.set('POSTGRES_PASSWORD', password);
54
+ values.set(
55
+ 'DATABASE_URL',
56
+ `postgres://${encodeURIComponent(db.user)}:${password}@db:5432/${encodeURIComponent(db.database)}`,
57
+ );
58
+ }
59
+ for (const [key, origin] of Object.entries(LOCAL_ORIGINS)) {
60
+ if (required.includes(key) || Object.hasOwn(input.schema, key)) values.set(key, origin);
61
+ }
62
+ for (const [key, decl] of Object.entries(input.schema)) {
63
+ if (decl.secret === true && !values.has(key)) values.set(key, input.secret());
64
+ }
65
+ for (const owed of FRAMEWORK_SECRETS) {
66
+ if (!configOwes(owed, input.facts) || values.has(owed.key)) continue;
67
+ if (owed.key === VAPID_PUBLIC_KEY_ENV && input.vapid !== undefined) {
68
+ values.set(owed.key, input.vapid.publicKey);
69
+ } else if (owed.key === VAPID_PRIVATE_KEY_ENV && input.vapid !== undefined) {
70
+ values.set(owed.key, input.vapid.privateKey);
71
+ } else if (owed.secret !== false) {
72
+ values.set(owed.key, input.secret());
73
+ }
74
+ }
75
+ const unfilled = [
76
+ ...Object.entries(input.schema)
77
+ .filter(([key, decl]) => requiredAtBoot(decl) && !values.has(key))
78
+ .map(([key]) => key),
79
+ ...required.filter((key) => !values.has(key) && !Object.hasOwn(input.schema, key)),
80
+ ];
81
+ const lines = [
82
+ '# Written by `x env init --production`. Secrets: never commit this file (.gitignore covers it),',
83
+ '# and keep a copy in a secret store — a new POSTGRES_PASSWORD does not reach a database that',
84
+ '# already initialised its volume with the old one.',
85
+ '# APP_URL and SYNC_URL are this box on localhost: set the host your users reach.',
86
+ ...[...values].map(([key, value]) => `${key}=${value}`),
87
+ ...unfilled.flatMap((key) => [
88
+ `# required: ${input.schema[key]?.description ?? 'the compose file refuses to parse without it'}`,
89
+ `${key}=`,
90
+ ]),
91
+ ];
92
+ return { contents: `${lines.join('\n')}\n`, written: [...values.keys()], unfilled };
93
+ }