@ultimat3/cli 19.2.0 → 19.3.2

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 (59) hide show
  1. package/CLAUDE.md +135 -9
  2. package/README.md +1 -1
  3. package/package.json +29 -29
  4. package/src/app-agents-md.ts +14 -3
  5. package/src/app-boundaries.ts +11 -2
  6. package/src/app-load.ts +96 -25
  7. package/src/budgets.ts +17 -6
  8. package/src/cmd-dev-fixture.ts +25 -0
  9. package/src/cmd-dev.ts +48 -46
  10. package/src/cmd-doctor.ts +61 -23
  11. package/src/cmd-generate.ts +25 -3
  12. package/src/cmd-i18n.ts +10 -3
  13. package/src/cmd-jobs.ts +56 -10
  14. package/src/cmd-test.ts +15 -10
  15. package/src/db-seed.ts +2 -1
  16. package/src/dev-queue.ts +16 -2
  17. package/src/dev-reload.ts +46 -0
  18. package/src/dev-render.ts +35 -9
  19. package/src/dev-runtime.ts +4 -1
  20. package/src/dev-sync.ts +11 -3
  21. package/src/dev-watch-tree.ts +226 -0
  22. package/src/dev-watch.ts +59 -37
  23. package/src/doctor-offline.ts +122 -0
  24. package/src/error-catalog.ts +4 -5
  25. package/src/fix-command.ts +40 -1
  26. package/src/fix-path.ts +10 -11
  27. package/src/flag-number.ts +15 -0
  28. package/src/generate-files.ts +24 -2
  29. package/src/generate-kinds.ts +54 -4
  30. package/src/generate-write.ts +25 -2
  31. package/src/gitignore.ts +145 -0
  32. package/src/hold.ts +50 -17
  33. package/src/index.ts +1 -1
  34. package/src/island-bundle.ts +2 -1
  35. package/src/island-states-load.ts +2 -1
  36. package/src/jobs-driver.ts +4 -1
  37. package/src/mcp-host.ts +18 -9
  38. package/src/parse.ts +17 -0
  39. package/src/path-segments.ts +14 -0
  40. package/src/prerender.ts +46 -20
  41. package/src/retry-memo.ts +37 -0
  42. package/src/scaffold-fixture.ts +17 -0
  43. package/src/serve.ts +40 -5
  44. package/src/source-files.ts +3 -1
  45. package/src/sw-artifacts.ts +71 -7
  46. package/src/templates/action.ts +47 -16
  47. package/src/templates/admin-page.ts +49 -1
  48. package/src/templates/island.ts +4 -2
  49. package/src/templates/scaffold-container.ts +12 -0
  50. package/src/templates/scaffold-docs.ts +7 -0
  51. package/src/templates/scaffold-entries.ts +4 -2
  52. package/src/templates/scaffold-repo.ts +7 -2
  53. package/src/templates/slice-foundation.ts +36 -0
  54. package/src/test-passes.ts +79 -0
  55. package/src/test-shards.ts +110 -36
  56. package/src/verify-checks.ts +11 -8
  57. package/src/verify-floor.ts +59 -3
  58. package/src/verify-step.ts +4 -4
  59. package/src/verify-tests.ts +14 -2
@@ -7,6 +7,8 @@
7
7
  // app.config.ts the root marker a real `x dev` cannot start without; `ai.mcp` by default
8
8
  // apps/web/mcp.ts the app's own MCP endpoint, mounted by the web role
9
9
  // apps/web/runtime.ts the app's middleware, reaching a development process
10
+ // .git/HEAD the fixture is its own repository — see the entry below
11
+ // apps/web/app/hello/* an SSR page with a component, edited on disk while `x dev` runs
10
12
  // apps/web/app/notes/* a memory-backed entity and a live query, fed by the in-process bridge
11
13
  // apps/web/app/posts/* an action, a policy and a query, mounted as HTTP routes
12
14
  // apps/web/site/pricing/* a static page with its own stylesheet, under the CSP `x dev` sends
