@ultimat3/cli 29.3.2 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +13 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +28 -8
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +42 -138
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +17 -27
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
package/src/cmd-ci.ts CHANGED
@@ -4,12 +4,21 @@
4
4
  // command fetches the runs, opens only the failed steps' log, and hands back the findings the gate
5
5
  // already wrote — in the same three-line shape every other Ultimate error is printed in.
6
6
 
7
- import { UltimateError } from '@ultimat3/core';
7
+ import { renderThrowable, UltimateError } from '@ultimat3/core';
8
+ import { parseDuration } from '@ultimat3/time';
8
9
  import type { CiLogLine } from './ci-log';
9
10
  import { findingKey, findingsFrom, jobsInLog, parseLogLines, tailOf } from './ci-log';
10
11
  import type { CiJob, CiRun } from './ci-runs';
11
12
  import { failedLog, isFailed, isRunning, latestPerWorkflow, listRuns, viewRun } from './ci-runs';
12
- import { CI_EVENTS, ciSpec, TAIL_LINES } from './cmd-ci-spec';
13
+ import type { CiTestedAnswer, TestedCondition } from './ci-tested';
14
+ import {
15
+ ciTested,
16
+ ghTestedReads,
17
+ TESTED_CALL_TIMEOUT_MS,
18
+ TESTED_CONDITIONS,
19
+ withDeadline,
20
+ } from './ci-tested';
21
+ import { CI_EVENTS, ciSpec, TAIL_LINES, TESTED_MAX_AGE, TESTED_WORKFLOW } from './cmd-ci-spec';
13
22
  import type { CliCommand, CommandContext } from './command';
14
23
  import { BadFlagError } from './errors';
15
24
  import { parseIntFlag } from './flag-number';
@@ -40,6 +49,11 @@ export const CI_MESSAGE_KEYS = [
40
49
  'cli.ci.logs.empty',
41
50
  ] as const;
42
51
 
52
+ /** One reason per `x ci tested` answer, keyed by the condition that decided it. */
53
+ export const CI_TESTED_MESSAGE_KEYS = TESTED_CONDITIONS.map(
54
+ (condition) => `cli.ci.tested.${condition}` as const,
55
+ );
56
+
43
57
  /** How far back a branch's run history is read before "the latest run of each workflow". */
44
58
  export const RUN_LOOKBACK = 20;
45
59
 
