@ultimat3/cli 21.0.0 → 22.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) 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 +23 -17
  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} +22 -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/sync-url.ts +21 -1
  178. package/src/templates/action.ts +30 -16
  179. package/src/templates/entity.ts +12 -7
  180. package/src/templates/index.ts +1 -1
  181. package/src/templates/job.ts +10 -7
  182. package/src/templates/policy.ts +20 -2
  183. package/src/templates/resource-create.ts +127 -0
  184. package/src/templates/resource-form-island.ts +49 -12
  185. package/src/templates/resource.ts +10 -4
  186. package/src/templates/scaffold-app.ts +4 -1
  187. package/src/templates/scaffold-auth.ts +3 -1
  188. package/src/templates/scaffold-container-compose.ts +184 -0
  189. package/src/templates/scaffold-container.ts +24 -159
  190. package/src/templates/scaffold-dashboard-example.ts +2 -2
  191. package/src/templates/scaffold-db-package.ts +16 -3
  192. package/src/templates/scaffold-demo-org.ts +41 -0
  193. package/src/templates/scaffold-entries.ts +1 -1
  194. package/src/templates/scaffold-env.ts +6 -0
  195. package/src/templates/scaffold-helm-templates.ts +66 -7
  196. package/src/templates/scaffold-helm.ts +27 -5
  197. package/src/templates/scaffold-i18n.ts +15 -10
  198. package/src/templates/scaffold-repo.ts +13 -10
  199. package/src/templates/scaffold-roles.ts +38 -10
  200. package/src/templates/slice-foundation.ts +1 -1
  201. package/src/templates/wrap.ts +4 -1
  202. package/src/test-passes.ts +2 -1
  203. package/src/test-workers.ts +26 -0
  204. package/src/ts-scan.ts +3 -6
  205. package/src/tsconfig-references.ts +1 -2
  206. package/src/verify-checks.ts +41 -39
  207. package/src/verify-e2e.ts +10 -7
  208. package/src/verify-run.ts +11 -1
  209. package/src/verify-step.ts +3 -3
  210. package/src/verify-tests.ts +1 -26
  211. package/src/verify-typecheck.ts +28 -0
  212. package/src/web-binding.ts +2 -2
  213. package/src/workspace-graph.ts +10 -33
  214. package/src/cdp-browser.ts +0 -94
  215. package/src/cdp-connection.ts +0 -247
  216. package/src/cdp-e2e-page.ts +0 -180
  217. package/src/cdp-e2e-session.ts +0 -199
  218. package/src/cdp-errors.ts +0 -56
  219. package/src/cdp-launch.ts +0 -193
  220. package/src/cdp-offline-script.ts +0 -73
  221. package/src/cdp-pipe.ts +0 -77
  222. package/src/e2e-app.ts +0 -103
  223. package/src/e2e-browser-handle.ts +0 -55
  224. package/src/e2e-dom-fixture.ts +0 -117
  225. package/src/e2e-driver.ts +0 -117
  226. package/src/e2e-errors.ts +0 -117
  227. package/src/e2e-evaluate.ts +0 -156
  228. package/src/e2e-locator.ts +0 -86
  229. package/src/e2e-page.ts +0 -153
  230. package/src/e2e-preload.ts +0 -64
  231. package/src/e2e-probe.ts +0 -23
  232. package/src/e2e-selection.ts +0 -182
  233. package/src/e2e-spawn.ts +0 -169
  234. package/src/measurement-actor.ts +0 -26
  235. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
  236. /package/src/{dev-render.ts → runtime-render.ts} +0 -0
package/src/cmd-deploy.ts CHANGED
@@ -5,13 +5,22 @@
5
5
  import { join } from 'node:path';
6
6
  import { ERROR_DOCS_URL } from '@ultimat3/core';
7
7
  import { requireAppRoot } from './app-root';
8
+ import {
9
+ type HelmTarget,
10
+ helmUpgradeArgs,
11
+ readHelmTimeout,
12
+ readLabel,
13
+ readReleaseName,
14
+ readRollout,
15
+ } from './cmd-deploy-helm';
16
+ import { deploySpec } from './cmd-deploy-spec';
8
17
  import type { CliCommand, CommandContext } from './command';
9
18
  import { BadFlagError, UnknownCommandError } from './errors';
10
19
  import { msg } from './messages';
11
20
  import type { CommandResult, JsonValue } from './output';
12
21
  import { flagBool, flagString } from './parse';
13
22
  import { quoteArg } from './shell-quote';
14
- import { PROD_ENV_FILE } from './templates/scaffold-container';
23
+ import { PROD_ENV_FILE } from './templates/scaffold-container-compose';
15
24
 
