@ultimat3/cli 21.0.0 → 22.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 (234) hide show
  1. package/CLAUDE.md +84 -1650
  2. package/README.md +40 -2
  3. package/package.json +30 -30
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-load.ts +37 -5
  8. package/src/app-openapi.ts +27 -0
  9. package/src/app-permissions.ts +0 -0
  10. package/src/app-root.ts +1 -1
  11. package/src/async-pages.ts +32 -0
  12. package/src/bin.ts +7 -1
  13. package/src/boundary-findings.ts +36 -0
  14. package/src/browser-launcher-fake-html.ts +80 -0
  15. package/src/browser-launcher-fake.ts +165 -0
  16. package/src/browser-launcher-port.ts +159 -0
  17. package/src/browser-launcher.ts +26 -133
  18. package/src/budgets.ts +11 -1
  19. package/src/cdp-shot-a11y.ts +77 -0
  20. package/src/cdp-shot-clock.ts +14 -0
  21. package/src/cdp-shot-driver.ts +150 -0
  22. package/src/cdp-shot-element.ts +147 -0
  23. package/src/cdp-shot-errors.ts +62 -0
  24. package/src/cdp-shot-keys.ts +152 -0
  25. package/src/cdp-shot-page.ts +230 -0
  26. package/src/cdp-shot-watch.ts +241 -0
  27. package/src/cmd-affected-spec.ts +24 -0
  28. package/src/cmd-affected.ts +3 -19
  29. package/src/cmd-build-spec.ts +16 -0
  30. package/src/cmd-build.ts +46 -20
  31. package/src/cmd-ci-spec.ts +24 -0
  32. package/src/cmd-ci.ts +17 -21
  33. package/src/cmd-db-branch.ts +2 -2
  34. package/src/cmd-db-spec.ts +99 -0
  35. package/src/cmd-db.ts +15 -94
  36. package/src/cmd-deploy-helm.ts +136 -0
  37. package/src/cmd-deploy-spec.ts +40 -0
  38. package/src/cmd-deploy.ts +111 -43
  39. package/src/cmd-dev-spec.ts +25 -0
  40. package/src/cmd-dev.ts +55 -36
  41. package/src/cmd-docs-spec.ts +16 -0
  42. package/src/cmd-docs.ts +2 -11
  43. package/src/cmd-doctor-spec.ts +21 -0
  44. package/src/cmd-doctor.ts +4 -18
  45. package/src/cmd-env-spec.ts +18 -0
  46. package/src/cmd-env.ts +4 -13
  47. package/src/cmd-errors-spec.ts +23 -0
  48. package/src/cmd-errors.ts +3 -17
  49. package/src/cmd-fix-spec.ts +18 -0
  50. package/src/cmd-fix.ts +4 -13
  51. package/src/cmd-generate-spec.ts +31 -0
  52. package/src/cmd-generate.ts +46 -42
  53. package/src/cmd-i18n-spec.ts +16 -0
  54. package/src/cmd-i18n.ts +7 -14
  55. package/src/cmd-jobs-spec.ts +56 -0
  56. package/src/cmd-jobs.ts +3 -50
  57. package/src/cmd-manifest-spec.ts +15 -0
  58. package/src/cmd-manifest.ts +19 -20
  59. package/src/cmd-mcp-spec.ts +21 -0
  60. package/src/cmd-mcp.ts +2 -15
  61. package/src/cmd-new-spec.ts +33 -0
  62. package/src/cmd-new.ts +38 -33
  63. package/src/cmd-policy-spec.ts +13 -0
  64. package/src/cmd-policy.ts +2 -9
  65. package/src/cmd-pr-spec.ts +39 -0
  66. package/src/cmd-pr.ts +5 -65
  67. package/src/cmd-registries-spec.ts +32 -0
  68. package/src/cmd-registries.ts +4 -25
  69. package/src/cmd-routes-spec.ts +12 -0
  70. package/src/cmd-routes.ts +2 -7
  71. package/src/cmd-secrets-spec.ts +19 -0
  72. package/src/cmd-secrets.ts +22 -34
  73. package/src/cmd-shot-island.ts +12 -13
  74. package/src/cmd-shot-spec.ts +53 -0
  75. package/src/cmd-shot.ts +11 -60
  76. package/src/cmd-tasks-spec.ts +21 -0
  77. package/src/cmd-tasks.ts +2 -16
  78. package/src/cmd-test-spec.ts +54 -0
  79. package/src/cmd-test.ts +4 -49
  80. package/src/cmd-verify-spec.ts +28 -0
  81. package/src/cmd-verify.ts +3 -23
  82. package/src/db-seed.ts +4 -2
  83. package/src/dev-dashboard.ts +4 -4
  84. package/src/dev-lock.ts +18 -2
  85. package/src/dev-port.ts +21 -0
  86. package/src/dev-route-table.ts +3 -3
  87. package/src/dispatch.ts +12 -5
  88. package/src/document-styles.ts +1 -1
  89. package/src/drift.ts +4 -18
  90. package/src/error-catalog.ts +16 -9
  91. package/src/error-codes.ts +28 -30
  92. package/src/error-pages.ts +19 -7
  93. package/src/errors.ts +14 -0
  94. package/src/favicon.ts +2 -2
  95. package/src/fix-imports.ts +1 -1
  96. package/src/fix-scan.ts +2 -9
  97. package/src/flag-reads.ts +1 -2
  98. package/src/foreign-text.ts +36 -0
  99. package/src/framework-schema.ts +5 -6
  100. package/src/generate-feature.ts +42 -0
  101. package/src/generate-files.ts +7 -3
  102. package/src/generate-grants.ts +83 -0
  103. package/src/generate-kinds.ts +56 -4
  104. package/src/i18n-index.ts +59 -10
  105. package/src/icon-assets.ts +1 -1
  106. package/src/image-prepare.ts +14 -0
  107. package/src/import-scan.ts +63 -0
  108. package/src/index.ts +67 -348
  109. package/src/invocation-flags.ts +26 -0
  110. package/src/island-bundle.ts +6 -1
  111. package/src/island-capture.ts +6 -6
  112. package/src/island-harness-script.ts +6 -2
  113. package/src/island-realtime.ts +15 -8
  114. package/src/island-shot.ts +5 -0
  115. package/src/island-store.ts +131 -0
  116. package/src/island-verdict.ts +1 -1
  117. package/src/job-registration.ts +42 -0
  118. package/src/jobs-driver.ts +2 -2
  119. package/src/load-findings.ts +51 -0
  120. package/src/mcp-db-target.ts +1 -1
  121. package/src/mcp-errors.ts +28 -27
  122. package/src/mcp-host.ts +27 -12
  123. package/src/mcp-ui-diff.ts +27 -0
  124. package/src/mcp-ui-inspect.ts +4 -4
  125. package/src/mcp-ui-interact.ts +21 -15
  126. package/src/mcp-ui.ts +19 -15
  127. package/src/measure-database.ts +73 -0
  128. package/src/measure-paths.ts +74 -0
  129. package/src/measure-scope.ts +74 -0
  130. package/src/messages.ts +1 -3
  131. package/src/metrics-endpoint.ts +1 -1
  132. package/src/otlp-export.ts +1 -1
  133. package/src/output.ts +6 -0
  134. package/src/permission-grants.ts +86 -0
  135. package/src/prerender-out.ts +25 -0
  136. package/src/prerender.ts +121 -102
  137. package/src/pwa-artifacts.ts +3 -3
  138. package/src/reexport-manifest.ts +2 -1
  139. package/src/registry.ts +80 -56
  140. package/src/role-realtime.ts +36 -0
  141. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  142. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  143. package/src/role-start-types.ts +112 -0
  144. package/src/{dev-roles.ts → role-start.ts} +42 -115
  145. package/src/{dev-sync.ts → role-sync.ts} +18 -8
  146. package/src/root-env.ts +67 -0
  147. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  148. package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
  149. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  150. package/src/runtime-jobs.ts +87 -0
  151. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
  152. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  153. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  154. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  155. package/src/runtime-realtime.ts +55 -0
  156. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  157. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  158. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  159. package/src/scaffold-fixture.ts +28 -6
  160. package/src/scaffold-typecheck.ts +6 -3
  161. package/src/schema-drift.ts +7 -1
  162. package/src/script-csp.ts +5 -2
  163. package/src/secrets-rotation.ts +59 -0
  164. package/src/serve-boot.ts +192 -0
  165. package/src/serve-drain.ts +24 -0
  166. package/src/serve-entry.ts +6 -0
  167. package/src/serve-env.ts +116 -0
  168. package/src/serve-types.ts +55 -0
  169. package/src/serve.ts +44 -347
  170. package/src/shot-server.ts +2 -2
  171. package/src/shot-settle.ts +10 -1
  172. package/src/shot-theme.ts +3 -3
  173. package/src/shot-verdict.ts +16 -7
  174. package/src/signal-shred.ts +27 -0
  175. package/src/solid-loader.ts +26 -2
  176. package/src/static-report.ts +8 -1
  177. package/src/templates/action.ts +30 -16
  178. package/src/templates/entity.ts +12 -7
  179. package/src/templates/index.ts +1 -1
  180. package/src/templates/job.ts +10 -7
  181. package/src/templates/policy.ts +20 -2
  182. package/src/templates/resource-create.ts +127 -0
  183. package/src/templates/resource-form-island.ts +49 -12
  184. package/src/templates/resource.ts +10 -4
  185. package/src/templates/scaffold-app.ts +4 -1
  186. package/src/templates/scaffold-auth.ts +3 -1
  187. package/src/templates/scaffold-container-compose.ts +184 -0
  188. package/src/templates/scaffold-container.ts +24 -159
  189. package/src/templates/scaffold-dashboard-example.ts +2 -2
  190. package/src/templates/scaffold-db-package.ts +16 -3
  191. package/src/templates/scaffold-demo-org.ts +41 -0
  192. package/src/templates/scaffold-entries.ts +1 -1
  193. package/src/templates/scaffold-helm-templates.ts +66 -7
  194. package/src/templates/scaffold-helm.ts +27 -5
  195. package/src/templates/scaffold-i18n.ts +15 -10
  196. package/src/templates/scaffold-repo.ts +13 -10
  197. package/src/templates/scaffold-roles.ts +38 -10
  198. package/src/templates/slice-foundation.ts +1 -1
  199. package/src/templates/wrap.ts +4 -1
  200. package/src/test-passes.ts +2 -1
  201. package/src/test-workers.ts +26 -0
  202. package/src/ts-scan.ts +3 -6
  203. package/src/tsconfig-references.ts +1 -2
  204. package/src/verify-checks.ts +41 -39
  205. package/src/verify-e2e.ts +10 -7
  206. package/src/verify-run.ts +11 -1
  207. package/src/verify-step.ts +3 -3
  208. package/src/verify-tests.ts +1 -26
  209. package/src/verify-typecheck.ts +28 -0
  210. package/src/web-binding.ts +2 -2
  211. package/src/workspace-graph.ts +10 -33
  212. package/src/cdp-browser.ts +0 -94
  213. package/src/cdp-connection.ts +0 -247
  214. package/src/cdp-e2e-page.ts +0 -180
  215. package/src/cdp-e2e-session.ts +0 -199
  216. package/src/cdp-errors.ts +0 -56
  217. package/src/cdp-launch.ts +0 -193
  218. package/src/cdp-offline-script.ts +0 -73
  219. package/src/cdp-pipe.ts +0 -77
  220. package/src/e2e-app.ts +0 -103
  221. package/src/e2e-browser-handle.ts +0 -55
  222. package/src/e2e-dom-fixture.ts +0 -117
  223. package/src/e2e-driver.ts +0 -117
  224. package/src/e2e-errors.ts +0 -117
  225. package/src/e2e-evaluate.ts +0 -156
  226. package/src/e2e-locator.ts +0 -86
  227. package/src/e2e-page.ts +0 -153
  228. package/src/e2e-preload.ts +0 -64
  229. package/src/e2e-probe.ts +0 -23
  230. package/src/e2e-selection.ts +0 -182
  231. package/src/e2e-spawn.ts +0 -169
  232. package/src/measurement-actor.ts +0 -26
  233. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
  234. /package/src/{dev-render.ts → runtime-render.ts} +0 -0
