@ultimat3/cli 19.1.3 → 19.3.1

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 (65) hide show
  1. package/CLAUDE.md +125 -8
  2. package/package.json +29 -29
  3. package/src/app-boundaries.ts +11 -2
  4. package/src/app-load.ts +5 -1
  5. package/src/app-openapi.ts +13 -5
  6. package/src/app-permissions.ts +0 -0
  7. package/src/browser-launcher.ts +53 -4
  8. package/src/budgets.ts +60 -7
  9. package/src/cmd-dev.ts +49 -39
  10. package/src/cmd-doctor.ts +61 -23
  11. package/src/cmd-generate.ts +5 -2
  12. package/src/cmd-i18n.ts +10 -3
  13. package/src/cmd-jobs.ts +56 -10
  14. package/src/cmd-shot.ts +3 -1
  15. package/src/cmd-test.ts +15 -10
  16. package/src/db-seed.ts +2 -1
  17. package/src/dev-queue.ts +16 -2
  18. package/src/dev-reload.ts +46 -0
  19. package/src/dev-render.ts +28 -7
  20. package/src/dev-roles.ts +9 -8
  21. package/src/dev-runtime.ts +4 -1
  22. package/src/dev-sync.ts +17 -3
  23. package/src/dev-watch-tree.ts +226 -0
  24. package/src/dev-watch.ts +75 -0
  25. package/src/doctor-offline.ts +122 -0
  26. package/src/duplicate-packages.ts +278 -0
  27. package/src/error-catalog.ts +4 -5
  28. package/src/error-codes.ts +6 -0
  29. package/src/fix-command.ts +40 -1
  30. package/src/fix-path.ts +10 -11
  31. package/src/flag-number.ts +15 -0
  32. package/src/generate-kinds.ts +54 -4
  33. package/src/generate-write.ts +25 -2
  34. package/src/gitignore.ts +145 -0
  35. package/src/hold.ts +50 -17
  36. package/src/i18n-registration.ts +34 -5
  37. package/src/index.ts +3 -1
  38. package/src/island-bundle.ts +123 -10
  39. package/src/island-harness.ts +11 -4
  40. package/src/island-states-load.ts +2 -1
  41. package/src/jobs-driver.ts +4 -1
  42. package/src/mcp-errors.ts +2 -0
  43. package/src/mcp-host.ts +21 -9
  44. package/src/parse.ts +17 -0
  45. package/src/path-segments.ts +14 -0
  46. package/src/prerender.ts +68 -16
  47. package/src/retry-memo.ts +37 -0
  48. package/src/serve.ts +17 -2
  49. package/src/shot-browser.ts +23 -4
  50. package/src/source-files.ts +3 -1
  51. package/src/static-report.ts +21 -1
  52. package/src/style-bundle.ts +124 -0
  53. package/src/style-csp.ts +14 -12
  54. package/src/style-routes.ts +56 -0
  55. package/src/sw-artifacts.ts +84 -12
  56. package/src/templates/admin-page.ts +49 -1
  57. package/src/templates/resource-form-island.ts +13 -3
  58. package/src/templates/scaffold-container.ts +12 -0
  59. package/src/templates/scaffold-repo.ts +13 -2
  60. package/src/test-passes.ts +79 -0
  61. package/src/test-shards.ts +110 -36
  62. package/src/verify-checks.ts +13 -7
  63. package/src/verify-step.ts +4 -4
  64. package/src/verify-tests.ts +33 -8
  65. package/src/web-binding.ts +22 -0
package/src/cmd-dev.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  // alongside it — mounted, never re-implemented — so an agent can introspect the running app.
5
5
  // No Docker, no env setup: an unset variable means the embedded default.
6
6
 
7
- import { watch } from 'node:fs';
8
7
  import { join } from 'node:path';
9
8
  import { devShellStyle } from '@ultimat3/admin/dev';
10
9
  import type { Role } from '@ultimat3/core';
@@ -29,6 +28,7 @@ import { devDashboardRoutes, devPanels } from './dev-dashboard';
29
28
  import { liveFeedLabel } from './dev-live-feed';
30
29
  import { clearLock, preflight, writeLock } from './dev-lock';
31
30
  import { createStatementLedger } from './dev-n-plus-one';
31
+ import { coalesceReloads } from './dev-reload';
32
32
  import { appRoutes } from './dev-render';
33
33
  import { replicaOverrides } from './dev-replica';
34
34
  import type { RunningRoles } from './dev-roles';
@@ -39,10 +39,12 @@ import type { DevServices } from './dev-services';
39
39
  import { describeServices, reportedUrls, resolveServices } from './dev-services';
40
40
  import { storageRoutes } from './dev-storage';
41
41
  import { createTraceRecorder } from './dev-traces';
42
+ import { watchTree } from './dev-watch-tree';
42
43
  import { intFlagOr, PORT_RANGE } from './flag-number';
43
44
  import { holdUntilShutdown } from './hold';
44
45
  import type { IslandBundle } from './island-bundle';
