@ultimat3/cli 23.0.0 → 25.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 (296) hide show
  1. package/CLAUDE.md +28 -13
  2. package/README.md +32 -7
  3. package/package.json +29 -29
  4. package/src/admin-catalog-keys.ts +2 -2
  5. package/src/affected.ts +30 -36
  6. package/src/api-registration.ts +89 -0
  7. package/src/app-artifacts.ts +1 -1
  8. package/src/app-auth.ts +4 -27
  9. package/src/app-boundaries.ts +17 -4
  10. package/src/app-config-load.ts +51 -0
  11. package/src/app-env.ts +26 -25
  12. package/src/app-load.ts +38 -14
  13. package/src/app-manifest.ts +4 -10
  14. package/src/app-mcp.ts +31 -46
  15. package/src/app-name.ts +52 -0
  16. package/src/app-permissions-borrowed.ts +123 -0
  17. package/src/app-permissions-site.ts +48 -0
  18. package/src/app-permissions.ts +7 -17
  19. package/src/app-root.ts +9 -5
  20. package/src/bin.ts +3 -7
  21. package/src/browser-launcher.ts +4 -2
  22. package/src/budgets-charged.ts +60 -0
  23. package/src/budgets-scan.ts +76 -0
  24. package/src/budgets-unmeasured.ts +91 -0
  25. package/src/budgets.ts +117 -154
  26. package/src/cdp-shot-driver.ts +20 -1
  27. package/src/cdp-shot-watch.ts +35 -31
  28. package/src/ci-log.ts +0 -0
  29. package/src/cmd-auth-spec.ts +16 -0
  30. package/src/cmd-auth.ts +65 -0
  31. package/src/cmd-build-spec.ts +7 -1
  32. package/src/cmd-build.ts +151 -22
  33. package/src/cmd-db-backfill.ts +26 -13
  34. package/src/cmd-db-branch.ts +2 -2
  35. package/src/cmd-db.ts +10 -6
  36. package/src/cmd-deploy-compose.ts +94 -0
  37. package/src/cmd-deploy-drain.ts +48 -0
  38. package/src/cmd-deploy-helm.ts +7 -6
  39. package/src/cmd-deploy-start-first.ts +178 -0
  40. package/src/cmd-deploy.ts +158 -36
  41. package/src/cmd-dev.ts +18 -356
  42. package/src/cmd-doctor-spec.ts +2 -9
  43. package/src/cmd-doctor.ts +57 -80
  44. package/src/cmd-env.ts +55 -16
  45. package/src/cmd-generate.ts +89 -32
  46. package/src/cmd-help.ts +19 -6
  47. package/src/cmd-i18n.ts +21 -17
  48. package/src/cmd-jobs-spec.ts +7 -22
  49. package/src/cmd-jobs.ts +33 -140
  50. package/src/cmd-manifest-spec.ts +1 -1
  51. package/src/cmd-manifest.ts +6 -25
  52. package/src/cmd-mcp.ts +71 -22
  53. package/src/cmd-new.ts +36 -8
  54. package/src/cmd-planned.ts +24 -8
  55. package/src/cmd-routes.ts +15 -5
  56. package/src/cmd-secrets.ts +47 -6
  57. package/src/cmd-shot-matrix.ts +22 -7
  58. package/src/cmd-shot-spec.ts +10 -2
  59. package/src/cmd-shot.ts +46 -41
  60. package/src/cmd-tasks.ts +2 -2
  61. package/src/cmd-test-spec.ts +1 -7
  62. package/src/cmd-test.ts +14 -30
  63. package/src/cmd-verify.ts +4 -4
  64. package/src/coverage-lcov.ts +4 -2
  65. package/src/db-backfill.ts +2 -2
  66. package/src/db-replay-engine.ts +4 -10
  67. package/src/db-subscribes.ts +27 -16
  68. package/src/dev-boot.ts +290 -0
  69. package/src/dev-child-watch.ts +63 -12
  70. package/src/dev-dashboard-guard.ts +76 -0
  71. package/src/dev-dashboard.ts +56 -30
  72. package/src/dev-lock.ts +100 -31
  73. package/src/dev-rebuild.ts +80 -0
  74. package/src/dev-restart-retire.ts +46 -0
  75. package/src/dev-route-table.ts +30 -8
  76. package/src/dev-supervisor.ts +114 -22
  77. package/src/dev-traces.ts +53 -20
  78. package/src/dispatch.ts +17 -13
  79. package/src/doctor-app-url.ts +39 -0
  80. package/src/doctor-auth.ts +95 -0
  81. package/src/doctor-offline.ts +6 -5
  82. package/src/document-options.ts +7 -0
  83. package/src/drift-replica-identity.ts +42 -0
  84. package/src/drift.ts +29 -6
  85. package/src/error-catalog.ts +6 -9
  86. package/src/error-codes.ts +29 -3
  87. package/src/error-page-csp.ts +14 -15
  88. package/src/error-unthrown.ts +29 -11
  89. package/src/errors.ts +54 -83
  90. package/src/exec.ts +8 -0
  91. package/src/favicon.ts +2 -2
  92. package/src/fix-command.ts +9 -1
  93. package/src/framework-env.ts +148 -0
  94. package/src/framework-schema-apply.ts +78 -0
  95. package/src/framework-schema-stamp.ts +96 -0
  96. package/src/framework-schema.ts +25 -3
  97. package/src/generate-catalog-locales.ts +79 -0
  98. package/src/generate-files.ts +4 -3
  99. package/src/generate-kinds.ts +18 -0
  100. package/src/generate-plural.ts +50 -0
  101. package/src/generate-shadow.ts +95 -0
  102. package/src/generate-write.ts +47 -13
  103. package/src/gh-target.ts +12 -7
  104. package/src/hold.ts +56 -1
  105. package/src/i18n-audit.ts +25 -9
  106. package/src/i18n-index.ts +5 -6
  107. package/src/i18n-registration.ts +7 -9
  108. package/src/import-scan.ts +28 -3
  109. package/src/index.ts +2 -6
  110. package/src/island-bundle.ts +57 -79
  111. package/src/island-capture.ts +1 -1
  112. package/src/island-duplicate-refusal.ts +57 -0
  113. package/src/island-duplicates.ts +221 -0
  114. package/src/island-entry-missing.ts +64 -0
  115. package/src/island-harness-route.ts +20 -4
  116. package/src/island-harness-script.ts +23 -2
  117. package/src/island-identity.ts +17 -14
  118. package/src/island-package-dedupe.ts +150 -0
  119. package/src/island-realtime.ts +36 -16
  120. package/src/island-routes.ts +2 -2
  121. package/src/island-runtime.ts +241 -0
  122. package/src/island-sources.ts +105 -0
  123. package/src/island-states-load.ts +6 -0
  124. package/src/island-store.ts +90 -6
  125. package/src/jobs-json.ts +0 -26
  126. package/src/jobs-report.ts +36 -20
  127. package/src/local-cli-handoff.ts +90 -0
  128. package/src/loopback-host.ts +13 -0
  129. package/src/manifest-staleness.ts +62 -0
  130. package/src/mcp-errors.ts +20 -4
  131. package/src/mcp-host.ts +22 -14
  132. package/src/mcp-test-run.ts +38 -0
  133. package/src/mcp-ui-inspect.ts +3 -2
  134. package/src/mcp-ui-interact.ts +3 -2
  135. package/src/mcp-ui.ts +8 -7
  136. package/src/measure-scope.ts +15 -10
  137. package/src/messages.ts +29 -16
  138. package/src/metrics-endpoint.ts +28 -3
  139. package/src/migration-extensions.ts +1 -1
  140. package/src/otlp-export.ts +2 -2
  141. package/src/output.ts +24 -0
  142. package/src/page-navigation.ts +7 -23
  143. package/src/page-speculation.ts +19 -31
  144. package/src/parse.ts +0 -8
  145. package/src/permission-grants.ts +3 -3
  146. package/src/posix-path.ts +30 -0
  147. package/src/prerender-locales.ts +6 -4
  148. package/src/prerender-out.ts +34 -7
  149. package/src/prerender-report.ts +58 -0
  150. package/src/prerender-served.ts +52 -0
  151. package/src/prerender.ts +71 -103
  152. package/src/pwa-artifacts.ts +33 -94
  153. package/src/pwa-manifest-locales.ts +9 -9
  154. package/src/registry.ts +2 -0
  155. package/src/retry-memo.ts +1 -1
  156. package/src/revalidated-response.ts +4 -3
  157. package/src/role-realtime.ts +7 -1
  158. package/src/role-replicator.ts +124 -29
  159. package/src/role-start.ts +69 -85
  160. package/src/role-sync.ts +119 -25
  161. package/src/root-env.ts +157 -28
  162. package/src/route-islands.ts +69 -0
  163. package/src/runtime-admin.ts +4 -1
  164. package/src/runtime-assets.ts +26 -9
  165. package/src/runtime-bindings.ts +27 -7
  166. package/src/runtime-cache.ts +46 -55
  167. package/src/runtime-hooks.ts +2 -1
  168. package/src/runtime-idempotency-scope.ts +23 -0
  169. package/src/runtime-isr-outcome.ts +100 -0
  170. package/src/runtime-isr.ts +36 -0
  171. package/src/runtime-jobs.ts +76 -41
  172. package/src/runtime-live-feed.ts +1 -1
  173. package/src/runtime-mail.ts +26 -0
  174. package/src/runtime-mfa-warning.ts +29 -0
  175. package/src/runtime-notify-retention.ts +10 -46
  176. package/src/runtime-overrides.ts +4 -4
  177. package/src/runtime-purge.ts +38 -16
  178. package/src/runtime-queue.ts +43 -22
  179. package/src/runtime-realtime.ts +11 -45
  180. package/src/runtime-render.ts +136 -114
  181. package/src/runtime-replica.ts +3 -8
  182. package/src/runtime-services.ts +49 -26
  183. package/src/runtime-storage-upload.ts +2 -2
  184. package/src/runtime-storage.ts +13 -8
  185. package/src/schema-diff.ts +16 -0
  186. package/src/schema-drift.ts +45 -15
  187. package/src/secrets-rotation.ts +8 -9
  188. package/src/seo-routes.ts +80 -9
  189. package/src/serve-boot.ts +51 -12
  190. package/src/serve-drain.ts +11 -33
  191. package/src/serve-env.ts +9 -6
  192. package/src/serve-retire.ts +95 -0
  193. package/src/serve-web.ts +31 -15
  194. package/src/serve.ts +27 -10
  195. package/src/server-barrels.ts +6 -3
  196. package/src/shot-browser.ts +1 -1
  197. package/src/shot-locale.ts +6 -33
  198. package/src/shot-settle.ts +3 -3
  199. package/src/shot-theme.ts +1 -1
  200. package/src/shot-viewport.ts +152 -0
  201. package/src/signal-shred.ts +26 -12
  202. package/src/site-asset-routes.ts +2 -2
  203. package/src/site-assets.ts +3 -5
  204. package/src/site-config.ts +9 -48
  205. package/src/site-seo.ts +7 -5
  206. package/src/sitemap-lastmod.ts +5 -0
  207. package/src/solid-loader.ts +12 -3
  208. package/src/source-files.ts +19 -3
  209. package/src/static-report.ts +5 -0
  210. package/src/stored-object-headers.ts +43 -0
  211. package/src/style-routes.ts +2 -2
  212. package/src/sw-artifacts.ts +8 -8
  213. package/src/sw-precache-plan.ts +34 -4
  214. package/src/sync-authenticator.ts +3 -3
  215. package/src/sync-url.ts +3 -3
  216. package/src/tasks-facts.ts +3 -29
  217. package/src/templates/action.ts +7 -4
  218. package/src/templates/backfill.ts +4 -4
  219. package/src/templates/entity.ts +2 -2
  220. package/src/templates/github/ci.yml.ts +28 -18
  221. package/src/templates/github/image.yml.ts +92 -0
  222. package/src/templates/github/runner.ts +18 -0
  223. package/src/templates/guard-bare-error.ts +16 -2
  224. package/src/templates/guard-unzoned-date.ts +56 -6
  225. package/src/templates/index.ts +1 -1
  226. package/src/templates/job-body-test.ts +2 -2
  227. package/src/templates/job.ts +6 -6
  228. package/src/templates/naming.ts +28 -0
  229. package/src/templates/query.ts +9 -8
  230. package/src/templates/resource-page.ts +9 -7
  231. package/src/templates/resource-service.ts +2 -2
  232. package/src/templates/route.ts +26 -14
  233. package/src/templates/scaffold-api.ts +2 -3
  234. package/src/templates/scaffold-app.ts +8 -3
  235. package/src/templates/scaffold-auth.ts +205 -70
  236. package/src/templates/scaffold-bin.ts +122 -0
  237. package/src/templates/scaffold-claude-commands.ts +11 -11
  238. package/src/templates/scaffold-container-compose.ts +10 -1
  239. package/src/templates/scaffold-container.ts +16 -8
  240. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  241. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  242. package/src/templates/scaffold-dashboard-example.ts +112 -172
  243. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  244. package/src/templates/scaffold-dashboard.ts +9 -5
  245. package/src/templates/scaffold-db-client.ts +4 -2
  246. package/src/templates/scaffold-db-package.ts +2 -2
  247. package/src/templates/scaffold-docs.ts +20 -77
  248. package/src/templates/scaffold-domain-package.ts +0 -4
  249. package/src/templates/scaffold-entries.ts +4 -3
  250. package/src/templates/scaffold-env.ts +6 -3
  251. package/src/templates/scaffold-gitattributes.ts +59 -0
  252. package/src/templates/scaffold-helm-fences.ts +76 -0
  253. package/src/templates/scaffold-helm-templates.ts +100 -19
  254. package/src/templates/scaffold-helm.ts +62 -5
  255. package/src/templates/scaffold-http.ts +1 -1
  256. package/src/templates/scaffold-i18n.ts +15 -2
  257. package/src/templates/scaffold-icon.ts +2 -2
  258. package/src/templates/scaffold-repo.ts +27 -16
  259. package/src/templates/scaffold-shell.ts +55 -37
  260. package/src/test-counts.ts +2 -6
  261. package/src/test-ignores.ts +25 -0
  262. package/src/test-only-shipped.ts +193 -0
  263. package/src/test-passes.ts +0 -15
  264. package/src/test-select.ts +7 -15
  265. package/src/test-shards.ts +40 -95
  266. package/src/test-workers.ts +26 -4
  267. package/src/theme-boot.ts +8 -33
  268. package/src/theme-brand.ts +66 -0
  269. package/src/trusted-hops-error.ts +17 -0
  270. package/src/trusted-hops.ts +49 -0
  271. package/src/unscanned-admin.ts +2 -2
  272. package/src/unthrown-codes.ts +26 -0
  273. package/src/verify-checks.ts +29 -48
  274. package/src/verify-coverage-run.ts +3 -2
  275. package/src/verify-deadline.ts +207 -33
  276. package/src/verify-merge.ts +108 -43
  277. package/src/verify-part-step.ts +98 -0
  278. package/src/verify-role-load-probe.ts +12 -2
  279. package/src/verify-role-load.ts +9 -4
  280. package/src/verify-run.ts +32 -8
  281. package/src/verify-shard.ts +13 -2
  282. package/src/verify-stalled.ts +152 -0
  283. package/src/verify-step.ts +2 -0
  284. package/src/verify-test-run.ts +3 -2
  285. package/src/verify-tests.ts +5 -3
  286. package/src/worker-bundle.ts +2 -2
  287. package/src/workspace-checks.ts +53 -7
  288. package/src/workspace-graph.ts +3 -2
  289. package/src/write-line.ts +82 -7
  290. package/src/browser-launcher-fake-html.ts +0 -80
  291. package/src/browser-launcher-fake.ts +0 -168
  292. package/src/build-errors.ts +0 -54
  293. package/src/jobs-drain.ts +0 -163
  294. package/src/scaffold-typecheck.ts +0 -227
  295. package/src/thrown-by.ts +0 -24
  296. package/src/verify-errors.ts +0 -58