@@ -24,6 +26,12 @@ import { resetAppLoad } from './app-load';
24
26
  export const DEV_FIXTURE_FILES: Readonly<Record<string, string>> = {
25
27
  'package.json': JSON.stringify({ name: 'dev-fixture', version: '1.4.0' }),
26
28
 
29
+ // Its own repository, so the ignore walk stops HERE. The framework's root `.gitignore` lists
30
+ // `packages/cli/.dev-fixture/`, and `devIgnore` honours every ancestor up to a `.git` — without
31
+ // this marker the watcher admitted the fixture root and nothing under it, so no save in this
32
+ // tree ever reached `rebuild`, and the reload path was booted by every run and exercised by none.
33
+ '.git/HEAD': 'ref: refs/heads/main\n',
34
+
27
35
  // The root marker a real `x dev` cannot start without, and where `ai.mcp` is declared — by
28
36
  // default `{ expose: true, path: '/mcp' }`, which is what the MCP mount reads.
29
37
  'app.config.ts': `import { defineConfig } from '@ultimat3/core';
@@ -98,6 +106,23 @@ export const echoPost = action({
98
106
  return { word: input.word };
99
107
  },
100
108
  });
109
+ `,
110
+
111
+ // A page with a body, on the surface an author edits all day. What the reload test rewrites: the
112
+ // module must be imported again for the new body to be served, which `import()` alone never does.
113
+ 'apps/web/app/hello/page.tsx': `import { defineRoute } from '@ultimat3/render';
114
+
115
+ export const config = defineRoute({
116
+ render: 'ssr',
117
+ hydrate: 'visible',
118
+ offline: 'runtime',
119
+ budget: { js: '60kb' },
120
+ meta: () => ({ title: 'Hello', description: 'A page that is edited while x dev runs' }),
121
+ });
122
+
123
+ export function Page() {
124
+ return <p>generation one</p>;
125
+ }
101
126
  `,
102
127
 
103
128
  // A stylesheet the page imports, because that import is what registers it — and the document's
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';
@@ -17,7 +16,6 @@ import { MANIFEST_FILENAME } from '@ultimat3/manifest';
17
16
  import { describeRoutes } from '@ultimat3/render';
18
17
  import { apiRoutes } from './api-routes';
19
18
  import { loadSignInPath } from './app-auth';
20
- import { loadApp } from './app-load';
21
19
  import { appManifest } from './app-manifest';
22
20
  import { mountAppMcp } from './app-mcp';
23
21
  import { requireAppRoot } from './app-root';
@@ -29,6 +27,7 @@ import { devDashboardRoutes, devPanels } from './dev-dashboard';
29
27
  import { liveFeedLabel } from './dev-live-feed';
30
28
  import { clearLock, preflight, writeLock } from './dev-lock';
31
29
  import { createStatementLedger } from './dev-n-plus-one';
30
+ import { coalesceReloads } from './dev-reload';
32
31
  import { appRoutes } from './dev-render';
33
32
  import { replicaOverrides } from './dev-replica';
34
33
  import type { RunningRoles } from './dev-roles';
@@ -39,7 +38,7 @@ import type { DevServices } from './dev-services';
39
38
  import { describeServices, reportedUrls, resolveServices } from './dev-services';
40
39
  import { storageRoutes } from './dev-storage';
41
40
  import { createTraceRecorder } from './dev-traces';
42
- import { isIgnoredPath } from './dev-watch';
41
+ import { watchTree } from './dev-watch-tree';
43
42
  import { intFlagOr, PORT_RANGE } from './flag-number';
44
43
  import { holdUntilShutdown } from './hold';
45
44
  import type { IslandBundle } from './island-bundle';
@@ -88,34 +87,21 @@ interface DevState {
88
87
  reloads: number;
89
88
  /** A save that will not build. Replaced on every attempt, so a fixed file clears it. */
90
89
  reloadFinding: Finding | undefined;
90
+ /**
91
+ * Modules that would not import and primitives that would not register, as of the LAST scan —
92
+ * the boot's, then each rebuild's. A page saved with a syntax error is a finding here until the
93
+ * save that fixes it, and the boot's list alone would never show it.
94
+ */
95
+ appFindings: readonly Finding[];
91
96
  /**
92
97
  * The client entries, rebuilt on the same tick as the manifest. An island is the one module this
93
- * process never imports, so a fresh `Bun.build` is the whole of its reload — no module cache to
94
- * invalidate, which is exactly why editing one takes effect where editing a route does not.
98
+ * process never imports, so a fresh `Bun.build` is the whole of its reload. The route module
99
+ * beside it is re-imported by that same scan (`app-load.ts`) — the two are one generation, or
100
+ * a save serves a new island under an old page, which is what it did until 2026-09-07.
95
101
  */
96
102
  islands: IslandBundle;
97
103
  }
98
104
 
99
- /**
100
- * Debounced: a save that touches five files is one reload, not five. What counts as a save at all
101
- * is `dev-watch.ts` — a reload is a full `appManifest()` plus a `buildIslands()` over every island,
102
- * so a write this cannot rule out is the most expensive no-op the dev loop has.
103
- */
104
- function watchApp(root: string, onChange: (file: string) => void): () => void {
105
- let timer: ReturnType<typeof setTimeout> | undefined;
106
- let last = '';
107
- const watcher = watch(root, { recursive: true }, (_event, filename) => {
108
- if (filename === null || isIgnoredPath(filename)) return;
109
- last = filename;
110
- if (timer !== undefined) clearTimeout(timer);
111
- timer = setTimeout(() => onChange(last), 30);
112
- });
113
- return () => {
114
- if (timer !== undefined) clearTimeout(timer);
115
- watcher.close();
116
- };
117
- }
118
-
119
105
  export interface StartDevOptions {
120
106
  readonly root: string;
121
107
  readonly port: number;
@@ -155,11 +141,16 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
155
141
  // uninstalled, and nothing more (axiom 6).
156
142
  const statements = createStatementLedger();
157
143
  setStatementObserver(statements.observer);
158
- const app = await loadApp(options.root);
144
+ // ONE load at boot, the same call the rebuild below makes: the manifest and the findings are
145
+ // two projections of one scan. Until 2026-09-07 this was `loadApp` for the findings and then
146
+ // `appManifest` — which loads again — for the manifest, so a save landing between the two put
147
+ // `/_x`'s findings and its manifest on different registration states.
148
+ const app = await appManifest(options.root);
159
149
  const state: DevState = {
160
- manifest: (await appManifest(options.root)).manifest,
150
+ manifest: app.manifest,
161
151
  reloads: 0,
162
152
  reloadFinding: undefined,
153
+ appFindings: app.findings,
163
154
  islands: await buildIslands(options.root),
164
155
  };
165
156
  // The manifest's build id is a content hash of every fact below it, so a dev document's
@@ -294,22 +285,33 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
294
285
  ...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
295
286
  });