@@ -88,6 +102,7 @@ function readEvent(raw: string | undefined): (typeof CI_EVENTS)[number] | undefi
88
102
  export const ciCommand: CliCommand = {
89
103
  spec: ciSpec,
90
104
  async run(ctx: CommandContext): Promise<CommandResult> {
105
+ if (ctx.args.subcommand === 'tested') return tested(ctx);
91
106
  // Filters are validated, and crossed with --run, before any subprocess runs: a refusal must
92
107
  // not cost a gh round trip, and a filter beside --run would otherwise be silently ignored.
93
108
  const rawRun = flagString(ctx.args, 'run');
@@ -130,6 +145,88 @@ export const ciCommand: CliCommand = {
130
145
  },
131
146
  };
132
147
 
148
+ /**
149
+ * `x ci tested`: may this push skip the jobs a green PR run already ran on this exact tree? Exit 0
150
+ * on every answer — the workflow reads `data.tested`, and a doubt anywhere (no `gh`, no remote, an
151
+ * API that did not answer) is `tested: false`, a full run. Only a malformed flag refuses.
152
+ */
153
+ async function tested(ctx: CommandContext): Promise<CommandResult> {
154
+ const rawAge = flagString(ctx.args, 'max-age') ?? TESTED_MAX_AGE;
155
+ const maxAgeMs = readMaxAge(rawAge);
156
+ const workflow = flagString(ctx.args, 'workflow') ?? TESTED_WORKFLOW;
157
+ const flag = flagString(ctx.args, 'repo');
158
+ let repo = flag ?? '';
159
+ let sha = ctx.env['GITHUB_SHA'] ?? '';
160
+ let answer: CiTestedAnswer;
161
+ // Every subprocess this answer waits on is killed past its deadline, and that is a doubt too.
162
+ const host = { ...ctx, ...withDeadline(ctx, TESTED_CALL_TIMEOUT_MS) };
163
+ try {
164
+ const resolved = await resolveRepo(host, 'ci tested', flag);
165
+ repo = resolved.slug;
166
+ if (sha === '') sha = await headSha(host);
167
+ answer =
168
+ sha === ''
169
+ ? { tested: false, condition: 'sha' }
170
+ : await ciTested({
171
+ sha,
172
+ repo,
173
+ maxAgeMs,
174
+ now: Date.now(),
175
+ reads: ghTestedReads(host, resolved, workflow),
176
+ });
177
+ } catch (error) {
178
+ if (error instanceof BadFlagError) throw error;
179
+ answer = { tested: false, condition: 'api', detail: renderThrowable(error) };
180
+ }
181
+ const reason = testedReason(answer.condition, {
182
+ pr: answer.pr ?? '?',
183
+ run: answer.run ?? '?',
184
+ repo,
185
+ workflow,
186
+ maxAge: rawAge,
187
+ detail: answer.detail ?? '',
188
+ });
189
+ return {
190
+ ok: true,
191
+ command: 'ci',
192
+ summary: reason,
193
+ data: {
194
+ sha,
195
+ repo,
196
+ workflow,
197
+ tested: answer.tested,
198
+ condition: answer.condition,
199
+ reason,
200
+ ...(answer.pr === undefined ? {} : { pr: answer.pr }),
201
+ ...(answer.run === undefined ? {} : { run: answer.run }),
202
+ },
203
+ };
204
+ }
205
+
206
+ const testedReason = (
207
+ condition: TestedCondition,
208
+ params: Readonly<Record<string, string | number>>,
209
+ ): string => msg(`cli.ci.tested.${condition}`, params);
210
+
211
+ function readMaxAge(raw: string): number {
212
+ try {
213
+ return parseDuration(raw);
214
+ } catch (error) {
215
+ throw new BadFlagError({
216
+ flag: 'max-age',
217
+ command: 'ci tested',
218
+ reason: `${quoteArg(raw)} is not a duration: ${renderThrowable(error)}`,
219
+ fix: 'x ci tested --max-age 24h --json',
220
+ });
221
+ }
222
+ }
223
+
224
+ /** The commit a local run asks about; none is a doubt, which the caller answers as one. */
225
+ async function headSha(ctx: CommandContext): Promise<string> {
226
+ const result = await ctx.runner(['git', 'rev-parse', 'HEAD'], { cwd: ctx.cwd });
227
+ return result.ok ? result.stdout.trim() : '';
228
+ }
229
+
133
230
  interface JobFailure {
134
231
  readonly job: string;
135
232
  readonly conclusion: string;
@@ -0,0 +1,134 @@
1
+ // `x client customer` — the app's own published document (`openapi.json`, or a bearer mount's
2
+ // declared file) compiled into a client a customer's TypeScript imports with no framework on its
3
+ // path. Read from the COMMITTED file: what a customer is handed, and what `drift` hashes back.
4
+
5
+ // why: Bun ships no path-join primitive.
6
+ import { isAbsolute, join } from 'node:path';
7
+ import { apiDeclaration } from '@ultimat3/action';
8
+ import { loadApp } from './app-load';
9
+ import { OPENAPI_FILE } from './app-openapi';
10
+ import { requireAppRoot } from './app-root';
11
+ import { emitCustomer } from './client-emit-customer';
12
+ import { stamped } from './client-emit-header';
13
+ import { parseSource, sourceHash } from './cmd-client-source';
14
+ import { compileOrRefuse, writeFiles } from './cmd-client-write';
15
+ import type { CommandContext } from './command';
16
+ import { BadFlagError } from './errors';
17
+ import { msg } from './messages';
18
+ import type { CommandResult } from './output';
19
+ import { flagList, flagString } from './parse';
20
+ import { posixRelative } from './posix-path';
21
+ import { quoteArg } from './shell-quote';
22
+
23
+ const COMMAND = 'client customer';
24
+
25
+ /** The document's file: `openapi.json`, or the one a `--mount` declares as its own. */
26
+ function documentFile(mount: string | undefined): string {
27
+ if (mount === undefined) return OPENAPI_FILE;
28
+ const mounts = apiDeclaration().http?.mounts ?? [];
29
+ const found = mounts.find((candidate) => candidate.prefix === mount);
30
+ if (found === undefined) {
31
+ throw new BadFlagError({
32
+ flag: 'mount',
33
+ command: COMMAND,
34
+ reason: `no bearer mount has the prefix ${quoteArg(mount)} (declared: ${mounts.map((m) => m.prefix).join(', ') || 'none'})`,
35
+ fix: 'x routes --json # every mounted path; pass one of the declared prefixes',
36
+ });
37
+ }
38
+ if (found.openapi === undefined) {
39
+ throw new BadFlagError({
40
+ flag: 'mount',
41
+ command: COMMAND,
42
+ reason: `the mount ${quoteArg(mount)} declares no openapi file, so it publishes no document a client can be checked against`,
43
+ fix: `x manifest --json # after adding openapi: 'openapi${mount.replace(/\//g, '.')}.json' to that mount in apps/web/api/index.ts`,
44
+ });
45
+ }
46
+ return found.openapi;
47
+ }
48
+
49
+ /** `{ name, version, exports }` — nothing a registry publish would need us to decide. */
50
+ const packageJson = (name: string, version: string): string =>
51
+ `${JSON.stringify(
52
+ {
53
+ name,
54
+ version,
55
+ type: 'module',
56
+ exports: { '.': './index.ts', './types': './types.ts' },
57
+ files: ['index.ts', 'types.ts'],
58
+ },
59
+ null,
60
+ 2,
61
+ )}\n`;
62
+
63
+ export async function runCustomer(ctx: CommandContext): Promise<CommandResult> {
64
+ const root = requireAppRoot(COMMAND, ctx.cwd).dir;
65
+ const outFlag = flagString(ctx.args, 'out');
66
+ if (outFlag === undefined) {
67
+ throw new BadFlagError({
68
+ flag: 'out',
69
+ command: COMMAND,
70
+ reason: 'names no directory to write the client into',
71
+ fix: 'x client customer --out apps/web/clients/customer --json',
72
+ });
73
+ }
74
+ const out = posixRelative(root, isAbsolute(outFlag) ? outFlag : join(ctx.cwd, outFlag)) || '.';
75
+ const mount = flagString(ctx.args, 'mount');
76
+ const only = flagList(ctx.args, 'only');
77
+ const packageName = flagString(ctx.args, 'package');
78
+ // A mount's file is the app's declaration, so the app is loaded to read it; the plain document
79
+ // is `openapi.json` by definition and needs no load.
80
+ if (mount !== undefined) await loadApp(root);
81
+ const file = documentFile(mount);
82
+ const committed = Bun.file(join(root, file));
83
+ if (!(await committed.exists())) {
84
+ throw new BadFlagError({
85
+ flag: mount === undefined ? 'out' : 'mount',
86
+ command: COMMAND,
87
+ reason: `${file} has not been written, so there is no published document to compile`,
88
+ fix: 'x manifest --json',
89
+ });
90
+ }
91
+ const { document } = parseSource(await committed.text(), file);
92
+ const compiled = compileOrRefuse(document, file, only.length === 0 ? undefined : only, COMMAND);
93
+ const emitted = emitCustomer(compiled);
94
+ const flags = [
95
+ `--out ${quoteArg(out)}`,
96
+ ...(mount === undefined ? [] : [`--mount ${quoteArg(mount)}`]),
97
+ ...(only.length === 0 ? [] : [`--only ${only.join(',')}`]),
98
+ ...(packageName === undefined ? [] : [`--package ${quoteArg(packageName)}`]),
99
+ ];
100
+ const stamp = {
101
+ command: `x client customer ${flags.join(' ')}`,
102
+ source: file,
103
+ sourceHash: sourceHash(document),
104
+ };
105
+ const files = new Map([
106
+ [`${out}/types.ts`, stamped(stamp, emitted['types.ts'])],
107
+ [`${out}/index.ts`, stamped(stamp, emitted['index.ts'])],
108
+ ]);
109
+ if (packageName !== undefined) {
110
+ files.set(`${out}/package.json`, packageJson(packageName, compiled.version));
111
+ }
112
+ const written = await writeFiles(root, files);
113
+ return {
114
+ ok: true,
115
+ command: 'client',
116
+ summary: msg('cli.client.customer.written', {
117
+ operations: compiled.operations.length,
118
+ types: compiled.types.size,
119
+ source: file,
120
+ dir: out,
121
+ }),
122
+ lines: written.map((path) => msg('cli.client.file', { path })),
123
+ data: {
124
+ kind: 'customer',
125
+ dir: out,
126
+ source: file,
127
+ mount: mount ?? null,
128
+ only: only.length === 0 ? null : [...only],
129
+ operations: compiled.operations.map((op) => op.name),
130
+ types: compiled.types.size,
131
+ written: [...written],
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,81 @@
1
+ // Where `x client vendor` reads its document from: `--from` (a file, or an https URL read through
2
+ // `outbound({ dial: 'public' })`), else the client's committed `source.json` — so a regeneration
3
+ // with no flags is reproducible and offline. One parse, one canonical hash.
4
+
5
+ // why: Bun ships no path-join primitive.
6
+ import { isAbsolute, join } from 'node:path';
7
+ import { renderFixShellArg } from '@ultimat3/core';
8
+ import { outbound } from '@ultimat3/core/outbound';
9
+ import { sha256 } from './client-emit-header';
10
+ import { BadFlagError } from './errors';
11
+ import { ClientSpecInvalidError } from './openapi-refuse';
12
+
13
+ /** The longest a vendor's document may take to arrive. */
14
+ export const SOURCE_DEADLINE_MS = 30_000;
15
+
16
+ export interface ClientSource {
17
+ readonly document: unknown;
18
+ /** The document as committed: two-space JSON, so a regeneration rewrites the same bytes. */
19
+ readonly text: string;
20
+ }
21
+
22
+ /**
23
+ * The hash a stamp records: of the PARSED document, so reformatting `source.json` (an editor, the
24
+ * app's Biome) is not a change of contract — only a change of what it says is.
25
+ */
26
+ export const sourceHash = (document: unknown): string => sha256(JSON.stringify(document));
27
+
28
+ export function parseSource(raw: string, label: string): ClientSource {
29
+ let document: unknown;
30
+ try {
31
+ document = JSON.parse(raw);
32
+ } catch {
33
+ // The parser's own message names a character offset in a file the reader did not write.
34
+ throw new ClientSpecInvalidError({ source: label, reason: 'it is not JSON' });
35
+ }
36
+ return { document, text: `${JSON.stringify(document, null, 2)}\n` };
37
+ }
38
+
39
+ /** `--from` as given, or the committed copy at `committed` (app-root-relative). */
40
+ export async function readClientSource(input: {
41
+ readonly from: string | undefined;
42
+ readonly cwd: string;
43
+ readonly root: string;
44
+ readonly committed: string;
45
+ readonly name: string;
46
+ }): Promise<ClientSource> {
47
+ const { from } = input;
48
+ if (from === undefined) {
49
+ const file = Bun.file(join(input.root, input.committed));
50
+ if (!(await file.exists())) {
51
+ throw new BadFlagError({
52
+ flag: 'from',
53
+ command: 'client vendor',
54
+ reason: `${input.committed} does not exist yet, so there is no document to regenerate from`,
55
+ fix: `x client vendor ${renderFixShellArg(input.name, 'billing')} --from ./openapi.json --json # the vendor's document, a file or an https URL`,
56
+ });
57
+ }
58
+ return parseSource(await file.text(), input.committed);
59
+ }
60
+ if (/^https?:\/\//.test(from)) {
61
+ const answer = await outbound({
62
+ url: from,
63
+ vendor: input.name,
64
+ dial: 'public',
65
+ deadlineMs: SOURCE_DEADLINE_MS,
66
+ headers: { accept: 'application/json' },
67
+ });
68
+ return parseSource(answer.text(), from);
69
+ }
70
+ const path = isAbsolute(from) ? from : join(input.cwd, from);
71
+ const file = Bun.file(path);
72
+ if (!(await file.exists())) {
73
+ throw new BadFlagError({
74
+ flag: 'from',
75
+ command: 'client vendor',
76
+ reason: `${from} does not exist`,
77
+ fix: `x client vendor ${renderFixShellArg(input.name, 'billing')} --from ./openapi.json --json # a path from the current directory, or an https URL`,
78
+ });
79
+ }
80
+ return parseSource(await file.text(), from);
81
+ }
@@ -0,0 +1,48 @@
1
+ // `x client`'s declaration, apart from its body: the parser, `x help` and the `errors` step read it
2
+ // without loading `cmd-client.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const CLIENT_SUBCOMMANDS = ['vendor', 'customer'] as const;
7
+
8
+ export const clientSpec: CommandSpec = {
9
+ name: 'client',
10
+ summary:
11
+ 'a typed client compiled from an OpenAPI document: a vendor you call, or the one your customers call you with',
12
+ usage:
13
+ 'x client vendor <name> [--from <file|url>] [--only <tag,…>] [--json] · x client customer --out <dir> [--mount <prefix>] [--only <tag,…>] [--package <name>] [--json]',
14
+ requiresApp: true,
15
+ subcommands: [...CLIENT_SUBCOMMANDS],
16
+ // Both write files, into two different places: neither is a default a bare `x client` may pick.
17
+ flags: [
18
+ {
19
+ name: 'from',
20
+ type: 'string',
21
+ summary: "the document, a file or an https URL; omitted, the client's committed source.json",
22
+ subcommands: ['vendor'],
23
+ },
24
+ {
25
+ name: 'only',
26
+ type: 'string',
27
+ summary: 'keep only operations carrying one of these comma-separated tags',
28
+ },
29
+ {
30
+ name: 'out',
31
+ type: 'string',
32
+ summary: 'the directory the client is written to',
33
+ subcommands: ['customer'],
34
+ },
35
+ {
36
+ name: 'mount',
37
+ type: 'string',
38
+ summary: "a bearer mount's prefix — its own document, not the app's",
39
+ subcommands: ['customer'],
40
+ },
41
+ {
42
+ name: 'package',
43
+ type: 'string',
44
+ summary: 'also write a package.json with this name, ready to publish',
45
+ subcommands: ['customer'],
46
+ },
47
+ ],
48
+ };
@@ -0,0 +1,41 @@
1
+ // The two halves both `x client` subcommands share: the compile, refused when `--only` keeps
2
+ // nothing, and the write of every file a generation produces.
3
+
4
+ // why: Bun ships no path-join primitive.
5
+ import { join } from 'node:path';
6
+ import { BadFlagError } from './errors';
7
+ import { compileOpenApi } from './openapi-compile';
8
+ import type { CompiledDocument } from './openapi-compile-ir';
9
+
10
+ /** Where every vendor client lives, app-root-relative. */
11
+ export const CLIENTS_DIR = 'apps/web/clients';
12
+
13
+ /** The compile, refused when `--only` kept nothing: an empty client is a typo, not a client. */
14
+ export function compileOrRefuse(
15
+ document: unknown,
16
+ source: string,
17
+ only: readonly string[] | undefined,
18
+ command: string,
19
+ ): CompiledDocument {
20
+ const compiled = compileOpenApi(document, { source, only });
21
+ if (only !== undefined && compiled.operations.length === 0) {
22
+ throw new BadFlagError({
23
+ flag: 'only',
24
+ command,
25
+ reason: `no operation in ${source} carries a tag in ${only.join(',')}`,
26
+ fix: 'x help client --json # then rerun without --only, and pick tags from the operations it lists',
27
+ });
28
+ }
29
+ return compiled;
30
+ }
31
+
32
+ /** Writes every file (`Bun.write` makes the directories); answers the app-root-relative paths written. */
33
+ export async function writeFiles(
34
+ root: string,
35
+ files: ReadonlyMap<string, string>,
36
+ ): Promise<readonly string[]> {
37
+ for (const [path, text] of files) {
38
+ await Bun.write(join(root, path), text);
39
+ }
40
+ return [...files.keys()];
41
+ }
@@ -0,0 +1,111 @@
1
+ // `x client` — one OpenAPI compiler, two emitters. `vendor <name>` compiles a third party's
2
+ // document into `apps/web/clients/<name>/` (outbound() and `t` schemas); `customer --out <dir>`
3
+ // compiles the app's own published document into a client with no framework import at all.
4
+ //
5
+ // CLI wiring only: the compiler is `openapi-compile.ts`, the bytes are `client-emit-*.ts`.
6
+
7
+ // why: Bun ships no path-join primitive.
8
+ import { isAbsolute, join } from 'node:path';
9
+ import { requireAppRoot } from './app-root';
10
+ import { readStamp, stamped } from './client-emit-header';
11
+ import { emitVendor } from './client-emit-vendor';
12
+ import { runCustomer } from './cmd-client-customer';
13
+ import { readClientSource, sourceHash } from './cmd-client-source';
14
+ import { clientSpec } from './cmd-client-spec';
15
+ import { CLIENTS_DIR, compileOrRefuse, writeFiles } from './cmd-client-write';
16
+ import type { CliCommand, CommandContext } from './command';
17
+ import { BadFlagError, MissingPositionalError } from './errors';
18
+ import { msg } from './messages';
19
+ import type { CommandResult } from './output';
20
+ import { flagList, flagString } from './parse';
21
+ import { posixRelative } from './posix-path';
22
+ import { quoteArg } from './shell-quote';
23
+
24
+ const VENDOR_NAME = /^[a-z][a-z0-9-]{0,63}$/;
25
+
26
+ /** `--only`, or — on a regeneration that passes none — the tags the client was generated with. */
27
+ async function onlyFor(
28
+ ctx: CommandContext,
29
+ existing: string,
30
+ ): Promise<readonly string[] | undefined> {
31
+ const given = flagList(ctx.args, 'only');
32
+ if (given.length > 0) return given;
33
+ const file = Bun.file(existing);
34
+ if (!(await file.exists())) return undefined;
35
+ const stamp = readStamp(await file.text())?.stamp;
36
+ const kept = stamp === undefined ? undefined : /(?:^| )--only (\S+)/.exec(stamp.command)?.[1];
37
+ return kept === undefined ? undefined : kept.split(',').filter((tag) => tag.length > 0);
38
+ }
39
+
40
+ async function runVendor(ctx: CommandContext): Promise<CommandResult> {
41
+ const root = requireAppRoot('client vendor', ctx.cwd).dir;
42
+ const name = ctx.args.positionals[0];
43
+ if (name === undefined) {
44
+ throw new MissingPositionalError({
45
+ command: 'client vendor',
46
+ positional: 'name',
47
+ example: 'x client vendor billing --from ./openapi.json --json',
48
+ });
49
+ }
50
+ if (!VENDOR_NAME.test(name)) {
51
+ throw new BadFlagError({
52
+ flag: 'name',
53
+ command: 'client vendor',
54
+ reason: `${quoteArg(name)} is not a lower-case slug (a-z, 0-9, -), so it cannot name a directory and a vendor label`,
55
+ fix: 'x client vendor billing --from ./openapi.json --json',
56
+ });
57
+ }
58
+ const dir = `${CLIENTS_DIR}/${name}`;
59
+ const committed = `${dir}/source.json`;
60
+ const fromFlag = flagString(ctx.args, 'from');
61
+ const from =
62
+ fromFlag === undefined || isAbsolute(fromFlag) || /^https?:/.test(fromFlag)
63
+ ? fromFlag
64
+ : posixRelative(root, join(ctx.cwd, fromFlag));
65
+ const source = await readClientSource({ from: fromFlag, cwd: ctx.cwd, root, committed, name });
66
+ const only = await onlyFor(ctx, join(root, dir, 'client.ts'));
67
+ const compiled = compileOrRefuse(source.document, from ?? committed, only, 'client vendor');
68
+ const emitted = emitVendor(name, compiled);
69
+ const stamp = {
70
+ command: `x client vendor ${name}${only === undefined ? '' : ` --only ${only.join(',')}`}`,
71
+ source: committed,
72
+ sourceHash: sourceHash(source.document),
73
+ };
74
+ const written = await writeFiles(
75
+ root,
76
+ new Map([
77
+ [committed, source.text],
78
+ [`${dir}/schemas.ts`, stamped(stamp, emitted['schemas.ts'])],
79
+ [`${dir}/client.ts`, stamped(stamp, emitted['client.ts'])],
80
+ ]),
81
+ );
82
+ return {
83
+ ok: true,
84
+ command: 'client',
85
+ summary: msg('cli.client.vendor.written', {
86
+ name,
87
+ operations: compiled.operations.length,
88
+ types: compiled.types.size,
89
+ source: from ?? committed,
90
+ dir,
91
+ }),
92
+ lines: written.map((path) => msg('cli.client.file', { path })),
93
+ data: {
94
+ kind: 'vendor',
95
+ name,
96
+ dir,
97
+ source: committed,
98
+ only: only === undefined ? null : [...only],
99
+ operations: compiled.operations.map((op) => op.name),
100
+ types: compiled.types.size,
101
+ written: [...written],
102
+ },
103
+ };
104
+ }
105
+
106
+ export const clientCommand: CliCommand = {
107
+ spec: clientSpec,
108
+ async run(ctx: CommandContext): Promise<CommandResult> {
109
+ return ctx.args.subcommand === 'customer' ? runCustomer(ctx) : runVendor(ctx);
110
+ },
111
+ };
@@ -0,0 +1,120 @@
1
+ // `x db recount <entity>.<column>`: the counters `countedIn` declares, compared with the truth and
2
+ // — with `--write` — set to it. A counter drifts only when its trigger was missing or disabled
3
+ // (`X_LIFECYCLE_TRIGGER_MISSING`); this is the repair, and the dry run is the report.
4
+
5
+ import {
6
+ CROSS_TENANT_SETTING,
7
+ raw,
8
+ recountDriftSql,
9
+ recountSql,
10
+ setLocal,
11
+ withTransaction,
12
+ } from '@ultimat3/db';
13
+ import { countersOf, describeCounter, getEntity } from '@ultimat3/entity';
14
+ import { loadApp } from './app-load';
15
+ import type { CommandContext } from './command';
16
+ import { DevAlreadyRunningError, liveDevLock } from './dev-lock';
17
+ import { BadFlagError } from './errors';
18
+ import { msg } from './messages';
19
+ import type { CommandResult } from './output';
20
+ import { flagBool } from './parse';
21
+ import { resolveServices } from './runtime-bindings';
22
+ import { startDb } from './runtime-queue';
23
+ import { quoteArg } from './shell-quote';
24
+
25
+ /** Rows of drift a report lists; the count is always whole. */
26
+ const SHOWN = 50;
27
+
28
+ interface DriftRow {
29
+ readonly key: string;
30
+ readonly stored: number;
31
+ readonly actual: number;
32
+ }
33
+
34
+ const refuse = (target: string, reason: string): never => {
35
+ throw new BadFlagError({
36
+ flag: 'name',
37
+ command: 'db recount',
38
+ reason: `"${target}" ${reason}`,
39
+ fix: 'x entities list --json # then x db recount <entity>.<column> naming a countedIn column',
40
+ });
41
+ };
42
+
43
+ /** `<entity>.<column>` to the one counter it names, after the app's modules declared them. */
44
+ function counterNamed(target: string) {
45
+ const [name, column, ...rest] = target.split('.');
46
+ if (name === undefined || column === undefined || rest.length > 0) {
47
+ return refuse(target, 'is not <entity>.<column>');
48
+ }
49
+ const parent = getEntity(name)?.core;
50
+ if (parent === undefined) return refuse(target, `names no entity "${name}"`);
51
+ const counter = countersOf(parent).find((each) => each.column === column);
52
+ if (counter === undefined)
53
+ return refuse(target, `names no column any countedIn keeps on ${name}`);
54
+ return { parent, counter: describeCounter(counter) };
55
+ }
56
+
57
+ export async function runRecountCommand(ctx: CommandContext, root: string): Promise<CommandResult> {
58
+ const target = ctx.args.positionals[0];
59
+ if (target === undefined) return refuse('', 'is missing: name the counter as <entity>.<column>');
60
+ const { findings } = await loadApp(root);
61
+ const { parent, counter } = counterNamed(target);
62
+ const write = flagBool(ctx.args, 'write');
63
+ const services = resolveServices(root, ctx.env);
64
+ if (services.db.mode === 'embedded') {
65
+ const holder = liveDevLock(services.stateDir);
66
+ if (holder !== undefined) {
67
+ throw new DevAlreadyRunningError({
68
+ lock: holder,
69
+ stateDir: services.stateDir,
70
+ embeddedDb: true,
71
+ });
72
+ }
73
+ }
74
+ const started = startDb(services, ctx.env);
75
+ try {
76
+ // One transaction, cross-tenant: a counter is every tenant's, and under forced row security an
77
+ // unset tenant would match no row.
78
+ const drift = await withTransaction(
79
+ async (tx) => {
80
+ await setLocal(tx, { [CROSS_TENANT_SETTING]: 'on' });
81
+ const rows = await tx.query<DriftRow>(raw(recountDriftSql(parent.$table, counter)));
82
+ if (write && rows.length > 0) await tx.execute(raw(recountSql(parent.$table, counter)));
83
+ return rows;
84
+ },
85
+ { client: started.client },
86
+ );
87
+ return {
88
+ ok: findings.length === 0 && (write || drift.length === 0),
89
+ command: 'db',
90
+ summary: msg(write ? 'cli.db.recount.repaired' : 'cli.db.recount.drift', {
91
+ target,
92
+ count: drift.length,
93
+ }),
94
+ findings: [
95
+ ...findings,
96
+ ...(write || drift.length === 0
97
+ ? []
98
+ : [
99
+ {
100
+ code: 'X_LIFECYCLE_TRIGGER_MISSING',
101
+ cause: `${drift.length} row(s) of ${parent.$table} hold a ${counter.column} that is not the count of ${counter.table} rows pointing at them`,
102
+ fix: `x db recount ${quoteArg(target)} --write --json`,
103
+ },
104
+ ]),
105
+ ],
106
+ lines: drift
107
+ .slice(0, SHOWN)
108
+ .map((row) => ` ${row.key} stored ${row.stored} actual ${row.actual}`),
109
+ data: {
110
+ target,
111
+ write,
112
+ drifted: drift.length,
113
+ rows: drift.slice(0, SHOWN).map((row) => ({ ...row })),
114
+ },
115
+ };
116
+ } finally {
117
+ await started.client.close();
118
+ await started.replica?.close();
119
+ }
120
+ }