45
46
  import { buildIslands } from './island-bundle';
47
+ import { FRAME_STYLE } from './island-harness';
46
48
  import { islandHarnessRoutes } from './island-harness-route';
47
49
  import { islandRoutes } from './island-routes';
48
50
  import { loadIslandStates } from './island-states-load';
@@ -53,6 +55,8 @@ import { flagString } from './parse';
53
55
  import { loadPwaArtifacts } from './pwa-artifacts';
54
56
  import { metricsPortFor } from './serve';
55
57
  import { loopFacts, loopFinding, loopNotice } from './statement-loop';
58
+ import { styleBundle } from './style-bundle';
59
+ import { styleRoutes } from './style-routes';
56
60
  import { serviceWorkerArtifacts } from './sw-artifacts';
57
61
  import { serviceWorkerRoutes } from './sw-routes';
58
62
 
@@ -92,22 +96,6 @@ interface DevState {
92
96
  islands: IslandBundle;
93
97
  }
94
98
 
95
- /** Debounced: a save that touches five files is one reload, not five. */
96
- function watchApp(root: string, onChange: (file: string) => void): () => void {
97
- let timer: ReturnType<typeof setTimeout> | undefined;
98
- let last = '';
99
- const watcher = watch(root, { recursive: true }, (_event, filename) => {
100
- if (filename === null || filename.includes('.x/') || filename.includes('node_modules')) return;
101
- last = filename;
102
- if (timer !== undefined) clearTimeout(timer);
103
- timer = setTimeout(() => onChange(last), 30);
104
- });
105
- return () => {
106
- if (timer !== undefined) clearTimeout(timer);
107
- watcher.close();
108
- };
109
- }
110
-
111
99
  export interface StartDevOptions {
112
100
  readonly root: string;
113
101
  readonly port: number;
@@ -191,7 +179,13 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
191
179
  const serviceWorker =
192
180
  pwa === undefined
193
181
  ? undefined
194
- : serviceWorkerArtifacts({ pwa, buildId, routes: describeRoutes(), islands: state.islands });
182
+ : serviceWorkerArtifacts({
183
+ pwa,
184
+ buildId,
185
+ routes: describeRoutes(),
186
+ islands: state.islands,
187
+ styles: styleBundle(),
188
+ });
195
189
 
196
190
  // The app's own MCP endpoint, discovered from `apps/<app>/mcp.ts` and mounted through the SAME
197
191
  // call `runRole` makes — `POST /mcp` answered 404 in every process the framework booted until
@@ -215,6 +209,10 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
215
209
  // The chunks the documents below name. Mounted before the app's routes for the reason
216
210
  // `/icons` and `/media` are: a page route must not be able to shadow an asset URL.
217
211
  ...islandRoutes(() => state.islands),
212
+ // And the stylesheet every one of those documents links. Read through the getter for the
213
+ // reason the islands are: a rebuilt island registers CSS, which mints a new URL, and a table
214
+ // captured at boot would answer 404 for the href the document now carries.
215
+ ...styleRoutes(() => styleBundle()),
218
216
  // `x shot --island`'s harness, in the `/_x` dev namespace so no app route can shadow it. It
219
217
  // lives here rather than in a second server because everything it needs is in THIS process:
220
218
  // the built chunks, the app's stylesheet registry, and the one embedded Postgres a checkout
@@ -258,10 +256,12 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
258
256
  // The same seam `serve.ts` passes: the app's own error page is a FILE, so the root is what
259
257
  // `startWeb` needs to find one.
260
258
  root: options.root,
261
- // The one document this process serves that the app did not write; `startRoles` covers the
262
- // app's own surfaces itself. `x dev` sends the policy report-only, so an uncovered `<style>`
263
- // here is a console report rather than a blank page — which is how this reached production.
264
- inlineStyles: [await devShellStyle()],
259
+ // The documents this process serves that the app did not write — the `/_x` shell and the
260
+ // screenshot harness's frame. The app's OWN surfaces need no entry any more: their CSS is a
261
+ // content-hashed file `'self'` already admits (`style-bundle.ts`). `x dev` sends the policy
262
+ // report-only, so an uncovered `<style>` here is a console report rather than a blank page —
263
+ // which is how this reached production.
264
+ inlineStyles: [await devShellStyle(), FRAME_STYLE],
265
265
  // The fourth surface, and the only one an author sees without leaving the page they broke:
266
266
  // the overlay renders this request's own loops under the error it is already showing.
267
267
  // `serve.ts` boots through the same `startRoles` and passes nothing, so production has no
@@ -274,22 +274,32 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
274
274
  ...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
275
275
  });
276
276
 
