@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
@@ -0,0 +1,53 @@
1
+ // `x shot`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-shot.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const shotSpec: CommandSpec = {
7
+ name: 'shot',
8
+ summary: 'photograph one route, one island in a state it declares, or every island in the app',
9
+ usage:
10
+ 'x shot <route> | --island <name> [--state <id>] | --all-islands [--port 0] [--out <dir>] [--settle 2000] [--json]',
11
+ requiresApp: true,
12
+ flags: [
13
+ { name: 'port', type: 'string', summary: 'dev port (0 lets the kernel pick a free one)' },
14
+ { name: 'out', type: 'string', summary: 'where shot.png and verdict.json are written' },
15
+ { name: 'full', type: 'boolean', summary: 'whole page, not the fold', default: true },
16
+ { name: 'settle', type: 'string', summary: 'ms to wait after load before capturing' },
17
+ { name: 'timeout', type: 'string', summary: 'ms one navigation may take' },
18
+ { name: 'browser', type: 'string', summary: 'Chrome or Chromium binary to launch' },
19
+ {
20
+ name: 'cdp-url',
21
+ type: 'string',
22
+ summary: 'attach to a browser somebody else is running (a provider session, a sidecar)',
23
+ },
24
+ { name: 'allow-hosts', type: 'string', summary: 'extra hosts the page may request' },
25
+ {
26
+ name: 'theme',
27
+ type: 'string',
28
+ summary: "light or dark, stored as the visitor's choice; absent is the app's own default",
29
+ },
30
+ // A FLAG on `x shot` and never a second command: photographing a route and photographing a
31
+ // component are one job with two subjects, and a parallel command would be the second path
32
+ // axiom 1 refuses.
33
+ {
34
+ name: 'island',
35
+ type: 'string',
36
+ summary: 'photograph one island in every state it declares',
37
+ },
38
+ {
39
+ name: 'state',
40
+ type: 'string',
41
+ summary: 'one declared state of that island, not all of them',
42
+ },
43
+ // Its own SPELLING and never `--island` with no value: the parser refuses a bare `--island`
44
+ // ("expects a value") and `--island=` is an empty name, so "every island" had no form a
45
+ // reader could type that could not be read as a mistyped one. A boolean cannot be confused
46
+ // with a name, and `x shot --all-islands` says what it does beside `x shot --island <name>`.
47
+ {
48
+ name: 'all-islands',
49
+ type: 'boolean',
50
+ summary: 'every island in the app, in every state it declares, plus an index.md',
51
+ },
52
+ ],
53
+ };
package/src/cmd-shot.ts CHANGED
@@ -8,10 +8,10 @@
8
8
  import { mkdirSync } from 'node:fs';
9
9
  import { join, resolve } from 'node:path';
10
10
  import { IDLE_HYDRATE_TIMEOUT_MS } from '@ultimat3/render';
11
- import type { ColorScheme, ScrapeDriver, ScrapePage, ScrapeSession } from '@ultimat3/scraping';
12
- import { DEFAULT_PAGE_TIMEOUT_MS, systemScrapeClock } from '@ultimat3/scraping';
13
11
  import { requireAppRoot } from './app-root';
14
12
  import { appBrowser } from './browser-launcher';
