@ultimat3/cli 29.4.0 → 30.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1314 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -0,0 +1,214 @@
1
+ // `x upgrade`'s decision, pure: the pins an app holds and the versions npm publishes in, the one hop
2
+ // a run may make and the wiki section that walks it out. One major per run, so a red gate is that
3
+ // hop's. No I/O: `cmd-upgrade.ts` reads, this decides, and every refusal is thrown from here.
4
+
5
+ import { UltimateError } from '@ultimat3/core';
6
+ import { quoteArg } from './shell-quote';
7
+
8
+ export interface UpgradePin {
9
+ /** The manifest holding it, relative to the app root. */
10
+ readonly file: string;
11
+ readonly name: string;
12
+ readonly spec: string;
13
+ }
14
+
15
+ export interface UpgradeSection {
16
+ readonly title: string;
17
+ readonly url: string;
18
+ /** The same section as an `x docs` topic, answered whole from the wiki the CLI tarball ships. */
19
+ readonly local: string;
20
+ }
21
+
22
+ export interface UpgradeHop {
23
+ /** The lowest version any workspace pins. */
24
+ readonly from: string;
25
+ readonly to: string;
26
+ /** What a bare `x upgrade` would take after this hop, or null when nothing is left to take. */
27
+ readonly next: string | null;
28
+ /** Already where this run was aimed: the newest release only when `next` is null. */
29
+ readonly atTarget: boolean;
30
+ /** Null when no major is crossed: a walkthrough exists per major and nothing under one. */
31
+ readonly section: UpgradeSection | null;
32
+ }
33
+
34
+ const WIKI_PAGE = 'https://github.com/developerz-ai/ultimate/wiki/Upgrading';
35
+
36
+ /** The walkthroughs whose heading names one release rather than `<major-1>.x`, as recorded. */
37
+ const RECORDED_FROM: ReadonlyMap<number, string> = new Map([
38
+ [5, '4.1.0'],
39
+ [4, '3.0.0'],
40
+ [3, '2.0.0'],
41
+ ]);
42
+
43
+ const PIN = /^(?:\^|~|>=)?(\d+)(?:\.(\d+|x))?(?:\.(\d+|x))?$/;
44
+ const RELEASE = /^(\d+)\.(\d+)\.(\d+)$/;
45
+
46
+ type Triple = readonly [number, number, number];
47
+
48
+ const part = (text: string | undefined): number =>
49
+ text === undefined || text === 'x' ? 0 : Number(text);
50
+
51
+ /** The version a dependency spec starts from, or undefined when it names none (`workspace:*`). */
52
+ export function pinVersion(spec: string): string | undefined {
53
+ const match = PIN.exec(spec.trim());
54
+ if (match === null) return undefined;
55
+ return `${part(match[1])}.${part(match[2])}.${part(match[3])}`;
56
+ }
57
+
58
+ /** A plain release's three numbers; a prerelease is not a release, so it answers undefined. */
59
+ export function releaseOf(version: string): Triple | undefined {
60
+ const match = RELEASE.exec(version);
61
+ return match === null ? undefined : [part(match[1]), part(match[2]), part(match[3])];
62
+ }
63
+
64
+ const tripleOf = (version: string): Triple => releaseOf(version) ?? [0, 0, 0];
65
+
66
+ const compare = (a: string, b: string): number => {
67
+ const left = tripleOf(a);
68
+ const right = tripleOf(b);
69
+ return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
70
+ };
71
+
72
+ const majorOf = (version: string): number => tripleOf(version)[0];
73
+
74
+ /** The walkthrough that gets an app into `major`, built from the heading rule. */
75
+ export function sectionFor(major: number): UpgradeSection {
76
+ const from = RECORDED_FROM.get(major) ?? `${String(major - 1)}.x`;
77
+ const title = `${from} → ${String(major)}.0.0, entry by entry`;
78
+ // GitHub's slug, read off the live page: lowercase, every other character dropped, then one
79
+ // hyphen per space — the arrow leaves two spaces, so `28x--2900-entry-by-entry`.
80
+ const anchor = title
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9 _-]/g, '')
83
+ .replaceAll(' ', '-');
84
+ return { title, url: `${WIKI_PAGE}#${anchor}`, local: `wiki.Upgrading#${anchor}` };
85
+ }
86
+
87
+ const newestOfMajor = (major: number, released: readonly string[]): string | undefined =>
88
+ released.filter((version) => majorOf(version) === major).at(-1);
89
+
90
+ const nextMajorNewest = (from: string, released: readonly string[]): string | undefined => {
91
+ const major = released.map(majorOf).find((candidate) => candidate > majorOf(from));
92
+ return major === undefined ? undefined : newestOfMajor(major, released);
93
+ };
94
+
95
+ /** What a bare run takes from `from`: the next major's newest, else this major's newest. */
96
+ const bareTarget = (from: string, released: readonly string[]): string | undefined => {
97
+ const next = nextMajorNewest(from, released);
98
+ if (next !== undefined) return next;
99
+ const newest = newestOfMajor(majorOf(from), released);
100
+ return newest !== undefined && compare(newest, from) > 0 ? newest : undefined;
101
+ };
102
+
103
+ const unreadablePin = (cause: string, file: string): UltimateError =>
104
+ new UltimateError({
105
+ code: 'X_UPGRADE_PIN_UNREADABLE',
106
+ cause,
107
+ fix: `x upgrade --json # after editing ${file} so every @ultimat3/* dependency carries a version`,
108
+ });
109
+
110
+ /** Every pin's version, or the refusal naming the first pin that has none. */
111
+ function readVersions(pins: readonly UpgradePin[]): readonly string[] {
112
+ if (pins.length === 0) {
113
+ throw unreadablePin(
114
+ 'no workspace package.json depends on any @ultimat3/* package, so there is no version to hop from',
115
+ 'package.json',
116
+ );
117
+ }
118
+ return pins.map((pin) => {
119
+ const version = pinVersion(pin.spec);
120
+ if (version === undefined) {
121
+ throw unreadablePin(
122
+ `${pin.file} depends on ${pin.name} as "${pin.spec}", which names no version to hop from`,
123
+ pin.file,
124
+ );
125
+ }
126
+ return version;
127
+ });
128
+ }
129
+
130
+ /**
131
+ * The version that brings a mixed set level: the newest release in the high pin's major at or above
132
+ * it, since the pin itself may name nothing npm has. Past every release of its major, the next one.
133
+ */
134
+ const aligningVersion = (high: string, released: readonly string[]): string => {
135
+ const newest = newestOfMajor(majorOf(high), released);
136
+ if (newest !== undefined && compare(newest, high) >= 0) return newest;
137
+ return nextMajorNewest(high, released) ?? high;
138
+ };
139
+
140
+ const tooWide = (cause: string, target: string | undefined): UltimateError =>
141
+ new UltimateError({
142
+ code: 'X_UPGRADE_HOP_TOO_WIDE',
143
+ cause,
144
+ fix: target === undefined ? 'x upgrade --json' : `x upgrade --to ${quoteArg(target)} --json`,
145
+ });
146
+
147
+ /** The refusals of an asked-for target; a target that passes is a legal hop. */
148
+ function checkTarget(input: {
149
+ readonly to: string;
150
+ readonly low: string;
151
+ readonly high: string;
152
+ readonly released: readonly string[];
153
+ }): void {
154
+ const { to, low, high, released } = input;
155
+ if (!released.includes(to)) {
156
+ throw new UltimateError({
157
+ code: 'X_UPGRADE_VERSION_UNPUBLISHED',
158
+ cause: `--to ${to} is not a release npm publishes for @ultimat3/cli`,
159
+ fix: 'bun pm view @ultimat3/cli versions --json # then: x upgrade --to <one of them> --json',
160
+ });
161
+ }
162
+ // A mixed set has one way out, so every refusal over it names that way: a fix aimed at the next
163
+ // major of the lowest pin would itself be refused, for sitting below the highest.
164
+ const aligning = aligningVersion(high, released);
165
+ const way = low === high ? bareTarget(low, released) : aligning;
166
+ if (compare(to, high) < 0) {
167
+ throw tooWide(`--to ${to} is below the ${high} this app already pins`, way);
168
+ }
169
+ // The second operand is what makes the mixed refusal's own fix runnable: `^6` beside `^19` is told
170
+ // `--to <19.x>`, which brings the straggler level and crosses nothing the rest of the app has not.
171
+ const widest = Math.max(majorOf(nextMajorNewest(low, released) ?? low), majorOf(aligning));
172
+ if (majorOf(to) > widest) {
173
+ throw tooWide(
174
+ `--to ${to} skips a major: from ${low} a run reaches ${String(widest)}.x at most, so each hop's gate attributes its red to that hop`,
175
+ way,
176
+ );
177
+ }
178
+ }
179
+
180
+ /** The one hop this run may make, or the refusal saying why it may not. */
181
+ export function planHop(input: {
182
+ readonly pins: readonly UpgradePin[];
183
+ readonly versions: readonly string[];
184
+ readonly to?: string;
185
+ }): UpgradeHop {
186
+ const { pins, to } = input;
187
+ const distinct = [...new Set(readVersions(pins))].sort(compare);
188
+ const low = distinct[0] ?? '0.0.0';
189
+ const high = distinct.at(-1) ?? low;
190
+ const released = input.versions.filter((version) => releaseOf(version) !== undefined);
191
+ released.sort(compare);
192
+
193
+ if (to === undefined && distinct.length > 1) {
194
+ // The stragglers, not the first few: `^19.4.0` nineteen times says nothing about the one `^19.3.1`.
195
+ const stragglers = pins.filter((pin) => pinVersion(pin.spec) !== high);
196
+ const held = stragglers.map((pin) => `${pin.file}: ${pin.name} ${pin.spec}`);
197
+ throw new UltimateError({
198
+ code: 'X_UPGRADE_MIXED_PINS',
199
+ cause: `the workspaces pin ${distinct.join(', ')}, and the framework only moves in lockstep. ${String(held.length)} of ${String(pins.length)} below ${high}: ${held.slice(0, 6).join('; ')}`,
200
+ fix: `x upgrade --to ${quoteArg(aligningVersion(high, released))} --json`,
201
+ });
202
+ }
203
+ if (to !== undefined) checkTarget({ to, low, high, released });
204
+
205
+ const target = to ?? bareTarget(low, released) ?? low;
206
+ const after = bareTarget(target, released) ?? null;
207
+ return {
208
+ from: low,
209
+ to: target,
210
+ next: after,
211
+ atTarget: distinct.length === 1 && target === low,
212
+ section: majorOf(target) > majorOf(high) ? sectionFor(majorOf(target)) : null,
213
+ };
214
+ }
@@ -0,0 +1,53 @@
1
+ // What npm publishes, as `bun pm view` answers it: every version of the CLI, and the Bun floor of one.
2
+ // Through the app's own registry config, because `bun pm view` runs from the app root. Any failure
3
+ // here is `X_UPGRADE_REGISTRY_UNREACHABLE`, and it can only fire before the hop has written anything.
4
+
5
+ import { UltimateError } from '@ultimat3/core';
6
+ import { t } from '@ultimat3/schema';
7
+ import type { ExecResult, Runner } from './exec';
8
+ import { execOutput } from './exec';
9
+
10
+ const PACKAGE = '@ultimat3/cli';
11
+ const VERSIONS = t.array(t.string);
12
+ const ENGINES = t.object({ bun: t.optional(t.string) });
13
+
14
+ const unreachable = (result: ExecResult): UltimateError =>
15
+ new UltimateError({
16
+ code: 'X_UPGRADE_REGISTRY_UNREACHABLE',
17
+ cause: `${result.command.slice(1).join(' ')} exited ${String(result.code)}: ${execOutput(result).slice(-500) || 'no output'}`,
18
+ fix: 'bun pm view @ultimat3/cli version # once this answers, re-run: x upgrade --json',
19
+ });
20
+
21
+ /** A child's stdout as JSON, or undefined: a foreign answer that will not parse is not a crash. */
22
+ export const parseJson = (text: string): unknown => {
23
+ try {
24
+ return JSON.parse(text);
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ };
29
+
30
+ const view = async (runner: Runner, cwd: string, args: readonly string[]): Promise<ExecResult> => {
31
+ const result = await runner([process.execPath, 'pm', 'view', ...args, '--json'], { cwd });
32
+ if (!result.ok) throw unreachable(result);
33
+ return result;
34
+ };
35
+
36
+ /** Every version npm publishes for the CLI, as npm orders them. */
37
+ export async function publishedVersions(runner: Runner, cwd: string): Promise<readonly string[]> {
38
+ const result = await view(runner, cwd, [PACKAGE, 'versions']);
39
+ const parsed = VERSIONS.safeParse(parseJson(result.stdout));
40
+ if (parsed.issues !== undefined) throw unreachable(result);
41
+ return parsed.value as readonly string[];
42
+ }
43
+
44
+ /** `engines.bun` of one release; undefined when that release declares none. */
45
+ export async function releaseBunRange(
46
+ runner: Runner,
47
+ cwd: string,
48
+ version: string,
49
+ ): Promise<string | undefined> {
50
+ const result = await view(runner, cwd, [`${PACKAGE}@${version}`, 'engines']);
51
+ const parsed = ENGINES.safeParse(parseJson(result.stdout));
52
+ return parsed.issues === undefined ? (parsed.value as { readonly bun?: string }).bun : undefined;
53
+ }
@@ -0,0 +1,139 @@
1
+ // What `x upgrade` says: the one report every outcome carries, the terminal's lines projected from it,
2
+ // and the sentence naming what to do next. No decisions and no I/O: `cmd-upgrade.ts` hands in the facts.
3
+
4
+ import { msg } from './messages';
5
+ import type { JsonValue } from './output';
6
+ import type { AgentsLinkState } from './upgrade-agents-link';
7
+ import type { BunEdit, BunManual } from './upgrade-bun-floor';
8
+ import type { GateFinding, HopGate, RepoScript } from './upgrade-gate';
9
+ import type { UpgradeHop, UpgradePin } from './upgrade-plan';
10
+
11
+ export interface PlannedWrites {
12
+ /** Absolute path to final text; a manifest holding both a pin and a floor is written once. */
13
+ readonly files: ReadonlyMap<string, string>;
14
+ readonly edited: readonly BunEdit[];
15
+ readonly manual: readonly BunManual[];
16
+ }
17
+
18
+ /** The strongest check a hop had: the verdict `done` rests on, stated rather than implied. */
19
+ export type HopProof = 'verify' | 'scripts' | 'none';
20
+
21
+ export const proofOf = (gate: HopGate | undefined, scripts: readonly RepoScript[]): HopProof =>
22
+ gate !== undefined ? 'verify' : scripts.length > 0 ? 'scripts' : 'none';
23
+
24
+ /** What the agent does next, as a sentence naming the commands. */
25
+ export const thenLine = (input: {
26
+ readonly hop: UpgradeHop;
27
+ readonly dryRun: boolean;
28
+ readonly app: boolean;
29
+ readonly gate: HopGate | undefined;
30
+ readonly scripts: readonly RepoScript[];
31
+ }): string => {
32
+ const { hop, dryRun, app, gate, scripts } = input;
33
+ if (hop.atTarget) {
34
+ return hop.next === null
35
+ ? msg('cli.upgrade.then.atLatest', { version: hop.to })
36
+ : msg('cli.upgrade.then.atTarget', { version: hop.to, next: hop.next });
37
+ }
38
+ if (dryRun) {
39
+ return app
40
+ ? msg('cli.upgrade.then.dryRun')
41
+ : msg('cli.upgrade.then.dryRunNoApp', {
42
+ scripts: scripts.map((script) => `bun run ${script.name}`).join(', ') || 'nothing',
43
+ });
44
+ }
45
+ if (!app) {
46
+ if (scripts.length === 0) return msg('cli.upgrade.then.noProof', { to: hop.to });
47
+ return scripts.every((script) => script.ok === true)
48
+ ? msg('cli.upgrade.then.scriptsGreen', { to: hop.to })
49
+ : msg('cli.upgrade.then.scriptsRed', { to: hop.to });
50
+ }
51
+ if (gate?.verify.ok !== true) return msg('cli.upgrade.then.red', { to: hop.to });
52
+ return hop.next === null
53
+ ? msg('cli.upgrade.then.last', { to: hop.to })
54
+ : msg('cli.upgrade.then.walk', { to: hop.to, next: hop.next });
55
+ };
56
+
57
+ const findingLines = (findings: readonly GateFinding[]): readonly string[] =>
58
+ findings.flatMap((finding) => [
59
+ ` ${finding.code}${finding.step === undefined ? '' : ` [${finding.step}]`}${finding.at === undefined ? '' : ` (${finding.at})`}`,
60
+ ` fix: ${finding.fix}`,
61
+ ]);
62
+
63
+ /** The terminal's form of the report: the url first, then each finding's `fix:`. */
64
+ export const humanLines = (input: {
65
+ readonly hop: UpgradeHop;
66
+ readonly writes: PlannedWrites;
67
+ readonly gate: HopGate | undefined;
68
+ readonly scripts: readonly RepoScript[];
69
+ readonly then: string;
70
+ readonly agentsMd?: AgentsLinkState;
71
+ }): readonly string[] => {
72
+ const { hop, writes, gate, scripts, then, agentsMd } = input;
73
+ return [
74
+ ...(hop.section === null
75
+ ? []
76
+ : [msg('cli.upgrade.read', { title: hop.section.title, url: hop.section.url })]),
77
+ ...writes.edited.map((edit) =>
78
+ msg('cli.upgrade.bunEdited', { path: edit.path, from: edit.from, to: edit.to }),
79
+ ),
80
+ ...writes.manual.map((site) =>
81
+ msg('cli.upgrade.bunManual', { path: site.path, line: site.line, why: site.why }),
82
+ ),
83
+ ...(agentsMd === 'link' || agentsMd === 'differs'
84
+ ? [msg(agentsMd === 'link' ? 'cli.upgrade.agentsMd.link' : 'cli.upgrade.agentsMd.differs')]
85
+ : []),
86
+ ...(gate?.guards.missing ?? []).map((guard) => msg('cli.upgrade.guard', { fix: guard.add })),
87
+ ...(gate?.guards.differs ?? []).map((guard) =>
88
+ msg('cli.upgrade.guard', { fix: guard.refresh }),
89
+ ),
90
+ ...findingLines([...(gate?.manifest.findings ?? []), ...(gate?.verify.findings ?? [])]),
91
+ ...scripts
92
+ .filter((script) => script.ok === false)
93
+ .map((script) => msg('cli.upgrade.scriptRed', { name: script.name })),
94
+ msg('cli.upgrade.then', { then }),
95
+ ];
96
+ };
97
+
98
+ const asJson = (value: unknown): JsonValue => value as JsonValue;
99
+
100
+ /** The one shape every outcome reports: `--json` and the terminal project this and nothing else. */
101
+ export const report = (input: {
102
+ readonly hop: UpgradeHop;
103
+ readonly done: boolean;
104
+ readonly dryRun: boolean;
105
+ readonly pins: readonly UpgradePin[];
106
+ readonly bunFrom: string | null;
107
+ readonly bunTo: string | null;
108
+ readonly writes: PlannedWrites;
109
+ readonly app: boolean;
110
+ readonly gate: HopGate | undefined;
111
+ readonly scripts: readonly RepoScript[];
112
+ readonly then: string;
113
+ /** What the hop does to `AGENTS.md` (`upgrade-agents-link.ts`); absent when no hop is planned. */
114
+ readonly agentsMd?: AgentsLinkState;
115
+ }): JsonValue => {
116
+ const { hop, done, dryRun, pins, bunFrom, bunTo, writes, app, gate, scripts, then } = input;
117
+ return asJson({
118
+ from: hop.from,
119
+ to: hop.to,
120
+ next: hop.next,
121
+ done,
122
+ dryRun,
123
+ app,
124
+ proof: dryRun ? 'none' : proofOf(gate, scripts),
125
+ section: hop.section,
126
+ pins: hop.atTarget
127
+ ? []
128
+ : pins.map((pin) => ({ file: pin.file, name: pin.name, from: pin.spec, to: hop.to })),
129
+ bun: { from: bunFrom, to: bunTo, edited: writes.edited, manual: writes.manual },
130
+ agentsMd: input.agentsMd ?? null,
131
+ guards: gate?.guards ?? null,
132
+ manifest: gate?.manifest ?? null,
133
+ verify: gate?.verify ?? null,
134
+ scripts,
135
+ then,
136
+ });
137
+ };
138
+
139
+ export const NO_WRITES: PlannedWrites = { files: new Map(), edited: [], manual: [] };
@@ -0,0 +1,74 @@
1
+ // Where `x upgrade` works: the app, or a repository that holds pins and no `app.config.ts`, which can
2
+ // still hop and just cannot be gated. Never a directory above the repository the cwd is in, and never
3
+ // a workspace member when a root claims it: the hop moves the whole set or none of it.
4
+
5
+ // why: a submodule's or worktree's `.git` is a file, and Bun.file cannot tell either kind exists.
6
+ import { existsSync } from 'node:fs';
7
+ // why: Bun exposes no path-join, dirname or relative primitive.
8
+ import { dirname, join, relative, resolve } from 'node:path';
9
+ import { UltimateError } from '@ultimat3/core';
10
+ import { appDirsBelow, findAppRoot, requireAppRoot } from './app-root';
11
+ import { parseManifest, readManifests, readPins } from './upgrade-pins';
12
+ import { workspaceGlobs, workspaceManifestPaths } from './workspace-graph';
13
+
14
+ export interface UpgradeRoot {
15
+ readonly dir: string;
16
+ /** Whether `dir` holds `app.config.ts`, so `x verify` can answer for it. */
17
+ readonly app: boolean;
18
+ }
19
+
20
+ /** `from`, then each directory above it, stopping at the repository's own root: any `.git`. */
21
+ function ancestors(from: string): readonly string[] {
22
+ const dirs: string[] = [];
23
+ let dir = from;
24
+ for (;;) {
25
+ dirs.push(dir);
26
+ const parent = dirname(dir);
27
+ if (parent === dir || existsSync(join(dir, '.git'))) return dirs;
28
+ dir = parent;
29
+ }
30
+ }
31
+
32
+ const holdsPins = async (dir: string): Promise<boolean> =>
33
+ (await Bun.file(join(dir, 'package.json')).exists()) &&
34
+ readPins(await readManifests(dir)).length > 0;
35
+
36
+ const claims = async (root: string, member: string): Promise<boolean> => {
37
+ const path = join(root, 'package.json');
38
+ const file = Bun.file(path);
39
+ if (!(await file.exists())) return false;
40
+ const globs = workspaceGlobs(parseManifest(path, await file.text()));
41
+ const manifest = `${relative(root, member).replaceAll('\\', '/')}/package.json`;
42
+ return (await workspaceManifestPaths(root, globs)).includes(manifest);
43
+ };
44
+
45
+ /** The app at or above `from`; else the nearest manifest holding pins, or the root claiming it. */
46
+ export async function findUpgradeRoot(from: string): Promise<UpgradeRoot> {
47
+ const start = resolve(from);
48
+ const app = findAppRoot(start);
49
+ if (app !== undefined) return { dir: app.dir, app: true };
50
+
51
+ const chain = ancestors(start);
52
+ let nearest: number | undefined;
53
+ for (const [index, dir] of chain.entries()) {
54
+ if (await holdsPins(dir)) {
55
+ nearest = index;
56
+ break;
57
+ }
58
+ }
59
+ const held = nearest === undefined ? undefined : chain[nearest];
60
+ if (held === undefined) {
61
+ throw new UltimateError({
62
+ code: 'X_UPGRADE_PIN_UNREADABLE',
63
+ cause: `no package.json at or above ${start} depends on any @ultimat3/* package, so there is nothing to hop from`,
64
+ fix: 'x upgrade --json # from the directory whose package.json holds the @ultimat3/* dependencies',
65
+ });
66
+ }
67
+ let dir = held;
68
+ for (const above of chain.slice((nearest ?? 0) + 1)) {
69
+ if (await claims(above, dir)) dir = above;
70
+ }
71
+ // An app below is gated from its own directory: a hop from here would claim no gate it could have.
72
+ if (appDirsBelow(dir).length > 0) requireAppRoot('upgrade', dir);
73
+ return { dir, app: false };
74
+ }
@@ -0,0 +1,118 @@
1
+ // The budgets step's first half: when the stats on disk cannot be read as this tree's, run the one
2
+ // builder (`x build --target static`) through the step's runner, so the build is a tagged child
3
+ // under the step's deadline. Its output goes to `.x/verify-static`, never over a deployable
4
+ // `.x/static`; the stats it weighs always land in `.x/build-stats.json` (`prerender.ts`).
5
+
6
+ // why: Bun exposes no path-join primitive; every path here is assembled from the app root.
7
+ import { join } from 'node:path';
8
+ import { ERROR_DOCS_URL, isCompiledBundle } from '@ultimat3/core';
9
+ import { t } from '@ultimat3/schema';
10
+ import { BUILD_STATS_FILE, readBuildStats } from './budgets';
11
+ import type { BudgetsFreshness } from './budgets-freshness';
12
+ import { budgetsFreshness } from './budgets-freshness';
13
+ import type { Runner } from './exec';
14
+ import { execOutput } from './exec';
15
+ import { msg } from './messages';
16
+ import type { Finding } from './output';
17
+ import { newestSourceMtime } from './source-mtime';
18
+ import { documentIn } from './upgrade-gate';
19
+
20
+ /** Where the gate's own build writes its pages: inside `.x/`, gitignored, apart from `.x/static`. */
21
+ export const VERIFY_STATIC_OUT = join('.x', 'verify-static');
22
+
23
+ /** `x build`'s argv, spawned as this process's own CLI: the source entry, or a compiled binary. */
24
+ export function verifyBuildCommand(): readonly string[] {
25
+ // `--no-capture`: the gate weighs script bytes, and a card is a picture — so a machine with no
26
+ // Chrome stays green on an app that declares `og.capture` (`cmd-shot.ts`'s header is why).
27
+ const words = [
28
+ 'build',
29
+ '--target',
30
+ 'static',
31
+ '--no-preflight',
32
+ '--no-capture',
33
+ '--out',
34
+ VERIFY_STATIC_OUT,
35
+ ];
36
+ const self = isCompiledBundle(import.meta.dir) ? [] : [join(import.meta.dir, 'bin.ts')];
37
+ return [process.execPath, ...self, ...words, '--json'];
38
+ }
39
+
40
+ const statsMtime = async (path: string): Promise<number | undefined> => {
41
+ const file = Bun.file(path);
42
+ return (await file.exists()) ? file.lastModified : undefined;
43
+ };
44
+
45
+ /** The stats file's state against this tree. */
46
+ export async function statsFreshness(root: string): Promise<BudgetsFreshness> {
47
+ const stats = await readBuildStats(root);
48
+ const path = join(root, BUILD_STATS_FILE);
49
+ return budgetsFreshness({
50
+ stats,
51
+ statsMtimeMs: stats === undefined ? undefined : await statsMtime(path),
52
+ newestSourceMtimeMs: await newestSourceMtime(root),
53
+ });
54
+ }
55
+
56
+ const FINDING = t.object({
57
+ code: t.string,
58
+ cause: t.string,
59
+ fix: t.string,
60
+ docs: t.optional(t.string),
61
+ at: t.optional(t.string),
62
+ });
63
+ const BUILD_DOCUMENT = t.object({
64
+ findings: t.optional(t.array(FINDING)),
65
+ data: t.optional(t.object({ output: t.optional(t.string) })),
66
+ });
67
+
68
+ interface BuildDocument {
69
+ readonly findings?: readonly Finding[];
70
+ readonly data?: { readonly output?: string };
71
+ }
72
+
73
+ export interface MeasuredFirst {
74
+ readonly state: BudgetsFreshness;
75
+ /** Whether this step spawned the build. */
76
+ readonly built: boolean;
77
+ /** The build's own findings when it failed: the step's, unchanged. Empty when it passed. */
78
+ readonly findings: readonly Finding[];
79
+ /** One line saying the step built and why, then a failed build's output. Absent when fresh. */
80
+ readonly output?: string;
81
+ }
82
+
83
+ /**
84
+ * Build first unless the stats are fresh. A failed build's findings are returned as the build
85
+ * reported them, its code and its `fix:`; a build that printed no document is `X_BUILD_FAILED` with
86
+ * what it printed, and the command that shows it again.
87
+ */
88
+ export async function measureIfStale(ctx: {
89
+ readonly root: string;
90
+ readonly runner: Runner;
91
+ }): Promise<MeasuredFirst> {
92
+ const state = await statsFreshness(ctx.root);
93
+ if (state === 'fresh') return { state, built: false, findings: [] };
94
+ const command = verifyBuildCommand();
95
+ const result = await ctx.runner(command, { cwd: ctx.root });
96
+ const ran = msg('cli.verify.budgets.built', {
97
+ state,
98
+ out: VERIFY_STATIC_OUT,
99
+ ms: result.durationMs,
100
+ });
101
+ if (result.ok) return { state, built: true, findings: [], output: ran };
102
+ const parsed = BUILD_DOCUMENT.safeParse(documentIn(result.stdout));
103
+ const document = parsed.issues === undefined ? (parsed.value as BuildDocument) : undefined;
104
+ const reported = document?.findings ?? [];
105
+ const output = document?.data?.output ?? execOutput(result);
106
+ const findings: readonly Finding[] =
107
+ reported.length > 0
108
+ ? reported
109
+ : [
110
+ {
111
+ code: 'X_BUILD_FAILED',
112
+ cause: `${command.slice(-8).join(' ')} exited ${String(result.code)} with no JSON report, so x verify could not weigh this tree`,
113
+ fix: 'x build --target static --json',
114
+ docs: ERROR_DOCS_URL,
115
+ },
116
+ ];
117
+ return { state, built: true, findings, output: [ran, output].join('\n') };
118
+ }