package/CLAUDE.md CHANGED
@@ -44,25 +44,25 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
44
44
  |---|---|
45
45
  | `verify-checks.ts` / `verify-step.ts` / `verify-run.ts` | the step list, the outcome shape, the run order (`BESIDE_SERIAL_SUITES`) |
46
46
  | `verify-floor.ts` | `x.verify.json`: a floor step that ran zero tests is `X_VERIFY_SUITE_VANISHED` |
47
- | `coverage-floor.ts` / `coverage-lcov.ts` / `coverage-source.ts` / `verify-coverage-run.ts` | an APP's `unit` step holds `coverage` in `x.verify.json` over its whole source tree. The suite runs as fixed slices, one plain `bun test` each — never `--parallel --coverage`, whose merge moves with the core count. `coverage-lcov.ts` is the ONE lcov reader; `scripts/coverage-gate.ts` reads through it. A shard defers to `verify-merge.ts` (`coverageRiders`); `doctor-coverage.ts` lists the excludes |
48
- | `verify-deadline.ts` / `verify-progress.ts` | a step past its deadline is `X_VERIFY_STEP_TIMEOUT`: its children carry `ULTIMATE_VERIFY_STEP` and are killed by that tag; `--json` streams a line per finished step to stderr. A `fix:` names `ctx.command` — `bun run verify` at the framework root |
47
+ | `coverage-floor.ts` / `coverage-lcov.ts` / `coverage-source.ts` / `verify-coverage-run.ts` | an APP's `unit` step holds `coverage` in `x.verify.json` over its whole source tree. The suite runs as fixed slices, one plain `bun test` each — never `--parallel --coverage`, whose merge moves with the core count. `coverage-lcov.ts` is the ONE lcov reader; `scripts/coverage-gate.ts` reads through it. A shard defers to `verify-merge.ts` (`coverageRiders`), which also holds the shards' `files` to a partition of the corpus they hashed and reads each part step through `verify-part-step.ts`; `doctor-coverage.ts` lists the excludes |
48
+ | `verify-deadline.ts` / `verify-stalled.ts` / `verify-progress.ts` | a step past its deadline is `X_VERIFY_STEP_TIMEOUT`: its children carry `ULTIMATE_VERIFY_STEP` and are killed by that tag; the finding names the test file `bun test` had not finished (`at`, `meta.inFlight`) and its `fix:` runs that file; `--json` streams a line per finished step to stderr. A `fix:` names `ctx.command` — `bun run verify` at the framework root |
49
49
  | `load-findings.ts` | module-load failures are reported once, by `manifest`; other steps point there |