277
- const stopWatching = watchApp(options.root, (file) => {
278
- const started = performance.now();
279
- void Promise.all([appManifest(options.root), buildIslands(options.root)])
280
- .then(([{ manifest }, islands]) => {
281
- state.manifest = manifest;
282
- state.islands = islands;
283
- state.reloads += 1;
284
- state.reloadFinding = undefined;
285
- options.onReload?.(file, Math.round(performance.now() - started));
286
- })
287
- // Same rule as a module that will not import: a save the manifest cannot be rebuilt from is
288
- // a finding on `/_x`, never an unhandled rejection that takes the dev server down.
289
- .catch((error: unknown) => {
290
- state.reloadFinding = { ...findingFrom(error), at: file };
291
- });
292
- });
277
+ // One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
278
+ // trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
279
+ const rebuild = coalesceReloads(
280
+ async (file) => {
281
+ const started = performance.now();
282
+ const [{ manifest }, islands] = await Promise.all([
283
+ appManifest(options.root),
284
+ buildIslands(options.root),
285
+ ]);
286
+ state.manifest = manifest;
287
+ state.islands = islands;
288
+ state.reloads += 1;
289
+ state.reloadFinding = undefined;
290
+ options.onReload?.(file, Math.round(performance.now() - started));
291
+ },
292
+ // Same rule as a module that will not import: a save the manifest cannot be rebuilt from is
293
+ // a finding on `/_x`, never an unhandled rejection that takes the dev server down.
294
+ (error: unknown, file: string) => {
295
+ state.reloadFinding = { ...findingFrom(error), at: file };
296
+ },
297
+ );
298
+ // Watched one directory at a time, so an ignored one costs no descriptor at all — `dev-watch.ts`
299
+ // decides which, from the app's own `.gitignore`. A recursive watch on the root registered one
300
+ // inotify descriptor per directory in the tree, `.git/`, `node_modules/` and the `.x/` this
301
+ // process writes to included, and filtered the events afterwards.
302
+ const watcher = watchTree({ root: options.root, onChange: rebuild });
293
303
 
