@ultimat3/cli 29.4.0 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -0,0 +1,380 @@
1
+ // `x deploy verify`: does production serve this build, does it hold, and does a worker of this build
2
+ // claim every queue. HTTP and Postgres only (axiom 7): pods are SAMPLED through the load balancer,
3
+ // never enumerated, and a GitOps roll that never ran `x deploy` is checked the same way.
4
+
5
+ import type { Clock } from '@ultimat3/core';
6
+ import {
7
+ BUILD_ID_HEADER,
8
+ renderCauseValue,
9
+ renderThrowable,
10
+ systemClock,
11
+ UltimateError,
12
+ } from '@ultimat3/core';
13
+ import { deadlineSignal } from '@ultimat3/core/outbound';
14
+ import type { QueueDemand, WorkerRecord } from '@ultimat3/jobs';
15
+ import { requireAppRoot } from './app-root';
16
+ import type { CommandContext } from './command';
17
+ import { failed, ok } from './command';
18
+ import type { WaitOutcome } from './deploy-verify-findings';
19
+ import {
20
+ queueReadFinding,
21
+ resultData,
22
+ sampleFinding,
23
+ unservedFinding,
24
+ } from './deploy-verify-findings';
25
+ import type { VerifyOptions } from './deploy-verify-flags';
26
+ import { readBuild, readOptions, VERIFY_COMMAND } from './deploy-verify-flags';
27
+ import type { QueueGrade } from './deploy-verify-grade';
28
+ import {
29
+ emptyTally,
30
+ failureOf,
31
+ foldSample,
32
+ gradeQueues,
33
+ UNGRADED,
34
+ windowClosed,
35
+ } from './deploy-verify-grade';
36
+ import { BadFlagError } from './errors';
37
+ import { msg } from './messages';
38
+ import type { CommandResult, Finding } from './output';
39
+
40
+ /** One sample's own limit: a pod that hangs is a bad sample, never the whole wait gone. */
41
+ const SAMPLE_TIMEOUT_MS = 10_000;
42
+ /**
43
+ * The least a sample gets, however little of the wait is left. The last sleep ends ON the deadline,
44
+ * and that sample cut to nothing tallied a healthy origin as `unreachable`, every run. So `--timeout`
45
+ * can overrun by this much, once.
46
+ */
47
+ const SAMPLE_FLOOR_MS = 1_000;
48
+
49
+ /** What the queue grade reads. `close` lets the short-lived pool go. */
50
+ export interface QueueReader {
51
+ workers(): Promise<readonly WorkerRecord[]>;
52
+ /**
53
+ * `JobDriver.demand()`: running on a worker that still serves, plus due rows of unpaused queues.
54
+ * Never `ready + running`, which counts a retiring old worker's runs and a paused queue's rows as
55
+ * work an `--idle-ok` pool is missing.
56
+ */
57
+ demand(): Promise<readonly QueueDemand[]>;
58
+ close(): Promise<void>;
59
+ }
60
+
61
+ export interface ExpectedQueues {
62
+ readonly queues: readonly string[];
63
+ /** A module that would not import leaves the set partial, which grades nothing honestly. */
64
+ readonly findings: readonly Finding[];
65
+ }
66
+
67
+ export interface DeployVerifyDeps {
68
+ readonly clock: Clock;
69
+ sleep(ms: number): Promise<void>;
70
+ expectedQueues(root: string): Promise<ExpectedQueues>;
71
+ /**
72
+ * `env` is the one `DATABASE_URL` came from, so the pooler mode is read from it too.
73
+ * `statementTimeoutMs` is what is left of `--timeout`: the server cancels a read past it.
74
+ */
75
+ openQueue(
76
+ databaseUrl: string,
77
+ env: CommandContext['env'],
78
+ statementTimeoutMs: number,
79
+ ): Promise<QueueReader>;
80
+ /**
81
+ * Resolves once `ms` of real time pass: what a hung read is raced against. `signal` disarms it,
82
+ * aborted the moment the read settles, so no timer holds the process open past the verdict.
83
+ */
84
+ expire(ms: number, signal: AbortSignal): Promise<void>;
85
+ }
86
+
87
+ /**
88
+ * Resolves once `ms` of real time pass; `signal`'s abort disarms it. Core's `deadlineSignal`, the
89
+ * one undoable deadline: a timer left armed would hold the process open past the verdict.
90
+ */
91
+ export function expireAfter(ms: number, signal: AbortSignal): Promise<void> {
92
+ return new Promise<void>((resolve) => {
93
+ const deadline = deadlineSignal(ms, signal);
94
+ deadline.signal.addEventListener(
95
+ 'abort',
96
+ () => {
97
+ const fired = deadline.timedOut();
98
+ deadline[Symbol.dispose]();
99
+ if (fired) resolve();
100
+ },
101
+ { once: true },
102
+ );
103
+ });
104
+ }
105
+
106
+ /** Imported when asked: the verdict on the build alone never loads the app or a database driver. */
107
+ const realDeps: DeployVerifyDeps = {
108
+ clock: systemClock,
109
+ sleep: (ms) => Bun.sleep(ms),
110
+ expectedQueues: async (root) => (await import('./deploy-verify-queues')).expectedQueues(root),
111
+ openQueue: async (url, env, statementTimeoutMs) =>
112
+ (await import('./deploy-verify-queues')).openQueue(url, env, statementTimeoutMs),
113
+ expire: expireAfter,
114
+ };
115
+
116
+ interface Answered {
117
+ readonly status: number | null;
118
+ readonly build: string | null;
119
+ readonly cached: boolean;
120
+ readonly failure?: string;
121
+ }
122
+
123
+ /**
124
+ * One `GET /readyz` on a connection of its own, so the load balancer chooses again: a kept-alive
125
+ * socket would ask one pod the same question for the whole window. A redirect is an answer to tally,
126
+ * never to follow: another host's `/readyz` says nothing about this origin.
127
+ */
128
+ async function takeSample(origin: string, limitMs: number): Promise<Answered> {
129
+ try {
130
+ const response = await fetch(`${origin}/readyz`, {
131
+ keepalive: false,
132
+ redirect: 'manual',
133
+ // Past a CDN or a caching proxy: the question is the fleet, never a stored copy of it.
134
+ headers: { 'cache-control': 'no-cache', pragma: 'no-cache' },
135
+ signal: AbortSignal.timeout(limitMs),
136
+ });
137
+ await response.body?.cancel();
138
+ const header = response.headers.get(BUILD_ID_HEADER);
139
+ const age = Number(response.headers.get('age') ?? '0');
140
+ return {
141
+ status: response.status,
142
+ build: header === null || header === '' ? null : header,
143
+ cached: Number.isNaN(age) || age > 0,
144
+ };
145
+ } catch (error) {
146
+ return { status: null, build: null, cached: false, failure: renderThrowable(error) };
147
+ }
148
+ }
149
+
150
+ /** The least time a database read gets, and the floor of its server-side `statement_timeout`. */
151
+ const READ_FLOOR_MS = 1_000;
152
+
153
+ /** The queue half: the set to grade, and how to reach the rows. Absent: no `DATABASE_URL`. */
154
+ interface QueuePlan {
155
+ readonly expected: readonly string[];
156
+ open(statementTimeoutMs: number): Promise<QueueReader>;
157
+ }
158
+
159
+ /** The last grade a read produced, and the last read that failed. No grade: none ever landed. */
160
+ interface QueueRead {
161
+ readonly grade: QueueGrade | undefined;
162
+ readonly lastFailure: string | undefined;
163
+ /**
164
+ * The grade came from a read on THIS pass. A kept grade is detail for the report and never a
165
+ * pass: workers can go between a read that landed and the next that failed.
166
+ */
167
+ readonly current: boolean;
168
+ }
169
+
170
+ interface Verdict {
171
+ readonly wait: WaitOutcome;
172
+ /** `undefined`: no queue plan, or the window never closed so nothing was read. */
173
+ readonly read: QueueRead | undefined;
174
+ }
175
+
176
+ /** One read of both halves, bounded by `limitMs` of real time. A miss is a failed read. */
177
+ async function readQueues(
178
+ reader: QueueReader,
179
+ input: {
180
+ readonly expected: readonly string[];
181
+ readonly build: string;
182
+ readonly idleOk: readonly string[];
183
+ },
184
+ limitMs: number,
185
+ deps: DeployVerifyDeps,
186
+ ): Promise<QueueGrade> {
187
+ const disarm = new AbortController();
188
+ const expired = deps.expire(limitMs, disarm.signal).then(() => {
189
+ throw new UltimateError({
190
+ code: 'X_DB_UNAVAILABLE',
191
+ cause: `a read of x_job_workers outlasted one --interval or what was left of --timeout (${limitMs} ms)`,
192
+ fix: 'x jobs list --json',
193
+ });
194
+ });
195
+ let rows: readonly [readonly WorkerRecord[], readonly QueueDemand[]];
196
+ try {
197
+ rows = await Promise.race([Promise.all([reader.workers(), reader.demand()]), expired]);
198
+ } finally {
199
+ disarm.abort();
200
+ }
201
+ const [workers, demand] = rows;
202
+ return gradeQueues({
203
+ ...input,
204
+ workers,
205
+ depth: new Map(demand.map((queue) => [queue.queue, queue.demand])),
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Sample until the window holds AND, with a queue plan, every queue is served, both on the same
211
+ * pass: web rolling back while the workers still roll is red, never a window that closed earlier.
212
+ * The queues are read only once the window has closed, then on every pass; the web role and the
213
+ * workers roll independently, so one read right after the window would fail a healthy roll. A failed
214
+ * read keeps the last grade. `--timeout` ends the whole wait, reads included.
215
+ */
216
+ async function awaitVerdict(
217
+ options: VerifyOptions,
218
+ build: string,
219
+ deadline: number,
220
+ deps: DeployVerifyDeps,
221
+ plan: QueuePlan | undefined,
222
+ ): Promise<Verdict> {
223
+ // One bad sample per window's worth of samples is the most a settled fleet is allowed.
224
+ const perWindow = Math.max(1, Math.ceil(options.windowMs / options.intervalMs));
225
+ let tally = emptyTally();
226
+ let lastFailure: string | undefined;
227
+ let reader: QueueReader | undefined;
228
+ let read: QueueRead | undefined;
229
+ const left = (): number => deadline - deps.clock.monotonic();
230
+ /** One `/readyz` sample, folded in; whether the window holds after it. */
231
+ const sample = async (): Promise<boolean> => {
232
+ const answered = await takeSample(
233
+ options.origin,
234
+ Math.max(SAMPLE_FLOOR_MS, Math.min(SAMPLE_TIMEOUT_MS, left())),
235
+ );
236
+ lastFailure = answered.failure ?? lastFailure;
237
+ tally = foldSample(
238
+ tally,
239
+ {
240
+ at: deps.clock.monotonic(),
241
+ status: answered.status,
242
+ build: answered.build,
243
+ cached: answered.cached,
244
+ },
245
+ build,
246
+ );
247
+ return windowClosed(tally, options.windowMs, perWindow);
248
+ };
249
+ try {
250
+ for (;;) {
251
+ // Every pass starts with nothing current: a grade from an earlier pass is history.
252
+ if (read !== undefined) read = { ...read, current: false };
253
+ let closed = await sample();
254
+ if (closed && plan !== undefined) {
255
+ // One --interval at most: the window's last sample is never older than that when the
256
+ // queues are judged, and the server stops the statement at the same bound.
257
+ const bound = Math.max(READ_FLOOR_MS, Math.min(options.intervalMs, left()));
258
+ reader ??= await plan.open(bound);
259
+ try {
260
+ const grade = await readQueues(
261
+ reader,
262
+ { expected: plan.expected, build, idleOk: options.idleOk },
263
+ bound,
264
+ deps,
265
+ );
266
+ read = { grade, lastFailure: read?.lastFailure, current: true };
267
+ } catch (error) {
268
+ // One reset connection is a read lost, never the verdict: the last grade stands.
269
+ read = { grade: read?.grade, lastFailure: renderThrowable(error), current: false };
270
+ }
271
+ // Served is judged against web as it answers NOW: a sample taken after the read landed,
272
+ // so a rollback during the read is red, never a green from the sample before it.
273
+ if (read.current && read.grade?.unserved.length === 0) closed = await sample();
274
+ }
275
+ // The one success rule: this pass's sample holds the window and, with a plan, this pass's
276
+ // read landed and serves every queue. Nothing kept from an earlier pass counts.
277
+ const served =
278
+ plan === undefined || (read?.current === true && read.grade?.unserved.length === 0);
279
+ const wait = { tally, closed, lastFailure };
280
+ if ((closed && served) || left() <= 0) return { wait, read };
281
+ await deps.sleep(Math.min(options.intervalMs, left()));
282
+ }
283
+ } finally {
284
+ await reader?.close();
285
+ }
286
+ }
287
+
288
+ /** The set first, so a typo or an app that will not load costs no wait. */
289
+ async function readExpected(
290
+ root: string,
291
+ options: VerifyOptions,
292
+ deps: DeployVerifyDeps,
293
+ ): Promise<ExpectedQueues> {
294
+ const expected = await deps.expectedQueues(root);
295
+ if (expected.findings.length > 0) return expected;
296
+ const stray = options.idleOk.find((queue) => !expected.queues.includes(queue));
297
+ if (stray !== undefined) {
298
+ throw new BadFlagError({
299
+ flag: 'idle-ok',
300
+ command: VERIFY_COMMAND,
301
+ reason: `${renderCauseValue(stray)} is not a queue of this app (${expected.queues.map((queue) => renderCauseValue(queue)).join(', ')}), so it would exempt nothing`,
302
+ fix: 'x jobs list --json # data.depth.queues names what each queue holds',
303
+ });
304
+ }
305
+ return expected;
306
+ }
307
+
308
+ export async function runDeployVerify(
309
+ ctx: CommandContext,
310
+ deps: DeployVerifyDeps = realDeps,
311
+ ): Promise<CommandResult> {
312
+ const root = requireAppRoot('deploy', ctx.cwd).dir;
313
+ const options = readOptions(ctx);
314
+ const build = await readBuild(ctx, root);
315
+ // Read off the env directly: `resolveServices` falls back to the embedded database and would grade
316
+ // an empty one, so a CI job with no credentials would "pass" having read nothing.
317
+ const raw = ctx.env['DATABASE_URL'];
318
+ const databaseUrl = raw === undefined || raw.trim() === '' ? undefined : raw;
319
+ const expected = databaseUrl === undefined ? undefined : await readExpected(root, options, deps);
320
+ if (expected !== undefined && expected.findings.length > 0) {
321
+ return failed(
322
+ 'deploy',
323
+ msg('cli.deploy.unverified', { build, count: expected.findings.length }),
324
+ {
325
+ findings: expected.findings,
326
+ },
327
+ );
328
+ }
329
+
330
+ const deadline = deps.clock.monotonic() + options.timeoutMs;
331
+ const plan: QueuePlan | undefined =
332
+ databaseUrl === undefined || expected === undefined
333
+ ? undefined
334
+ : {
335
+ expected: expected.queues,
336
+ open: (statementTimeoutMs) => deps.openQueue(databaseUrl, ctx.env, statementTimeoutMs),
337
+ };
338
+ const { wait, read } = await awaitVerdict(options, build, deadline, deps, plan);
339
+ if (!wait.closed) {
340
+ return failed('deploy', msg('cli.deploy.unverified', { build, count: 1 }), {
341
+ findings: [sampleFinding(failureOf(wait.tally, build), wait, build, options)],
342
+ data: resultData(build, options.windowMs, wait.tally, read?.grade ?? UNGRADED),
343
+ });
344
+ }
345
+
346
+ const samples = wait.tally.total;
347
+ const held = { build, windowMs: options.windowMs, samples };
348
+ if (plan === undefined) {
349
+ return ok('deploy', msg('cli.deploy.verifiedUngraded', held), {
350
+ data: resultData(build, options.windowMs, wait.tally, UNGRADED),
351
+ });
352
+ }
353
+ const queues = read?.grade;
354
+ if (queues === undefined) {
355
+ return failed('deploy', msg('cli.deploy.unverified', { build, count: 1 }), {
356
+ findings: [queueReadFinding(read?.lastFailure)],
357
+ data: resultData(build, options.windowMs, wait.tally, UNGRADED),
358
+ });
359
+ }
360
+ const data = resultData(build, options.windowMs, wait.tally, queues);
361
+ // A kept grade that says served is not a pass: the read that would confirm it never landed.
362
+ if (queues.unserved.length === 0 && read?.current !== true) {
363
+ return failed('deploy', msg('cli.deploy.unverified', { build, count: 1 }), {
364
+ findings: [queueReadFinding(read?.lastFailure)],
365
+ data,
366
+ });
367
+ }
368
+ if (queues.unserved.length > 0) {
369
+ return failed(
370
+ 'deploy',
371
+ msg('cli.deploy.unverified', { build, count: queues.unserved.length }),
372
+ {
373
+ findings: queues.unserved.map((unserved) => unservedFinding(unserved, options.origin)),
374
+ data,
375
+ },
376
+ );
377
+ }
378
+ const verified = { ...held, served: queues.served.length, idle: queues.idle.length };
379
+ return ok('deploy', msg('cli.deploy.verified', verified), { data });
380
+ }
package/src/cmd-deploy.ts CHANGED
@@ -4,8 +4,14 @@
4
4
 
5
5
  import { join } from 'node:path';
6
6
  import { ERROR_DOCS_URL } from '@ultimat3/core';
7
+ import { loadEnvSchema } from './app-env';
7
8
  import { requireAppRoot } from './app-root';
8
- import { type ComposeStrategy, composeStrategies, readComposeFile } from './cmd-deploy-compose';
9
+ import {
10
+ type ComposeStrategy,
11
+ composeStrategies,
12
+ DEFAULT_IMAGE,
13
+ readComposeFile,
14
+ } from './cmd-deploy-compose';
9
15
  import { readHelmDrainOverrides } from './cmd-deploy-drain';
10
16
  import {
11
17
  type HelmTarget,
@@ -15,6 +21,7 @@ import {
15
21
  readReleaseName,
16
22
  readRollout,
17
23
  } from './cmd-deploy-helm';
24
+ import { composeEnvPreflight, lastLine, roleLastLogLine } from './cmd-deploy-preflight';
18
25
  import { deploySpec } from './cmd-deploy-spec';
19
26
  import { runStartFirst, startFirstUp } from './cmd-deploy-start-first';
20
27
  import type { CliCommand, CommandContext } from './command';
@@ -344,8 +351,11 @@ const stepLine = (env: Readonly<Record<string, string>>, command: readonly strin
344
351
  export const deployCommand: CliCommand = {
345
352
  spec: deploySpec,
346
353
  async run(ctx: CommandContext): Promise<CommandResult> {
354
+ if (ctx.args.subcommand === 'verify') {
355
+ return (await import('./cmd-deploy-verify')).runDeployVerify(ctx);
356
+ }
347
357
  const root = requireAppRoot('deploy', ctx.cwd).dir;
348
- const image = flagString(ctx.args, 'image') ?? 'ultimate-app:dev';
358
+ const image = flagString(ctx.args, 'image') ?? DEFAULT_IMAGE;
349
359
  // No "is there a chart?" branch. It threw X_NOT_IMPLEMENTED — "this build does not implement
350
360
  // helm" — over a build that implements it completely (`planDeploy` above); what was missing was
351
361
  // a FILE, and its fix said to copy it from the framework repository, which `packages/cli`'s
@@ -356,15 +366,12 @@ export const deployCommand: CliCommand = {
356
366
  const request = await readDeployRequest(ctx, method, root);
357
367
  // Read before the plan, because the plan IS the rollout: which compose roles can start their
358
368
  // new container first is a fact of the app's compose file, and a dry run reports it too.
369
+ const composeText =
370
+ request.method === 'compose' ? await readComposeFile(composeFileOf(root)) : undefined;
359
371
  const plan =
360
372
  request.method === 'helm'
361
373
  ? planDeploy(image, 'helm', root, request)
362
- : planDeploy(
363
- image,
364
- 'compose',
365
- root,
366
- composeStrategies(await readComposeFile(composeFileOf(root))),
367
- );
374
+ : planDeploy(image, 'compose', root, composeStrategies(composeText));
368
375
  // What the helm flags resolved to — the release above all, which is read off `app.config.ts`
369
376
  // and so is not something the operator typed. Absent on compose, which has no release.
370
377
  const target: { readonly [key: string]: JsonValue } =
@@ -405,11 +412,28 @@ export const deployCommand: CliCommand = {
405
412
  ),
406
413
  };
407
414
  }
415
+ // Before the first step: a compose step without its env file fails as `exited 1` and says no
416
+ // more, so the variables it lacks are named here, and nothing runs.
417
+ const preflight =
418
+ request.method === 'compose'
419
+ ? await composeEnvPreflight(root, composeText, ctx.env, await loadEnvSchema(root))
420
+ : undefined;
421
+ if (preflight !== undefined) {
422
+ return {
423
+ ok: false,
424
+ command: 'deploy',
425
+ summary: msg('cli.deploy.plan', { images: 1, roles }),
426
+ findings: [preflight],
427
+ data: planJson,
428
+ };
429
+ }
408
430
  // helm prints its release record (`--output json`), so the report carries helm's own verdict
409
431
  // and revision rather than an exit code standing in for one. Compose has no such record.
410
432
  let rollout: { readonly [key: string]: JsonValue } = {};
411
433
  for (const step of plan.steps) {
412
434
  const options = { cwd: root, env: plan.env };
435
+ // A one-shot's container output is its step's own; a serving role's is read off its log.
436
+ let output = '';
413
437
  // A start-first step is a sequence — list, scale up, wait, stop the old — and reports the
414
438
  // command of the sub-step that failed, so the fix line below is still the one to rerun.
415
439
  const outcome =
@@ -428,11 +452,20 @@ export const deployCommand: CliCommand = {
428
452
  : await ctx.runner(step.command, options).then((result) => {
429
453
  if (request.method === 'helm')
430
454
  rollout = { rollout: { ...readRollout(request, result.stdout) } };
455
+ if (!result.ok) output = lastLine(result);
431
456
  return result.ok
432
457
  ? { ok: true as const }
433
458
  : { ok: false as const, code: result.code, command: step.command };
434
459
  });
435
460
  if (!outcome.ok) {
461
+ if (output === '' && request.method === 'compose' && !ONE_SHOT_ROLES.includes(step.role)) {
462
+ output = await roleLastLogLine({
463
+ base: composeBase(root),
464
+ role: step.role,
465
+ runner: ctx.runner,
466
+ options,
467
+ });
468
+ }
436
469
  return {
437
470
  ok: false,
438
471
  command: 'deploy',
@@ -440,7 +473,7 @@ export const deployCommand: CliCommand = {
440
473
  findings: [
441
474
  {
442
475
  code: 'X_DEPLOY_FAILED',
443
- cause: `role "${step.role}" step exited ${outcome.code}${cleanupNote(outcome)}`,
476
+ cause: `role "${step.role}" step exited ${outcome.code}${output === '' ? '' : `: ${output}`}${cleanupNote(outcome)}`,
444
477
  fix: `${stepLine(plan.env, outcome.command)} # run it directly to see the full output`,
445
478
  docs: ERROR_DOCS_URL,
446
479
  },
package/src/cmd-docs.ts CHANGED
@@ -6,9 +6,11 @@
6
6
 
7
7
  import { ERROR_DOCS_URL } from '@ultimat3/core';
8
8
  import type { DocEntry, DocHit } from '@ultimat3/manifest';
9
- import { nearestTopics, scanInstalledDocs, searchDocs } from '@ultimat3/manifest';
9
+ import { nearestTopics, searchDocs } from '@ultimat3/manifest';
10
10
  import { DEFAULT_LIMIT, docsSpec } from './cmd-docs-spec';
11
11
  import type { CliCommand, CommandContext } from './command';
12
+ import { parseWikiTopic, sectionResult } from './docs-section';
13
+ import { loadDocs, wikiSource } from './docs-sources';
12
14
  import { MissingPositionalError } from './errors';
13
15
  import { frameworkScopeDir } from './framework-scope';
14
16
  import { parseLimitFlag } from './jobs-report';
@@ -38,6 +40,19 @@ const asJson = (hit: DocHit): JsonValue => ({
38
40
  score: hit.score,
39
41
  });
40
42
 
43
+ /**
44
+ * The tarball ships `wiki/` (its `prepack`), so an install without it is broken in the same way as
45
+ * one whose CLI cannot see `@ultimat3/core`: the answer is still given from the packages, and the
46
+ * finding says what is missing rather than leaving the wiki's absence to look like no answer.
47
+ */
48
+ const noWikiFinding = (): Finding => ({
49
+ code: 'X_CLI_UNEXPECTED',
50
+ cause: '@ultimat3/cli ships no wiki/ directory, so no wiki page could be searched',
51
+ fix: 'bun install && x doctor --json',
52
+ docs: ERROR_DOCS_URL,
53
+ at: import.meta.dir,
54
+ });
55
+
41
56
  /** The exact path an agent opens next — package-relative is ambiguous across 29 packages. */
42
57
  const locate = (entry: DocEntry): string => `${entry.package}/${entry.source}`;
43
58
 
@@ -96,7 +111,11 @@ function codeResult(code: string, query: string): CommandResult {
96
111
  * related at all, the honest fallback is the list of packages actually installed — the universe
97
112
  * the question could have been about — rather than five topics picked for sharing letters.
98
113
  */
99
- function missResult(query: string, entries: readonly DocEntry[]): CommandResult {
114
+ function missResult(
115
+ query: string,
116
+ entries: readonly DocEntry[],
117
+ findings: readonly Finding[],
118
+ ): CommandResult {
100
119
  const suggestions = nearestTopics(entries, query);
101
120
  const packages = [...new Set(entries.map((entry) => entry.package))].sort();
102
121
  const next =
@@ -108,6 +127,7 @@ function missResult(query: string, entries: readonly DocEntry[]): CommandResult
108
127
  command: 'docs',
109
128
  summary: msg('cli.docs.none', { query }),
110
129
  lines: [...next, ...alsoTry()],
130
+ ...(findings.length > 0 ? { findings } : {}),
111
131
  data: { matches: [], suggestions: [...suggestions], packages, query },
112
132
  };
113
133
  }
@@ -131,6 +151,14 @@ export const docsCommand: CliCommand = {
131
151
  const code = CODE_QUERY.exec(query)?.[0];
132
152
  if (code !== undefined) return codeResult(code, query);
133
153
 
154
+ // A wiki topic is answered with its whole section, before any package is scanned.
155
+ const topic = parseWikiTopic(query);
156
+ if (topic !== undefined) {
157
+ const wiki = await wikiSource();
158
+ const section = wiki === undefined ? undefined : await sectionResult(wiki, topic, query);
159
+ if (section !== undefined) return section;
160
+ }
161
+
134
162
  const scope = frameworkScopeDir();
135
163
  if (scope === undefined) {
136
164
  const finding = unresolvedFinding();
@@ -143,20 +171,22 @@ export const docsCommand: CliCommand = {
143
171
  };
144
172
  }
145
173
 
146
- const entries = await scanInstalledDocs(scope);
174
+ const { entries, wiki } = await loadDocs(scope);
175
+ const findings = wiki === undefined ? [noWikiFinding()] : [];
147
176
  // `x jobs list --limit`'s reader, and its `command` parameter exists for exactly this second
148
177
  // caller. A local `Number.parseInt` accepted `--limit 1e9` as 1 and answered with one match,
149
178
  // and fell silently through to the default for `abc`, `0` and `-3` — a bound other than the one
150
179
  // typed, from the same binary that refuses all four one command over.
151
180
  const limit = parseLimitFlag(flagString(ctx.args, 'limit'), 'docs') ?? DEFAULT_LIMIT;
152
181
  const hits = searchDocs(entries, query, limit);
153
- if (hits.length === 0) return missResult(query, entries);
182
+ if (hits.length === 0) return missResult(query, entries, findings);
154
183
 
155
184
  return {
156
185
  ok: true,
157
186
  command: 'docs',
158
187
  summary: msg('cli.docs.found', { count: hits.length, query }),
159
188
  lines: [...humanLines(hits)],
189
+ ...(findings.length > 0 ? { findings } : {}),
160
190
  data: { matches: hits.map(asJson), suggestions: [], query },
161
191
  };
162
192
  },