@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
@@ -0,0 +1,225 @@
1
+ // The Bun floor, as a plain text edit per pin site: `engines.bun`, `.bun-version`, `packageManager`,
2
+ // `setup-bun`, `oven/bun:` tags. Pure over one file's text. Text and not a parse, because the rest of
3
+ // the file is the author's to the byte. A site is only ever raised; one this cannot read is listed.
4
+
5
+ // why: Bun exposes no basename primitive.
6
+ import { basename } from 'node:path';
7
+
8
+ export interface BunEdit {
9
+ readonly path: string;
10
+ readonly site: string;
11
+ readonly from: string;
12
+ readonly to: string;
13
+ }
14
+
15
+ /** A site only its author can move, with the line to open and why. */
16
+ export interface BunManual {
17
+ readonly path: string;
18
+ readonly line: number;
19
+ readonly text: string;
20
+ readonly why: string;
21
+ }
22
+
23
+ export interface BunFileEdit {
24
+ readonly text: string;
25
+ readonly edited: readonly BunEdit[];
26
+ readonly manual: readonly BunManual[];
27
+ }
28
+
29
+ const DOCKERFILE = /^(?:Dockerfile(?:\..+)?|.+\.[Dd]ockerfile)$/;
30
+ const YAML = /\.ya?ml$/;
31
+ const VERSION_TEXT = /\d+\.\d+(?:\.\d+)?/;
32
+
33
+ const isPackageManifest = (name: string): boolean => name === 'package.json';
34
+
35
+ /** Whether a tracked path can hold a pin site at all. */
36
+ export const isBunSite = (path: string): boolean => {
37
+ const name = basename(path);
38
+ return (
39
+ name === '.bun-version' || isPackageManifest(name) || YAML.test(name) || DOCKERFILE.test(name)
40
+ );
41
+ };
42
+
43
+ /** The version in an `engines.bun` range, or undefined for one that names none. */
44
+ export const floorOf = (range: string | undefined): string | undefined =>
45
+ range === undefined ? undefined : VERSION_TEXT.exec(range)?.[0];
46
+
47
+ const numbers = (version: string): readonly number[] => version.split('.').map(Number);
48
+
49
+ /** Below the floor, read at the version's own precision: `1.4` floats, so it reaches `1.4.4`. */
50
+ const isBelow = (version: string, floor: string): boolean => {
51
+ const held = numbers(version);
52
+ const wanted = numbers(floor);
53
+ for (let at = 0; at < held.length; at += 1) {
54
+ const left = held[at] ?? 0;
55
+ const right = wanted[at] ?? 0;
56
+ if (left !== right) return left < right;
57
+ }
58
+ return false;
59
+ };
60
+
61
+ /** The floor at the version's precision, so `1.3-alpine` becomes `1.4-alpine` and still floats. */
62
+ const atPrecision = (version: string, floor: string): string =>
63
+ floor.split('.').slice(0, version.split('.').length).join('.');
64
+
65
+ /** A range's version is its lower bound, never a floating tag: `>=1.4` admits 1.4.0. */
66
+ const lowerBound = (version: string): string =>
67
+ [...version.split('.'), '0', '0'].slice(0, 3).join('.');
68
+
69
+ const lineAt = (text: string, offset: number): number => text.slice(0, offset).split('\n').length;
70
+
71
+ /** One comparator over one version: the only range a raise cannot turn into a different claim. */
72
+ const PLAIN_RANGE = /^(?:>=|\^|~)?\s*\d+\.\d+(?:\.\d+)?$/;
73
+ const EXACT = /^\d+\.\d+\.\d+$/;
74
+
75
+ const BY_HAND = (floor: string): string =>
76
+ `this spelling cannot be raised by a text edit: set it to Bun ${floor} or above`;
77
+
78
+ const editManifest = (path: string, source: string, floor: string): BunFileEdit => {
79
+ const edited: BunEdit[] = [];
80
+ const manual: BunManual[] = [];
81
+ const listed = (offset: number, text: string): void => {
82
+ manual.push({ path, line: lineAt(source, offset), text, why: BY_HAND(floor) });
83
+ };
84
+ let text = source.replace(
85
+ /("engines"\s*:\s*\{[^}]*?"bun"\s*:\s*")([^"]*)(")/,
86
+ (whole, head: string, range: string, tail: string, offset: number) => {
87
+ const version = floorOf(range);
88
+ if (version === undefined || !isBelow(lowerBound(version), floor)) return whole;
89
+ if (!PLAIN_RANGE.test(range)) {
90
+ listed(offset + head.length, `"bun": "${range}"`);
91
+ return whole;
92
+ }
93
+ const to = range.replace(version, floor);
94
+ edited.push({ path, site: 'engines.bun', from: range, to });
95
+ return `${head}${to}${tail}`;
96
+ },
97
+ );
98
+ // Offsets into `source`: the `engines` edit above can only move a later offset by a few bytes, and
99
+ // never across a line.
100
+ text = text.replace(
101
+ /("packageManager"\s*:\s*"bun@)([^"]*)(")/,
102
+ (whole, head: string, value: string, tail: string, offset: number) => {
103
+ const version = floorOf(value);
104
+ if (version === undefined || !isBelow(version, floor)) return whole;
105
+ if (!EXACT.test(value)) {
106
+ listed(offset + head.length, `"packageManager": "bun@${value}"`);
107
+ return whole;
108
+ }
109
+ edited.push({ path, site: 'packageManager', from: value, to: floor });
110
+ return `${head}${floor}${tail}`;
111
+ },
112
+ );
113
+ return { text, edited, manual };
114
+ };
115
+
116
+ const editVersionFile = (path: string, source: string, floor: string): BunFileEdit => {
117
+ const value = source.trim();
118
+ const version = floorOf(value);
119
+ if (version === undefined || !isBelow(version, floor)) {
120
+ return { text: source, edited: [], manual: [] };
121
+ }
122
+ if (!EXACT.test(value)) {
123
+ return {
124
+ text: source,
125
+ edited: [],
126
+ manual: [{ path, line: 1, text: value, why: BY_HAND(floor) }],
127
+ };
128
+ }
129
+ return {
130
+ text: source.replace(value, floor),
131
+ edited: [{ path, site: '.bun-version', from: value, to: floor }],
132
+ manual: [],
133
+ };
134
+ };
135
+
136
+ const BUN_VERSION_KEY = /^(\s*(?:-\s*)?bun-version:\s*)(['"]?)([^'"\s#][^'"#]*?)\2(\s*(?:#.*)?)$/;
137
+ const IMAGE = /oven\/bun:([^\s'"@]+)(@sha256:[0-9a-f]+)?/;
138
+ /** A suffix is a Docker variant (`-alpine`): `1.3.x` is no version with a suffix. */
139
+ const TAG = /^(\d+\.\d+(?:\.\d+)?)(-.*)?$/;
140
+
141
+ /** One line of a YAML file or Dockerfile: the edited text, and what it had to report. */
142
+ function editLine(input: {
143
+ readonly path: string;
144
+ readonly line: string;
145
+ readonly number: number;
146
+ readonly floor: string;
147
+ readonly edited: BunEdit[];
148
+ readonly manual: BunManual[];
149
+ }): string {
150
+ const { path, line, number, floor, edited, manual } = input;
151
+ // Prose: a Dockerfile's header names the digest it pins, and rewriting that lies about the file.
152
+ if (line.trimStart().startsWith('#')) return line;
153
+ const listed = (why: string): string => {
154
+ manual.push({ path, line: number, text: line.trim(), why });
155
+ return line;
156
+ };
157
+ const belowInSomeSpelling = (value: string): boolean => {
158
+ const version = floorOf(value);
159
+ return version !== undefined && isBelow(version, floor);
160
+ };
161
+
162
+ const key = BUN_VERSION_KEY.exec(line);
163
+ if (key !== null) {
164
+ const [, head = '', quote = '', value = '', tail = ''] = key;
165
+ if (value.includes('$')) {
166
+ return listed('the version is an expression: edit where it is defined');
167
+ }
168
+ if (!/^\d+\.\d+(?:\.\d+)?$/.test(value)) {
169
+ return belowInSomeSpelling(value) ? listed(BY_HAND(floor)) : line;
170
+ }
171
+ if (!isBelow(value, floor)) return line;
172
+ const to = atPrecision(value, floor);
173
+ edited.push({ path, site: 'setup-bun', from: value, to });
174
+ return `${head}${quote}${to}${quote}${tail}`;
175
+ }
176
+
177
+ const image = IMAGE.exec(line);
178
+ if (image === null) return line;
179
+ const [whole, tag = '', digest] = image;
180
+ if (tag.includes('$')) return listed('the tag is a variable: edit where it is defined');
181
+ const parts = TAG.exec(tag);
182
+ const version = parts?.[1];
183
+ if (digest !== undefined) {
184
+ const inspect = `by hand (docker buildx imagetools inspect oven/bun:${floor}-alpine)`;
185
+ if (version === undefined || isBelow(version, floor)) {
186
+ return listed(
187
+ `a digest pins this image: move the tag and digest together to oven/bun:${floor}'s, ${inspect}`,
188
+ );
189
+ }
190
+ // `1.4-alpine@sha256:…` at a 1.4.4 floor: the tag reaches it and the digest may not, and which
191
+ // patch a digest holds is not on this line. `1.4.4-alpine@…` and `1.5-alpine@…` say enough.
192
+ return version === atPrecision(version, floor) && version.split('.').length < 3
193
+ ? listed(
194
+ `a digest pins a floating tag, so its patch cannot be read here: confirm it is ${floor} or above, ${inspect}`,
195
+ )
196
+ : line;
197
+ }
198
+ if (version === undefined) return belowInSomeSpelling(tag) ? listed(BY_HAND(floor)) : line;
199
+ if (!isBelow(version, floor)) return line;
200
+ const to = atPrecision(version, floor);
201
+ const suffix = parts?.[2] ?? '';
202
+ edited.push({ path, site: 'oven/bun', from: tag, to: `${to}${suffix}` });
203
+ return line.replace(whole, `oven/bun:${to}${suffix}`);
204
+ }
205
+
206
+ const editLines = (path: string, source: string, floor: string): BunFileEdit => {
207
+ const edited: BunEdit[] = [];
208
+ const manual: BunManual[] = [];
209
+ // Split on `\n` alone, so a CRLF file's `\r` goes through its line untouched — except that a `$`
210
+ // anchor needs it gone, so it is lifted off and put back.
211
+ const lines = source.split('\n').map((raw, index) => {
212
+ const cr = raw.endsWith('\r') ? '\r' : '';
213
+ const line = cr === '' ? raw : raw.slice(0, -1);
214
+ return `${editLine({ path, line, number: index + 1, floor, edited, manual })}${cr}`;
215
+ });
216
+ return { text: lines.join('\n'), edited, manual };
217
+ };
218
+
219
+ /** One file's sites raised to `floor`. Run twice, the second run changes nothing. */
220
+ export function editBunSites(path: string, source: string, floor: string): BunFileEdit {
221
+ const name = basename(path);
222
+ if (name === '.bun-version') return editVersionFile(path, source, floor);
223
+ if (isPackageManifest(name)) return editManifest(path, source, floor);
224
+ return editLines(path, source, floor);
225
+ }
@@ -0,0 +1,228 @@
1
+ // A hop's gate, run by the CLI the hop just installed: `x manifest`, `x doctor --json`, `x verify
2
+ // --json`. Never in this process, because the installed CLI is the one that knows the installed
3
+ // framework (`local-cli.ts`), and it may be many majors older than this one: each answer is read as
4
+ // a foreign document, and one that will not parse is a finding, never a green.
5
+
6
+ // why: Bun exposes no path-join primitive; the installed bin is assembled from the app root.
7
+ import { join } from 'node:path';
8
+ import { t } from '@ultimat3/schema';
9
+ import type { ExecResult, Runner } from './exec';
10
+ import { execOutput } from './exec';
11
+ import { LOCAL_CLI_BIN } from './local-cli';
12
+ import { parseJson } from './upgrade-registry';
13
+
14
+ const FINDING = t.object({
15
+ code: t.string,
16
+ cause: t.string,
17
+ fix: t.string,
18
+ at: t.optional(t.string),
19
+ });
20
+ const STEP = t.object({ name: t.string, ok: t.boolean, findings: t.optional(t.array(FINDING)) });
21
+ const VERIFY = t.object({
22
+ ok: t.boolean,
23
+ summary: t.string,
24
+ steps: t.optional(t.array(STEP)),
25
+ findings: t.optional(t.array(FINDING)),
26
+ });
27
+ const MANIFEST = t.object({ ok: t.boolean, findings: t.optional(t.array(FINDING)) });
28
+ const REPO_MANIFEST = t.object({ scripts: t.optional(t.record(t.string)) });
29
+ const GUARD_MISSING = t.object({ name: t.string, add: t.string });
30
+ const GUARD_DIFFERS = t.object({ name: t.string, refresh: t.string });
31
+ const DOCTOR = t.object({
32
+ data: t.optional(
33
+ t.object({
34
+ guards: t.optional(
35
+ t.object({
36
+ missing: t.optional(t.array(GUARD_MISSING)),
37
+ differs: t.optional(t.array(GUARD_DIFFERS)),
38
+ }),
39
+ ),
40
+ }),
41
+ ),
42
+ });
43
+
44
+ export interface GateFinding {
45
+ /** The gate step that raised it; absent for one raised outside any step. */
46
+ readonly step?: string;
47
+ readonly code: string;
48
+ readonly cause: string;
49
+ readonly fix: string;
50
+ readonly at?: string;
51
+ }
52
+
53
+ interface RawFinding {
54
+ readonly code: string;
55
+ readonly cause: string;
56
+ readonly fix: string;
57
+ readonly at?: string;
58
+ }
59
+
60
+ interface VerifyDocument {
61
+ readonly ok: boolean;
62
+ readonly summary: string;
63
+ readonly steps?: readonly {
64
+ readonly name: string;
65
+ readonly ok: boolean;
66
+ readonly findings?: readonly RawFinding[];
67
+ }[];
68
+ readonly findings?: readonly RawFinding[];
69
+ }
70
+
71
+ interface DoctorDocument {
72
+ readonly data?: {
73
+ readonly guards?: {
74
+ readonly missing?: readonly { readonly name: string; readonly add: string }[];
75
+ readonly differs?: readonly { readonly name: string; readonly refresh: string }[];
76
+ };
77
+ };
78
+ }
79
+
80
+ export interface HopGuards {
81
+ /** False when the hop's `x doctor` predates the listing: unknown, which is not the same as none. */
82
+ readonly reported: boolean;
83
+ readonly missing: readonly { readonly name: string; readonly add: string }[];
84
+ readonly differs: readonly { readonly name: string; readonly refresh: string }[];
85
+ }
86
+
87
+ export interface HopGate {
88
+ readonly manifest: { readonly ok: boolean; readonly findings: readonly GateFinding[] };
89
+ readonly guards: HopGuards;
90
+ readonly verify: {
91
+ readonly ok: boolean;
92
+ readonly summary: string;
93
+ /** The steps that went red, whether or not they carried a finding. */
94
+ readonly failed: readonly string[];
95
+ readonly findings: readonly GateFinding[];
96
+ };
97
+ }
98
+
99
+ /**
100
+ * The one JSON document on a child's stdout. Whole first; then its last line that parses, because
101
+ * the CLI that keeps fd 1 to one document under `--json` is a recent one and a hop can install one
102
+ * that still logs a boot line ahead of it.
103
+ */
104
+ export function documentIn(stdout: string): unknown {
105
+ const whole = parseJson(stdout.trim());
106
+ if (whole !== undefined) return whole;
107
+ const lines = stdout.split('\n').filter((line) => line.trimStart().startsWith('{'));
108
+ return lines
109
+ .reverse()
110
+ .map(parseJson)
111
+ .find((parsed) => parsed !== undefined);
112
+ }
113
+
114
+ const flatten = (document: {
115
+ readonly steps?: VerifyDocument['steps'];
116
+ readonly findings?: readonly RawFinding[];
117
+ }): readonly GateFinding[] => [
118
+ ...(document.steps ?? []).flatMap((step) =>
119
+ (step.findings ?? []).map((finding) => ({ step: step.name, ...pick(finding) })),
120
+ ),
121
+ ...(document.findings ?? []).map(pick),
122
+ ];
123
+
124
+ /** Named fields only: a finding's `meta` carries structures this report promises nothing about. */
125
+ const pick = (finding: RawFinding): RawFinding => ({
126
+ code: finding.code,
127
+ cause: finding.cause,
128
+ fix: finding.fix,
129
+ ...(finding.at === undefined ? {} : { at: finding.at }),
130
+ });
131
+
132
+ const INSTALLED_VERIFY = `bun ${LOCAL_CLI_BIN.replaceAll('\\', '/')} verify --json`;
133
+
134
+ const unreadable = (result: ExecResult): GateFinding => ({
135
+ code: 'X_UPGRADE_GATE_UNREADABLE',
136
+ cause: `${result.command.slice(2).join(' ')} exited ${String(result.code)} with no JSON report: ${execOutput(result).slice(-800) || 'no output'}`,
137
+ fix: INSTALLED_VERIFY,
138
+ });
139
+
140
+ const readVerify = (result: ExecResult): HopGate['verify'] => {
141
+ const parsed = VERIFY.safeParse(documentIn(result.stdout));
142
+ if (parsed.issues !== undefined) {
143
+ return {
144
+ ok: false,
145
+ summary: `x verify exited ${String(result.code)} with no JSON report`,
146
+ failed: [],
147
+ findings: [unreadable(result)],
148
+ };
149
+ }
150
+ const document = parsed.value as VerifyDocument;
151
+ return {
152
+ // Both have to say so: an exit code of 1 under a document saying `ok` is not a verdict to round up.
153
+ ok: result.ok && document.ok,
154
+ summary: document.summary,
155
+ failed: (document.steps ?? []).filter((step) => !step.ok).map((step) => step.name),
156
+ findings: flatten(document),
157
+ };
158
+ };
159
+
160
+ const readManifest = (result: ExecResult): HopGate['manifest'] => {
161
+ const parsed = MANIFEST.safeParse(documentIn(result.stdout));
162
+ if (parsed.issues !== undefined) return { ok: false, findings: [unreadable(result)] };
163
+ const document = parsed.value as { readonly findings?: readonly RawFinding[] };
164
+ return { ok: result.ok, findings: flatten(document) };
165
+ };
166
+
167
+ /** A doctor that exits red is normal (a port, an env key); only its listing is read here. */
168
+ const readGuards = (result: ExecResult): HopGuards => {
169
+ const parsed = DOCTOR.safeParse(documentIn(result.stdout));
170
+ const guards =
171
+ parsed.issues === undefined ? (parsed.value as DoctorDocument).data?.guards : undefined;
172
+ if (guards === undefined) return { reported: false, missing: [], differs: [] };
173
+ return { reported: true, missing: guards.missing ?? [], differs: guards.differs ?? [] };
174
+ };
175
+
176
+ /** The three, in order, each told apart by `onStage` so a long gate is not a long silence. */
177
+ export async function runHopGate(input: {
178
+ readonly runner: Runner;
179
+ readonly root: string;
180
+ readonly onStage: (stage: 'manifest' | 'doctor' | 'verify') => void;
181
+ }): Promise<HopGate> {
182
+ const { runner, root, onStage } = input;
183
+ const installed = (args: readonly string[]): Promise<ExecResult> =>
184
+ runner([process.execPath, join(root, LOCAL_CLI_BIN), ...args], { cwd: root });
185
+ onStage('manifest');
186
+ const manifest = readManifest(await installed(['manifest', '--json']));
187
+ onStage('doctor');
188
+ const guards = readGuards(await installed(['doctor', '--json']));
189
+ onStage('verify');
190
+ const verify = readVerify(await installed(['verify', '--json']));
191
+ return { manifest, guards, verify };
192
+ }
193
+
194
+ /** What a repository with no app can still be asked, in the order an author would run them. */
195
+ const REPO_SCRIPTS = ['typecheck', 'test'] as const;
196
+
197
+ export interface RepoScript {
198
+ readonly name: string;
199
+ /** Null when named and not run: a dry run. */
200
+ readonly ok: boolean | null;
201
+ readonly code?: number;
202
+ readonly output?: string;
203
+ }
204
+
205
+ /** The ones this manifest declares. Read as JSON only after `readManifests` has already parsed it. */
206
+ export function repoScripts(manifestText: string): readonly string[] {
207
+ const parsed = REPO_MANIFEST.safeParse(parseJson(manifestText));
208
+ if (parsed.issues !== undefined) return [];
209
+ const declared = (parsed.value as { readonly scripts?: Readonly<Record<string, string>> })
210
+ .scripts;
211
+ return REPO_SCRIPTS.filter((name) => declared !== undefined && Object.hasOwn(declared, name));
212
+ }
213
+
214
+ /** Each declared script, one after the other, red or not: the report owes every answer. */
215
+ export async function runRepoScripts(input: {
216
+ readonly runner: Runner;
217
+ readonly root: string;
218
+ readonly names: readonly string[];
219
+ readonly onStage: (stage: string) => void;
220
+ }): Promise<readonly RepoScript[]> {
221
+ const ran: RepoScript[] = [];
222
+ for (const name of input.names) {
223
+ input.onStage(name);
224
+ const result = await input.runner([process.execPath, 'run', name], { cwd: input.root });
225
+ ran.push({ name, ok: result.ok, code: result.code, output: execOutput(result).slice(-800) });
226
+ }
227
+ return ran;
228
+ }
@@ -0,0 +1,63 @@
1
+ // Where an app's `@ultimat3/*` pins live: the root manifest and every workspace's. Reading and writing
2
+ // go through one pattern, so the set the plan judged is the set the hop moves — `overrides` and
3
+ // `resolutions` included, since an override left behind holds the old version in place.
4
+
5
+ // why: Bun exposes no path-join primitive; Bun.file takes one already joined.
6
+ import { join } from 'node:path';
7
+ import { UltimateError } from '@ultimat3/core';
8
+ import { pinVersion, type UpgradePin } from './upgrade-plan';
9
+ import { workspaceGlobs, workspaceManifestPaths } from './workspace-graph';
10
+
11
+ const ROOT_MANIFEST = 'package.json';
12
+
13
+ export interface WorkspaceManifest {
14
+ /** Relative to the app root, POSIX. */
15
+ readonly file: string;
16
+ readonly text: string;
17
+ }
18
+
19
+ const PIN_ENTRY = /("(@ultimat3\/[a-z0-9-]+)"\s*:\s*")([^"]*)(")/g;
20
+
21
+ /** A manifest's JSON, or X_UPGRADE_PIN_UNREADABLE naming the file: never a raw SyntaxError. */
22
+ export const parseManifest = (file: string, text: string): unknown => {
23
+ try {
24
+ return JSON.parse(text);
25
+ } catch {
26
+ throw new UltimateError({
27
+ code: 'X_UPGRADE_PIN_UNREADABLE',
28
+ cause: `${file} is not valid JSON, so its @ultimat3/* pins cannot be read`,
29
+ fix: `bun install # names the line in ${file} to repair; then: x upgrade --json`,
30
+ });
31
+ }
32
+ };
33
+
34
+ /** The root manifest and every workspace's, read once, as text: the hop edits the text. */
35
+ export async function readManifests(root: string): Promise<readonly WorkspaceManifest[]> {
36
+ const rootText = await Bun.file(join(root, ROOT_MANIFEST)).text();
37
+ const files = await workspaceManifestPaths(
38
+ root,
39
+ workspaceGlobs(parseManifest(ROOT_MANIFEST, rootText)),
40
+ );
41
+ const members = await Promise.all(
42
+ files
43
+ .filter((file) => file !== ROOT_MANIFEST)
44
+ .map(async (file) => ({ file, text: await Bun.file(join(root, file)).text() })),
45
+ );
46
+ return [{ file: ROOT_MANIFEST, text: rootText }, ...members];
47
+ }
48
+
49
+ export function readPins(manifests: readonly WorkspaceManifest[]): readonly UpgradePin[] {
50
+ return manifests.flatMap(({ file, text }) =>
51
+ [...text.matchAll(PIN_ENTRY)].map(([, , name = '', spec = '']) => ({ file, name, spec })),
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Every pin at the exact version, the rest of the file byte-identical. Exact whatever it was: a
57
+ * caret is how `^19.0.0` beside `^19.4.0` came to be mixed, and a range is a silent upgrade.
58
+ * A spec naming no version never reaches here (`planHop` refused it), and is left as written.
59
+ */
60
+ export const rewritePins = (text: string, to: string): string =>
61
+ text.replace(PIN_ENTRY, (whole, head: string, _name: string, spec: string, tail: string) =>
62
+ pinVersion(spec) === undefined ? whole : `${head}${to}${tail}`,
63
+ );