294
304
  server = {
295
305
  url: running.url ?? `http://localhost:${options.port}`,
@@ -313,7 +323,7 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
313
323
  runtime,
314
324
  panels,
315
325
  async stop() {
316
- stopWatching();
326
+ watcher.close();
317
327
  await running.stop();
318
328
  await runtime.stop();
319
329
  // Released after the roles: a span opened by an in-flight request still has an exporter to
package/src/cmd-doctor.ts CHANGED
@@ -4,16 +4,25 @@
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
- import { ERROR_DOCS_URL, tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
7
+ import {
8
+ ENV_EXAMPLE_PATH,
9
+ ERROR_DOCS_URL,
10
+ tryResolveEnvironment,
11
+ usesDevCursorSecret,
12
+ } from '@ultimat3/core';
8
13
  import { checkDb, createPostgresClient } from '@ultimat3/db';
9
14
  import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
10
15
  import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
11
16
  import type { CliCommand, CommandContext } from './command';
12
17
  import { checkMigrationSnapshots } from './db-snapshot';
13
- import { intFlagOr, neighbouringPort, PORT_RANGE } from './flag-number';
18
+ import { syncPortFor } from './dev-sync';
19
+ import type { OfflineFallbackFact } from './doctor-offline';
20
+ import { offlineFallbackFinding, offlineFallbackProbe } from './doctor-offline';
21
+ import { intFlagOr, PORT_RANGE, portPairAfter } from './flag-number';
14
22
  import { ICON_SOURCE } from './icon-assets';
15
23
  import { msg } from './messages';
16
24
  import type { CommandResult, Finding } from './output';
25
+ import { findingFrom } from './output';
17
26
  import type { ParsedArgs } from './parse';
18
27
  import { portFree } from './port-probe';
19
28
  import { checkMigrationDrift } from './schema-drift';
@@ -59,6 +68,12 @@ export interface DoctorProbe {
59
68
  * separate remedies — one is "generate a migration", the other is "this migration is incomplete".
60
69
  */
61
70
  snapshots(): Promise<readonly Finding[]>;
71
+ /**
72
+ * What the app declared as its offline fallback, and which routes it really serves. A FACT and
73
+ * not a finding, because deciding is `offlineFallbackFinding`'s and this seam's whole job is
74
+ * reaching the disk — the same split `drift()` makes one question over.
75
+ */
76
+ offlineFallback(): Promise<OfflineFallbackFact>;
62
77
  }
63
78
 
64
79
  const finding = (code: string, cause: string, fix: string, at?: string): Finding =>
@@ -66,7 +81,8 @@ const finding = (code: string, cause: string, fix: string, at?: string): Finding
66
81
  ? { code, cause, fix, docs: ERROR_DOCS_URL }
67
82
  : { code, cause, fix, docs: ERROR_DOCS_URL, at };
68
83
 
69
- export const OFFLINE_FALLBACK = 'apps/web/app/offline.tsx';
84
+ /** The file `x doctor` reports missing, and the one the reader creates. */
85
+ export const ENV_DEVELOPMENT = '.env.development';
70
86
 
71
87
  /** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
72
88
  const DEFAULT_DOCTOR_PORT = 3000;
@@ -80,12 +96,30 @@ const DEFAULT_DOCTOR_PORT = 3000;
80
96
  * derives `PORT = .port - 1` from it, so it is part of the contract `x dev` runs by.
81
97
  *
82
98
  * The suggested port moves BOTH: `x dev --port N` occupies N and N+1, so a free N beside a taken
83
- * N+1 is still not a runnable command.
99
+ * N+1 is still not a runnable command. It did not move both until 2026-09 — the line was
100
+ * `neighbouringPort(probe.port)`, which for the sync finding IS the port the finding is about, and
101
+ * a docblock claiming otherwise is how it survived. `portPairAfter` is the one reader of that rule
102
+ * and `dev-sync.ts`'s own refusal shares it.
103
+ *
104
+ * The sync port is `syncPortFor`, never `neighbouringPort` again: that helper answers 65534 for a
105
+ * web port of 65535 — BELOW the web port, and a port `x dev` never binds — where the boot refuses
106
+ * the run outright with `X_PORT_INVALID`. A probe that reports on a socket the command would never
107
+ * open is answering a question nobody asked, so the boot's own rule decides, and its refusal is
108
+ * reported instead of a probe. Alone, and ahead of the web port: there is no runnable `x dev` at
109
+ * this port whatever the other one answers, and a second finding about it is noise over the cause.
84
110
  */
85
111
  async function portFindings(probe: DoctorProbe): Promise<readonly Finding[]> {
112
+ let syncPort: number;
113
+ try {
114
+ syncPort = syncPortFor(probe.port);
115
+ } catch (error) {
116
+ // The boot's own error, carried whole — `findingFrom` reads its code, cause and fix off the
117
+ // value rather than rendering it, which is what `scripts/catch-render.ts` requires.
118
+ return [findingFrom(error)];
119
+ }
86
120
  const wanted = [
87
121
  { port: probe.port, role: 'web' },
88
- { port: neighbouringPort(probe.port), role: 'sync' },
122
+ { port: syncPort, role: 'sync' },
89
123
  ] as const;
90
124
  const findings: Finding[] = [];
91
125
  for (const entry of wanted) {
@@ -94,10 +128,7 @@ async function portFindings(probe: DoctorProbe): Promise<readonly Finding[]> {
94
128
  finding(
95
129
  'X_PORT_IN_USE',
96
130
  `port ${entry.port} is already listening, and \`x dev --port ${probe.port}\` binds it for the ${entry.role} role`,
97
- // Unchanged, and deliberately: the neighbour below the top of the range is the one port
98
- // `x dev` is guaranteed to accept (`X_CLI_BAD_FLAG` otherwise), which is what
99
- // `cmd-doctor.test.ts` pins by parsing this line with `x dev`'s own flag reader.
100
- `x dev --port ${neighbouringPort(probe.port)}`,
131
+ `x dev --port ${portPairAfter(probe.port)}`,
101
132
  ),
102
133
  );
103
134
  }
@@ -125,13 +156,20 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
125
156
  );
126
157
  return findings;
127
158
  }
128
- if (!probe.exists('.env.development')) {
159
+ if (!probe.exists(ENV_DEVELOPMENT)) {
129
160
  findings.push(
130
161
  finding(
131
162
  'X_ENV_MISSING',
132
- '.env.development is missing, so committed defaults cannot be read',
133
- 'x new --force to restore the committed defaults, or create .env.development',
134
- '.env.development',
163
+ `${ENV_DEVELOPMENT} is missing, so committed defaults cannot be read`,
164
+ // The file write, named — `X_PWA_ICON_MISSING`'s shape below, and for its reason. This
165
+ // said `x new --force`, which cannot run where the reader is standing: `x new` takes a
166
+ // <name> positional (reproduced: `x new --force --json` inside an app answers
167
+ // `X_CLI_BAD_FLAG`), and with one it scaffolds a SECOND app beside the broken one.
168
+ // `.env.example` is the committed projection of `envSchema`, so the copy lands every
169
+ // declared key with its default and blank secrets; `x env example` writes it where an app
170
+ // has none, and `X_ENV_EXAMPLE_DRIFT` is what reports that.
171
+ `cp ${ENV_EXAMPLE_PATH} ${ENV_DEVELOPMENT}`,
172
+ ENV_DEVELOPMENT,
135
173
  ),
136
174
  );
137
175
  }
@@ -183,16 +221,12 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
183
221
  ),
184
222
  );
185
223
  }
186
- if (!probe.exists(OFFLINE_FALLBACK)) {
187
- findings.push(
188
- finding(
189
- 'X_PWA_NO_OFFLINE_FALLBACK',
190
- `${OFFLINE_FALLBACK} is missing, so an offline navigation falls back to the browser error page`,
191
- 'x g route offline --surface app',
192
- OFFLINE_FALLBACK,
193
- ),
194
- );
195
- }
224
+ // The DECLARED fallback against the ROUTE TABLE, never a filename: this probed
225
+ // `apps/web/app/offline.tsx`, which is not a route file at all (`assertRouteFilename` refuses
226
+ // it), so no app could clear the finding and its own `fix:` did not either. `doctor-offline.ts`
227
+ // holds the rule and the reasons.
228
+ const offline = offlineFallbackFinding(await probe.offlineFallback());
229
+ if (offline !== undefined) findings.push(offline);
196
230
  const database = await probe.database();