296
287
 
297
- const stopWatching = watchApp(options.root, (file) => {
298
- const started = performance.now();
299
- void Promise.all([appManifest(options.root), buildIslands(options.root)])
300
- .then(([{ manifest }, islands]) => {
301
- state.manifest = manifest;
302
- state.islands = islands;
303
- state.reloads += 1;
304
- state.reloadFinding = undefined;
305
- options.onReload?.(file, Math.round(performance.now() - started));
306
- })
307
- // Same rule as a module that will not import: a save the manifest cannot be rebuilt from is
308
- // a finding on `/_x`, never an unhandled rejection that takes the dev server down.
309
- .catch((error: unknown) => {
310
- state.reloadFinding = { ...findingFrom(error), at: file };
311
- });
312
- });
288
+ // One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
289
+ // trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
290
+ const rebuild = coalesceReloads(
291
+ async (file) => {
292
+ const started = performance.now();
293
+ const [{ manifest, findings }, islands] = await Promise.all([
294
+ appManifest(options.root),
295
+ buildIslands(options.root),
296
+ ]);
297
+ state.manifest = manifest;
298
+ state.appFindings = findings;
299
+ state.islands = islands;
300
+ state.reloads += 1;
301
+ state.reloadFinding = undefined;
302
+ options.onReload?.(file, Math.round(performance.now() - started));
303
+ },
304
+ // Same rule as a module that will not import: a save the manifest cannot be rebuilt from is
305
+ // a finding on `/_x`, never an unhandled rejection that takes the dev server down.
306
+ (error: unknown, file: string) => {
307
+ state.reloadFinding = { ...findingFrom(error), at: file };
308
+ },
309
+ );
310
+ // Watched one directory at a time, so an ignored one costs no descriptor at all — `dev-watch.ts`
311
+ // decides which, from the app's own `.gitignore`. A recursive watch on the root registered one
312
+ // inotify descriptor per directory in the tree, `.git/`, `node_modules/` and the `.x/` this
313
+ // process writes to included, and filtered the events afterwards.
314
+ const watcher = watchTree({ root: options.root, onChange: rebuild });
313
315
 