13
+ import type { ShotColorScheme, ShotDriver, ShotPage, ShotSession } from './browser-launcher-port';
14
+ import { DEFAULT_PAGE_TIMEOUT_MS, systemShotClock } from './cdp-shot-clock';
15
15
  import {
16
16
  islandShot,
17
17
  islandShotResult,
@@ -22,6 +22,7 @@ import {
22
22
  refuseSweepWithRoute,
23
23
  refuseSweepWithState,
24
24
  } from './cmd-shot-island';
25
+ import { shotSpec } from './cmd-shot-spec';
25
26
  import type { CliCommand, CommandContext } from './command';
26
27
  import { BadFlagError, MissingPositionalError } from './errors';
27
28
  import { intFlagOr, PORT_RANGE } from './flag-number';
@@ -181,7 +182,7 @@ const intFlag = (
181
182
  export interface ShotRun {
182
183
  readonly route: string;
183
184
  readonly outDir: string;
184
- readonly driver: ScrapeDriver;
185
+ readonly driver: ShotDriver;
185
186
  readonly boot: () => Promise<ShotServer>;
186
187
  readonly settleMs: number;
187
188
  readonly timeoutMs: number;
@@ -200,7 +201,7 @@ export interface ShotRun {
200
201
  * neither: the box's own preference and the app's own default — what `x shot` has always done,
201
202
  * and the point of `defaultMode` — and `ui.shot` names one explicitly for exactly that reason.
202
203
  */
203
- readonly colorScheme?: ColorScheme | undefined;
204
+ readonly colorScheme?: ShotColorScheme | undefined;
204
205
  readonly now?: (() => Date) | undefined;
205
206
  /**
206
207
  * Something to do with the page AFTER the islands settled and BEFORE the picture — `ui.inspect`
@@ -209,7 +210,7 @@ export interface ShotRun {
209
210
  * caller who never calls it gets the count from the first settle.
210
211
  */
211
212
  readonly act?:
212
- | ((page: ScrapePage, settle: () => Promise<IslandCount | null>) => Promise<void>)
213
+ | ((page: ShotPage, settle: () => Promise<IslandCount | null>) => Promise<void>)
213
214
  | undefined;
214
215
  }
215
216
 
@@ -226,7 +227,7 @@ const quietly = async (stop: () => Promise<void>): Promise<void> => {
226
227
  */
227
228
  export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
228
229
  const server = await options.boot();
229
- let session: ScrapeSession | undefined;
230
+ let session: ShotSession | undefined;
230
231
  try {
231
232
  const requestedUrl = new URL(options.route, server.url).toString();
232
233
  session = await options.driver.open({
@@ -235,7 +236,7 @@ export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
235
236
  // inside your network is the widest SSRF surface an app can own, and a screenshot command is
236
237
  // not the place to open it by default. Every refusal lands in the verdict's `refused` count.
237
238
  rules: { allowHosts: allowHostsFrom(server.url, options.extraHosts) },
238
- clock: systemScrapeClock,
239
+ clock: systemShotClock,
239
240
  timeoutMs: options.timeoutMs,
240
241
  });
241
242
  const page = session.page;
@@ -312,54 +313,7 @@ export const shotResult = (artifacts: ShotArtifacts): CommandResult => ({
312
313
  });
313
314
 
314
315
  export const shotCommand: CliCommand = {
315
- spec: {
316
- name: 'shot',
317
- summary: 'photograph one route, one island in a state it declares, or every island in the app',
318
- usage:
319
- 'x shot <route> | --island <name> [--state <id>] | --all-islands [--port 0] [--out <dir>] [--settle 2000] [--json]',
320
- requiresApp: true,
321
- flags: [
322
- { name: 'port', type: 'string', summary: 'dev port (0 lets the kernel pick a free one)' },
323
- { name: 'out', type: 'string', summary: 'where shot.png and verdict.json are written' },
324
- { name: 'full', type: 'boolean', summary: 'whole page, not the fold', default: true },
325
- { name: 'settle', type: 'string', summary: 'ms to wait after load before capturing' },
326
- { name: 'timeout', type: 'string', summary: 'ms one navigation may take' },
327
- { name: 'browser', type: 'string', summary: 'browser executable puppeteer-core launches' },
328
- {
329
- name: 'cdp-url',
330
- type: 'string',
331
- summary: 'attach to a browser somebody else is running (a provider session, a sidecar)',
332
- },
333
- { name: 'allow-hosts', type: 'string', summary: 'extra hosts the page may request' },
334
- {
335
- name: 'theme',
336
- type: 'string',
337
- summary: "light or dark, stored as the visitor's choice; absent is the app's own default",
338
- },
339
- // A FLAG on `x shot` and never a second command: photographing a route and photographing a
340
- // component are one job with two subjects, and a parallel command would be the second path
341
- // axiom 1 refuses.
342
- {
343
- name: 'island',
344
- type: 'string',
345
- summary: 'photograph one island in every state it declares',
346
- },
347
- {
348
- name: 'state',
349
- type: 'string',
350
- summary: 'one declared state of that island, not all of them',
351
- },
352
- // Its own SPELLING and never `--island` with no value: the parser refuses a bare `--island`
353
- // ("expects a value") and `--island=` is an empty name, so "every island" had no form a
354
- // reader could type that could not be read as a mistyped one. A boolean cannot be confused
355
- // with a name, and `x shot --all-islands` says what it does beside `x shot --island <name>`.
356
- {
357
- name: 'all-islands',
358
- type: 'boolean',
359
- summary: 'every island in the app, in every state it declares, plus an index.md',
360
- },
361
- ],
362
- },
316
+ spec: shotSpec,
363
317
  async run(ctx: CommandContext): Promise<CommandResult> {
364
318
  const root = requireAppRoot('shot', ctx.cwd).dir;
365
319
  // Every value read before anything boots: a typo must not cost a browser and a dev server to
@@ -396,8 +350,7 @@ export const shotCommand: CliCommand = {
396
350
  // Which browser this run gets — start one here, or attach to one somebody else is running.
397
351
  // Decided by `shot-browser.ts` over plain inputs, and decided HERE, before a dev server or a
398
352
  // provider session exists to pay for a typo. It also PROBES for an installed Chrome and refuses
399
- // when there is none: `puppeteer-core` bundles no browser, so a missing one used to surface as
400
- // a library throw one embedded Postgres later.
353
+ // when there is none, so a missing browser costs no embedded Postgres boot.
401
354
  const { cdpUrl, executablePath } = shotBrowserChoice({
402
355
  cdpFlag: flagString(ctx.args, 'cdp-url'),
403
356
  browserFlag: flagString(ctx.args, 'browser'),
@@ -423,10 +376,8 @@ export const shotCommand: CliCommand = {
423
376
  await islandShot({ ...shared, island, ...(state === undefined ? {} : { state }) }),
424
377
  );
425
378
  }
426
- // Resolved before the boot for the same reason: an app with no browser installed must not pay
427
- // an embedded Postgres to be told to run `bun add -d puppeteer-core`.
379
+ // Built before the boot, for the same reason.
428
380
  const driver = await appBrowser({
429
- root,
430
381
  ...(executablePath === undefined ? {} : { executablePath }),
431
382
  ...(cdpUrl === undefined ? {} : { cdpUrl }),
432
383
  });
@@ -0,0 +1,21 @@
1
+ // `x tasks`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-tasks.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const tasksSpec: CommandSpec = {
7
+ name: 'tasks',
8
+ summary: 'cron tasks, their timezone and their next run',
9
+ usage: 'x tasks [list|show <name>] [--count n] [--json]',
10
+ requiresApp: true,
11
+ subcommands: ['list', 'show'],
12
+ defaultSubcommand: 'list',
13
+ flags: [
14
+ {
15
+ name: 'count',
16
+ type: 'string',
17
+ summary: 'show: how many upcoming occurrences to list',
18
+ subcommands: ['show'],
19
+ },
20
+ ],
21
+ };
package/src/cmd-tasks.ts CHANGED
@@ -8,6 +8,7 @@ import type { TaskHandle } from '@ultimat3/jobs';
8
8
  import type { CronPhrases } from '@ultimat3/time';
9
9
  import { loadApp } from './app-load';
10
10
  import { requireAppRoot } from './app-root';
11
+ import { tasksSpec } from './cmd-tasks-spec';
11
12
  import type { CliCommand, CommandContext } from './command';
12
13
  import { BadFlagError, DeclarationUnknownError } from './errors';
13
14
  import { msg } from './messages';
@@ -130,22 +131,7 @@ function runShow(ctx: CommandContext, nowMs: number, findings: readonly Finding[
130
131
  }
131
132
 
132
133
  export const tasksCommand: CliCommand = {
133
- spec: {
134
- name: 'tasks',
135
- summary: 'cron tasks, their timezone and their next run',
136
- usage: 'x tasks [list|show <name>] [--count n] [--json]',
137
- requiresApp: true,
138
- subcommands: ['list', 'show'],
139
- defaultSubcommand: 'list',
140
- flags: [
141
- {
142
- name: 'count',
143
- type: 'string',
144
- summary: 'show: how many upcoming occurrences to list',
145
- subcommands: ['show'],
146
- },
147
- ],
148
- },
134
+ spec: tasksSpec,
149
135
  async run(ctx: CommandContext): Promise<CommandResult> {
150
136
  const root = requireAppRoot('tasks', ctx.cwd).dir;
151
137
  const { findings } = await loadApp(root);
@@ -0,0 +1,54 @@
1
+ // `x test`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-test.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import { TEST_TYPES } from '@ultimat3/testing/test-types';
5
+ import { DEFAULT_BASE } from './affected';
6
+ import type { CommandSpec } from './parse';
7
+ import { SERIAL_TYPES, WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
8
+
9
+ export const testSpec: CommandSpec = {
10
+ name: 'test',
11
+ summary:
12
+ 'run one test type — or the whole suite — across N workers, one isolated database per worker',
13
+ usage: `x test [${TEST_TYPES.join('|')}] [--filter text] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--json] [-- <bun test flags>]`,
14
+ positionalChoices: TEST_TYPES,
15
+ // The one command that hands a tail to another tool — `bun test` — and the reason
16
+ // `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
17
+ // dropped both, so a run that measured no coverage reported exactly what a coverage run does.
18
+ passthrough: true,
19
+ flags: [
20
+ {
21
+ name: 'workers',
22
+ type: 'string',
23
+ summary: `bun worker count (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING}); clamped to the file count, and to 1 for ${SERIAL_TYPES.join(' and ')}`,
24
+ },
25
+ {
26
+ name: 'worker',
27
+ type: 'string',
28
+ summary:
29
+ 'run only shard I of an N-way split of the selection, serially — one CI job\u2019s share',
30
+ },
31
+ { name: 'filter', type: 'string', summary: 'only files whose path contains this substring' },
32
+ {
33
+ name: 'sample',
34
+ type: 'string',
35
+ summary:
36
+ 'run at most N files of the selected type — a fast signal for the eval loop, never a gate',
37
+ },
38
+ {
39
+ name: 'affected',
40
+ type: 'boolean',
41
+ summary: 'only the workspaces a diff touches, and everything that depends on one of them',
42
+ },
43
+ {
44
+ name: 'base',
45
+ type: 'string',
46
+ summary: `--affected: git ref to diff against, merge-base style (default: ${DEFAULT_BASE})`,
47
+ },
48
+ {
49
+ name: 'dirty',
50
+ type: 'boolean',
51
+ summary: '--affected: also count uncommitted work, whichever agent in this checkout made it',
52
+ },
53
+ ],
54
+ };
package/src/cmd-test.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { AffectedScope } from './affected';
9
9
  import { affectedScope, affectedScopeJson, DEFAULT_BASE, inScope } from './affected';
10
+ import { testSpec } from './cmd-test-spec';
10
11
  import type { CliCommand, CommandContext } from './command';
11
12
  import { ok } from './command';
12
13
  import { BadFlagError, NoTestFilesError } from './errors';
@@ -18,9 +19,9 @@ import { flagBool, flagString } from './parse';
18
19
  import { quoteArg } from './shell-quote';
19
20
  import { discoverTests, missingSelection, readSample, readType, sampleFiles } from './test-select';
20
21
  import { runShards } from './test-shards';
21
- import { defaultWorkers, WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
22
+ import { defaultWorkers, SERIAL_TYPES, WORKER_CEILING } from './test-workers';
22
23
  import type { TestType } from './verify-tests';
23
- import { SERIAL_TYPES, TEST_TYPES } from './verify-tests';
24
+ import { TEST_TYPES } from './verify-tests';
24
25
 
25
26
  /**
26
27
  * `--workers` and `--shard`. `Number.parseInt` alone accepted `4abc` and `4.9` as four, while
@@ -100,53 +101,7 @@ const withScope = (result: CommandResult, scope: AffectedScope): CommandResult =
100
101
  });
101
102
 
102
103
  export const testCommand: CliCommand = {
103
- spec: {
104
- name: 'test',
105
- summary:
106
- 'run one test type — or the whole suite — across N workers, one isolated database per worker',
107
- usage: `x test [${TEST_TYPES.join('|')}] [--filter text] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--json] [-- <bun test flags>]`,
108
- positionalChoices: TEST_TYPES,
109
- // The one command that hands a tail to another tool — `bun test` — and the reason
110
- // `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
111
- // dropped both, so a run that measured no coverage reported exactly what a coverage run does.
112
- passthrough: true,
113
- flags: [
114
- {
115
- name: 'workers',
116
- type: 'string',
117
- summary: `bun worker count (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING}); clamped to the file count, and to 1 for ${SERIAL_TYPES.join(' and ')}`,
118
- },
119
- {
120
- name: 'worker',
121
- type: 'string',
122
- summary:
123
- 'run only shard I of an N-way split of the selection, serially — one CI job\u2019s share',
124
- },
125
- { name: 'filter', type: 'string', summary: 'only files whose path contains this substring' },
126
- {
127
- name: 'sample',
128
- type: 'string',
129
- summary:
130
- 'run at most N files of the selected type — a fast signal for the eval loop, never a gate',
131
- },
132
- {
133
- name: 'affected',
134
- type: 'boolean',
135
- summary: 'only the workspaces a diff touches, and everything that depends on one of them',
136
- },
137
- {
138
- name: 'base',
139
- type: 'string',
140
- summary: `--affected: git ref to diff against, merge-base style (default: ${DEFAULT_BASE})`,
141
- },
142
- {
143
- name: 'dirty',
144
- type: 'boolean',
145
- summary:
146
- '--affected: also count uncommitted work, whichever agent in this checkout made it',
147
- },
148
- ],
149
- },
104
+ spec: testSpec,
150
105
  async run(ctx: CommandContext): Promise<CommandResult> {
151
106
  const type = readOnlyType(ctx.args.positionals);
152
107
  const filter = flagString(ctx.args, 'filter');
@@ -0,0 +1,28 @@
1
+ // `x verify`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-verify.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+ import { WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
6
+
7
+ export const verifySpec: CommandSpec = {
8
+ name: 'verify',
9
+ summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
10
+ usage: 'x verify [--only <step>] [--workers N] [--json]',
11
+ requiresApp: true,
12
+ // Two flags, and only one of them narrows. `--workers` changes how wide the test steps
13
+ // spread, never which steps run. `--only` runs one step and says so in both renderers —
14
+ // never silently, which is the whole of what makes it safe to have.
15
+ flags: [
16
+ {
17
+ name: 'workers',
18
+ type: 'string',
19
+ summary: `test processes per parallel step (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
20
+ },
21
+ {
22
+ name: 'only',
23
+ type: 'string',
24
+ summary:
25
+ 'run ONE step by name — an iteration loop, NOT A GATE RUN; the gate is this command with no flag',
26
+ },
27
+ ],
28
+ };
package/src/cmd-verify.ts CHANGED
@@ -9,13 +9,14 @@
9
9
 
10
10
  import { nearestName } from '@ultimat3/core';
11
11
  import { requireAppRoot } from './app-root';
12
+ import { verifySpec } from './cmd-verify-spec';
12
13
  import type { CliCommand, CommandContext } from './command';
13
14
  import { BadFlagError } from './errors';
14
15
  import { readIntFlag } from './flag-number';
15
16
  import type { CommandResult } from './output';
16
17
  import type { ParsedArgs } from './parse';
17
18
  import { flagString } from './parse';
18
- import { WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
19
+ import { WORKER_CEILING, WORKER_FLOOR } from './test-workers';
19
20
  import { VERIFY_STEPS } from './verify-checks';
20
21
  import { runVerify } from './verify-run';
21
22
  import type { VerifyStepName } from './verify-step';
@@ -28,28 +29,7 @@ export { VERIFY_STEPS } from './verify-checks';
28
29
  export { runVerify } from './verify-run';
29
30
 
30
31
  export const verifyCommand: CliCommand = {
31
- spec: {
32
- name: 'verify',
33
- summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
34
- usage: 'x verify [--only <step>] [--workers N] [--json]',
35
- requiresApp: true,
36
- // Two flags, and only one of them narrows. `--workers` changes how wide the test steps
37
- // spread, never which steps run. `--only` runs one step and says so in both renderers —
38
- // never silently, which is the whole of what makes it safe to have.
39
- flags: [
40
- {
41
- name: 'workers',
42
- type: 'string',
43
- summary: `test processes per parallel step (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
44
- },
45
- {
46
- name: 'only',
47
- type: 'string',
48
- summary:
49
- 'run ONE step by name — an iteration loop, NOT A GATE RUN; the gate is this command with no flag',
50
- },
51
- ],
52
- },
32
+ spec: verifySpec,
53
33
  async run(ctx: CommandContext): Promise<CommandResult> {
54
34
  const root = requireAppRoot('verify', ctx.cwd).dir;
55
35
  // Both readers before the run: an unrunnable flag must be refused in milliseconds, not after
package/src/db-seed.ts CHANGED
@@ -16,6 +16,7 @@ import { BadFlagError } from './errors';
16
16
  import type { Finding, JsonValue } from './output';
17
17
  import { findingFrom } from './output';
18
18
  import { hasPathSegment } from './path-segments';
19
+ import { isTest } from './source-files';
19
20
  import { renderTable } from './table';
20
21
 
21
22
  /**
@@ -102,10 +103,11 @@ export async function discoverSeeds(root: string): Promise<SeedDiscovery> {
102
103
  const seen = new Set<string>();
103
104
  for (const pattern of SEED_GLOBS) {
104
105
  for await (const absolute of new Bun.Glob(pattern).scan({ cwd: root, absolute: true })) {
105
- if (hasPathSegment(absolute, 'node_modules') || absolute.includes('.test.')) continue;
106
+ const file = relative(root, absolute).split(sep).join('/');
107
+ // Root-relative, as `loadApp` tests it: an absolute path under `my.test.app/` is no test.
108
+ if (hasPathSegment(absolute, 'node_modules') || isTest(file)) continue;
106
109
  if (seen.has(absolute)) continue;
107
110
  seen.add(absolute);
108
- const file = relative(root, absolute).split(sep).join('/');
109
111
  let module: Record<string, unknown>;
110
112
  try {
111
113
  module = (await import(absolute)) as Record<string, unknown>;
@@ -22,14 +22,14 @@ import { json as jsonResponse } from '@ultimat3/http';
22
22
  import type { MemoryMailDriver } from '@ultimat3/mail';
23
23
  import { isMemoryDriver } from '@ultimat3/mail';
24
24
  import type { Manifest } from '@ultimat3/manifest';
25
- import { checkAppBoundaries } from './app-boundaries';
26
25
  import { appManifest, readAppManifest } from './app-manifest';
26
+ import { appBoundaryFindings } from './boundary-findings';
27
27
  import type { StatementLedger } from './dev-n-plus-one';
28
28
  import { devPolicyMatrix } from './dev-policy';
29
- import type { RunningServices } from './dev-runtime';
30
- import type { DevServices } from './dev-services';
31
29
  import type { TraceRecorder } from './dev-traces';
32
30
  import type { Finding } from './output';
31
+ import type { DevServices } from './runtime-bindings';
32
+ import type { RunningServices } from './runtime-services';
33
33
  import { loopFacts } from './statement-loop';
34
34
 
35
35
  export interface DevStatus {
@@ -195,7 +195,7 @@ const boundariesPanel = (input: DevDashboardInput): DevPanel<BoundariesPanelData
195
195
  titleKey: 'dev.panel.boundaries.title',
196
196
  questionKey: 'dev.panel.boundaries.question',
197
197
  async data(): Promise<BoundariesPanelData> {
198
- return { findings: await checkAppBoundaries(input.root) };
198
+ return { findings: await appBoundaryFindings(input.root) };
199
199
  },
200
200
  });
201
201
 
package/src/dev-lock.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  // The lock file is what makes the second one nameable at all: nothing else in the process can tell
15
15
  // "another dev server owns this directory" from "the database is broken".
16
16
 
17
- import { closeSync, mkdirSync, openSync, unlinkSync, writeFileSync } from 'node:fs';
17
+ import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { stringField, UltimateError } from '@ultimat3/core';
20
20
  import { exec, type Runner } from './exec';
@@ -75,11 +75,27 @@ export const isProcessAlive = (pid: number): boolean => {
75
75
  }
76
76
  };
77
77
 
78
+ /**
79
+ * The lock a LIVE `x dev` holds on this state directory, or `undefined` — for a command that must
80
+ * not run beside one (`x db reset` deletes the data directory that process has open). A missing,
81
+ * unreadable or stale lock is no holder: only `preflight` may clear one.
82
+ */
83
+ export const liveDevLock = (stateDir: string): DevLock | undefined => {
84
+ let raw: string;
85
+ try {
86
+ raw = readFileSync(lockPath(stateDir), 'utf-8');
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ const lock = parseLock(raw);
91
+ return lock !== null && isProcessAlive(lock.pid) ? lock : undefined;
92
+ };
93
+
78
94
  /**
79
95
  * Refused before boot, so the failure names the process holding the directory.
80
96
  *
81
97
  * The lock is on the CHECKOUT, not on the database, and the cause says so. `x dev` is one process
82
- * running every role (`dev-roles.ts`), so a second one is unsupported whatever the services are.
98
+ * running every role (`role-start.ts`), so a second one is unsupported whatever the services are.
83
99
  * The embedded-Postgres sentence is appended only when the database actually IS embedded — with an
84
100
  * external `DATABASE_URL` it would name a mechanism that is not in play, which is the same defect
85
101
  * as the message this whole module replaced.
@@ -0,0 +1,21 @@
1
+ // Which port `x dev` binds: `--port`, else `PORT` from the environment, else 3000. The flag used to
2
+ // declare `default: '3000'`, and a default is indistinguishable from a caller's value — so the
3
+ // scaffold's own `.env.development` `PORT=` was read by nothing and every app booted on 3000.
4
+
5
+ import { PORT_RANGE, readIntFlag } from './flag-number';
6
+ import type { ParsedArgs } from './parse';
7
+ import { portFromEnv } from './serve';
8
+
9
+ export const DEFAULT_DEV_PORT = 3000;
10
+
11
+ /** The `metricsPortFor` shape: an explicit value, then the env, then the constant. */
12
+ export const devPortFor = (
13
+ args: ParsedArgs,
14
+ env: Readonly<Record<string, string | undefined>>,
15
+ ): number =>
16
+ readIntFlag(args, {
17
+ name: 'port',
18
+ command: 'dev',
19
+ ...PORT_RANGE,
20
+ example: `x dev --port ${DEFAULT_DEV_PORT}`,
21
+ }) ?? portFromEnv(env);
@@ -7,11 +7,8 @@ import { describeRoutes } from '@ultimat3/render';
7
7
  import type { Storage } from '@ultimat3/storage';
8
8
  import { apiRoutes } from './api-routes';
9
9
  import { mountAppMcp } from './app-mcp';
10
- import { assetRoutes } from './dev-assets';
11
10
  import type { DevDashboardInput } from './dev-dashboard';
12
11
  import { devDashboardRoutes } from './dev-dashboard';
13
- import { appRoutes } from './dev-render';
14
- import { storageRoutes } from './dev-storage';
15
12
  import { errorPageStyleSources } from './error-page-csp';
16
13
  import type { IslandBundle } from './island-bundle';
17
14
  import { islandHarnessRoutes } from './island-harness-route';
@@ -19,6 +16,9 @@ import { islandRoutes } from './island-routes';
19
16
  import { loadIslandStates } from './island-states-load';
20
17
  import { pageSync } from './page-sync';
21
18
  import { loadPwaArtifacts } from './pwa-artifacts';
19
+ import { assetRoutes } from './runtime-assets';
20
+ import { appRoutes } from './runtime-render';
21
+ import { storageRoutes } from './runtime-storage';
22
22
  import { styleBundle } from './style-bundle';
23
23
  import { styleRoutes } from './style-routes';
24
24
  import { serviceWorkerArtifacts } from './sw-artifacts';
package/src/dispatch.ts CHANGED
@@ -16,6 +16,7 @@ import { exitCodeFor, findingFrom, render } from './output';
16
16
  import type { ParsedArgs } from './parse';
17
17
  import { parseArgs, wantsJson } from './parse';
18
18
  import { commandFor, SPECS } from './registry';
19
+ import { rootEnvAdditions } from './root-env';
19
20
 
20
21
  export interface DispatchOptions {
21
22
  readonly argv: readonly string[];
@@ -120,14 +121,20 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
120
121
  ? { ...args, command: 'help', positionals: [args.command] }
121
122
  : args;
122
123
 
124
+ const cwd = resolveCwd(
125
+ options.cwd,
126
+ typeof args.flags.get('cwd') === 'string' ? String(args.flags.get('cwd')) : undefined,
127
+ );
128
+ // The ROOT's `.env*`, not the cwd's: Bun loaded the cwd's at startup, and a command run from
129
+ // `apps/web` finds its root by walking up. Written into the process env too when that IS the env
130
+ // passed in, because a package reading `Bun.env` directly must see what the command sees.
131
+ const added = await rootEnvAdditions(cwd, options.env);
132
+ if (options.env === Bun.env) Object.assign(Bun.env, added);
123
133
  const ctx: CommandContext = {
124
134
  args: helpArgs,
125
- cwd: resolveCwd(
126
- options.cwd,
127
- typeof args.flags.get('cwd') === 'string' ? String(args.flags.get('cwd')) : undefined,
128
- ),
135
+ cwd,
129
136
  runner: options.runner ?? exec,
130
- env: options.env,
137
+ env: options.env === Bun.env ? options.env : { ...added, ...options.env },
131
138
  bunVersion: options.bunVersion,
132
139
  ...(options.invocation === undefined ? {} : { invocation: options.invocation }),
133
140
  };
@@ -25,7 +25,7 @@ export const definesRootCustomProperties = (css: string): boolean => ROOT_CUSTOM
25
25
 
26
26
  export interface SurfaceDocument {
27
27
  readonly surface: Surface;
28
- /** Exactly the CSS `dev-render.ts` would inline into a document on this surface. */
28
+ /** Exactly the CSS `runtime-render.ts` would inline into a document on this surface. */
29
29
  readonly css: string;
30
30
  }
31
31