197
231
  if (database !== null) findings.push(database);
198
232
  findings.push(...(await probe.drift()));
@@ -264,6 +298,10 @@ export function probeFor(cwd: string, bunVersion: string, port: number): DoctorP
264
298
  database: () => probeDatabase(process.env['DATABASE_URL']),
265
299
  drift: async () => (root === undefined ? [] : checkMigrationDrift(root)),
266
300
  snapshots: async () => (root === undefined ? [] : checkMigrationSnapshots(root)),
301
+ // `routes: undefined` outside an app is "not judged", which is what the caller already is:
302
+ // `runDoctor` returns on `X_NOT_IN_APP` before this can be asked.
303
+ offlineFallback: async () =>
304
+ root === undefined ? { fallback: null, routes: undefined } : offlineFallbackProbe(root),
267
305
  };
268
306
  }
269
307
 
@@ -8,7 +8,7 @@ import { appManifest, writeAppManifest } from './app-manifest';
8
8
  import { requireAppRoot } from './app-root';
9
9
  import type { CliCommand, CommandContext } from './command';
10
10
  import { generate } from './generate-files';
11
- import { GENERATORS, readKind, readName, readSurface } from './generate-kinds';
11
+ import { GENERATORS, readKind, readName, readPermission, readSurface } from './generate-kinds';
12
12
  import { containedPath, writeFiles } from './generate-write';
13
13
  import { resolveCatalogModule } from './i18n-audit';
14
14
  import { syncI18nIndex } from './i18n-index';
