@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
package/src/cmd-doctor.ts CHANGED
@@ -6,16 +6,23 @@ import { existsSync } from 'node:fs';
6
6
  import { dirname, join, resolve, sep } from 'node:path';
7
7
  import { ENV_EXAMPLE_PATH, ERROR_DOCS_URL } from '@ultimat3/core';
8
8
  import { checkDb, PGLITE_FIX, PGLITE_MISSING, PGLITE_PACKAGE, postgresClient } from '@ultimat3/db';
9
+ import { loadApp } from './app-load';
10
+ import { IMPORT_UNRESOLVED } from './app-load-unresolved';
9
11
  import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
10
12
  import { doctorSpec } from './cmd-doctor-spec';
11
13
  import type { CliCommand, CommandContext } from './command';
12
14
  import { checkMigrationSnapshots } from './db-snapshot';
13
15
  import { appUrlFindings } from './doctor-app-url';
14
16
  import { type AuthStorageFact, authStorageFindings, authStorageProbe } from './doctor-auth';
17
+ import { chartAlertsProbe, withChartAlertsListing } from './doctor-chart-alerts';
15
18
  import { coverageExcludesProbe, withCoverageExcludes } from './doctor-coverage';
16
19
  import { shippedGuardsProbe, withGuardListing } from './doctor-guards';
17
20
  import type { OfflineFallbackFact } from './doctor-offline';
18
21
  import { offlineFallbackFinding, offlineFallbackProbe } from './doctor-offline';
22
+ import type { PoolerFact } from './doctor-pooler';
23
+ import { NO_POOLER_FACT, poolerFindings, poolerProbe, withPoolerReport } from './doctor-pooler';
24
+ import { rollingSinceProbe, withRollingReminder } from './doctor-rolling';
25
+ import { rowSecurityProbe } from './doctor-row-security';
19
26
  import type { SealedKeysFact } from './doctor-sealed';
20
27
  import { sealedKeyFindings, sealedKeysProbe } from './doctor-sealed';
21
28
  import { PORT_RANGE, portPairAfter, readIntFlag } from './flag-number';
@@ -97,6 +104,10 @@ export interface DoctorProbe {
97
104
  * secrets, and tables no release reads. Asked of an external database only.
98
105
  */
99
106
  authStorage(): Promise<AuthStorageFact>;
107
+ /** The statement bound the session really reads, and what a pooler refused (`doctor-pooler.ts`). */
108
+ pooler(): Promise<PoolerFact>;
109
+ /** With `database.rowSecurity` on, whether the serving role is exempt (`doctor-row-security.ts`). */
110
+ rowSecurity?(): Promise<readonly Finding[]>;
100
111
  }
101
112
 
102
113
  /** What `x doctor` reads about the embedded database, without opening it. */
@@ -201,7 +212,12 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
201
212
  }
202
213
  if (probe.root === undefined) {
203
214
  findings.push(
204
- finding('X_NOT_IN_APP', 'no app.config.ts at or above the working directory', 'x new myapp'),
215
+ // Not `x new`: outside an app `x` is on no PATH, and `bunx x` fetches an unrelated npm package.
216
+ finding(
217
+ 'X_NOT_IN_APP',
218
+ 'no app.config.ts at or above the working directory',
219
+ 'bunx create-ultimate myapp && cd myapp',
220
+ ),
205
221
  );
206
222
  return findings;
207
223
  }
@@ -250,12 +266,19 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
250
266
  // holds the rule and the reasons.
251
267
  const offline = offlineFallbackFinding(await probe.offlineFallback());
252
268
  if (offline !== undefined) findings.push(offline);
253
- const database = await probe.database();
269
+ const pooler = await probe.pooler();
270
+ findings.push(...poolerFindings(pooler));
271
+ // A pooler's refusal is every other database probe's answer too, and its fix is the true one: this
272
+ // one's says "set DATABASE_URL" to an operator whose URL is right.
273
+ const database = pooler.refused === undefined ? await probe.database() : null;
254
274
  if (database !== null) findings.push(database);
255
275
  // The other half of the same question, and the half a bare VM lands on: `database()` is silent
256
276
  // where there is no `DATABASE_URL`, and that silence IS the bare-VM configuration.
257
277
  const embedded = embeddedDatabaseFinding(await probe.embeddedDatabase());
258
278
  if (embedded !== undefined) findings.push(embedded);