16
25
  /**
17
26
  * Ordered, and the order is the design. `migrate` GATES — it runs to completion before anything
@@ -105,8 +114,40 @@ export function helmImageOverrides(image: string): readonly string[] {
105
114
  : ['--set', `image.repository=${repository}`, '--set', `image.tag=${tag}`];
106
115
  }
107
116
 
108
- export function planDeploy(image: string, method: DeployMethod, root: string): DeployPlan {
117
+ /** What was asked for: the method, and for helm the release it is aimed at. */
118
+ export type DeployRequest =
119
+ | { readonly method: 'compose' }
120
+ | ({ readonly method: 'helm' } & HelmTarget);
121
+
122
+ /**
123
+ * The plan for one method. A helm plan takes its target — release, namespace, timeout — because
124
+ * the release is read off `app.config.ts`, which only the command can import; the compose plan
125
+ * needs none, so its three-argument form is unchanged.
126
+ */
127
+ export function planDeploy(image: string, method: 'compose', root: string): DeployPlan;
128
+ export function planDeploy(
129
+ image: string,
130
+ method: 'helm',
131
+ root: string,
132
+ target: HelmTarget,
133
+ ): DeployPlan;
134
+ export function planDeploy(
135
+ image: string,
136
+ method: DeployMethod,
137
+ root: string,
138
+ target?: HelmTarget,
139
+ ): DeployPlan {
109
140
  if (method === 'helm') {
141
+ // A caller from plain JS can still reach this without one; `app` for every app is the
142
+ // defect the target exists to end, so it is refused rather than defaulted back.
143
+ if (target === undefined) {
144
+ throw new BadFlagError({
145
+ flag: 'release',
146
+ command: 'deploy',
147
+ reason: 'a helm plan names its release, and planDeploy was called with no target',
148
+ fix: 'x deploy --method helm --release my-app --json',
149
+ });
150
+ }
110
151
  // `repo@sha256:…` is a reference this chart cannot express: it renders `repository:tag` and
111
152
  // has no digest branch, so passing one through would deploy `repo@sha256:…:<appVersion>` —
112
153
  // a tag no registry has. Refused here rather than by a `helm upgrade` failing halfway.
@@ -121,19 +162,7 @@ export function planDeploy(image: string, method: DeployMethod, root: string): D
121
162
  return {
122
163
  image,
123
164
  env: {},
124
- steps: [
125
- {
126
- role: 'all',
127
- command: [
128
- 'helm',
129
- 'upgrade',
130
- '--install',
131
- 'app',
132
- join(root, 'docker', 'helm'),
133
- ...helmImageOverrides(image),
134
- ],
135
- },
136
- ],
165
+ steps: [{ role: 'all', command: helmUpgradeArgs(root, target, helmImageOverrides(image)) }],
137
166
  };
138
167
  }
139
168
  return {
@@ -171,33 +200,50 @@ export function planDeploy(image: string, method: DeployMethod, root: string): D
171
200
  * renderers and the failure `fix:` go through here, so the plan `--json` reports, the plan the
172
201
  * terminal shows and the line the refusal hands back can never name three different deployments.
173
202
  */
203
+ /** The flags only a helm deploy reads. Named once: the spec, the refusal and the reader agree. */
204
+ const HELM_ONLY_FLAGS = ['release', 'namespace', 'timeout'] as const;
205
+
206
+ /**
207
+ * The method and its target. A helm-only flag on the compose method is REFUSED, never dropped:
208
+ * `--namespace staging` on a compose deploy would otherwise run against the one box there is and
209
+ * report success, which is the silent direction every declared-and-ignored knob in this repo took.
210
+ */
211
+ export async function readDeployRequest(
212
+ ctx: CommandContext,
213
+ method: DeployMethod,
214
+ root: string,
215
+ ): Promise<DeployRequest> {
216
+ if (method === 'compose') {
217
+ const stray = HELM_ONLY_FLAGS.find((flag) => flagString(ctx.args, flag) !== undefined);
218
+ if (stray !== undefined) {
219
+ throw new BadFlagError({
220
+ flag: stray,
221
+ command: 'deploy',
222
+ reason: 'it applies to --method helm only; a compose deploy has one box and no release',
223
+ fix: 'x deploy --method helm --release my-app --namespace my-namespace --json',
224
+ });
225
+ }
226
+ return { method };
227
+ }
228
+ const namespace = flagString(ctx.args, 'namespace');
229
+ return {
230
+ method,
231
+ release: await readReleaseName(root, flagString(ctx.args, 'release')),
232
+ namespace: namespace === undefined ? undefined : readLabel('namespace', namespace),
233
+ timeout: readHelmTimeout(flagString(ctx.args, 'timeout')),
234
+ };
235
+ }
236
+
174
237
  const stepLine = (env: Readonly<Record<string, string>>, command: readonly string[]): string =>
175
- [...Object.entries(env).map(([name, value]) => `${name}=${quoteArg(value)}`), ...command].join(
176
- ' ',
177
- );
238
+ [
239
+ ...Object.entries(env).map(([name, value]) => `${name}=${quoteArg(value)}`),
240
+ // Every word quoted where it must be: the chart and compose paths are joined from the app
241
+ // root, and a root holding a space or a `$(` pasted back as two words, or as a second command.
242
+ ...command.map(quoteArg),
243
+ ].join(' ');
178
244
 
179
245
  export const deployCommand: CliCommand = {
180
- spec: {
181
- name: 'deploy',
182
- summary: 'run the container deploy plan: migrate first, then the serving roles',
183
- usage: 'x deploy --image repo/app:tag [--method compose|helm] [--dry-run] [--json]',
184
- requiresApp: true,
185
- flags: [
186
- { name: 'image', type: 'string', summary: 'image reference to deploy' },
187
- { name: 'method', type: 'string', summary: 'compose | helm', default: 'compose' },
188
- { name: 'dry-run', type: 'boolean', summary: 'print the plan, run nothing' },
189
- // `--critical` was here and is gone. It parsed, it was echoed into the plan JSON as
190
- // `critical: <bool>`, and no file in `packages/` read that field — so the flag changed
191
- // nothing about what `x deploy` did, on either method. `flag-reads.ts`'s
192
- // `X_CLI_FLAG_UNREAD` passed it, because that gate proves a flag is READ and this one was:
193
- // into a field with no reader. It is not coming back: `@ultimat3/pwa`'s
194
- // `updateSignal({ reason: 'security' })`, the call it was to have triggered, is **deleted**
195
- // as of 9.0.0 for having had no runtime caller of its own, and nothing in the framework
196
- // force-navigates a client. A deploy also has no channel to one — the plan is
197
- // `docker compose up` / `helm upgrade`, and the client's build id is read by `http` (tier 2)
198
- // and `sync` (tier 3), neither of which may import a tier-4 package to act on it.
199
- ],
200
- },
246
+ spec: deploySpec,
201
247
  async run(ctx: CommandContext): Promise<CommandResult> {
202
248
  const root = requireAppRoot('deploy', ctx.cwd).dir;
203
249
  const image = flagString(ctx.args, 'image') ?? 'ultimate-app:dev';
@@ -208,10 +254,27 @@ export const deployCommand: CliCommand = {
208
254
  // `docker/docker-compose.prod.yml`. An app that deleted the chart gets helm's own error through
209
255
  // X_DEPLOY_FAILED, whose fix is the exact command to rerun.
210
256
  const method = readMethod(flagString(ctx.args, 'method'));
211
- const plan = planDeploy(image, method, root);
212
- const planJson: JsonValue = {
257
+ const request = await readDeployRequest(ctx, method, root);
258
+ const plan =
259
+ request.method === 'helm'
260
+ ? planDeploy(image, 'helm', root, request)
261
+ : planDeploy(image, 'compose', root);
262
+ // What the helm flags resolved to — the release above all, which is read off `app.config.ts`
263
+ // and so is not something the operator typed. Absent on compose, which has no release.
264
+ const target: { readonly [key: string]: JsonValue } =
265
+ request.method === 'helm'
266
+ ? {
267
+ helm: {
268
+ release: request.release,
269
+ namespace: request.namespace ?? null,
270
+ timeout: request.timeout,
271
+ },
272
+ }
273
+ : {};
274
+ const planJson: { readonly [key: string]: JsonValue } = {
213
275
  image: plan.image,
214
276
  method,
277
+ ...target,
215
278
  // Reported, because it is what makes `image` above true on the compose method — a dry run
216
279
  // that names an image the steps do not carry is the defect this field closed.
217
280
  env: { ...plan.env },
@@ -231,8 +294,13 @@ export const deployCommand: CliCommand = {
231
294
  ),
232
295
  };
233
296
  }
297
+ // helm prints its release record (`--output json`), so the report carries helm's own verdict
298
+ // and revision rather than an exit code standing in for one. Compose has no such record.
299
+ let rollout: { readonly [key: string]: JsonValue } = {};
234
300
  for (const step of plan.steps) {
235
301
  const result = await ctx.runner(step.command, { cwd: root, env: plan.env });
302
+ if (request.method === 'helm')
303
+ rollout = { rollout: { ...readRollout(request, result.stdout) } };
236
304
  if (!result.ok) {
237
305
  return {
238
306
  ok: false,
@@ -246,7 +314,7 @@ export const deployCommand: CliCommand = {
246
314
  docs: ERROR_DOCS_URL,
247
315
  },
248
316
  ],
249
- data: planJson,
317
+ data: { ...planJson, ...rollout },
250
318
  };
251
319
  }
252
320
  }
@@ -254,7 +322,7 @@ export const deployCommand: CliCommand = {
254
322
  ok: true,
255
323
  command: 'deploy',
256
324
  summary: msg('cli.deploy.plan', { images: 1, roles }),
257
- data: planJson,
325
+ data: { ...planJson, ...rollout },
258
326
  };
259
327
  },
260
328
  };
@@ -0,0 +1,25 @@
1
+ // `x dev`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-dev.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+ import { DEV_ROLES } from './role-start-types';
6
+
7
+ export const devSpec: CommandSpec = {
8
+ name: 'dev',
9
+ summary: 'all roles in one process: embedded services, sub-second reload, /_x mounted',
10
+ usage: 'x dev [--port 3000] [--role web,worker] [--once] [--json]',
11
+ requiresApp: true,
12
+ flags: [
13
+ // No `default`: a default reads exactly like a value the caller typed, and it outranked
14
+ // `PORT` from the app's `.env.development` — `devPortFor` supplies the 3000 last.
15
+ { name: 'port', type: 'string', summary: 'HTTP port (default: PORT, else 3000)' },
16
+ {
17
+ name: 'role',
18
+ type: 'string',
19
+ // `replicator` is named because it is selectable and NOT default — it takes a replication
20
+ // slot on a shared database, which is not something every `x dev` should do by starting.
21
+ summary: `roles to run (default: all of ${DEV_ROLES.join(',')}; replicator is opt-in)`,
22
+ },
23
+ { name: 'once', type: 'boolean', summary: 'boot, report, exit — for smoke tests and CI' },
24
+ ],
25
+ };
package/src/cmd-dev.ts CHANGED
@@ -17,25 +17,18 @@ import { loadSignInPath } from './app-auth';
17
17
  import { appManifest } from './app-manifest';
18
18
  import { requireAppRoot } from './app-root';
19
19
  import { loadAppRuntime } from './app-runtime';
20
+ import { devSpec } from './cmd-dev-spec';
20
21
  import type { CliCommand, CommandContext } from './command';
21
22
  import type { DevDashboardInput, DevStatus } from './dev-dashboard';
22
23
  import { devPanels } from './dev-dashboard';
23
- import { declareDevEnvironment } from './dev-environment';
24
- import { liveFeedLabel } from './dev-live-feed';
24
+ import { declareDevEnvironment, needsDevEnvironmentDeclaration } from './dev-environment';
25
25
  import { clearLock, preflight, writeLock } from './dev-lock';
26
26
  import { createStatementLedger } from './dev-n-plus-one';
27
+ import { devPortFor } from './dev-port';
27
28
  import { coalesceReloads } from './dev-reload';
28
- import { replicaOverrides } from './dev-replica';
29
- import type { RunningRoles } from './dev-roles';
30
- import { DEV_BINDING, DEV_ROLES, selectRoles, startRoles } from './dev-roles';
31
29
  import { devRouteTable } from './dev-route-table';
32
- import type { RunningServices } from './dev-runtime';
33
- import { cdnLabel, describeCdn, describeMail, mailLabel, startServices } from './dev-runtime';
34
- import type { DevServices } from './dev-services';
35
- import { describeServices, reportedUrls, resolveServices } from './dev-services';
36
30
  import { createTraceRecorder } from './dev-traces';
37
31
  import { watchTree } from './dev-watch-tree';
38
- import { intFlagOr, PORT_RANGE } from './flag-number';
39
32
  import { holdUntilShutdown } from './hold';
40
33
  import type { IslandBundle } from './island-bundle';
41
34
  import { buildIslands } from './island-bundle';
@@ -44,10 +37,23 @@ import { msg } from './messages';
44
37
  import type { CommandResult, Finding } from './output';
45
38
  import { findingFrom } from './output';
46
39
  import { flagString } from './parse';
47
- import { metricsPortFor } from './serve';
40
+ import type { RunningRoles } from './role-start';
41
+ import { DEV_BINDING, DEV_ROLES, selectRoles, startRoles } from './role-start';
42
+ import type { DevServices } from './runtime-bindings';
43
+ import {
44
+ describeServices,
45
+ reportedUrls,
46
+ resolveServices,
47
+ withRealtimeEvents,
48
+ } from './runtime-bindings';
49
+ import { liveFeedLabel } from './runtime-live-feed';
50
+ import { replicaOverrides } from './runtime-replica';
51
+ import type { RunningServices } from './runtime-services';
52
+ import { cdnLabel, describeCdn, describeMail, mailLabel, startServices } from './runtime-services';
53
+ import { metricsPortFor, releaseBoot } from './serve';
48
54
  import { loopFacts, loopFinding, loopNotice } from './statement-loop';
49
55
 
50
- const DEFAULT_PORT = 3000;
56
+ const _DEFAULT_PORT = 3000;
51
57
 
52
58
  export interface DevServer {
53
59
  readonly url: string;
@@ -121,13 +127,41 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
121
127
  // EVERY boot: a scratch server (`x shot`, `ui.shot`) boots here too, and without this a
122
128
  // fail-closed dev actor installs nothing — the picture is of a 401. Idempotent.
123
129
  declareDevEnvironment(options.env);
124
- const services = resolveServices(options.root, options.env);
125
- const runtime: RunningServices = await startServices(services, options.env);
130
+ // The declaration lands on `process.env`; the env this boot passes on must carry it too, or every
131
+ // service resolved from `options.env` still reads no `ULTIMATE_ENV`.
132
+ const env = needsDevEnvironmentDeclaration(options.env)
133
+ ? { ...options.env, ULTIMATE_ENV: 'development' }
134
+ : options.env;
135
+ const resolved = resolveServices(options.root, env);
136
+ const runtime: RunningServices = await startServices(resolved, env);
137
+ // The events binding the boot line reports is the bus the runtime chose, read off it.
138
+ const services = withRealtimeEvents(resolved, env, runtime.realtime);
139
+ // `serve.ts`'s `releaseBoot` shape: everything acquired from here on is released, newest first,
140
+ // if the boot throws — a failed `x dev` left PGlite holding `.x/pgdata` for the retry to meet.
141
+ const acquired: (() => void | Promise<void>)[] = [() => runtime.stop()];
142
+ try {
143
+ return await bootDev(options, services, runtime, acquired);
144
+ } catch (error) {
145
+ await releaseBoot(acquired);
146
+ throw error;
147
+ }
148
+ }
149
+
150
+ async function bootDev(
151
+ options: StartDevOptions,
152
+ services: ReturnType<typeof resolveServices>,
153
+ runtime: RunningServices,
154
+ acquired: (() => void | Promise<void>)[],
155
+ ): Promise<DevServer> {
126
156
  // Installed before the app loads, so a span opened during registration is already recorded.
127
157
  // Tracing is always on in the framework and free until an exporter is configured; `x dev` is
128
158
  // what configures one, which is the whole reason `/_x/timeline` has anything to draw.
129
159
  const traces = createTraceRecorder();
130
160
  configureTelemetry({ exporter: traces.exporter });
161
+ acquired.push(() => {
162
+ configureTelemetry({ exporter: noopExporter });
163
+ traces.reset();
164
+ });
131
165
  // Installed at the same moment and for the same reason: an observer is the single switch that
132
166
  // turns statement instrumentation on at all (`@ultimat3/db`'s `observe.ts`), so the timeline's
133
167
  // SQL rows and the repeat counts arrive together rather than through two toggles. `serve.ts`
@@ -135,6 +169,10 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
135
169
  // uninstalled, and nothing more (axiom 6).
136
170
  const statements = createStatementLedger();
137
171
  setStatementObserver(statements.observer);
172
+ acquired.push(() => {
173
+ setStatementObserver(undefined);
174
+ statements.reset();
175
+ });
138
176
  // ONE load at boot, the same call the rebuild below makes: the manifest and the findings are
139
177
  // two projections of one scan. Until 2026-09-07 this was `loadApp` for the findings and then
140
178
  // `appManifest` — which loads again — for the manifest, so a save landing between the two put
@@ -224,6 +262,7 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
224
262
  // standby — so this key does not exist on a homework app's boot at all.
225
263
  ...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
226
264
  });
265
+ acquired.push(() => running.stop());
227
266
 
228
267
  // One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
229
268
  // trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
@@ -295,32 +334,12 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
295
334
  }