package/README.md CHANGED
@@ -14,7 +14,7 @@ Commands and the `x verify` step count, `As of 2026-08`:
14
14
  | `x verify` | **the gate** | 20 named steps, each with pass/fail + duration |
15
15
  | `x g <primitive> <name>` | scaffolds a primitive **with a passing test** | never a TODO stub |
16
16
  | `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 |
17
- | `x mcp serve` | `@ultimat3/mcp`'s 13 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
17
+ | `x mcp serve` | `@ultimat3/mcp`'s 18 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
18
18
  | `x doctor` | environment, ports, drift, PWA prerequisites | every finding carries a fix command |
19
19
  | `x deploy` | container deploy plan | compose or helm; zero platform primitives |
20
20
  | `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
@@ -118,7 +118,7 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
118
118
  | `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 |
119
119
  | `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 |
120
120
  | `measurement-actor.ts` | the actor a weigh-and-discard render runs as — every permission, never served |
121
- | `dev-live-feed.ts` | what feeds the sync node this process booted: the in-process row observer under the embedded database, the WAL decoder with a real one, nothing without the role — `live=` on the ready line |
121
+ | `runtime-live-feed.ts` | what feeds the sync node this process booted: the in-process row observer under the embedded database, the WAL decoder with a real one, nothing without the role — `live=` on the ready line |
122
122
  | `app-manifest.ts` | `x.manifest.json`, projected by `@ultimat3/manifest` |