50
50
  | `boundary-findings.ts` / `app-boundaries.ts` / `boundary-cuts.ts` | surface + layer rules; a finding's `fix:` is the concrete cut |
51
51
  | `app-transport.ts` / `browser-transport.ts` / `transport-calls.ts` / `import-closure.ts` / `server-barrels.ts` | `boundaries`, built in: one browser transport. `app-transport.ts` reuses the files `readAppSources` read (no second walk), resolves through the app's tsconfig `paths` and workspace manifests, reads the app's own `packages/*` lazily, and STOPS at `node_modules`. `browser-transport.ts` is the pure rule — `scripts/browser-transport.ts` calls the same function with this repo's seam FILES. Never a `guards/` file: an app must not be able to delete it |
52
52
  | `guards.ts` / `guard-sources.ts` | an app's `guards/*.ts`, discovered, run on `boundaries`, held to the error contract; every `check(root, sources)` shares ONE walk, read and Sass compile per file (`GuardSources`) |
53
53
  | `templates/scaffold-guards.ts` | `SHIPPED` — the one list of shipped guards: `x new` writes all, `x g guard <name>` writes one, `doctor-guards.ts` lists the ones an app lacks (a listing, never a finding). A new one is a `templates/guard-<name>.ts` + a row + `guard-<name>.test.ts`; `scaffold-guards-style.test.ts` holds embedded scales to `@ultimat3/ui`, the framework's own sheets to the rules, and both tracked apps byte-equal to the templates |
54
54
  | `error-contract.ts` / `ts-scan.ts` / `fix-scan.ts` / `fix-imports.ts` / `fix-command.ts` / `fix-path.ts` | the `errors` step: every `fix:` names a runnable command, call or existing file |
55
- | `workspace-checks.ts` / `workspace-graph.ts` / `tsconfig-references.ts` / `publish-closure.ts` | `package-shape`, `filesize`. A published package's `files` negates `*.test.ts` and `*-fixture.ts`, and no entry point (`exports`, `bin`) may reach a `-fixture.ts` — `X_PACKAGE_SHAPE` names the file to rename |
56
- | `app-permissions.ts` / `permission-grants.ts` | `policy`: every permission granted or required is declared, and every one required is granted by some role (`X_PERMISSION_UNGRANTED`) |
55
+ | `workspace-checks.ts` / `test-only-shipped.ts` / `workspace-graph.ts` / `tsconfig-references.ts` / `publish-closure.ts` | `package-shape`, `filesize`. A published package's `files` negates `*.test.ts` and `*-fixture.ts`, and no entry point (`exports`, `bin`) may reach a `-fixture.ts` — `X_PACKAGE_SHAPE` names the file to rename; a `src/` module only tests import must be `*-fixture.ts` (`X_PACKAGE_TEST_ONLY_SHIPPED`) |
56
+ | `app-permissions.ts` / `permission-grants.ts` / `app-permissions-borrowed.ts` / `app-permissions-site.ts` | `policy`: every permission granted or required is declared, every one required is granted by some role (`X_PERMISSION_UNGRANTED`), and every one an app rule requires is declared by an app module, not only by a package (`X_PERMISSION_BORROWED`, read off `permissionDeclarationSites()`) |
57
57
  | `job-registration.ts` | `manifest`: no job or task under a positional `anonymous-*` name (`X_JOB_UNREGISTERED`) |
58
58
  | `unscanned-admin.ts` | `manifest`: a `defineAdmin()` outside the app scan (`apps/*/src/`) is `X_ADMIN_UNSCANNED`, with the `git mv` that mounts it |
59
59
  | `verify-role-load.ts` / `verify-role-load-probe.ts` | `manifest`: a child imports the app as a worker does, then the rest module by module; a registration only a document-reaching module makes is `X_ROLE_LOAD_INCOMPLETE` with the module and the import the API index lacks |