@@ -63,7 +63,10 @@ export const generateCommand: CliCommand = {
63
63
  const surface = readSurface(flagString(ctx.args, 'surface'), kind, name);
64
64
  const locales = resolveLocales(flagList(ctx.args, 'locales'));
65
65
  const at = flagString(ctx.args, 'at');
66
- const permission = flagString(ctx.args, 'permission');
66
+ // Read with the two above, and refused here for their reason: the value is spliced into the
67
+ // emitted source three times, and a value that is not a `<resource>:<verb>` is a page the app
68
+ // cannot compile — found after the files are on disk, which is the worst place to find it.
69
+ const permission = readPermission(flagString(ctx.args, 'permission'), kind);
67
70
  // Read before a file is planned, like the flags above: which module a generated component
68
71
  // imports `useT()` from is a fact about THIS app, and `generate` is a pure function.
69
72
  const catalogModule = await resolveCatalogModule(root);
package/src/cmd-i18n.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  // would, and `node:path` because Bun exposes no path API to build what either of them takes.
9
9
  import { type FileHandle, mkdir, open } from 'node:fs/promises';
10
10
  import { dirname, join } from 'node:path';
11
+ import { stringField } from '@ultimat3/core';
11
12
  import type { Catalog } from '@ultimat3/i18n';
12
13
  import { auditCatalogs, catalogKeys } from '@ultimat3/i18n';
13
14
  import { loadApp } from './app-load';
@@ -40,9 +41,15 @@ export const I18N_SUBCOMMANDS = ['check', 'add', 'sync'] as const;
40
41
  /** `ExtractReport` is plain JSON by construction — same idiom as `cmd-registries.ts`'s `asJson`. */
41
42
  const asJson = (value: object): Record<string, JsonValue> => value as Record<string, JsonValue>;
42
43
 
43
- /** `open`'s failure when the file is already there — the one errno this command translates. */
44
- const isAlreadyExists = (error: unknown): boolean =>
45
- typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST';
44
+ /**
45
+ * `open`'s failure when the file is already there — the one errno this command translates.
46
+ *
47
+ * `stringField`, never `'code' in error && error.code`: `in` narrows for the COMPILER and promises
48
+ * the runtime nothing, so the read still happens on a value this process did not build and a
49
+ * throwing getter takes the command down one line after the guard written to make it safe. The
50
+ * same repair `dev-lock.ts` took for the cast spelling of the identical read.
51
+ */
52
+ const isAlreadyExists = (error: unknown): boolean => stringField(error, 'code') === 'EEXIST';
46
53
 
47
54
  /**
48
55
  * The exclusive half of the create: `wx` fails rather than truncates, so an existing catalog is
package/src/cmd-jobs.ts CHANGED
@@ -5,10 +5,11 @@
5
5
  // `jobs-table.ts`, and getting hold of the queue at all is `jobs-driver.ts` — shared with `x db`.
6
6
 
7
7
  import type { JobDriver } from '@ultimat3/jobs';
8
- import { cancelJob, createMemoryDriver, createNatsDriver, createRedisDriver } from '@ultimat3/jobs';
8
+ import { cancelJob, createNatsDriver, createRedisDriver } from '@ultimat3/jobs';
9
9
  import { requireAppRoot } from './app-root';
10
10
  import type { CliCommand, CommandContext } from './command';
11
11
  import { BadFlagError, JobUnknownError, MissingPositionalError } from './errors';
12
+ import type { DrainOutcome } from './jobs-drain';
12
13
  import { drainJobs } from './jobs-drain';
13
14
  import { withJobDriver } from './jobs-driver';
14
15
  import {
@@ -28,7 +29,32 @@ import { flagBool, flagString } from './parse';
28
29
 
29
30
  export const JOBS_SUBCOMMANDS = ['ls', 'show', 'retry', 'cancel', 'drain'] as const;
30
31
 
31
- const DRAIN_TARGETS = ['memory', 'redis', 'nats'] as const;
32
+ /**
33
+ * The drivers a drain may move work ONTO — every one of them durable, and that is the whole rule.
34
+ * Closed, and read three ways: the flag summary, the refusal, and the `memory` case below.
35
+ */
36
+ export const DRAIN_TARGETS = ['redis', 'nats'] as const;
37
+
38
+ /**
39
+ * `memory` was on that list until 2026-09 and could not be: `createMemoryDriver()` is a `Map` in
40
+ * THIS process, so `x jobs drain --to memory` enqueued each job into it and then `ack`ed the
41
+ * durable row off the source. Reproduced against two real drivers — source ready 1 -> 0, target
42
+ * ready 1, `ok: true` — and the target dies with the command. `wiki/CLI-Reference.md` said the
43
+ * crash window "duplicates a job … instead of losing it"; this target lost every one of them.
44
+ *
45
+ * Refused by NAME rather than folded into the unknown-value message, for `cmd-deploy.ts`'s
46
+ * `readMethod` reason: `--to memory` is a spelling that used to work, so a reader who types it is
47
+ * owed the fact that it moved work into a process that is about to exit, not a list of words.
48
+ */
49
+ function refuseMemoryTarget(): never {
50
+ throw new BadFlagError({
51
+ flag: 'to',
52
+ command: 'jobs',
53
+ reason:
54
+ 'memory is a Map inside this process — the drain would ack every durable row and lose the copy when the command exits',
55
+ fix: 'x jobs drain --to redis --json # or --to nats; --dry-run reports the plan and moves nothing',
56
+ });
57
+ }
32
58
 
33
59
  function requireIdPositional(ctx: CommandContext, sub: string): string {
34
60
  const id = ctx.args.positionals[0];
@@ -57,10 +83,13 @@ function requireEnvUrl(env: CommandContext['env'], name: string, target: string)
57
83
 
58
84
  /**
59
85
  * `redis`/`nats` are honest `X_NOT_IMPLEMENTED` stubs in `@ultimat3/jobs` — building one here is
60
- * fine even though every `enqueue` on it will fail; `drainJobs` reports that per record.
86
+ * fine even though every `enqueue` on it will fail; `drainJobs` reports that per record. What is
87
+ * NOT fine is a target that accepts every enqueue and then vanishes, which is why `memory` is
88
+ * refused first and by name rather than falling into the closed-set message below.
61
89
  * Exported so a test can drive the `--to`/env-var validation without a driver or a boot.
62
90
  */
63
91
  export function buildDrainTarget(to: string | undefined, env: CommandContext['env']): JobDriver {
92
+ if (to === 'memory') refuseMemoryTarget();
64
93
  if (to === undefined || !(DRAIN_TARGETS as readonly string[]).includes(to)) {
65
94
  throw new BadFlagError({
66
95
  flag: 'to',
@@ -68,7 +97,6 @@ export function buildDrainTarget(to: string | undefined, env: CommandContext['en
68
97
  reason: `expects one of: ${DRAIN_TARGETS.join(', ')}`,
69
98
  });
70
99
  }
71
- if (to === 'memory') return createMemoryDriver();
72
100
  if (to === 'redis') return createRedisDriver({ url: requireEnvUrl(env, 'REDIS_URL', 'redis') });
73
101
  return createNatsDriver({ servers: [requireEnvUrl(env, 'NATS_URL', 'nats')] });
74
102
  }
@@ -174,11 +202,13 @@ async function runCancel(driver: JobDriver, ctx: CommandContext): Promise<Comman
174
202
  * A skipped candidate is not an error — a job whose `runAt` has not arrived is unclaimable by
175
203
  * design — so it carries no `X_*` finding. It still fails the command: `x jobs drain` is run to
176
204
  * empty a driver, and a partial move that exited 0 would read as "the queue is clear".
205
+ *
206
+ * Exported, and separate from the flag reading above it, because every target the flag now accepts
207
+ * needs a server: a test can produce a real outcome from two drivers and render THAT, where
208
+ * driving the whole command would need a redis or a nats to move anything at all.
177
209
  */
178
- async function runDrain(driver: JobDriver, ctx: CommandContext): Promise<CommandResult> {
179
- const target = buildDrainTarget(flagString(ctx.args, 'to'), ctx.env);
180
- const dryRun = flagBool(ctx.args, 'dry-run');
181
- const outcome = await drainJobs(driver, target, dryRun);
210
+ export function drainResult(outcome: DrainOutcome): CommandResult {
211
+ const dryRun = outcome.dryRun;
182
212
  const findings = outcome.failures.map((failure) => failure.finding);
183
213
  const lines: string[] = [];
184
214
  if (outcome.skipped.length > 0) {
@@ -212,6 +242,15 @@ async function runDrain(driver: JobDriver, ctx: CommandContext): Promise<Command
212
242
  };
213
243
  }
214
244
 
245
+ /** The move, then the render. The target is built ABOVE `withJobDriver` — see `run` below. */
246
+ async function runDrain(
247
+ driver: JobDriver,
248
+ target: JobDriver,
249
+ ctx: CommandContext,
250
+ ): Promise<CommandResult> {
251
+ return drainResult(await drainJobs(driver, target, flagBool(ctx.args, 'dry-run')));
252
+ }
253
+
215
254
  export const jobsCommand: CliCommand = {
216
255
  spec: {
217
256
  name: 'jobs',
@@ -245,7 +284,7 @@ export const jobsCommand: CliCommand = {
245
284
  {
246
285
  name: 'to',
247
286
  type: 'string',
248
- summary: 'drain: target driver — memory, redis, nats',
287
+ summary: `drain: target driver — ${DRAIN_TARGETS.join(', ')}`,
249
288
  subcommands: ['drain'],
250
289
  },
251
290
  {
@@ -259,11 +298,18 @@ export const jobsCommand: CliCommand = {
259
298
  async run(ctx: CommandContext): Promise<CommandResult> {
260
299
  const root = requireAppRoot('jobs', ctx.cwd).dir;
261
300
  const sub = ctx.args.subcommand ?? 'ls';
301
+ // BEFORE `withJobDriver`, which boots the SOURCE queue and pings it. `--to` is a flag, so
302
+ // whether it names a durable driver is answerable with no server at all — and reading it
303
+ // inside meant `x jobs drain --to memory` on a box whose database is down reported the boot
304
+ // failure instead of `X_CLI_BAD_FLAG`, i.e. the operator repaired Postgres to be told the
305
+ // word they typed was refused by name. It also opens a connection to a target the command
306
+ // then refuses, which is a socket nothing closes.
307
+ const target = sub === 'drain' ? buildDrainTarget(flagString(ctx.args, 'to'), ctx.env) : null;
262
308
  return withJobDriver(root, ctx, (driver) => {
263
309
  if (sub === 'show') return runShow(driver, ctx);
264
310
  if (sub === 'retry') return runRetry(driver, ctx);
265
311
  if (sub === 'cancel') return runCancel(driver, ctx);
266
- if (sub === 'drain') return runDrain(driver, ctx);
312
+ if (target !== null) return runDrain(driver, target, ctx);
267
313
  return runLs(driver, ctx);
268
314
  });
269
315
  },
package/src/cmd-shot.ts CHANGED
@@ -322,7 +322,9 @@ export const shotCommand: CliCommand = {
322
322
  const timeoutMs = intFlag(ctx.args, 'timeout', 1, DEFAULT_PAGE_TIMEOUT_MS);
323
323
  // Which browser this run gets — start one here, or attach to one somebody else is running.
324
324
  // Decided by `shot-browser.ts` over plain inputs, and decided HERE, before a dev server or a
325
- // provider session exists to pay for a typo.
325
+ // provider session exists to pay for a typo. It also PROBES for an installed Chrome and refuses
326
+ // when there is none: `puppeteer-core` bundles no browser, so a missing one used to surface as
327
+ // a library throw one embedded Postgres later.
326
328
  const { cdpUrl, executablePath } = shotBrowserChoice({
327
329
  cdpFlag: flagString(ctx.args, 'cdp-url'),
328
330
  browserFlag: flagString(ctx.args, 'browser'),
package/src/cmd-test.ts CHANGED
@@ -104,8 +104,12 @@ export const testCommand: CliCommand = {
104
104
  name: 'test',
105
105
  summary:
106
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]`,
107
+ usage: `x test [${TEST_TYPES.join('|')}] [--filter text] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--json] [-- <bun test flags>]`,
108
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,
109
113
  flags: [
110
114
  {
111
115
  name: 'workers',
@@ -173,15 +177,13 @@ export const testCommand: CliCommand = {
173
177
  }
174
178
  const files = sample === undefined ? selected : sampleFiles(selected, sample);
175
179
  const requested = readIndex(ctx.args, 'workers', 1) ?? defaultWorkers();
176
- // A serial type is serial HERE TOO, `As of 2026-08-27`. `verify-tests.ts` routes `live` and
177
- // `e2e` through `runSerial` and this command never read the same list, so `x verify` ran one
178
- // process over the very files `x test live --workers 8` ran eight over — two answers to one
179
- // question, which is axiom 1, and the dangerous one is the command a human types while
180
- // debugging. What makes them serial is not a preference: a logical replication slot is named
181
- // at the Postgres CLUSTER level, so a per-worker database does not isolate it and two workers
182
- // race `pg_create_logical_replication_slot`; `e2e` shares one built `dist/` and one browser
183
- // profile. Neither is visible without a real `TEST_DATABASE_URL`, which is why the split
184
- // measured green for as long as it did.
180
+ // The width a `--worker` index is judged against, and nothing else: which files really run one
181
+ // at a time is `test-passes.ts`, off the FILES rather than off the positional. This line read
182
+ // the positional alone until 2026-09 and the comment here claimed it made a serial type serial
183
+ // — true for `x test live`, false for the bare `x test --workers 8` that selects every type,
184
+ // which ran the same `live` and `e2e` files eight at a time. It stays because a `--worker` run
185
+ // is one process per CI JOB: sharding a serial type across N of them is the cluster-wide race
186
+ // in a second disguise, and refusing the index is where that is caught.
185
187
  const ceiling = type !== undefined && SERIAL_TYPES.includes(type) ? 1 : files.length;
186
188
  const workers = Math.max(1, Math.min(requested, ceiling));
187
189
  const only = readIndex(ctx.args, 'worker', 0);
@@ -208,6 +210,9 @@ export const testCommand: CliCommand = {
208
210
  // corpus, so its shard 2 is a different shard 2 — reproducing nothing, which is the one
209
211
  // thing `reproduceFor` exists to prevent.
210
212
  ...(scope === undefined ? {} : { affected: scope.selection }),
213
+ // The fifth input to the split, and the one a rerun most obviously needs: `--coverage`
214
+ // changes what a run measures, and the reproduce line carries it back out.
215
+ ...(ctx.args.passthrough.length === 0 ? {} : { passthrough: ctx.args.passthrough }),
211
216
  });
212
217
  return scope === undefined ? result : withScope(result, scope);
213
218
  },
package/src/db-seed.ts CHANGED
@@ -15,6 +15,7 @@ import { isSeed, SEED_TIERS, seedTiersFor } from '@ultimat3/entity';
15
15
  import { BadFlagError } from './errors';
16
16
  import type { Finding, JsonValue } from './output';
17
17
  import { findingFrom } from './output';
18
+ import { hasPathSegment } from './path-segments';
18
19
  import { renderTable } from './table';
19
20
 
20
21
  /**
@@ -101,7 +102,7 @@ export async function discoverSeeds(root: string): Promise<SeedDiscovery> {
101
102
  const seen = new Set<string>();
102
103
  for (const pattern of SEED_GLOBS) {
103
104
  for await (const absolute of new Bun.Glob(pattern).scan({ cwd: root, absolute: true })) {
104
- if (absolute.includes('node_modules') || absolute.includes('.test.')) continue;
105
+ if (hasPathSegment(absolute, 'node_modules') || absolute.includes('.test.')) continue;
105
106
  if (seen.has(absolute)) continue;
106
107
  seen.add(absolute);
107
108
  const file = relative(root, absolute).split(sep).join('/');
package/src/dev-queue.ts CHANGED
@@ -78,8 +78,16 @@ export interface RunningQueue {
78
78
  * Before this, `defaultClient()` was the only composer of a replicated pair in the framework and
79
79
  * it runs only from `baseClient()` — the client an app installed NONE for. This line installs one,
80
80
  * so `DATABASE_REPLICA_URL` was read by no booted process at all.
81
+ *
82
+ * `env` is REQUIRED, and that is the repair: it defaulted to `process.env` and `startQueue` passed
83
+ * nothing, so the standby was decided from the process while the middleware that opens the
84
+ * `withReplicaReads` scope was decided from the boot's own `options.env` (`cmd-dev.ts`,
85
+ * `serve.ts`). Two sources for one question answer differently the moment a boot is handed an
86
+ * environment it did not inherit — a routed client with no scope, or a scope with no standby, and
87
+ * neither reports anything. A default here is what let the caller forget; the type is what stops
88
+ * the next one. Exported for the test that proves which environment decides.
81
89
  */
82
- function startDb(services: DevServices, env: ReplicaEnv = process.env): StartedDb {
90
+ export function startDb(services: DevServices, env: ReplicaEnv): StartedDb {
83
91
  const binding = services.db;
84
92
  const client =
85
93
  binding.mode === 'embedded'
@@ -218,8 +226,14 @@ async function releaseQueue(
218
226
  export async function startQueue(
219
227
  services: DevServices,
220
228
  overrides?: RuntimeOverrides,
229
+ /**
230
+ * The boot's own environment — `x dev`'s, the container role's, the CLI command's `ctx.env`.
231
+ * `process.env` is the default for a caller that has no other answer, and it is the ONLY place
232
+ * this file reads it: see `startDb` for what two readers of one question cost.
233
+ */
234
+ env: ReplicaEnv = process.env,
221
235
  ): Promise<RunningQueue> {
222
- const { client: db, replica } = startDb(services);
236
+ const { client: db, replica } = startDb(services, env);
223
237
  try {
224
238
  // Pay the Postgres boot here, so the first request is not the slow one and a broken database
225
239
  // fails at boot rather than on some later query.