296
335
 
297
336
  export const devCommand: CliCommand = {
298
- spec: {
299
- name: 'dev',
300
- summary: 'all roles in one process: embedded services, sub-second reload, /_x mounted',
301
- usage: 'x dev [--port 3000] [--role web,worker] [--once] [--json]',
302
- requiresApp: true,
303
- flags: [
304
- { name: 'port', type: 'string', summary: 'HTTP port', default: String(DEFAULT_PORT) },
305
- {
306
- name: 'role',
307
- type: 'string',
308
- // `replicator` is named because it is selectable and NOT default — it takes a replication
309
- // slot on a shared database, which is not something every `x dev` should do by starting.
310
- summary: `roles to run (default: all of ${DEV_ROLES.join(',')}; replicator is opt-in)`,
311
- },
312
- { name: 'once', type: 'boolean', summary: 'boot, report, exit — for smoke tests and CI' },
313
- ],
314
- },
337
+ spec: devSpec,
315
338
  async run(ctx: CommandContext): Promise<CommandResult> {
316
339
  const root = requireAppRoot('dev', ctx.cwd).dir;
317
340
  // Validated, not `parseInt`'d: `x dev --port abc` handed `NaN` to `Bun.serve`, which binds an
318
341
  // arbitrary port — a dev server reachable at an address nothing printed.
319
- const port = intFlagOr(
320
- ctx.args,
321
- { name: 'port', command: 'dev', ...PORT_RANGE, example: `x dev --port ${DEFAULT_PORT}` },
322
- DEFAULT_PORT,
323
- );
342
+ const port = devPortFor(ctx.args, ctx.env);
324
343
  const roles = selectRoles(flagString(ctx.args, 'role'));
325
344
  // BEFORE anything boots. Both failures this catches were reachable and both reported the wrong
326
345
  // thing: a taken port surfaced as X_CLI_UNEXPECTED wrapping "Is port 3000 in use?" with a `fix:`
@@ -0,0 +1,16 @@
1
+ // `x docs`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-docs.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ /** Matches printed by default. Enough to choose between, few enough to read all of. */
7
+ export const DEFAULT_LIMIT = 5;
8
+
9
+ export const docsSpec: CommandSpec = {
10
+ name: 'docs',
11
+ summary: 'the framework docs, answered offline from the installed packages',
12
+ usage: 'x docs "<question|topic|symbol>" [--limit <n>] [--json]',
13
+ flags: [
14
+ { name: 'limit', type: 'string', summary: `matches to return (default: ${DEFAULT_LIMIT})` },
15
+ ],
16
+ };
package/src/cmd-docs.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { ERROR_DOCS_URL } from '@ultimat3/core';
8
8
  import type { DocEntry, DocHit } from '@ultimat3/manifest';
9
9
  import { nearestTopics, scanInstalledDocs, searchDocs } from '@ultimat3/manifest';
10
+ import { DEFAULT_LIMIT, docsSpec } from './cmd-docs-spec';
10
11
  import type { CliCommand, CommandContext } from './command';
11
12
  import { MissingPositionalError } from './errors';
12
13
  import { frameworkScopeDir } from './framework-scope';
@@ -15,9 +16,6 @@ import { msg } from './messages';
15
16
  import type { CommandResult, Finding, JsonValue } from './output';
16
17
  import { flagString } from './parse';
17
18
 
18
- /** Matches printed by default. Enough to choose between, few enough to read all of. */
19
- const DEFAULT_LIMIT = 5;
20
-
21
19
  /** An install where the CLI cannot see its own dependency is broken, not merely undocumented. */
22
20
  const unresolvedFinding = (): Finding => ({
23
21
  code: 'X_CLI_UNEXPECTED',
@@ -115,14 +113,7 @@ function missResult(query: string, entries: readonly DocEntry[]): CommandResult
115
113
  }
116
114
 
117
115
  export const docsCommand: CliCommand = {
118
- spec: {
119
- name: 'docs',
120
- summary: 'the framework docs, answered offline from the installed packages',
121
- usage: 'x docs "<question|topic|symbol>" [--limit <n>] [--json]',
122
- flags: [
123
- { name: 'limit', type: 'string', summary: `matches to return (default: ${DEFAULT_LIMIT})` },
124
- ],
125
- },
116
+ spec: docsSpec,
126
117
  // `async` is load-bearing: a synchronous throw would escape every caller that awaits the
127
118
  // promise this signature promises, including the dispatcher's own error path.
128
119
  async run(ctx: CommandContext): Promise<CommandResult> {
@@ -0,0 +1,21 @@
1
+ // `x doctor`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-doctor.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ /** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
7
+ export const DEFAULT_DOCTOR_PORT = 3000;
8
+
9
+ export const doctorSpec: CommandSpec = {
10
+ name: 'doctor',
11
+ summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
12
+ usage: 'x doctor [--port 3000] [--json]',
13
+ flags: [
14
+ {
15
+ name: 'port',
16
+ type: 'string',
17
+ summary: 'port to test',
18
+ default: String(DEFAULT_DOCTOR_PORT),
19
+ },
20
+ ],
21
+ };
package/src/cmd-doctor.ts CHANGED
@@ -19,9 +19,9 @@ import {
19
19
  } from '@ultimat3/db';
20
20
  import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
21
21
  import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
22
+ import { DEFAULT_DOCTOR_PORT, doctorSpec } from './cmd-doctor-spec';
22
23
  import type { CliCommand, CommandContext } from './command';
23
24
  import { checkMigrationSnapshots } from './db-snapshot';
24
- import { syncPortFor } from './dev-sync';
25
25
  import type { OfflineFallbackFact } from './doctor-offline';
26
26
  import { offlineFallbackFinding, offlineFallbackProbe } from './doctor-offline';
27
27
  import { intFlagOr, PORT_RANGE, portPairAfter } from './flag-number';
@@ -31,6 +31,7 @@ import type { CommandResult, Finding } from './output';
31
31
  import { findingFrom } from './output';
32
32
  import type { ParsedArgs } from './parse';
33
33
  import { portFree } from './port-probe';
34
+ import { syncPortFor } from './role-sync';
34
35
  import { checkMigrationDrift } from './schema-drift';
35
36
 
36
37
  /**
@@ -129,9 +130,6 @@ export const embeddedDatabaseFinding = (fact: EmbeddedDatabase): Finding | undef
129
130
  /** The file `x doctor` reports missing, and the one the reader creates. */
130
131
  export const ENV_DEVELOPMENT = '.env.development';
131
132
 
132
- /** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
133
- const DEFAULT_DOCTOR_PORT = 3000;
134
-
135
133
  /**
136
134
  * Both ports `x dev` binds, each labelled with the role that wants it. `x dev --port 3999` printed
137
135
  * `web listening on 3999`, then died on 4000 as `X_CLI_UNEXPECTED` with a caught `Error` rendered
@@ -144,7 +142,7 @@ const DEFAULT_DOCTOR_PORT = 3000;
144
142
  * N+1 is still not a runnable command. It did not move both until 2026-09 — the line was
145
143
  * `neighbouringPort(probe.port)`, which for the sync finding IS the port the finding is about, and
146
144
  * a docblock claiming otherwise is how it survived. `portPairAfter` is the one reader of that rule
147
- * and `dev-sync.ts`'s own refusal shares it.
145
+ * and `role-sync.ts`'s own refusal shares it.
148
146
  *
149
147
  * The sync port is `syncPortFor`, never `neighbouringPort` again: that helper answers 65534 for a
150
148
  * web port of 65535 — BELOW the web port, and a port `x dev` never binds — where the boot refuses
@@ -400,19 +398,7 @@ export function probeFor(cwd: string, bunVersion: string, port: number): DoctorP
400
398
  }
401
399
 
402
400
  export const doctorCommand: CliCommand = {
403
- spec: {
404
- name: 'doctor',
405
- summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
406
- usage: 'x doctor [--port 3000] [--json]',
407
- flags: [
408
- {
409
- name: 'port',
410
- type: 'string',
411
- summary: 'port to test',
412
- default: String(DEFAULT_DOCTOR_PORT),
413
- },
414
- ],
415
- },
401
+ spec: doctorSpec,
416
402
  async run(ctx: CommandContext): Promise<CommandResult> {
417
403
  const port = doctorPort(ctx.args);
418
404
  const findings = await runDoctor(probeFor(ctx.cwd, ctx.bunVersion, port));
@@ -0,0 +1,18 @@
1
+ // `x env`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-env.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import { ENV_SCHEMA_EXPORT } from './app-env';
5
+ import { APP_CONFIG_FILE } from './app-root';
6
+ import type { CommandSpec } from './parse';
7
+
8
+ export const envSpec: CommandSpec = {
9
+ name: 'env',
10
+ summary: `the typed environment declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`,
11
+ usage: 'x env [check|example] [--json]',
12
+ requiresApp: true,
13
+ subcommands: ['check', 'example'],
14
+ // The bare `x env` answers the question the fix line on every `X_ENV_MISSING` in this
15
+ // framework already tells its reader to run.
16
+ defaultSubcommand: 'check',
17
+ flags: [],
18
+ };
package/src/cmd-env.ts CHANGED
@@ -5,8 +5,9 @@
5
5
  // Bun ships no path-join primitive, and `.env.example` is written app-root-relative.
6
6
  import { join } from 'node:path';
7
7
  import { checkEnv, ENV_EXAMPLE_PATH, ERROR_DOCS_URL, maskedEnvValues } from '@ultimat3/core';
8
- import { ENV_SCHEMA_EXPORT, envExampleFor, loadEnvSchema } from './app-env';
9
- import { APP_CONFIG_FILE, requireAppRoot } from './app-root';
8
+ import { envExampleFor, loadEnvSchema } from './app-env';
9
+ import { requireAppRoot } from './app-root';
10
+ import { envSpec } from './cmd-env-spec';
10
11
  import type { CliCommand, CommandContext } from './command';
11
12
  import { EnvSchemaMissingError } from './errors';
12
13
  import { msg } from './messages';
@@ -73,17 +74,7 @@ async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
73
74
  }
74
75
 
75
76
  export const envCommand: CliCommand = {
76
- spec: {
77
- name: 'env',
78
- summary: `the typed environment declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`,
79
- usage: 'x env [check|example] [--json]',
80
- requiresApp: true,
81
- subcommands: ['check', 'example'],
82
- // The bare `x env` answers the question the fix line on every `X_ENV_MISSING` in this
83
- // framework already tells its reader to run.
84
- defaultSubcommand: 'check',
85
- flags: [],
86
- },
77
+ spec: envSpec,
87
78
  async run(ctx: CommandContext): Promise<CommandResult> {
88
79
  // `subcommand`, never `positionals[0]`: the parser has already lifted a declared subcommand
89
80
  // out of the positionals, so reading the array here matches nothing and every invocation
@@ -0,0 +1,23 @@
1
+ // `x errors`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-errors.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const ERRORS_SUBCOMMANDS = ['explain', 'list'] as const;
7
+
8
+ export const errorsSpec: CommandSpec = {
9
+ name: 'errors',
10
+ summary: 'an X_* code, explained: cause, runnable fix, docs URL',
11
+ usage: 'x errors [explain <CODE>|list] [--json]',
12
+ subcommands: ERRORS_SUBCOMMANDS,
13
+ // `explain`, deliberately: the bare `x errors` then answers with `MissingPositionalError`,
14
+ // which names `<CODE>` and hands back a real invocation. `list` would silently print 200 rows
15
+ // to a caller who meant to explain one — see `MissingPositionalError`'s own note.
16
+ defaultSubcommand: 'explain',
17
+ // `x errors X_PERMISSION_UNKNOWN` is the form every reader tries first — `x help` prints
18
+ // `errors an X_* code, explained`, which reads as exactly that — and it answered
19
+ // `X_CLI_UNKNOWN_COMMAND … fix: x help`, which leads back to the line that suggested it.
20
+ // Safe to declare here and nowhere else so far: the only thing that is not `explain` or
21
+ // `list` in this slot is a code, and a near miss of either is still refused (#F16).
22
+ defaultSubcommandTakesPositional: true,
23
+ };
package/src/cmd-errors.ts CHANGED
@@ -5,6 +5,7 @@ import { nearestName, singleLine } from '@ultimat3/core';
5
5
  // a plausible-sounding explanation an agent would then act on.
6
6
 
7
7
  import type { ErrorExplanation } from '@ultimat3/mcp';
8
+ import { errorsSpec } from './cmd-errors-spec';
8
9
  import type { CliCommand, CommandContext } from './command';
9
10
  import type { ErrorCatalog } from './error-catalog';
10
11
  import { loadErrorCatalog } from './error-catalog';
@@ -14,7 +15,7 @@ import { explainErrorCode, explainEveryErrorCode } from './mcp-errors';
14
15
  import { msg } from './messages';
15
16
  import type { CommandResult, JsonValue } from './output';
16
17
 
17
- export const ERRORS_SUBCOMMANDS = ['explain', 'list'] as const;
18
+ export { ERRORS_SUBCOMMANDS } from './cmd-errors-spec';
18
19
 
19
20
  /**
20
21
  * `site` is the throw site as DATA, and it is why the `fix:` for a code whose fix is built at run
@@ -85,22 +86,7 @@ function listAll(catalog: ErrorCatalog): CommandResult {
85
86
  }
86
87
 
87
88
  export const errorsCommand: CliCommand = {
88
- spec: {
89
- name: 'errors',
90
- summary: 'an X_* code, explained: cause, runnable fix, docs URL',
91
- usage: 'x errors [explain <CODE>|list] [--json]',
92
- subcommands: ERRORS_SUBCOMMANDS,
93
- // `explain`, deliberately: the bare `x errors` then answers with `MissingPositionalError`,
94
- // which names `<CODE>` and hands back a real invocation. `list` would silently print 200 rows
95
- // to a caller who meant to explain one — see `MissingPositionalError`'s own note.
96
- defaultSubcommand: 'explain',
97
- // `x errors X_PERMISSION_UNKNOWN` is the form every reader tries first — `x help` prints
98
- // `errors an X_* code, explained`, which reads as exactly that — and it answered
99
- // `X_CLI_UNKNOWN_COMMAND … fix: x help`, which leads back to the line that suggested it.
100
- // Safe to declare here and nowhere else so far: the only thing that is not `explain` or
101
- // `list` in this slot is a code, and a near miss of either is still refused (#F16).
102
- defaultSubcommandTakesPositional: true,
103
- },
89
+ spec: errorsSpec,
104
90
  // `async` is load-bearing: a synchronous throw would escape every caller that awaits the
105
91
  // promise this signature promises, including the dispatcher's own error path.
106
92
  async run(ctx: CommandContext): Promise<CommandResult> {