60
- | `async-pages.ts` / `live-routes.ts` / `budgets.ts` | `budgets` riders: an async `Page` with no `load`, a live read no island imports, bytes per route |
61
- | `app-agents-md.ts` / `app-env.ts` / `app-openapi.ts` / `app-manifest.ts` | `manifest` and `contract-diff` |
62
- | `schema-drift.ts` / `drift.ts` / `db-destructive.ts` / `db-ungeneratable.ts` | `drift`: snapshot vs declarations, the source hash (core's `canonicalJson`), the header markers |
60
+ | `async-pages.ts` / `live-routes.ts` / `budgets.ts` / `budgets-unmeasured.ts` | `budgets` riders: an async `Page` with no `load`, a live read no island imports, bytes per route; an unweighed route's finding names a refused measurement actor and `defineMeasurementActor()` |
61
+ | `app-agents-md.ts` / `app-env.ts` / `app-openapi.ts` / `app-manifest.ts` | `manifest` and `contract-diff`. `framework-env.ts` is the ONE list of framework-required deploy secrets `.env.example`, `x env check` and `x doctor` read |
62
+ | `schema-drift.ts` / `drift.ts` / `drift-replica-identity.ts` / `db-destructive.ts` / `db-ungeneratable.ts` | `drift`: snapshot vs declarations (`REPLICA IDENTITY FULL` a params channel needs included — never a `subscribes:` table, #518), the source hash (core's `canonicalJson`), the header markers |
63
63
  | `schema-dump-drift.ts` | `drift`'s fourth rail, the only one that boots a database: committed `packages/db/schema/` vs a scratch replay, and that dump loaded back (`X_SCHEMA_DUMP_DRIFT`) |
64
64
  | `i18n-registration.ts` / `i18n-audit.ts` / `admin-catalog-keys.ts` | `i18n`; a mounted admin's keys are `AdminApp.catalogKeys()`, asked of every app catalog — never re-derived here (`X_CATALOG_MISSING_KEYS`) |
65
- | `error-unthrown.ts` | host check: a registered code nothing throws must say so |
65
+ | `error-unthrown.ts` / `unthrown-codes.ts` | host check: a registered code nothing throws is listed in `UNTHROWN_CODES` (a row's wording waives nothing); a listed code thrown again is `X_ERROR_CODE_UNTHROWN_STALE` |
66
66
 
67
67
  ### Generators (`x g`, `x new`)
68
68
 
@@ -70,6 +70,10 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
70
70
  |---|---|
71
71
  | `cmd-generate.ts` / `generate-files.ts` / `generate-kinds.ts` / `generate-write.ts` | argv → pure file list → writes (conflict-checked, `merge: 'json'` / `'if-absent'`) |
72
72
  | `generate-feature.ts` | a `--feature` naming no slice is `X_FEATURE_UNKNOWN`; nothing invents an entity |
73
+ | `generate-shadow.ts` | a name whose type spelling a planned file also uses as a global or local type (`promise`, `row`), or whose camel VALUE a planned file both imports and declares (`x g job job`, TS2440), is `X_CLI_BAD_FLAG`, decided on the planned files |
74
+ | `generate-plural.ts` | a planned `entity.ts` in an already-plural slice (`posts` → `postses`) is `X_CLI_BAD_FLAG`; the fix names the singular |
75
+ | `generate-catalog-locales.ts` | `--locales` never starts a catalog in an app with any: a missing non-default locale is `X_CLI_BAD_FLAG`, `fix: x i18n add <locale>`; non-default values land `⟦…⟧`-marked (`seedCatalog`) |
76
+ | `app-name.ts` | `x new`'s slug is asked of core's `defineConfig`, never a copied pattern; refused with a suggested name (`9lives` → `app-9lives`, `a` → `a-app`, cut at 64) |
73
77
  | `generate-grants.ts` | a written `policy.ts` grants `:read` to `member`, `:write` to `admin` in `apps/web/shared/roles.ts`; a written `entity.ts` declares its table's `<table>:read\|write\|delete` there and grants all three to `admin` — what the admin asks for the screen that entity just became |
74
78
  | `api-registration.ts` | a written job or task is listed in `apps/web/api/index.ts` |
75
79
  | `handle-registration.ts` / `workspace-dep-edit.ts` / `templates/scaffold-db-client.ts` | a written entity joins `const entities = { … }` in `packages/db/src/client.ts` (the typed handle every generated `repo.ts` reads), with the two manifest lines the new imports need; no anchor is `X_DB_HANDLE_UNREGISTERED` carrying the lines. `cmd-generate.ts`'s `nextSteps` prints what the new table owes |
@@ -85,23 +89,33 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
85
89
  | `serve.ts` / `serve-boot.ts` / `serve-entry.ts` / `serve-env.ts` | the production entry: roles from `ROLE`, bindings from env, `ROLE=migrate` = `x db migrate`. `serve-boot.ts` (services + roles) is ONE `await import()` in `serveApp`, so `migrate` loads neither |
86
90
  | `role-load.ts` / `document-graph.ts` / `serve-web.ts` | a role imports what it runs: `ROLE_LOADS` (declared over `Role`) — `worker`/`scheduler` import the API index and every module reaching no `.tsx`/stylesheet, verified against `x.manifest.json`'s jobs and tasks (`X_ROLE_LOAD_INCOMPLETE` imports everything); `serve-web.ts` is the web surface, behind `await import()`. `scanAppModules(root, { track: false })` is the container's scan — no reload graph, no error-code walk. Per-role module pins and `NEVER_AT_BOOT`: `serve-graph.test.ts` |
87
91
  | `module-imports.ts` | the files a module's source imports, resolved once for both graphs: `referenced` (reload) and `evaluated` (document) |
88
- | `cmd-dev.ts` / `dev-route-table.ts` | `x dev`: every role in one process, `/_x`, the watcher |
92
+ | `cmd-dev.ts` / `dev-boot.ts` / `dev-rebuild.ts` / `dev-route-table.ts` | `x dev`: the command (`cmd-dev.ts`, re-exports `startDev`); the boot — every role in one process, `/_x`, the watcher (`dev-boot.ts`); one save rebuilt as one generation (`dev-rebuild.ts`) |
89
93
  | `runtime-bindings.ts` | which service each binding points at (embedded or external); events follow `realtime.transport` / `urlEnv` |
90
94
  | `runtime-queue.ts` / `runtime-services.ts` | the db + queue pair; everything else and every ambient accessor |
91
- | `runtime-jobs.ts` / `runtime-realtime.ts` / `runtime-notify-retention.ts` / `runtime-cache.ts` / `runtime-purge.ts` / `runtime-replica.ts` | `app.config.ts` sections the boot obeys, and what each wires |
95
+ | `framework-schema-apply.ts` / `framework-schema-stamp.ts` | the framework tables: `ROLE=migrate`, `x dev` and every CLI command APPLY them in one transaction behind `MIGRATION_LOCK_KEY` (`pg_advisory_xact_lock`) with a bounded `lock_timeout`, then stamp the build on `x_jobs`' table comment; a serving role on an external database runs no DDL and VERIFIES the stamp (`X_FRAMEWORK_SCHEMA_UNAPPLIED`, a major skew is a warning). A comment, never a table: a new framework table moves every app's schema dump |
96
+ | `runtime-isr.ts` | the ISR controller both boots serve through, ATTACHED to `invalidateTags` and released on stop (`attachedIsr`); `appRoutes` without one builds and attaches its own |
97
+ | `runtime-mfa-warning.ts` / `runtime-idempotency-scope.ts` | boot warnings an operator can act on: unsealed MFA secrets (web role, `x auth seal-mfa`), a per-process idempotency store after the app loaded |
98
+ | `app-config-load.ts` | the ONE reader of an app's `app.config.ts`: `loadAppConfig(root)` — the `config` export through core's `defineConfig`, so validated and default-merged; `undefined` with no file; no `config` object is `X_CONFIG_INVALID`. Every boot fact (`app-auth.ts`, `theme-boot.ts`, `site-config.ts`, `pwa-artifacts.ts`, …) reads a section off it, and `startServices` loads it once. An `import(` of the config module anywhere else is `X_CONFIG_IMPORT_OUTSIDE_LOADER` (`scripts/lib/config-import.ts`) |
99
+ | `theme-brand.ts` | the ONE reader of an app's theme: `export const brand` in `apps/web/shared/theme.ts` (ui's `defineTheme`), inlined after the surface stylesheet with its hash in `style-src`; none → nothing emitted |
100
+ | `runtime-jobs.ts` / `runtime-realtime.ts` / `runtime-notify-retention.ts` / `runtime-cache.ts` / `runtime-purge.ts` / `runtime-replica.ts` | `app.config.ts` sections the boot obeys (`*Of(config)` over the one loaded config), and what each wires |
101
+ | `runtime-jobs.ts` (queues) | union of `jobs.queues` + registered queues; `WORKER_QUEUES` set = the EXACT set, `jobs.worker.queue-unserved` names the rest. `jobs.concurrency` passes as written |
102
+ | `serve-retire.ts` / `dev-restart-retire.ts` | the worker retire — `stop()`: claim nothing, abort nothing — on SIGUSR2 under `runRole` (armed before boot, then drain + exit 0; none on win32; the chart's `roles.worker.retireSeconds` preStop sends it), and before a supervised `x dev` restart's drain (#677) |
92
103
  | `role-start.ts` / `role-start-types.ts` / `role-realtime.ts` | `--role` selection and start/stop for `web`, `sync`, `worker`, `scheduler`; `realtime.enabled: false` drops `sync` and `replicator` |
93
104
  | `role-wake.ts` | the worker's LISTEN session (`@ultimat3/jobs`' `startQueueWake`) — worker role only, only over a client that `canListen` — and `queue_wake_live` |
94
105
  | `role-sync.ts` / `role-replicator.ts` / `runtime-live-feed.ts` | the sync node, the change feed |
95
- | `runtime-render.ts` / `runtime-assets.ts` / `runtime-storage.ts` / `runtime-hooks.ts` / `api-routes.ts` | the HTTP surface: pages, `/icons` + `/media`, `/_storage`, authz, the app's API — plus its bearer mounts (`apiMountRoutes`) and pages' bound `POST`s (`pagePostRoutes`), mounted by BOTH boots |
106
+ | `runtime-render.ts` / `runtime-isr-outcome.ts` / `route-islands.ts` / `runtime-assets.ts` / `runtime-storage.ts` / `runtime-hooks.ts` / `api-routes.ts` | the HTTP surface: pages (an `isr` hit never runs `load`, and a `load` redirect is answered per request, never stored or shared — `runtime-isr-outcome.ts`; `route-islands.ts` is a render's island collector and realtime boot), `/icons` + `/media`, `/_storage`, authz, the app's API — plus its bearer mounts (`apiMountRoutes`) and pages' bound `POST`s (`pagePostRoutes`), mounted by BOTH boots |
107
+ | `stored-object-headers.ts` | how `/_storage` and `/media` present a stored object: inline only for raster images, audio and video; every other type (`text/html`, SVG, XML, PDF) is `content-disposition: attachment` with a `sandbox` CSP, which http's security stage keeps beside the app's |
96
108
  | `runtime-overrides.ts` | the one field a host hands the framework a driver through — and `routes`, the plain routes for a wire format no primitive speaks (OAuth token endpoint) |
97
109
  | `app-openapi.ts` | `openapi.json` (complete when `defineApi({ openapi })` is declared) and each bearer mount's own document; staleness for all of them |
98
110
  | `script-csp.ts` / `style-csp.ts` / `style-bundle.ts` / `page-sync.ts` / `worker-bundle.ts` | CSP hashes, the CSS file, the page's sync target and worker |
99
111
  | `page-navigation.ts` | the client router for `navigation.client` surfaces — built, routed and named ONCE for `x dev`, the container and the static export; none opted in builds nothing |
100
112
  | `page-speculation.ts` | Speculation Rules for documents WITHOUT the router (`navigation.speculation`): the allow-list of pure-read pages, the one tag, and its `script-src` hash — composed once for all three writers. An allow-list, never "every link but": a prefetch is a real GET with cookies |
101
113
  | `static-document.ts` | a served `static` page: `304` on a matching `If-None-Match`, and (container only, `memoStatic`) the document kept after its first render. Never `x dev` — a save moves the stylesheet URL the kept document names |
102
- | `island-bundle.ts` / `island-store.ts` / `island-realtime.ts` / `solid-loader.ts` | islands: one `Bun.build` each, source-addressed; the image build writes a verified store the container loads |
114
+ | `island-bundle.ts` / `island-store.ts` / `island-sources.ts` / `island-realtime.ts` / `island-runtime.ts` / `solid-loader.ts` | islands: one `Bun.build` each, source-addressed; the image build writes a verified store the container loads — each chunk's recorded source files (`island-sources.ts`) are rehashed at read, so an edit since the build is a rebuild, never the old chunk. `island-runtime.ts` builds realtime's page runtime ONCE per build as `/islands/page-runtime.<id>.js` (resolved through `@ultimat3/realtime/page-runtime`), which a realtime island awaits where no page boot is rendered |
115
+ | `local-cli.ts` / `local-cli-handoff.ts` | a global `x` hands over to the app's own CLI (`local-cli.ts` decides whether); the hand-off forwards SIGTERM/SIGHUP at once and a SIGINT only if the child is still running after a 1 s grace (a terminal Ctrl-C already reached it), and exits with the child's code |
103
116
  | `serve-prebuilt.ts` / `serve-prebuilt-paths.ts` | the image's prebuilt store (`node_modules/.cache/ultimate`, never `.x/` — state, tmpfs): every role adopts its compiled stylesheets before importing the app; a boot that built an island or ran Sass logs `X_IMAGE_NOT_PREBUILT` once |
104
117
  | `dev-*.ts` | `x dev` only: dashboard sources, traces, the N+1 ledger, the watcher, the reload, the lock, the port |
118
+ | `dev-dashboard-guard.ts` / `loopback-host.ts` | `/_x` answers a loopback `Host` only (`X_DEV_HOST_REFUSED`, 421 — DNS rebinding); the SQL panel runs on a same-origin `POST /_x/db` only, through admin's `readOnlySql`. Checked in the route, never left to the app's `csrf` config |
105
119
 
106
120
  ### Build and data
107
121
 
@@ -118,7 +132,8 @@ Commands: `bun test packages/cli` (from the repo root — the test preload lives
118
132
  | File | Job |
119
133
  |---|---|
120
134
  | `cmd-registries.ts` / `cmd-jobs.ts` / `cmd-tasks.ts` / `cmd-policy.ts` / `cmd-i18n.ts` | project a framework registry; each pairs CLI wiring with a facts module |
121
- | `cmd-mcp.ts` / `mcp-host.ts` / `mcp-errors.ts` / `mcp-db-target.ts` | `x mcp serve`: 18 tools, two transports |
135
+ | `cmd-auth.ts` / `cmd-auth-spec.ts` / `doctor-auth.ts` | `x auth seal-mfa`: the one-shot that seals plaintext `x_users.mfa_secret` through auth's `sealMfaSecrets` (compare-and-set per row; `{ sealed, alreadySealed, skipped }`), over `startQueue`'s client, `open` injectable. `doctor-auth.ts` is the deploy-time half: unsealed secrets and retired framework tables (`RETIRED_TABLE_FIXES`, a `Map` holding a whole literal per table), asked of an external `DATABASE_URL` only |
136
+ | `cmd-mcp.ts` / `mcp-host.ts` / `mcp-errors.ts` / `mcp-db-target.ts` | `x mcp serve`: 18 tools, two transports; a taken port is `X_PORT_IN_USE`. `mcp-test-run.ts` folds `bun test`'s `N errors` line and exit code into `tests.run` |
122
137
  | `cmd-shot*.ts` / `cdp-shot-*.ts` / `browser-launcher*.ts` / `island-*` | `x shot` over raw CDP; `verdict.json` names its own blind spots. Not a gate step |
123
138
  | `cmd-pr.ts` / `cmd-ci.ts` | GitHub through `gh`, parsed against a schema. Not gate steps |
124
139
  | `error-catalog.ts` | imports every `@ultimat3/*` package so `x errors` answers for any code |
package/README.md CHANGED
@@ -16,14 +16,15 @@ Commands and the `x verify` step count, `As of 2026-08`:
16
16
  | `x g <primitive> <name>` | scaffolds a primitive **with a passing test** | never a TODO stub |
17
17
  | `x db gen\|migrate\|reset\|branch\|backfill` | everything DB | `branch` = copy-on-write clone + preview URL; `backfill` dry-runs unless `--write`. `x db studio` is **planned** — it parses, and exits `X_NOT_IMPLEMENTED` naming `/_x`'s db panel |
18
18
  | `x mcp serve` | `@ultimat3/mcp`'s 18 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
19
- | `x doctor` | environment, ports, drift, PWA prerequisites | every finding carries a fix command |
19
+ | `x doctor` | environment, ports, drift, PWA prerequisites, `APP_URL` | every finding carries a fix command; probes the port `x dev` binds ([which one](../../wiki/CLI-Reference.md#x-doctor)); the framework's deploy secrets only where the machine names `staging`/`production` |
20
20
  | `x deploy` | container deploy plan | compose or helm; zero platform primitives |
21
21
  | `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
22
22
  | `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `describe <name>`, straight off the registries |
23
23
  | `x tasks list\|show` | cron tasks | timezone and next run, off `registeredTasks()` |
24
- | `x jobs ls\|show\|retry\|cancel\|drain` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`, `drain --to` |
24
+ | `x jobs ls\|show\|retry\|cancel\|rm\|promote\|pause\|resume` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`; `ls --json` pages as `rows`/`nextCursor`/`hasMore`; `drain` is planned (`X_NOT_IMPLEMENTED`) |
25
25
  | `x test [type]` | one of the six test types, or all | same type rule as the gate; `--filter`, `--sample N` |
26
- | `x env check\|example` | the typed environment `envSchema` declares | and the `.env.example` rendered from it |
26
+ | `x env check\|example` | the typed environment `envSchema` declares | and the `.env.example` rendered from it, plus the framework's deploy-required secrets (`ULTIMATE_CURSOR_SECRET`, `STORAGE_SIGNING_SECRET` unless `S3_ENDPOINT` is set), which `check` asks for wherever the boot refuses them — outside `development`/`test`, or with no environment named |
27
+ | `x auth seal-mfa` | one-shot auth maintenance | seals every `x_users.mfa_secret` still in the clear; idempotent, `{ sealed, alreadySealed, skipped }` |
27
28
  | `x secrets show\|init\|edit\|set\|rotate` | the committed encrypted secrets | decrypted into the `envSchema` variables of the same names |
28
29
  | `x policy list\|explain <subject>` | which clause decided a permission, and why | five packages print `x policy explain` as a denial's `fix:` |
29
30
  | `x i18n check\|add\|sync` | catalogs: gaps, a new locale, key sync | all three of i18n's own error fixes name it |
@@ -35,6 +36,31 @@ Everything in [CLI reference](../../wiki/CLI-Reference.md)'s planned table is al
35
36
  and exits `X_NOT_IMPLEMENTED` with a `fix:` naming the closest shipped command — "not built yet"
36
37
  and "not a command" are different facts.
37
38
 
39
+ ## `x build`: who a weighed page renders as
40
+
41
+ `x build --target static` renders every budgeted `app/` route it cannot publish only to weigh it,
42
+ then discards the document. That render runs as core's `measurementActor()`: the actor
43
+ `defineMeasurementActor()` declares in `app.config.ts`, else a **service actor holding `*` with no
44
+ `orgId` and no roles**. A page whose policy names a role, or whose `load` reads a tenant-scoped
45
+ entity, is refused under the default, and the `budgets` step reports it as `X_BUDGET_UNMEASURED`
46
+ with the actor, the refusal's code and cause (`X_FORBIDDEN`, `X_UNAUTHENTICATED`,
47
+ `X_TENANCY_ACTOR_ORG_REQUIRED`), and the edit that weighs it:
48
+
49
+ ```ts
50
+ // app.config.ts — the one file every build imports; never used for a published site/ artifact
51
+ import { defineMeasurementActor, userActor } from '@ultimat3/core';
52
+
53
+ /** The org your dev seed creates. */
54
+ const DEMO_ORG_ID = '00000000-0000-4000-8000-000000000001';
55
+
56
+ defineMeasurementActor(() => userActor({ id: 'measure', orgId: DEMO_ORG_ID, roles: ['dev'] }));
57
+ ```
58
+
59
+ Pick the org your dev seed creates and the most-privileged role your gated pages require: the
60
+ weigh measures the page a holder of that role is served. Any other refusal (a missing database, a
61
+ `TypeError` in `load`) keeps the generic `fix:` — the `unmeasured` list of
62
+ `x build --target static --json` — with the code already in the cause.
63
+
38
64
  ## Which `x` runs
39
65
 
40
66
  **The app's own.** `x` is a workspace dependency, and `bunx x` / the `package.json` scripts resolve
@@ -135,7 +161,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
135
161
  | `cmd-*.ts` | one command group each |
136
162
  | `templates/` | scaffolding as typed string modules, not copied fixtures |
137
163
  | `app-load.ts` | import an app's modules so the framework registries hold it |
138
- | `app-mcp.ts` | the app's own MCP endpoint: `apps/<app>/mcp.ts` exports `mcp`, and both boots mount `POST config.ai.mcp.path` through this one call |
164
+ | `app-mcp.ts` | the app's own MCP endpoint: `apps/<app>/mcp.ts` exports `mcp`, and both boots mount `POST` at each endpoint's own `defineAppMcp({ path })` through this one call |
139
165
  | `app-runtime.ts` | the app's `RuntimeOverrides`: `apps/<app>/runtime.ts` exports `runtime`, read by `x dev` and by `runRole` when its caller passed none |
140
166
  | `local-cli.ts` | which `x` runs: a global CLI inside an app hands over to the app's own, because a second module instance is an empty registry |
141
167
  | `measurement-actor.ts` | the actor a weigh-and-discard render runs as — every permission, never served |
@@ -145,7 +171,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
145
171
  | `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
146
172
  | `app-transport.ts` / `browser-transport.ts` | one browser transport, on `boundaries` in every app: a raw `fetch(`, `new WebSocket(` or `new XMLHttpRequest(` in an island's closure is `X_BROWSER_TRANSPORT_BYPASS`; a server barrel there is `X_BROWSER_SERVER_BARREL` |
147
173
  | `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
148
- | `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each |
174
+ | `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each. `ROLE=worker` also retires on SIGUSR2 (`serve-retire.ts`, 25.0.0): stop claiming, finish every held job, abort nothing, drain, exit 0 — what the chart's opt-in `roles.worker.retireSeconds` `preStop` sends; a no-op on Windows ([Deployment](../../wiki/Deployment.md#retiring-a-worker-before-sigterm)) |
149
175
  | `prerender.ts` | `x build --target static`: which `site/` routes qualify, and where the bytes land |
150
176
  | `metrics-endpoint.ts` | the `METRICS_PATH` scrape listener every role opens, on `METRICS_PORT`; opening it starts the `process_*` series, labelled by `startMetricsEndpoint({ role })` |
151
177
  | `role-load.ts` / `document-graph.ts` / `serve-web.ts` | what each container role imports: `web`, `sync` and `replicator` every app module; `worker` and `scheduler` the API index plus every module that reaches no component or stylesheet |
@@ -192,14 +218,13 @@ a job boundary the class is gone and the `code` is what survives — match on th
192
218
  | Class | Code | Declared in |
193
219
  |---|---|---|
194
220
  | `BadFlagError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
195
- | `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/build-errors.ts` |
221
+ | `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/cmd-build.ts` |
196
222
  | `BunVersionError` | `X_BUN_VERSION` | `src/errors.ts` |
197
223
  | `CatalogExistsError` | `X_GENERATE_CONFLICT` | `src/errors.ts` |
198
224
  | `CdpBrowserMissingError` | `X_CDP_BROWSER_MISSING` | `src/cdp-errors.ts` |
199
225
  | `CdpCallFailedError` | `X_CDP_CALL_FAILED` | `src/cdp-errors.ts` |
200
226
  | `CdpLaunchFailedError` | `X_CDP_LAUNCH_FAILED` | `src/cdp-errors.ts` |
201
227
  | `CdpTimeoutError` | `X_CDP_TIMEOUT` | `src/cdp-errors.ts` |
202
- | `CliNotImplementedError` | `X_NOT_IMPLEMENTED` | `src/errors.ts` |
203
228
  | `DeclarationUnknownError` | `X_DECLARATION_UNKNOWN` | `src/errors.ts` |
204
229
  | `E2eAppFailedError` | `X_E2E_APP_FAILED` | `src/e2e-errors.ts` |
205
230
  | `E2eEvaluateCapturedError` | `X_E2E_EVALUATE_CAPTURED` | `src/e2e-errors.ts` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "23.0.0",
3
+ "version": "25.0.0",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "engines": {
33
- "bun": ">=1.4.0"
33
+ "bun": ">=1.4.2"
34
34
  },
35
35
  "scripts": {
36
36
  "typecheck": "tsc --noEmit -p tsconfig.json",
@@ -39,33 +39,33 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@babel/core": "^8.0.1",
42
- "@ultimat3/action": "23.0.0",
43
- "@ultimat3/admin": "23.0.0",
44
- "@ultimat3/ai": "23.0.0",
45
- "@ultimat3/auth": "23.0.0",
46
- "@ultimat3/cache": "23.0.0",
47
- "@ultimat3/core": "23.0.0",
48
- "@ultimat3/db": "23.0.0",
49
- "@ultimat3/entity": "23.0.0",
50
- "@ultimat3/flags": "23.0.0",
51
- "@ultimat3/http": "23.0.0",
52
- "@ultimat3/i18n": "23.0.0",
53
- "@ultimat3/jobs": "23.0.0",
54
- "@ultimat3/mail": "23.0.0",
55
- "@ultimat3/manifest": "23.0.0",
56
- "@ultimat3/mcp": "23.0.0",
57
- "@ultimat3/money": "23.0.0",
58
- "@ultimat3/notify": "23.0.0",
59
- "@ultimat3/policy": "23.0.0",
60
- "@ultimat3/pwa": "23.0.0",
61
- "@ultimat3/query": "23.0.0",
62
- "@ultimat3/realtime": "23.0.0",
63
- "@ultimat3/render": "23.0.0",
64
- "@ultimat3/schema": "23.0.0",
65
- "@ultimat3/seo": "23.0.0",
66
- "@ultimat3/storage": "23.0.0",
67
- "@ultimat3/testing": "23.0.0",
68
- "@ultimat3/time": "23.0.0",
42
+ "@ultimat3/action": "25.0.0",
43
+ "@ultimat3/admin": "25.0.0",
44
+ "@ultimat3/ai": "25.0.0",
45
+ "@ultimat3/auth": "25.0.0",
46
+ "@ultimat3/cache": "25.0.0",
47
+ "@ultimat3/core": "25.0.0",
48
+ "@ultimat3/db": "25.0.0",
49
+ "@ultimat3/entity": "25.0.0",
50
+ "@ultimat3/flags": "25.0.0",
51
+ "@ultimat3/http": "25.0.0",
52
+ "@ultimat3/i18n": "25.0.0",
53
+ "@ultimat3/jobs": "25.0.0",
54
+ "@ultimat3/mail": "25.0.0",
55
+ "@ultimat3/manifest": "25.0.0",
56
+ "@ultimat3/mcp": "25.0.0",
57
+ "@ultimat3/money": "25.0.0",
58
+ "@ultimat3/notify": "25.0.0",
59
+ "@ultimat3/policy": "25.0.0",
60
+ "@ultimat3/pwa": "25.0.0",
61
+ "@ultimat3/query": "25.0.0",
62
+ "@ultimat3/realtime": "25.0.0",
63
+ "@ultimat3/render": "25.0.0",
64
+ "@ultimat3/schema": "25.0.0",
65
+ "@ultimat3/seo": "25.0.0",
66
+ "@ultimat3/storage": "25.0.0",
67
+ "@ultimat3/testing": "25.0.0",
68
+ "@ultimat3/time": "25.0.0",
69
69
  "babel-preset-solid": "^1.9.15"
70
70
  }
71
71
  }
@@ -2,7 +2,7 @@
2
2
  // walked here — the admin owns the one derivation, so a key `defineAdmin` would draw and the
3
3
  // `i18n` step audits can never be two spellings of one rule.
4
4
 
5
- import { describeRoutes } from '@ultimat3/render';
5
+ import { describePages } from '@ultimat3/render';
6
6
 
7
7
  /**
8
8
  * The keys of every admin this process declared. Asked of the route table first: an app with no
@@ -10,7 +10,7 @@ import { describeRoutes } from '@ultimat3/render';
10
10
  * the dynamic import below is a cache hit.
11
11
  */
12
12
  export async function mountedAdminKeys(): Promise<readonly string[]> {
13
- if (!describeRoutes().some((route) => route.mount?.by === 'defineAdmin')) return [];
13
+ if (!describePages().some((route) => route.mount?.by === 'defineAdmin')) return [];
14
14
  const { adminMounts } = await import('@ultimat3/admin');
15
15
  return [...new Set(adminMounts().flatMap((app) => app.catalogKeys()))].sort();
16
16
  }
package/src/affected.ts CHANGED
@@ -25,23 +25,6 @@ import { readWorkspaceGraph } from './workspace-graph';
25
25
  /** The branch a change is measured against when `--base` says nothing. */
26
26
  export const DEFAULT_BASE = 'main';
27
27
 
28
- /**
29
- * Root files that belong to no workspace and change what every workspace compiles to: a compiler
30
- * option, a lint rule, the root manifest, the resolved dependency tree, the test preload, the app
31
- * config. A "scoped" run that skipped them reports green over packages the edit just broke.
32
- *
33
- * Matched on the WHOLE path — `packages/cli/package.json` is the cli workspace's own file and
34
- * reaches only cli's dependents, `package.json` at the root reaches everything.
35
- */
36
- export const ROOT_WIDE_FILES: readonly string[] = [
37
- 'app.config.ts',
38
- 'biome.json',
39
- 'bun.lock',
40
- 'bunfig.toml',
41
- 'package.json',
42
- 'tsconfig.json',
43
- ];
44
-
45
28
  /**
46
29
  * A file with no compilation unit behind it. A doc or a plan re-checks nothing, so it maps to no
47
30
  * workspace at all rather than to the one it happens to sit inside — `packages/cli/README.md` is
@@ -107,7 +90,13 @@ export interface AffectedPlan {
107
90
  readonly changed: readonly string[];
108
91
  /** The subset with no compilation unit behind it, named so an empty answer explains itself. */
109
92
  readonly ignored: readonly string[];
110
- /** The root files that forced every workspace in, empty when none did. */
93
+ /**
94
+ * The changed files no workspace owns, which forced every workspace in — and the root itself — or
95
+ * empty when none did. A root file (`tsconfig.base.json`, `biome.json`, `bun.lock`, the test
96
+ * preload, `scripts/`, `guards/`) changes what every workspace compiles to or is checked by, and
97
+ * a scoped run that skipped it reports green over packages the edit just broke. Matched on
98
+ * ownership, never on a list of names: a list misses the file nobody thought to add to it.
99
+ */
111
100
  readonly rootWide: readonly string[];
112
101
  readonly workspaces: readonly WorkspaceNode[];
113
102
  }
@@ -124,14 +113,15 @@ export function planAffected(
124
113
  ): AffectedPlan {
125
114
  const ignored = changed.filter(isDoc);
126
115
  const considered = changed.filter((path) => !isDoc(path));
127
- const rootWide = considered.filter((path) => ROOT_WIDE_FILES.includes(path));
128
- if (rootWide.length > 0) {
129
- return { changed, ignored, rootWide, workspaces: [...graph].sort(byName) };
130
- }
116
+ const rootWide: string[] = [];
131
117
  const seeds = new Set<string>();
132
118
  for (const path of considered) {
133
119
  const node = owningWorkspace(graph, path);
134
- if (node !== undefined) seeds.add(node.name);
120
+ if (node === undefined) rootWide.push(path);
121
+ else seeds.add(node.name);
122
+ }
123
+ if (rootWide.length > 0) {
124
+ return { changed, ignored, rootWide, workspaces: [...graph].sort(byName) };
135
125
  }
136
126
  const reached = withDependents(graph, seeds);
137
127
  return {
@@ -191,7 +181,9 @@ export async function gitRoot(runner: Runner, cwd: string, command: string): Pro
191
181
  throw new UltimateError({
192
182
  code: 'X_CLI_UNEXPECTED',
193
183
  cause: `x ${command} reads its diff from git and "git rev-parse --show-toplevel" exited ${result.code} in ${cwd}: ${singleLine(execOutput(result))}`,
194
- fix: `run x ${command} from inside a git checkout — confirm with: git rev-parse --show-toplevel`,
184
+ // Repeating `git rev-parse` HERE cannot help — it is what failed; the fix names how to get INTO
185
+ // a checkout from anywhere, then re-run.
186
+ fix: `cd "$(git -C '<a directory inside your checkout>' rev-parse --show-toplevel)" # then run x ${command} there — ${cwd} is not inside a git checkout`,
195
187
  });
196
188
  }
197
189
 
@@ -263,14 +255,20 @@ export interface AffectedScope {
263
255
  }
264
256
 
265
257
  /**
266
- * An empty prefix means the scan root IS an affected workspace, so every path it yields is in
267
- * scope; a prefix starting with `..` is a workspace outside that root, which has no file there to
268
- * select and must not be allowed to collapse into a match-everything empty string.
258
+ * The plan's scope, relative to the directory the caller scans. A ROOT-WIDE plan is the whole scan
259
+ * root (`''`): every workspace is in it, and so is every file no workspace owns — a `scripts/` test
260
+ * stayed out of the run while `package.json` changed when only workspace directories were listed.
261
+ *
262
+ * Otherwise an empty prefix means the scan root IS an affected workspace, so every path it yields
263
+ * is in scope; a prefix starting with `..` is a workspace outside that root, which has no file there
264
+ * to select and must not be allowed to collapse into a match-everything empty string.
269
265
  */
270
- const scopePrefixes = (root: string, cwd: string, dirs: readonly string[]): readonly string[] =>
271
- dirs
272
- .map((dir) => relative(cwd, join(root, dir)).split('\\').join('/'))
273
- .filter((path) => !path.startsWith('..'));
266
+ export const scopePrefixes = (plan: AffectedPlan, root: string, cwd: string): readonly string[] =>
267
+ plan.rootWide.length > 0
268
+ ? ['']
269
+ : plan.workspaces
270
+ .map((workspace) => relative(cwd, join(root, workspace.dir)).split('\\').join('/'))
271
+ .filter((path) => !path.startsWith('..'));
274
272
 
275
273
  export const inScope = (path: string, prefixes: readonly string[]): boolean =>
276
274
  prefixes.some((prefix) => prefix === '' || path === prefix || path.startsWith(`${prefix}/`));
@@ -300,11 +298,7 @@ export async function affectedScope(options: AffectedScopeOptions): Promise<Affe
300
298
  selection,
301
299
  root,
302
300
  plan,
303
- prefixes: scopePrefixes(
304
- root,
305
- options.cwd,
306
- plan.workspaces.map((workspace) => workspace.dir),
307
- ),
301
+ prefixes: scopePrefixes(plan, root, options.cwd),
308
302
  };
309
303
  }
310
304
 
@@ -4,8 +4,10 @@
4
4
  // `x.manifest.json` and in every dead-letter trace — under a green gate (plan 101 slice 11 b).
5
5
  // Actions and queries are listed for the TYPE: `Api` is what the browser's client is shaped from.
6
6
 
7
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
7
8
  import { API_INDEX } from './app-root';
8
9
  import { containedPath } from './generate-write';
10
+ import type { Finding } from './output';
9
11
  import { camel } from './templates/naming';
10
12
  import { wrapList } from './templates/wrap';
11
13
 
@@ -143,6 +145,93 @@ function withImport(source: string, importLine: string): string {
143
145
  return lines.join('\n');
144
146
  }
145
147
 
148
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
149
+
150
+ /** Every binding an import clause declares: `* as x`, `x`, `{ a, b as c, type d }`. */
151
+ const clauseBindings = (clause: string): readonly string[] =>
152
+ clause
153
+ .replace(/^type\s+/, '')
154
+ .replace(/[{}]/g, ',')
155
+ .split(',')
156
+ .map((part) =>
157
+ part
158
+ .trim()
159
+ .replace(/^type\s+/, '')
160
+ .replace(/^\*\s+as\s+/, ''),
161
+ )
162
+ .map(
163
+ (part) =>
164
+ part
165
+ .split(/\s+as\s+/)
166
+ .at(-1)
167
+ ?.trim() ?? '',
168
+ )
169
+ .filter((name) => IDENTIFIER.test(name));
170
+
171
+ /**
172
+ * Every top-level name the index declares, and what holds it: a namespace import by its
173
+ * specifier, anything else (`defineApi`, `api`, `Api`, a named import) by its own name. A second
174
+ * binding of any of them is a module that does not load — `import * as api` beside
175
+ * `export const api` is a ReferenceError in every app-loading command.
176
+ */
177
+ function declaredBindings(source: string): ReadonlyMap<string, string> {
178
+ const held = new Map<string, string>();
179
+ for (const found of source.matchAll(/^import\s+([^;]*?)\s+from\s+'([^']+)';/gm)) {
180
+ const [, clause = '', specifier = ''] = found;
181
+ const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(clause.trim())?.[1];
182
+ if (namespace !== undefined) held.set(namespace, specifier);
183
+ else for (const name of clauseBindings(clause)) held.set(name, `an import from ${specifier}`);
184
+ }
185
+ const declaration =
186
+ /^(?:export\s+)?(?:declare\s+)?(?:const|let|var|function\*?|class|type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm;
187
+ for (const found of source.matchAll(declaration)) {
188
+ const [, name = ''] = found;
189
+ if (!held.has(name)) held.set(name, `the index's own ${name}`);
190
+ }
191
+ return held;
192
+ }
193
+
194
+ /**
195
+ * A written primitive whose namespace binding the index already holds — for another module, or as
196
+ * one of its own names — is a finding, decided BEFORE anything is written. Skipped silently, the
197
+ * job stayed unregistered under a green run; inserted, the index stopped loading.
198
+ */
199
+ export function apiBindingFindings(
200
+ source: string,
201
+ written: readonly string[],
202
+ fix: string,
203
+ ): readonly Finding[] {
204
+ const held = new Map(declaredBindings(source));
205
+ const findings: Finding[] = [];
206
+ for (const entry of apiEntriesFor(written)) {
207
+ const holder = held.get(entry.binding);
208
+ if (holder === entry.specifier) continue;
209
+ if (holder === undefined) {
210
+ held.set(entry.binding, entry.specifier);
211
+ continue;
212
+ }
213
+ findings.push({
214
+ code: 'X_GENERATE_CONFLICT',
215
+ cause: `${API_INDEX} binds every listed module under its file name, and "${entry.binding}" is already ${holder.startsWith('.') ? `the namespace of ${holder}` : holder} — ${entry.specifier} cannot be listed under it`,
216
+ fix,
217
+ docs: ERROR_DOCS_URL,
218
+ at: API_INDEX,
219
+ });
220
+ }
221
+ return findings;
222
+ }
223
+
224
+ /** `apiBindingFindings` against the app's own index; none when the app has no index. */
225
+ export async function indexBindingFindings(
226
+ root: string,
227
+ written: readonly string[],
228
+ fix: string,
229
+ ): Promise<readonly Finding[]> {
230
+ const file = containedPath(root, API_INDEX);
231
+ if (!(await Bun.file(file).exists())) return [];
232
+ return apiBindingFindings(await Bun.file(file).text(), written, fix);
233
+ }
234
+
146
235
  /** Performs `insertApiEntries` on the app's index. Answers the paths it rewrote. */
147
236
  export async function registerGeneratedPrimitives(
148
237
  root: string,
@@ -1,6 +1,6 @@
1
1
  // The two generated contracts an app commits — `x.manifest.json` and `openapi.json` — written by one
2
2
  // function, because two commands wrote them and only one wrote both: `x g` refreshed the manifest
3
- // and left `openapi.json` behind, so its own output failed the `contract-diff` step next.
3
+ // and left `openapi.json` behind, so its own output failed the gate's `manifest` step next.
4
4
 
5
5
  import { join } from 'node:path'; // why: Bun ships no path-join primitive.
6
6
  import type { Manifest } from '@ultimat3/manifest';
package/src/app-auth.ts CHANGED
@@ -1,32 +1,9 @@
1
1
  // The one fact `x dev` and `serve.ts` need out of `app.config.ts` before a listener binds: where
2
- // this app's sign-in page is. Sibling of `app-env.ts`'s `loadEnvSchema`, and imports the config
3
- // for the same reason — a regex over the app's source is the pattern `app-load.ts` refuses.
2
+ // this app's sign-in page is, off the one loader (`app-config-load.ts`).
4
3
 
5
- import { existsSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- import { APP_CONFIG_FILE } from './app-root';
4
+ import { loadAppConfig } from './app-config-load';
8
5
 
9
- /** The export every app declares. Named, never default — the CLI and the runtime both import it. */
10
- export const APP_CONFIG_EXPORT = 'config';
11
-
12
- const isRecord = (value: unknown): value is Record<string, unknown> =>
13
- typeof value === 'object' && value !== null;
14
-
15
- /**
16
- * `auth.signInPath`, or `null` when the app declares none.
17
- *
18
- * Structural, not `instanceof`: `defineConfig` returns a plain object, and a config that resolved
19
- * through an older version of core simply has no `auth` section. `null` is the safe answer either
20
- * way — it is what turns the browser redirect off and leaves the problem document in place.
21
- */
6
+ /** `auth.signInPath`, or `null` when the app declares none or the root has no config file. */
22
7
  export async function loadSignInPath(root: string): Promise<string | null> {
23
- const configPath = join(root, APP_CONFIG_FILE);
24
- if (!existsSync(configPath)) return null;
25
- const module = (await import(configPath)) as Record<string, unknown>;
26
- const config = module[APP_CONFIG_EXPORT];
27
- if (!isRecord(config)) return null;
28
- const auth = config['auth'];
29
- if (!isRecord(auth)) return null;
30
- const path = auth['signInPath'];
31
- return typeof path === 'string' && path.startsWith('/') ? path : null;
8
+ return (await loadAppConfig(root))?.auth.signInPath ?? null;
32
9
  }
@@ -25,6 +25,7 @@ import { commentSafe } from './comment-safe';
25
25
  import { scanRuntimeImports, stripShebang } from './import-scan';
26
26
  import type { Finding } from './output';
27
27
  import { hasPathSegment } from './path-segments';
28
+ import { toPosix } from './posix-path';
28
29
  import { quoteArg } from './shell-quote';
29
30
 
30
31
  export const BOUNDARY_CODES = [
@@ -61,11 +62,21 @@ export const boundaryCodeOf = (rule: BoundaryRule): BoundaryCode => CODE_OF[rule
61
62
 
62
63
  const isRoute = (path: string): boolean => /\/(page|layout|route)\.[cm]?tsx?$/.test(path);
63
64
  const isService = (path: string): boolean => /\/service\.[cm]?ts$/.test(path);
65
+ /**
66
+ * A slice's `repo.ts` counts: it is the typed handle one hop away, and a page reading it skips the
67
+ * query that carries the read's policy, bound and cache tag — the scaffold's own dashboard showed
68
+ * every post to a viewer holding `dashboard:read` alone. Matched as a whole module name, resolved
69
+ * (`…/repo.ts`), NodeNext-spelled (`./repo.js`, `.mjs`, `.cjs`) or bare (`../post/repo`), so
70
+ * `report` and `repository` are names, not repos.
71
+ */
72
+ const isRepoSpecifier = (specifier: string): boolean =>
73
+ /(^|\/)repo(\/index)?(\.[cm]?[jt]sx?)?$/.test(specifier);
64
74
  const isDbSpecifier = (specifier: string): boolean =>
65
75
  /(^|\/)packages\/db($|\/)/.test(specifier) ||
66
76
  specifier.endsWith('/db') ||
67
77
  /^@[^/]+\/db$/.test(specifier) ||
68
- specifier === 'drizzle-orm';
78
+ specifier === 'drizzle-orm' ||
79
+ isRepoSpecifier(specifier);
69
80
  /**
70
81
  * `node:http` and `node:https` are HTTP too, and the anchor could not reach them: `http` had to
71
82
  * follow a `/` or start the string, and a `node:` specifier has a colon there. So the one spelling
@@ -104,7 +115,9 @@ export function resolveSpecifier(
104
115
  * the form `CANDIDATE_SUFFIXES` resolves and the form every app source already writes.
105
116
  */
106
117
  export function relativeSpecifier(fromFile: string, target: string): string {
107
- const path = relative(dirname(fromFile), target).replace(/\.[cm]?tsx?$/, '');
118
+ // Through `toPosix` even over the POSIX `relative`: a key spelt `apps\web\…` is ONE segment to
119
+ // it, and the specifier came out `./apps\web\…` — an import no host resolves.
120
+ const path = relative(dirname(toPosix(fromFile)), toPosix(target)).replace(/\.[cm]?tsx?$/, '');
108
121
  return path.startsWith('.') ? path : `./${path}`;
109
122
  }
110
123
 
@@ -190,7 +203,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
190
203
  if (isRoute(file.path) && isDbSpecifier(specifier)) {
191
204
  findings.push({
192
205
  code: 'X_BOUNDARY_ROUTE_TO_DB',
193
- cause: `route imports the database ("${specifier}") — routes call actions and queries`,
206
+ cause: `route imports the database ("${specifier}")${isRepoSpecifier(specifier) ? ', through a repo' : ''} — routes call actions and queries`,
194
207
  fix: generate('query', file.path, 'then call it from'),
195
208
  docs: ERROR_DOCS_URL,
196
209
  at: file.path,
@@ -232,7 +245,7 @@ export async function readAppSources(root: string): Promise<readonly SourceFile[
232
245
  const glob = new Bun.Glob(pattern);
233
246
  for await (const path of glob.scan({ cwd: root, absolute: false })) {
234
247
  if (hasPathSegment(path, 'node_modules') || path.includes('.test.')) continue;
235
- const posix = path.split('\\').join('/');
248
+ const posix = toPosix(path);
236
249
  files.push({ path: posix, source: await Bun.file(joinPath(root, posix)).text() });
237
250
  }
238
251
  }