123
123
  | `app-openapi.ts` | `openapi.json`, projected by `@ultimat3/action` |
124
124
  | `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
@@ -160,3 +160,41 @@ Every emitted source has a `<file>.test.ts` beside it that passes on the first r
160
160
 
161
161
  `X_CLI_UNKNOWN_COMMAND` `X_CLI_BAD_FLAG` `X_VERIFY_FAILED` `X_NOT_IN_APP` `X_BUN_VERSION`
162
162
  `X_NOT_IMPLEMENTED` `X_GUARD_INVALID` `X_GUARD_FAILED` `X_GUARD_FINDING_INVALID`
163
+
164
+ ### Error classes
165
+
166
+ Every error class `src/index.ts` exports, for `instanceof` inside one process. Across a wire or
167
+ a job boundary the class is gone and the `code` is what survives — match on that.
168
+
169
+ | Class | Code | Declared in |
170
+ |---|---|---|
171
+ | `BadFlagError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
172
+ | `BuildEntryMissingError` | `X_BUILD_ENTRY_MISSING` | `src/build-errors.ts` |
173
+ | `BunVersionError` | `X_BUN_VERSION` | `src/errors.ts` |
174
+ | `CatalogExistsError` | `X_GENERATE_CONFLICT` | `src/errors.ts` |
175
+ | `CdpBrowserMissingError` | `X_CDP_BROWSER_MISSING` | `src/cdp-errors.ts` |
176
+ | `CdpCallFailedError` | `X_CDP_CALL_FAILED` | `src/cdp-errors.ts` |
177
+ | `CdpLaunchFailedError` | `X_CDP_LAUNCH_FAILED` | `src/cdp-errors.ts` |
178
+ | `CdpTimeoutError` | `X_CDP_TIMEOUT` | `src/cdp-errors.ts` |
179
+ | `CliNotImplementedError` | `X_NOT_IMPLEMENTED` | `src/errors.ts` |
180
+ | `DeclarationUnknownError` | `X_DECLARATION_UNKNOWN` | `src/errors.ts` |
181
+ | `E2eAppFailedError` | `X_E2E_APP_FAILED` | `src/e2e-errors.ts` |
182
+ | `E2eEvaluateCapturedError` | `X_E2E_EVALUATE_CAPTURED` | `src/e2e-errors.ts` |
183
+ | `E2eEvaluateThrewError` | `X_E2E_EVALUATE_THREW` | `src/e2e-errors.ts` |
184
+ | `E2eEvaluateUnsupportedError` | `X_E2E_EVALUATE_UNSUPPORTED` | `src/e2e-errors.ts` |
185
+ | `E2eLocatorAmbiguousError` | `X_E2E_LOCATOR_AMBIGUOUS` | `src/e2e-errors.ts` |
186
+ | `E2eLocatorEmptyError` | `X_E2E_LOCATOR_EMPTY` | `src/e2e-errors.ts` |
187
+ | `E2eServiceWorkerAbsentError` | `X_E2E_SERVICE_WORKER_ABSENT` | `src/e2e-errors.ts` |
188
+ | `ErrorCodeUnknownError` | `X_ERROR_CODE_UNKNOWN` | `src/errors.ts` |
189
+ | `FixTargetUnknownError` | `X_FIX_TARGET_UNKNOWN` | `src/errors.ts` |
190
+ | `FrameworkSchemaFailedError` | `X_FRAMEWORK_SCHEMA_FAILED` | `src/schema-errors.ts` |
191
+ | `JobUnknownError` | `X_JOB_UNKNOWN` | `src/errors.ts` |
192
+ | `MissingPositionalError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
193
+ | `MissingSubcommandError` | `X_CLI_BAD_FLAG` | `src/errors.ts` |
194
+ | `NoTestFilesError` | `X_TEST_NO_FILES` | `src/errors.ts` |
195
+ | `NotInAppError` | `X_NOT_IN_APP` | `src/errors.ts` |
196
+ | `PortInvalidError` | `X_PORT_INVALID` | `src/errors.ts` |
197
+ | `QuerySubscribesUnknownError` | `X_QUERY_SUBSCRIBES_UNKNOWN` | `src/db-subscribes.ts` |
198
+ | `RoleUnknownError` | `X_ROLE_UNKNOWN` | `src/errors.ts` |
199
+ | `UnknownCommandError` | `X_CLI_UNKNOWN_COMMAND` | `src/errors.ts` |
200
+ | `VerifyFailedError` | `X_VERIFY_FAILED` | `src/errors.ts` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "21.0.0",
3
+ "version": "22.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",
@@ -14,7 +14,8 @@
14
14
  "provenance": true
15
15
  },
16
16
  "exports": {
17
- ".": "./src/index.ts"
17
+ ".": "./src/index.ts",
18
+ "./serve": "./src/serve-entry.ts"
18
19
  },
19
20
  "bin": {
20
21
  "x": "./src/bin.ts"
@@ -37,34 +38,33 @@
37
38
  },
38
39
  "dependencies": {
39
40
  "@babel/core": "^8.0.1",
40
- "@ultimat3/action": "21.0.0",
41
- "@ultimat3/admin": "21.0.0",
42
- "@ultimat3/ai": "21.0.0",
43
- "@ultimat3/auth": "21.0.0",
44
- "@ultimat3/cache": "21.0.0",
45
- "@ultimat3/core": "21.0.0",
46
- "@ultimat3/db": "21.0.0",
47
- "@ultimat3/entity": "21.0.0",
48
- "@ultimat3/flags": "21.0.0",
49
- "@ultimat3/http": "21.0.0",
50
- "@ultimat3/i18n": "21.0.0",
51
- "@ultimat3/jobs": "21.0.0",
52
- "@ultimat3/mail": "21.0.0",
53
- "@ultimat3/manifest": "21.0.0",
54
- "@ultimat3/mcp": "21.0.0",
55
- "@ultimat3/money": "21.0.0",
56
- "@ultimat3/notify": "21.0.0",
57
- "@ultimat3/policy": "21.0.0",
58
- "@ultimat3/pwa": "21.0.0",
59
- "@ultimat3/query": "21.0.0",
60
- "@ultimat3/realtime": "21.0.0",
61
- "@ultimat3/render": "21.0.0",
62
- "@ultimat3/schema": "21.0.0",
63
- "@ultimat3/scraping": "21.0.0",
64
- "@ultimat3/seo": "21.0.0",
65
- "@ultimat3/storage": "21.0.0",
66
- "@ultimat3/testing": "21.0.0",
67
- "@ultimat3/time": "21.0.0",
41
+ "@ultimat3/action": "22.0.0",
42
+ "@ultimat3/admin": "22.0.0",
43
+ "@ultimat3/ai": "22.0.0",
44
+ "@ultimat3/auth": "22.0.0",
45
+ "@ultimat3/cache": "22.0.0",
46
+ "@ultimat3/core": "22.0.0",
47
+ "@ultimat3/db": "22.0.0",
48
+ "@ultimat3/entity": "22.0.0",
49
+ "@ultimat3/flags": "22.0.0",
50
+ "@ultimat3/http": "22.0.0",
51
+ "@ultimat3/i18n": "22.0.0",
52
+ "@ultimat3/jobs": "22.0.0",
53
+ "@ultimat3/mail": "22.0.0",
54
+ "@ultimat3/manifest": "22.0.0",
55
+ "@ultimat3/mcp": "22.0.0",
56
+ "@ultimat3/money": "22.0.0",
57
+ "@ultimat3/notify": "22.0.0",
58
+ "@ultimat3/policy": "22.0.0",
59
+ "@ultimat3/pwa": "22.0.0",
60
+ "@ultimat3/query": "22.0.0",
61
+ "@ultimat3/realtime": "22.0.0",
62
+ "@ultimat3/render": "22.0.0",
63
+ "@ultimat3/schema": "22.0.0",
64
+ "@ultimat3/seo": "22.0.0",
65
+ "@ultimat3/storage": "22.0.0",
66
+ "@ultimat3/testing": "22.0.0",
67
+ "@ultimat3/time": "22.0.0",
68
68
  "babel-preset-solid": "^1.9.15"
69
69
  }
70
70
  }
@@ -0,0 +1,124 @@
1
+ // `x g job` / `x g task` / `x g resource` hand what they wrote to `apps/web/api/index.ts`. The scan
2
+ // registers actions and queries by export name on its own and registers NO job: a job module
3
+ // nothing lists keeps the positional `anonymous-job-2` that `job()` minted, on the queue row, in
4
+ // `x.manifest.json` and in every dead-letter trace — under a green gate (plan 101 slice 11 b).
5
+
6
+ import { containedPath } from './generate-write';
7
+ import { camel } from './templates/naming';
8
+ import { wrapList } from './templates/wrap';
9
+
10
+ /** The one file `defineApi` is called from in a scaffolded app. */
11
+ export const API_INDEX = 'apps/web/api/index.ts';
12
+
13
+ /** One module to import as a namespace and list under a `defineApi` key. */
14
+ export interface ApiEntry {
15
+ readonly key: 'jobs' | 'tasks';
16
+ /** The namespace binding: the file's own name, camelCased — `reindexPost`. */
17
+ readonly binding: string;
18
+ /** From `apps/web/api/`: `../app/post/jobs/reindex-post`. */
19
+ readonly specifier: string;
20
+ }
21
+
22
+ const PRIMITIVE_PATH =
23
+ /^apps\/web\/(?<rest>[^/]+\/[^/]+\/(?<dir>jobs|tasks)\/(?<file>[a-z0-9-]+))\.ts$/;
24
+
25
+ /** The job and task modules among the paths a generator wrote. Tests are not modules to list. */
26
+ export function apiEntriesFor(written: readonly string[]): readonly ApiEntry[] {
27
+ return written.flatMap((path) => {
28
+ const groups = PRIMITIVE_PATH.exec(path)?.groups;
29
+ if (groups === undefined) return [];
30
+ const { rest = '', dir, file = '' } = groups;
31
+ return [
32
+ { key: dir === 'tasks' ? 'tasks' : 'jobs', binding: camel(file), specifier: `../${rest}` },
33
+ ];
34
+ });
35
+ }
36
+
37
+ /** Every `[...]` entry of a `key: [...]` list in the `defineApi({ ... })` call. */
38
+ const listOf = (
39
+ source: string,
40
+ key: string,
41
+ ): { start: number; end: number; items: string[] } | undefined => {
42
+ const open = new RegExp(`\\n(\\s*)${key}: \\[`).exec(source);
43
+ if (open === null) return undefined;
44
+ const start = open.index + open[0].length;
45
+ const end = source.indexOf(']', start);
46
+ if (end === -1) return undefined;
47
+ const items = source
48
+ .slice(start, end)
49
+ .split(',')
50
+ .map((item) => item.trim())
51
+ .filter((item) => item.length > 0);
52
+ return { start, end, items };
53
+ };
54
+
55
+ /**
56
+ * `source` with each entry imported and listed. The import joins the `import * as` block in sorted
57
+ * position; the binding joins its list, which is created after `actions: [...]` when the call has
58
+ * none. An index that is not the scaffold's shape — no `defineApi({`, no `actions:` list — is
59
+ * returned untouched with the entries in `skipped`, and the `manifest` step's X_JOB_UNREGISTERED
60
+ * names the edit.
61
+ */
62
+ export function insertApiEntries(
63
+ source: string,
64
+ entries: readonly ApiEntry[],
65
+ ): { readonly source: string; readonly skipped: readonly ApiEntry[] } {
66
+ let next = source;
67
+ const skipped: ApiEntry[] = [];
68
+ for (const entry of entries) {
69
+ const importLine = `import * as ${entry.binding} from '${entry.specifier}';`;
70
+ const call = next.indexOf('defineApi({');
71
+ const actions = listOf(next, 'actions');
72
+ if (call === -1 || actions === undefined) {
73
+ skipped.push(entry);
74
+ continue;
75
+ }
76
+ const list = listOf(next, entry.key);
77
+ if (list?.items.includes(entry.binding) === true) continue;
78
+ if (list === undefined) {
79
+ // After `actions: [...],` — the order `x new` writes: actions, queries, jobs, tasks.
80
+ const after = next.indexOf('\n', actions.end);
81
+ const line = ` ${entry.key}: [${entry.binding}],`;
82
+ next = `${next.slice(0, after + 1)}${line}\n${next.slice(after + 1)}`;
83
+ } else {
84
+ const items = [...list.items, entry.binding];
85
+ const lineStart = next.lastIndexOf('\n', list.start) + 1;
86
+ const indent = /^\s*/.exec(next.slice(lineStart))?.[0] ?? '';
87
+ const rewritten = wrapList(indent, `${entry.key}: [`, items, ']');
88
+ next = `${next.slice(0, lineStart)}${rewritten}${next.slice(list.end + 1)}`;
89
+ }
90
+ if (!next.includes(importLine)) next = withImport(next, importLine);
91
+ }
92
+ return { source: next, skipped };
93
+ }
94
+
95
+ /**
96
+ * The import, in the `import * as` block Biome keeps sorted by specifier. Placed before the first
97
+ * relative import whose specifier sorts after it, or after the last one.
98
+ */
99
+ function withImport(source: string, importLine: string): string {
100
+ const specifier = /from '([^']+)'/.exec(importLine)?.[1] ?? '';
101
+ const lines = source.split('\n');
102
+ const relative = lines
103
+ .map((line, index) => ({ line, index, from: /^import .* from '(\.[^']*)';$/.exec(line)?.[1] }))
104
+ .filter((row) => row.from !== undefined);
105
+ const before = relative.find((row) => (row.from ?? '') > specifier);
106
+ const at = before?.index ?? (relative.at(-1)?.index ?? -1) + 1;
107
+ lines.splice(at, 0, importLine);
108
+ return lines.join('\n');
109
+ }
110
+
111
+ /** Performs `insertApiEntries` on the app's index. Answers the paths it rewrote. */
112
+ export async function registerGeneratedPrimitives(
113
+ root: string,
114
+ written: readonly string[],
115
+ ): Promise<readonly string[]> {
116
+ const entries = apiEntriesFor(written);
117
+ const file = containedPath(root, API_INDEX);
118
+ if (entries.length === 0 || !(await Bun.file(file).exists())) return [];
119
+ const before = await Bun.file(file).text();
120
+ const { source } = insertApiEntries(before, entries);
121
+ if (source === before) return [];
122
+ await Bun.write(file, source);
123
+ return [API_INDEX];
124
+ }
@@ -0,0 +1,39 @@
1
+ // The two generated contracts an app commits — `x.manifest.json` and `openapi.json` — written by one
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.
4
+
5
+ import { join } from 'node:path'; // why: Bun ships no path-join primitive.
6
+ import type { Manifest } from '@ultimat3/manifest';
7
+ import { MANIFEST_FILENAME } from '@ultimat3/manifest';
8
+ import { writeAppManifest } from './app-manifest';
9
+ import { OPENAPI_FILE, openApiJson } from './app-openapi';
10
+
11
+ export interface ArtifactOptions {
12
+ /** `x manifest --no-openapi` writes the manifest alone. */
13
+ readonly openapi: boolean;
14
+ /**
15
+ * Refresh only what is already committed. `x g` never introduces a generated file an app did not
16
+ * ask to maintain; `x manifest` is the asking.
17
+ */
18
+ readonly onlyExisting: boolean;
19
+ }
20
+
21
+ /** Writes the app's generated contracts from one projection. Answers the app-relative paths. */
22
+ export async function writeAppArtifacts(
23
+ root: string,
24
+ manifest: Manifest,
25
+ options: ArtifactOptions,
26
+ ): Promise<readonly string[]> {
27
+ const written: string[] = [];
28
+ const present = async (file: string): Promise<boolean> =>
29
+ !options.onlyExisting || (await Bun.file(join(root, file)).exists());
30
+ if (await present(MANIFEST_FILENAME)) {
31
+ await writeAppManifest(root, manifest);
32
+ written.push(MANIFEST_FILENAME);
33
+ }
34
+ if (options.openapi && (await present(OPENAPI_FILE))) {
35
+ await Bun.write(join(root, OPENAPI_FILE), openApiJson(manifest));
36
+ written.push(OPENAPI_FILE);
37
+ }
38
+ return written;
39
+ }
@@ -17,6 +17,7 @@ import { dirname, join, normalize, relative } from 'node:path/posix';
17
17
  import { ERROR_DOCS_URL } from '@ultimat3/core';
18
18
  import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
19
19
  import { checkSurfaceBoundary, importGraph, SURFACES } from '@ultimat3/render';
20
+ import { scanRuntimeImports, stripShebang } from './import-scan';
20
21
  import type { Finding } from './output';
21
22
  import { hasPathSegment } from './path-segments';
22
23
  import { quoteArg } from './shell-quote';
@@ -27,6 +28,7 @@ export const BOUNDARY_CODES = [
27
28
  'X_BOUNDARY_APP_TO_API',
28
29
  'X_BOUNDARY_ROUTE_TO_DB',
29
30
  'X_BOUNDARY_SERVICE_TO_HTTP',
31
+ 'X_BOUNDARY_SURFACE_IMPORT',
30
32
  ] as const;
31
33
 
32
34
  export type BoundaryCode = (typeof BOUNDARY_CODES)[number];
@@ -41,6 +43,8 @@ const CODE_OF: Readonly<Record<BoundaryRule, BoundaryCode>> = {
41
43
  'site-imports-app': 'X_BOUNDARY_SITE_TO_APP',
42
44
  'shared-is-a-leaf': 'X_BOUNDARY_SHARED_LEAF',
43
45
  'app-imports-api-at-runtime': 'X_BOUNDARY_APP_TO_API',
46
+ // Every other crossing `SURFACE_SPECS` does not allow: the table is the rule (`@ultimat3/render`).
47
+ 'surface-imports-surface': 'X_BOUNDARY_SURFACE_IMPORT',
44
48
  };
45
49
 
46
50
  /**
@@ -68,16 +72,7 @@ const isDbSpecifier = (specifier: string): boolean =>
68
72
  const isHttpSpecifier = (specifier: string): boolean =>
69
73
  specifier === '@ultimat3/http' || /(^|[/:])https?($|\/)/.test(specifier);
70
74
 
71
- /** The transpiler rejects a shebang, and an app's `bin/` entry points legitimately have one. */
72
- export const stripShebang = (source: string): string =>
73
- source.startsWith('#!') ? source.slice(source.indexOf('\n') + 1) : source;
74
-
75
- /** Bun's transpiler is the parser; a regex fallback would miss re-exports and dynamic imports. */
76
- export function scanRuntimeImports(file: SourceFile): readonly string[] {
77
- const loader = file.path.endsWith('x') ? 'tsx' : 'ts';
78
- const transpiler = new Bun.Transpiler({ loader });
79
- return transpiler.scanImports(stripShebang(file.source)).map((entry) => entry.path);
80
- }
75
+ export { scanRuntimeImports, stripShebang };
81
76
 
82
77
  const CANDIDATE_SUFFIXES = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'] as const;
83
78
 
package/src/app-load.ts CHANGED
@@ -5,13 +5,15 @@
5
5
 
6
6
  // Bun ships no `Bun.*` path API: `relative`/`sep` turn an absolute scan hit into the app-root-
7
7
  // relative POSIX path every finding and every manifest fact is keyed by.
8
- import { relative, sep } from 'node:path';
9
- import { registerActions } from '@ultimat3/action';
8
+ import { join, relative, sep } from 'node:path';
9
+ import { listActions, registerActions } from '@ultimat3/action';
10
+ import { describeEntities } from '@ultimat3/entity';
10
11
  import { localeConfig } from '@ultimat3/i18n';
12
+ import { registeredJobs, registeredTasks } from '@ultimat3/jobs';
11
13
  import type { ErrorCodeFact } from '@ultimat3/manifest';
12
- import { registerQueries } from '@ultimat3/query';
14
+ import { listQueries, registerQueries } from '@ultimat3/query';
13
15
  import type { RouteConfig } from '@ultimat3/render';
14
- import { isRouteConfig, pageComponentOf, registerRoute } from '@ultimat3/render';
16
+ import { isRouteConfig, pageComponentOf, registerRoute, routeEntries } from '@ultimat3/render';
15
17
  // For the SIDE EFFECT, and it is this module's to hold: importing `@ultimat3/render/server`
16
18
  // installs the `.tsx`/`.scss` Bun plugin, a plugin only transforms modules loaded AFTER it, and
17
19
  // every app module below is loaded by the dynamic `import()` in this file. Before the render
@@ -19,10 +21,12 @@ import { isRouteConfig, pageComponentOf, registerRoute } from '@ultimat3/render'
19
21
  // here is six hops through `error-contract` → `fix-command` → the command registry, which is an
20
22
  // accident one refactor away from compiling every app's `.tsx` to `React.createElement`.
21
23
  import '@ultimat3/render/server';
24
+ import { APP_CONFIG_FILE } from './app-root';
22
25
  import { collectDeclaredCodes } from './error-contract';
23
26
  import type { Finding } from './output';
24
27
  import { findingFrom } from './output';
25
28
  import { hasPathSegment } from './path-segments';
29
+ import { isTest } from './source-files';
26
30
 
27
31
  /** Every place an app keeps code the framework has to see. */
28
32
  const APP_GLOBS = [
@@ -112,8 +116,10 @@ export async function loadApp(root: string): Promise<LoadedApp> {
112
116
  // A SEGMENT, never a substring: an app checked out under
113
117
  // `~/dev/node_modules-experiments/myapp` answered `includes('node_modules')` for every
114
118
  // file it holds, so this loop imported none of them and the app registered nothing.
115
- if (hasPathSegment(absolute, 'node_modules') || absolute.includes('.test.')) continue;
119
+ // The ROOT-RELATIVE path is tested, never the absolute one: an app checked out under
120
+ // `~/work/my.test.app` matched `.test.` on every file and loaded none of them.
116
121
  const file = relative(root, absolute).split(sep).join('/');
122
+ if (hasPathSegment(absolute, 'node_modules') || isTest(file)) continue;
117
123
  if (ENTRY_POINT.test(file) || CLIENT_ENTRY_POINT.test(file) || STATES_FILE.test(file)) {
118
124
  continue;
119
125
  }
@@ -138,6 +144,16 @@ export async function loadApp(root: string): Promise<LoadedApp> {
138
144
  }
139
145
 
140
146
  files.sort();
147
+ // An app that imported NOTHING and reported nothing is a registry every later step reads as
148
+ // empty-and-fine. With an `app.config.ts` beside it, that is never what the author meant.
149
+ if (
150
+ files.length === 0 &&
151
+ findings.length === 0 &&
152
+ registersNothing() &&
153
+ (await Bun.file(join(root, APP_CONFIG_FILE)).exists())
154
+ ) {
155
+ findings.push(emptyAppFinding(root));
156
+ }
141
157
  // Read after the loop, never before it: `configureLocales` runs on the app's own import.
142
158
  return {
143
159
  root,
@@ -148,6 +164,22 @@ export async function loadApp(root: string): Promise<LoadedApp> {
148
164
  };
149
165
  }
150
166
 
167
+ /** No primitive in any registry this scan fills — a process that registered some itself is not empty. */
168
+ const registersNothing = (): boolean =>
169
+ listActions().length === 0 &&
170
+ listQueries().length === 0 &&
171
+ routeEntries().length === 0 &&
172
+ registeredJobs().length === 0 &&
173
+ registeredTasks().length === 0 &&
174
+ describeEntities().length === 0;
175
+
176
+ const emptyAppFinding = (root: string): Finding => ({
177
+ code: 'X_APP_EMPTY',
178
+ cause: `${root} has an ${APP_CONFIG_FILE} and loadApp imported no module under ${APP_GLOBS.join(', ')}, so every step reading the registries would check nothing`,
179
+ fix: 'x doctor --json',
180
+ at: APP_CONFIG_FILE,
181
+ });
182
+
151
183
  /**
152
184
  * Registers a module once; every later call replays whatever the first one reported — except for
153
185
  * a route module, which a later call re-registers from disk when its source has changed.
@@ -5,9 +5,13 @@
5
5
  // OpenAPI builder to drift from the ones the packages serve. Until 2026-09 only the actions were
6
6
  // here, and every `GET /_x/query/<name>` the server mounted was a route the spec had never heard of.
7
7
 
8
+ // why: Bun ships no path-join primitive.
9
+ import { join } from 'node:path';
8
10
  import { buildOpenApi, serializeOpenApi } from '@ultimat3/action';
11
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
9
12
  import type { Manifest } from '@ultimat3/manifest';
10
13
  import { queryOpenApiPaths } from '@ultimat3/query';
14
+ import type { Finding } from './output';
11
15
 
12
16
  export const OPENAPI_FILE = 'openapi.json';
13
17
 
@@ -18,3 +22,26 @@ export const openApiJson = (manifest: Manifest): string => {
18
22
  // can never shadow one with the other. `serializeOpenApi` sorts the merged keys.
19
23
  return serializeOpenApi({ ...document, paths: { ...document.paths, ...queryOpenApiPaths() } });
20
24
  };
25
+
26
+ /**
27
+ * The typed client is generated from `openapi.json`, so a stale spec ships a wrong client. One
28
+ * check, read by `x verify`'s `manifest` step AND `x manifest --check` — which looked at
29
+ * `x.manifest.json` alone and answered "fresh" over a spec the gate refused.
30
+ */
31
+ export async function openApiStaleness(
32
+ root: string,
33
+ manifest: Manifest,
34
+ ): Promise<readonly Finding[]> {
35
+ const path = join(root, OPENAPI_FILE);
36
+ if (!(await Bun.file(path).exists())) return [];
37
+ if ((await Bun.file(path).text()) === openApiJson(manifest)) return [];
38
+ return [
39
+ {
40
+ code: 'X_MANIFEST_STALE',
41
+ cause: `${OPENAPI_FILE} does not match the actions the code registers`,
42
+ fix: 'x manifest',
43
+ docs: ERROR_DOCS_URL,
44
+ at: OPENAPI_FILE,
45
+ },
46
+ ];
47
+ }
Binary file
package/src/app-root.ts CHANGED
@@ -24,7 +24,7 @@ export const MANIFEST_FILE = 'x.manifest.json';
24
24
  * (`bun run typecheck` is clean against `@types/bun@1.3.14`), and `>=1.4.0` therefore bars Bun 1.3
25
25
  * users for a capability the framework does not use. What refused it is a Bun 1.3.14 defect, not
26
26
  * the paperwork: a service shutdown against a destroyed database never resolves there
27
- * (`queue.stop()`, reproduced by `dev-runtime.live.test.ts`), so an app on a runtime this line
27
+ * (`queue.stop()`, reproduced by `runtime-services.live.test.ts`), so an app on a runtime this line
28
28
  * declared supported would hang on graceful shutdown the moment its database went away. The full
29
29
  * measurement is in `.github/actions/setup/action.yml`; read it before lowering this.
30
30
  */
@@ -0,0 +1,32 @@
1
+ // A route whose `Page` is an `async` function and which declares no `load`. It renders — the
2
+ // renderer awaits whatever the component returns — so no suite fails on it, and its data read
3
+ // silently skips everything `load` is for: the route's cache, `meta` computed from the data, the
4
+ // streaming boundary and the measurement render `x build` weighs. The social demo shipped eleven
5
+ // (plan 101 slice 11 l); nothing asked.
6
+
7
+ import type { RouteEntry } from '@ultimat3/render';
8
+ import { routeEntries } from '@ultimat3/render';
9
+ import type { Finding } from './output';
10
+
11
+ /** `async function Page` and `async () =>` both construct one; a sync one never does. */
12
+ const isAsync = (value: unknown): boolean =>
13
+ typeof value === 'function' && value.constructor.name === 'AsyncFunction';
14
+
15
+ export function asyncPageFinding(file: string): Finding {
16
+ return {
17
+ code: 'X_ROUTE_ASYNC_PAGE',
18
+ cause: `${file} exports an async Page, so its data read bypasses load's caching and meta`,
19
+ fix: `in ${file}: move each await in Page into export const load = (ctx) => …, and read it with the page's data prop`,
20
+ at: file,
21
+ };
22
+ }
23
+
24
+ /** Every registered route whose page component is async and whose config declares no `load`. */
25
+ export function asyncPageFindings(
26
+ entries: readonly Pick<RouteEntry, 'file' | 'config' | 'component'>[] = routeEntries(),
27
+ ): readonly Finding[] {
28
+ return entries
29
+ .filter((entry) => entry.config.load === undefined && isAsync(entry.component))
30
+ .map((entry) => asyncPageFinding(entry.file))
31
+ .sort((a, b) => (a.at ?? '').localeCompare(b.at ?? ''));
32
+ }
package/src/bin.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { dispatch } from './dispatch';
6
6
  import { resolveLocalCli } from './local-cli';
7
+ import { cwdFromArgv } from './root-env';
7
8
  // The writes themselves are `write-line.ts`: `create-ultimate`'s entry point needs the identical
8
9
  // one, and a second copy of a note about pipe truncation is a second copy that drifts. Two sinks,
9
10
  // because fd 1 is not always this process's to write on — `x mcp serve --transport stdio` hands it
@@ -15,7 +16,12 @@ import { writeErrorLine, writeLine } from './write-line';
15
16
  // `--json` consumer reading fd 1 sees exactly the child's one document. `process.execPath` is the
16
17
  // Bun that is already running, never a `PATH` lookup: the child's `import.meta.path` resolves to
17
18
  // the app's file, so its own `resolveLocalCli` answers "same file" and the chain stops at one hop.
18
- const local = resolveLocalCli({ cwd: process.cwd(), selfPath: import.meta.path, env: Bun.env });
19
+ // `--cwd` decides WHICH app's CLI, exactly as it decides which app `dispatch` runs against.
20
+ const local = resolveLocalCli({
21
+ cwd: cwdFromArgv(Bun.argv.slice(2), process.cwd()),
22
+ selfPath: import.meta.path,
23
+ env: Bun.env,
24
+ });
19
25
  if (local !== undefined) {
20
26
  writeErrorLine(`x: using the app's own @ultimat3/cli at ${local}`);
21
27
  const child = Bun.spawn([process.execPath, local, ...Bun.argv.slice(2)], {
@@ -0,0 +1,36 @@
1
+ // The surface findings `x verify` reports, each carrying the CONCRETE edit as its `fix:`. It said
2
+ // `x fix boundary <file>`, which printed a second fix and wrote nothing — a fix whose whole content
3
+ // was another command to run for the real one (plan 101 slice 11 j). The planner is the same one
4
+ // `x fix boundary` prints from, so the gate and the command cannot disagree about the cut.
5
+
6
+ import { appImportGraph, checkImportRules, readAppSources } from './app-boundaries';
7
+ import { planBoundaryCuts } from './boundary-cuts';
8
+ import type { Finding } from './output';
9
+
10
+ /** A cut and a finding name one violation when they share its code, its importer and its chain. */
11
+ const keyOf = (code: string, at: string | undefined, cause: string): string =>
12
+ [code, at ?? '', cause].join('\u0000');
13
+
14
+ /** `findings` with every surface violation's `fix` replaced by the cut that clears it. */
15
+ export function withCutEdits(
16
+ findings: readonly Finding[],
17
+ graph: Parameters<typeof planBoundaryCuts>[1],
18
+ ): readonly Finding[] {
19
+ const edits = new Map<string, string>();
20
+ for (const finding of findings) {
21
+ if (finding.at === undefined) continue;
22
+ for (const cut of planBoundaryCuts(finding.at, graph)) {
23
+ edits.set(keyOf(cut.code, cut.at, cut.cause), cut.edit);
24
+ }
25
+ }
26
+ return findings.map((finding) => {
27
+ const edit = edits.get(keyOf(finding.code, finding.at, finding.cause));
28
+ return edit === undefined ? finding : { ...finding, fix: edit };
29
+ });
30
+ }
31
+
32
+ /** Read the app's sources once, check them, and hand back findings whose fix is the edit. */
33
+ export async function appBoundaryFindings(root: string): Promise<readonly Finding[]> {
34
+ const files = await readAppSources(root);
35
+ return withCutEdits(checkImportRules(files), appImportGraph(files));
36
+ }