314
316
  server = {
315
317
  url: running.url ?? `http://localhost:${options.port}`,
@@ -326,14 +328,14 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
326
328
  get findings(): readonly Finding[] {
327
329
  const loops = statements.repeats().map(loopFacts).map(loopFinding);
328
330
  return state.reloadFinding === undefined
329
- ? [...app.findings, ...loops]
330
- : [...app.findings, state.reloadFinding, ...loops];
331
+ ? [...state.appFindings, ...loops]
332
+ : [...state.appFindings, state.reloadFinding, ...loops];
331
333
  },
332
334
  running,
333
335
  runtime,
334
336
  panels,
335
337
  async stop() {
336
- stopWatching();
338
+ watcher.close();
337
339
  await running.stop();
338
340
  await runtime.stop();
339
341
  // 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
 
@@ -7,8 +7,9 @@ import { MANIFEST_FILENAME } from '@ultimat3/manifest';
7
7
  import { appManifest, writeAppManifest } from './app-manifest';
8
8
  import { requireAppRoot } from './app-root';
9
9
  import type { CliCommand, CommandContext } from './command';
10
- import { generate } from './generate-files';
11
- import { GENERATORS, readKind, readName, readSurface } from './generate-kinds';
10
+ import { generate, sliceDir } from './generate-files';
11
+ import type { Generator } from './generate-kinds';
12
+ import { GENERATORS, readKind, readName, readPermission, readSurface } from './generate-kinds';
12
13
  import { containedPath, writeFiles } from './generate-write';
13
14
  import { resolveCatalogModule } from './i18n-audit';
14
15
  import { syncI18nIndex } from './i18n-index';
@@ -63,14 +64,20 @@ export const generateCommand: CliCommand = {
63
64
  const surface = readSurface(flagString(ctx.args, 'surface'), kind, name);
64
65
  const locales = resolveLocales(flagList(ctx.args, 'locales'));
65
66
  const at = flagString(ctx.args, 'at');
66
- const permission = flagString(ctx.args, 'permission');
67
+ // Read with the two above, and refused here for their reason: the value is spliced into the
68
+ // emitted source three times, and a value that is not a `<resource>:<verb>` is a page the app
69
+ // cannot compile — found after the files are on disk, which is the worst place to find it.
70
+ const permission = readPermission(flagString(ctx.args, 'permission'), kind);
67
71
  // Read before a file is planned, like the flags above: which module a generated component
68
72
  // imports `useT()` from is a fact about THIS app, and `generate` is a pure function.
69
73
  const catalogModule = await resolveCatalogModule(root);
74
+ // Read for the same reason: which errors the slice declares is written on THIS app's disk.
75
+ const sliceErrors = await readSliceErrors(root, kind, sliceDir(surface, featureFlag ?? name));
70
76
  const files = generate({
71
77
  kind,
72
78
  name,
73
79
  ...(featureFlag === undefined ? {} : { feature: featureFlag }),
80
+ ...(sliceErrors === undefined ? {} : { sliceErrors }),
74
81
  ...(at === undefined ? {} : { at }),
75
82
  ...(permission === undefined ? {} : { permission }),
76
83
  surface,
@@ -135,3 +142,18 @@ export const generateCommand: CliCommand = {
135
142
  };
136
143
  },
137
144
  };
145
+
146
+ /**
147
+ * The slice's `errors.ts`, for the two generators whose template throws from it. Only those two:
148
+ * a route or an island names no slice, and the "feature" the fallback derives for them is a path
149
+ * that exists nowhere — reading it would be answering a question nobody asked.
150
+ */
151
+ async function readSliceErrors(
152
+ root: string,
153
+ kind: Generator,
154
+ slice: string,
155
+ ): Promise<string | undefined> {
156
+ if (kind !== 'action' && kind !== 'mutator') return undefined;
157
+ const file = containedPath(root, `${slice}/errors.ts`);
158
+ return existsSync(file) ? await Bun.file(file).text() : undefined;
159
+ }
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-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('/');