279
+ // Asked only of a database that answered: an unreachable one is already the finding above.
280
+ const reachable = pooler.refused === undefined && database === null;
281
+ if (reachable) findings.push(...((await probe.rowSecurity?.()) ?? []));
259
282
  findings.push(...(await probe.drift()));
260
283
  // Last, and it is why `X_CLI_UNEXPECTED`'s `fix: x doctor --json` is not a dead end on the path an
261
284
  // author reaches it from: `x db gen` throwing `X_MIGRATION_SNAPSHOT_MISSING` used to be a
@@ -333,9 +356,17 @@ const installedAbove = (dir: string, specifier: string): boolean => {
333
356
  * The pool is CLOSED on every path — this command exits, and a held pool is a connection slot the
334
357
  * next `x db migrate` cannot have.
335
358
  */
336
- async function probeDatabase(url: string | undefined): Promise<Finding | null> {
359
+ async function probeDatabase(
360
+ url: string | undefined,
361
+ env: Readonly<Record<string, string | undefined>>,
362
+ ): Promise<Finding | null> {
337
363
  if (externalUrl(url) === undefined) return null;
338
- const client = postgresClient({ url, applicationName: 'x-doctor' });
364
+ let client: ReturnType<typeof postgresClient>;
365
+ try {
366
+ client = postgresClient({ url, env, applicationName: 'x-doctor' });
367
+ } catch (error) {
368
+ return findingFrom(error);
369
+ }
339
370
  try {
340
371
  const report = await checkDb(client);
341
372
  if (report.ok) return null;
@@ -369,7 +400,7 @@ export function probeFor(
369
400
  frameworkFindings: machineSecretFindings(env),
370
401
  exists: (relativePath) => (root === undefined ? false : existsSync(join(root, relativePath))),
371
402
  portFree,
372
- database: () => probeDatabase(process.env['DATABASE_URL']),
403
+ database: () => probeDatabase(process.env['DATABASE_URL'], env),
373
404
  // `root ?? cwd` because the walk needs a directory that exists; outside an app `runDoctor`
374
405
  // returns on `X_NOT_IN_APP` before this is ever asked, so the value only has to be honest.
375
406
  embeddedDatabase: async () => ({
@@ -377,23 +408,36 @@ export function probeFor(
377
408
  resolved: installedAbove(root ?? cwd, PGLITE_PACKAGE),
378
409
  }),
379
410
  drift: async () => (root === undefined ? [] : checkMigrationDrift(root)),
380
- snapshots: async () => (root === undefined ? [] : checkMigrationSnapshots(root)),
411
+ // The import half too: `X_CLI_UNEXPECTED` used to send a reader here and find this silent.
412
+ snapshots: async () =>
413
+ root === undefined
414
+ ? []
415
+ : [...(await unresolvedImportFindings(root)), ...(await checkMigrationSnapshots(root))],
381
416
  // `routes: undefined` outside an app is "not judged", which is what the caller already is:
382
417
  // `runDoctor` returns on `X_NOT_IN_APP` before this can be asked.
383
418
  offlineFallback: async () =>
384
419
  root === undefined ? { fallback: null, routes: undefined } : offlineFallbackProbe(root),
385
420
  sealedKeys: async () =>
386
421
  root === undefined ? { columns: undefined, keys: undefined } : sealedKeysProbe(root),
387
- authStorage: () => authStorageProbe(process.env['DATABASE_URL']),
422
+ authStorage: () => authStorageProbe(process.env['DATABASE_URL'], env),
423
+ pooler: () => poolerProbe(process.env['DATABASE_URL'], env),
424
+ rowSecurity: async () =>
425
+ root === undefined ? [] : rowSecurityProbe(root, process.env['DATABASE_URL'], env),
388
426
  };
389
427
  }
390
428
 
429
+ /** What a load reports, narrowed to the one failure a doctor can name a repair for. */
430
+ const unresolvedImportFindings = async (root: string): Promise<readonly Finding[]> =>
431
+ (await loadApp(root)).findings.filter((finding) => finding.code === IMPORT_UNRESOLVED);
432
+
391
433
  export const doctorCommand: CliCommand = {
392
434
  spec: doctorSpec,
393
435
  async run(ctx: CommandContext): Promise<CommandResult> {
394
436
  const port = doctorPort(ctx.args, ctx.env);
395
437
  const probe = probeFor(ctx.cwd, ctx.bunVersion, port, ctx.env);
396
- const findings = await runDoctor(probe);
438
+ // Asked once: `runDoctor` reads the verdict and the report beside it reads the numbers.
439
+ const pooler = probe.root === undefined ? NO_POOLER_FACT : await probe.pooler();
440
+ const findings = await runDoctor({ ...probe, pooler: async () => pooler });
397
441
  const listed = withGuardListing(
398
442
  {
399
443
  ok: findings.length === 0,
@@ -409,9 +453,22 @@ export const doctorCommand: CliCommand = {
409
453
  probe.root === undefined ? [] : await shippedGuardsProbe(probe.root),
410
454
  );
411
455
  // The second listing: what the coverage floor does not see (`doctor-coverage.ts`).
412
- return withCoverageExcludes(
456
+ const covered = withCoverageExcludes(
413
457
  listed,
414
458
  probe.root === undefined ? [] : await coverageExcludesProbe(probe.root),
415
459
  );
460
+ // The third: the monitoring templates the app's chart predates (`doctor-chart-alerts.ts`).
461
+ const charted = withChartAlertsListing(
462
+ covered,
463
+ probe.root === undefined ? [] : chartAlertsProbe(probe.root),
464
+ );
465
+ // A reminder, not a finding: the rolling rail is off until this is set (`doctor-rolling.ts`).
466
+ return withPoolerReport(
467
+ withRollingReminder(
468
+ charted,
469
+ probe.root === undefined ? undefined : await rollingSinceProbe(probe.root),
470
+ ),
471
+ pooler,
472
+ );
416
473
  },
417
474
  };
@@ -0,0 +1,25 @@
1
+ // `x drill`'s declaration, apart from its body: the parser, `x help` and the `errors` step read it
2
+ // without loading `cmd-drill.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const drillSpec: CommandSpec = {
7
+ name: 'drill',
8
+ summary:
9
+ 'fault the real services under web and worker processes, and fail on any framework contract that does not hold',
10
+ usage: 'x drill [<name>] [--list] [--budget-ms n] [--json]',
11
+ requiresApp: true,
12
+ flags: [
13
+ {
14
+ name: 'list',
15
+ type: 'boolean',
16
+ summary: 'print every drill, its fault and the properties it holds the app to; run nothing',
17
+ },
18
+ {
19
+ name: 'budget-ms',
20
+ type: 'string',
21
+ summary:
22
+ 'the request deadline the drills judge against, in place of the one derived from the app; printed in the verdict',
23
+ },
24
+ ],
25
+ };
@@ -0,0 +1,176 @@
1
+ // `x drill [<name>] [--list] [--budget-ms n]` — the app's web and worker roles as real processes on
2
+ // real services, each fault in `@ultimat3/testing`'s drill table played through a proxy or a
3
+ // signal, red on any framework contract that does not hold. CLI wiring only: the table, the fleet
4
+ // and the verdict are testing's. Not a gate step: it needs Postgres, and `x verify` needs nothing.
5
+
6
+ import type { AppConfig } from '@ultimat3/core';
7
+ import { DRAIN_DEADLINE_DEFAULT_MS, MAX_TIMER_MS } from '@ultimat3/core';
8
+ import type { Drill, DrillConfig, DrillDeclarations, DrillReport } from '@ultimat3/testing';
9
+ import {
10
+ bunDrillSql,
11
+ DRILL_NAMES,
12
+ DRILLS,
13
+ DrillFailedError,
14
+ DrillServiceMissingError,
15
+ drillBudgets,
16
+ drillNamed,
17
+ drillRoles,
18
+ drillServices,
19
+ runDrill,
20
+ startDrillFleet,
21
+ } from '@ultimat3/testing';
22
+ import { loadAppConfig } from './app-config-load';
23
+ import { requireAppRoot } from './app-root';
24
+ import { drillSpec } from './cmd-drill-spec';
25
+ import type { CliCommand, CommandContext } from './command';
26
+ import { drillSyncLane } from './drill-sync-lane';
27
+ import { drillTargets } from './drill-targets';
28
+ import { UnknownCommandError } from './errors';
29
+ import { readIntFlag } from './flag-number';
30
+ import { msg } from './messages';
31
+ import type { CommandResult, Finding, JsonValue } from './output';
32
+ import { flagBool } from './parse';
33
+
34
+ /** Read through `readIntFlag`, which refuses `5s`, `NaN` and a fraction with the flag's name. */
35
+ const BUDGET_FLAG = 'budget-ms';
36
+
37
+ /** How long a role may take to boot: a cold Bun over a whole app, islands included. */
38
+ const READY_TIMEOUT_MS = 90_000;
39
+
40
+ /** `@ultimat3/http`'s own default — the app's `configureHttp` is runtime code, not config. */
41
+ const HTTP_DEFAULT_DEADLINE_MS = 30_000;
42
+
43
+ const declarationsOf = (config: AppConfig | undefined): DrillDeclarations => ({
44
+ redis: config?.cache.tiers.includes('redis') ?? false,
45
+ nats:
46
+ config?.realtime.transport === 'nats'
47
+ ? { env: config.realtime.urlEnv ?? 'NATS_URL' }
48
+ : undefined,
49
+ });
50
+
51
+ /** The facts the thresholds derive from; `drillBudgets` screens every number it is handed. */
52
+ const drillConfigOf = (config: AppConfig | undefined): DrillConfig =>
53
+ config === undefined
54
+ ? {
55
+ requestTimeoutMs: HTTP_DEFAULT_DEADLINE_MS,
56
+ drainDeadlineMs: DRAIN_DEADLINE_DEFAULT_MS,
57
+ readinessGraceMs: 0,
58
+ readiness: 'dependencies',
59
+ }
60
+ : {
61
+ requestTimeoutMs: HTTP_DEFAULT_DEADLINE_MS,
62
+ drainDeadlineMs: config.drain.deadlineMs,
63
+ readinessGraceMs: config.drain.readinessGraceMs,
64
+ readiness: config.health.readiness,
65
+ };
66
+
67
+ const listing = (drill: Drill): JsonValue => ({
68
+ name: drill.name,
69
+ fault:
70
+ drill.fault.kind === 'service'
71
+ ? { service: drill.fault.service }
72
+ : { role: drill.fault.role, signal: drill.fault.signal },
73
+ holds: [...drill.holds],
74
+ });
75
+
76
+ const reportJson = (report: DrillReport): JsonValue =>
77
+ JSON.parse(
78
+ JSON.stringify(report, (_key, value: unknown) =>
79
+ value instanceof Map ? Object.fromEntries(value) : value,
80
+ ),
81
+ ) as JsonValue;
82
+
83
+ const findingsOf = (report: DrillReport): readonly Finding[] =>
84
+ report.verdict.failures.map((failure) => {
85
+ const error = new DrillFailedError({ drill: report.name, failure });
86
+ return { code: error.code, cause: error.cause, fix: error.fix };
87
+ });
88
+
89
+ export const drillCommand: CliCommand = {
90
+ spec: drillSpec,
91
+ async run(ctx: CommandContext): Promise<CommandResult> {
92
+ const root = requireAppRoot('drill', ctx.cwd).dir;
93
+ if (flagBool(ctx.args, 'list')) {
94
+ return {
95
+ ok: true,
96
+ command: 'drill',
97
+ summary: msg('cli.drill.listed', { count: DRILLS.length }),
98
+ lines: DRILLS.map((drill) => ` ${drill.name} ${drill.holds.join(', ')}`),
99
+ data: { drills: DRILLS.map(listing) },
100
+ };
101
+ }
102
+ const override = readIntFlag(ctx.args, {
103
+ name: BUDGET_FLAG,
104
+ command: 'drill',
105
+ min: 1,
106
+ max: MAX_TIMER_MS,
107
+ example: 'x drill pg-blackhole --budget-ms 5000 --json',
108
+ });
109
+ const name = ctx.args.positionals[0];
110
+ const named = name === undefined ? undefined : drillNamed(name);
111
+ if (name !== undefined && named === undefined) {
112
+ throw new UnknownCommandError({
113
+ path: `drill ${name}`,
114
+ known: DRILL_NAMES,
115
+ suggestion: 'drill --list --json',
116
+ });
117
+ }
118
+ const config = await loadAppConfig(root);
119
+ const declared = declarationsOf(config);
120
+ // Every drill's services, decided before any process starts. A named drill's gap is thrown; in
121
+ // a whole run an undeclared service is stated as not run — an unset URL still refuses it all.
122
+ const planned: { drill: Drill; services: ReturnType<typeof drillServices> }[] = [];
123
+ const notRun: string[] = [];
124
+ for (const drill of named === undefined ? DRILLS : [named]) {
125
+ try {
126
+ planned.push({ drill, services: drillServices(drill, ctx.env, declared) });
127
+ } catch (error) {
128
+ const undeclared =
129
+ named === undefined &&
130
+ error instanceof DrillServiceMissingError &&
131
+ error.meta?.['gap'] === 'undeclared';
132
+ if (!undeclared) throw error;
133
+ notRun.push(drill.name);
134
+ }
135
+ }
136
+ const budgets = drillBudgets(drillConfigOf(config), override);
137
+ const { targets, skipped } = await drillTargets(root);
138
+ const reports: DrillReport[] = [];
139
+ for (const { drill, services } of planned) {
140
+ const fleet = await startDrillFleet({
141
+ root,
142
+ services,
143
+ roles: drillRoles(drill),
144
+ readyTimeoutMs: READY_TIMEOUT_MS,
145
+ });
146
+ const sql = bunDrillSql(fleet.databaseUrl);
147
+ try {
148
+ const sync = drillSyncLane(fleet);
149
+ reports.push(await runDrill({ drill, fleet, budgets, targets, sql, sync }));
150
+ } finally {
151
+ await sql.close();
152
+ await fleet.stop();
153
+ }
154
+ }
155
+ const held = reports.filter((report) => report.verdict.ok).length;
156
+ const findings = reports.flatMap(findingsOf);
157
+ return {
158
+ ok: findings.length === 0 && held === reports.length,
159
+ command: 'drill',
160
+ summary: msg('cli.drill.summary', { held, ran: reports.length, notRun: notRun.length }),
161
+ lines: [
162
+ ...reports.map((report) => ` ${report.verdict.ok ? 'held' : 'FAILED'} ${report.name}`),
163
+ ...notRun.map((drill) => ` not run ${drill}`),
164
+ ],
165
+ findings,
166
+ data: {
167
+ budgets: { ...budgets, override: override ?? null },
168
+ targets: targets.map((target) => `${target.method} ${target.path}`),
169
+ // The actions the write lane left out, each with why: an empty lane is never a silent one.
170
+ skippedWrites: skipped.map((entry) => ({ ...entry })),
171
+ drills: reports.map(reportJson),
172
+ notRun,
173
+ },
174
+ };
175
+ },
176
+ };
@@ -0,0 +1,102 @@
1
+ // `x env init --production`: the one writer of `.env.production`, the file `x deploy --method
2
+ // compose` reads and nothing else wrote. Values come from the app's own compose file and env
3
+ // declaration (`env-production.ts`); secrets are minted here, written once, and never printed.
4
+
5
+ // why: Bun.write takes neither an exclusive-create flag nor a file mode, and a secrets file needs
6
+ // both: `wx` refuses a file that appeared since the check, 0600 keeps it from other users.
7
+ import { writeFile } from 'node:fs/promises';
8
+ // why: Bun ships no path-join primitive, and both files are app-root-relative.
9
+ import { join } from 'node:path';
10
+ import { UltimateError } from '@ultimat3/core';
11
+ import { generateVapidKeys } from '@ultimat3/pwa';
12
+ import { appSecretFacts, loadEnvSchema } from './app-env';
13
+ import { requireAppRoot } from './app-root';
14
+ import { readComposeFile } from './cmd-deploy-compose';
15
+ import type { CommandContext } from './command';
16
+ import { productionEnvFile } from './env-production';
17
+ import { BadFlagError } from './errors';
18
+ import { msg } from './messages';
19
+ import type { CommandResult } from './output';
20
+ import { flagBool } from './parse';
21
+ import { PROD_ENV_FILE } from './templates/scaffold-container-compose';
22
+
23
+ /**
24
+ * The file exists. Never overwritten: it holds the password the database volume was initialised
25
+ * with and the keys every session and cursor is signed with, so new ones lock the running deploy
26
+ * out of its own data.
27
+ */
28
+ export class EnvFileExistsError extends UltimateError {
29
+ constructor(input: { path: string }) {
30
+ super({
31
+ code: 'X_ENV_FILE_EXISTS',
32
+ cause: `${input.path} already exists, and x env init would replace the secrets this deploy runs with`,
33
+ // `NODE_ENV=production` is what makes Bun load `.env.production` into the check's process.
34
+ fix: `NODE_ENV=production x env check --json # the file is kept: edit a value in ${input.path} by hand`,
35
+ });
36
+ }
37
+ }
38
+
39
+ /** 32 random bytes as hex — `openssl rand -hex 32`, without needing openssl on the box. */
40
+ export const randomSecret = (): string =>
41
+ Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex');
42
+
43
+ /** `.gitignore` gains the file unless a line already names it; the scaffold's ignores `.env` only. */
44
+ async function ignoreEnvFile(root: string): Promise<boolean> {
45
+ const path = join(root, '.gitignore');
46
+ const file = Bun.file(path);
47
+ const text = (await file.exists()) ? await file.text() : '';
48
+ const lines = text.split(/\r?\n/).map((line) => line.trim());
49
+ if (lines.includes(PROD_ENV_FILE) || lines.includes(`/${PROD_ENV_FILE}`)) return false;
50
+ const separator = text === '' || text.endsWith('\n') ? '' : '\n';
51
+ await Bun.write(path, `${text}${separator}${PROD_ENV_FILE}\n`);
52
+ return true;
53
+ }
54
+
55
+ export async function initProductionEnv(
56
+ ctx: CommandContext,
57
+ secret: () => string = randomSecret,
58
+ ): Promise<CommandResult> {
59
+ if (!flagBool(ctx.args, 'production')) {
60
+ throw new BadFlagError({
61
+ flag: 'production',
62
+ command: 'env init',
63
+ reason: `init writes ${PROD_ENV_FILE}, the one environment a compose deploy reads`,
64
+ fix: 'x env init --production --json',
65
+ });
66
+ }
67
+ const root = requireAppRoot('env init', ctx.cwd).dir;
68
+ const path = join(root, PROD_ENV_FILE);
69
+ if (await Bun.file(path).exists()) throw new EnvFileExistsError({ path: PROD_ENV_FILE });
70
+ const facts = await appSecretFacts(root);
71
+ const env = productionEnvFile({
72
+ schema: (await loadEnvSchema(root)) ?? {},
73
+ compose: await readComposeFile(join(root, 'docker', 'docker-compose.prod.yml')),
74
+ facts,
75
+ secret,
76
+ vapid: facts.push ? await generateVapidKeys() : undefined,
77
+ });
78
+ // Ignored before it is written: a crash between the two must not leave secrets to `git add -A`.
79
+ const ignored = await ignoreEnvFile(root);
80
+ await writeFile(path, env.contents, { flag: 'wx', mode: 0o600 });
81
+ const complete = env.unfilled.length === 0;
82
+ return {
83
+ // Written either way — a rerun refuses the file — and red until every required key has a value.
84
+ ok: complete,
85
+ command: 'env',
86
+ summary: msg('cli.env.initialised', { path: PROD_ENV_FILE, count: env.written.length }),
87
+ lines: env.unfilled.map((key) => msg('cli.env.unfilled', { key, path: PROD_ENV_FILE })),
88
+ findings: env.unfilled.map((key) => ({
89
+ code: 'X_ENV_MISSING',
90
+ cause: `${key} is required, and nothing in this app says what its production value is`,
91
+ fix: `NODE_ENV=production x env check --json # after setting ${key} in ${PROD_ENV_FILE}`,
92
+ at: PROD_ENV_FILE,
93
+ })),
94
+ data: {
95
+ path: PROD_ENV_FILE,
96
+ written: [...env.written],
97
+ unfilled: [...env.unfilled],
98
+ gitignored: ignored,
99
+ },
100
+ exitCode: complete ? 0 : 1,
101
+ };
102
+ }
@@ -8,11 +8,18 @@ import type { CommandSpec } from './parse';
8
8
  export const envSpec: CommandSpec = {
9
9
  name: 'env',
10
10
  summary: `the typed environment declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`,
11
- usage: 'x env [check|example] [--json]',
11
+ usage: 'x env [check|example] [--json] | x env init --production [--json]',
12
12
  requiresApp: true,
13
- subcommands: ['check', 'example'],
13
+ subcommands: ['check', 'example', 'init'],
14
14
  // The bare `x env` answers the question the fix line on every `X_ENV_MISSING` in this
15
15
  // framework already tells its reader to run.
16
16
  defaultSubcommand: 'check',
17
- flags: [],
17
+ flags: [
18
+ {
19
+ name: 'production',
20
+ type: 'boolean',
21
+ summary: 'write .env.production: what x deploy --method compose reads, secrets generated',
22
+ subcommands: ['init'],
23
+ },
24
+ ],
18
25
  };
package/src/cmd-env.ts CHANGED
@@ -1,6 +1,6 @@
1
- // `x env` — the two things a typed environment owes an agent: the committed `.env.example` that
2
- // says which variables exist, and the answer to "does this process have them?". Both are
3
- // projections of the one `defineEnv` declaration in `app.config.ts`; neither reads a second list.
1
+ // `x env` — the things a typed environment owes an agent: the committed `.env.example` that says
2
+ // which variables exist, the answer to "does this process have them?", and (`init`,
3
+ // `cmd-env-init.ts`) the deploy's `.env.production`. All read the one `defineEnv` declaration.
4
4
 
5
5
  // Bun ships no path-join primitive, and `.env.example` is written app-root-relative.
6
6
  import { join } from 'node:path';
@@ -15,7 +15,7 @@ import { appSecretFacts, envExampleFor, loadEnvSchema } from './app-env';
15
15
  import { requireAppRoot } from './app-root';
16
16
  import { envSpec } from './cmd-env-spec';
17
17
  import type { CliCommand, CommandContext } from './command';
18
- import { frameworkSecretFindings } from './framework-env';
18
+ import { frameworkSecretFindings, namesNoEnvironment } from './framework-env';
19
19
  import { msg } from './messages';
20
20
  import type { CommandResult, Finding, JsonValue } from './output';
21
21
 
@@ -97,12 +97,17 @@ async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
97
97
  ...framework,
98
98
  ];
99
99
  const ok = findings.length === 0;
100
+ const verdict = ok
101
+ ? msg('cli.env.checked', { count: total })
102
+ : msg('cli.env.invalid', { count: findings.length, total });
100
103
  return {
101
104
  ok,
102
105
  command: 'env',
103
- summary: ok
104
- ? msg('cli.env.checked', { count: total })
105
- : msg('cli.env.invalid', { count: findings.length, total }),
106
+ // `x doctor` asks about this machine and stays quiet on the same shell, so an exit 1 here over
107
+ // a table that named nothing reads as a contradiction until this line says which question ran.
108
+ summary: namesNoEnvironment(ctx.env)
109
+ ? `${verdict} — ${msg('cli.env.checkedAsProduction')}`
110
+ : verdict,
106
111
  findings,
107
112
  data: {
108
113
  variables: total,
@@ -118,8 +123,8 @@ export const envCommand: CliCommand = {
118
123
  // `subcommand`, never `positionals[0]`: the parser has already lifted a declared subcommand
119
124
  // out of the positionals, so reading the array here matches nothing and every invocation
120
125
  // silently ran the default.
121
- return (ctx.args.subcommand ?? 'check') === 'example'
122
- ? writeExample(ctx)
123
- : checkProcessEnv(ctx);
126
+ const subcommand = ctx.args.subcommand ?? 'check';
127
+ if (subcommand === 'init') return (await import('./cmd-env-init')).initProductionEnv(ctx);
128
+ return subcommand === 'example' ? writeExample(ctx) : checkProcessEnv(ctx);
124
129
  },
125
130
  };
@@ -0,0 +1,60 @@
1
+ // `x errors explain <CODE> --recent` — the code's explanation, then what this app has seen of it:
2
+ // its groups in `x_error_groups` (count, first and last seen) and the replayable bundles
3
+ // `x_failures` holds, each with the `x repro` line that turns it into a failing test.
4
+
5
+ import { postgresErrorGroupsStore } from '@ultimat3/admin/error-groups';
6
+ import { postgresFailureStore } from '@ultimat3/db';
7
+ import { requireAppRoot } from './app-root';
8
+ import type { CommandContext } from './command';
9
+ import { withJobDriver } from './jobs-driver';
10
+ import type { CommandResult, JsonValue } from './output';
11
+
12
+ /** The newest groups read to find this code's: the store's own ceiling (`ERROR_GROUPS_MAX_LIMIT`). */
13
+ const GROUPS_READ = 500;
14
+
15
+ /** `explained` with the app's own sightings of `code` folded into its lines and its data. */
16
+ export function withRecent(
17
+ ctx: CommandContext,
18
+ code: string,
19
+ explained: CommandResult,
20
+ ): Promise<CommandResult> {
21
+ const root = requireAppRoot('errors explain --recent', ctx.cwd).dir;
22
+ return withJobDriver(root, ctx, async () => {
23
+ const groups = (await postgresErrorGroupsStore().list({ limit: GROUPS_READ })).filter(
24
+ (group) => group.code === code,
25
+ );
26
+ const bundles = await postgresFailureStore().list({ code });
27
+ const recent: JsonValue = {
28
+ groups: groups.map((group) => ({
29
+ fingerprint: group.fingerprint,
30
+ operation: group.operation,
31
+ count: group.count,
32
+ firstSeen: group.firstSeen,
33
+ lastSeen: group.lastSeen,
34
+ status: group.status,
35
+ })),
36
+ bundles: bundles.map((bundle) => ({
37
+ ...bundle,
38
+ repro: `x repro ${bundle.id} --write --verify`,
39
+ })),
40
+ };
41
+ const lines = [
42
+ ...(explained.lines ?? []),
43
+ ` recent: ${groups.length} group(s), ${bundles.length} captured bundle(s)`,
44
+ ...groups.map(
45
+ (group) =>
46
+ ` ${group.fingerprint} ${String(group.count).padStart(6)}× last ${group.lastSeen} ${group.operation ?? ''}`,
47
+ ),
48
+ ...bundles.map(
49
+ (bundle) => ` x repro ${bundle.id} --write --verify # ${bundle.name} at ${bundle.at}`,
50
+ ),
51
+ ];
52
+ const data =
53
+ typeof explained.data === 'object' &&
54
+ explained.data !== null &&
55
+ !Array.isArray(explained.data)
56
+ ? explained.data
57
+ : {};
58
+ return { ...explained, lines, data: { ...data, recent } };
59
+ });
60
+ }
@@ -8,7 +8,7 @@ export const ERRORS_SUBCOMMANDS = ['explain', 'list'] as const;
8
8
  export const errorsSpec: CommandSpec = {
9
9
  name: 'errors',
10
10
  summary: 'an X_* code, explained: cause, runnable fix, docs URL',
11
- usage: 'x errors [explain <CODE>|list] [--json]',
11
+ usage: 'x errors [explain <CODE> [--recent]|list] [--json]',
12
12
  subcommands: ERRORS_SUBCOMMANDS,
13
13
  // `explain`, deliberately: the bare `x errors` then answers with `MissingPositionalError`,
14
14
  // which names `<CODE>` and hands back a real invocation. `list` would silently print 200 rows
@@ -20,4 +20,12 @@ export const errorsSpec: CommandSpec = {
20
20
  // Safe to declare here and nowhere else so far: the only thing that is not `explain` or
21
21
  // `list` in this slot is a code, and a near miss of either is still refused (#F16).
22
22
  defaultSubcommandTakesPositional: true,
23
+ flags: [
24
+ {
25
+ name: 'recent',
26
+ type: 'boolean',
27
+ summary: "explain: this app's groups of the code and the captured bundles x repro replays",
28
+ subcommands: ['explain'],
29
+ },
30
+ ],
23
31
  };
package/src/cmd-errors.ts CHANGED
@@ -5,6 +5,7 @@ import { nearestName, singleLine } from '@ultimat3/core';
5
5
  // a plausible-sounding explanation an agent would then act on.
6
6
 
7
7
  import type { ErrorExplanation } from '@ultimat3/mcp';
8
+ import { withRecent } from './cmd-errors-recent';
8
9
  import { errorsSpec } from './cmd-errors-spec';
9
10
  import type { CliCommand, CommandContext } from './command';
10
11
  import type { ErrorCatalog } from './error-catalog';
@@ -14,6 +15,7 @@ import { ErrorCodeUnknownError, MissingPositionalError } from './errors';
14
15
  import { explainErrorCode, explainEveryErrorCode } from './mcp-errors';
15
16
  import { msg } from './messages';
16
17
  import type { CommandResult, JsonValue } from './output';
18
+ import { flagBool } from './parse';
17
19
 
18
20
  export { ERRORS_SUBCOMMANDS } from './cmd-errors-spec';
19
21
 
@@ -106,6 +108,8 @@ export const errorsCommand: CliCommand = {
106
108
  example: 'x errors list --json',
107
109
  });
108
110
  }
111
+ // `--recent`: what THIS app has seen of the code — groups and the bundles `x repro` replays.
112
+ if (flagBool(ctx.args, 'recent')) return withRecent(ctx, code, explainOne(code));
109
113
  return explainOne(code);
110
114
  },
111
115
  };
@@ -0,0 +1,19 @@
1
+ // `x exercise`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-exercise.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const exerciseSpec: CommandSpec = {
7
+ name: 'exercise',
8
+ summary: 'run every action, query and job once on generated rows, with no test written',
9
+ usage: 'x exercise [<name>] [--seed n] [--json]',
10
+ requiresApp: true,
11
+ flags: [
12
+ {
13
+ name: 'seed',
14
+ type: 'string',
15
+ summary:
16
+ 'the integer the seed graph and every generated input are drawn from; same seed, same pass',
17
+ },
18
+ ],
19
+ };