@ultimat3/cli 8.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CLAUDE.md +9 -3
  2. package/package.json +26 -25
  3. package/src/affected.ts +0 -3
  4. package/src/app-boundaries.ts +4 -5
  5. package/src/app-env.ts +7 -2
  6. package/src/app-load.ts +7 -0
  7. package/src/browser-launcher.ts +0 -2
  8. package/src/budgets.ts +4 -3
  9. package/src/cmd-build.ts +2 -2
  10. package/src/cmd-db-branch.ts +2 -2
  11. package/src/cmd-deploy.ts +20 -6
  12. package/src/cmd-docs.ts +2 -1
  13. package/src/cmd-doctor.ts +3 -5
  14. package/src/cmd-env.ts +2 -2
  15. package/src/cmd-fix.ts +2 -4
  16. package/src/cmd-new.ts +2 -2
  17. package/src/cmd-shot.ts +22 -2
  18. package/src/db-finding.ts +2 -2
  19. package/src/db-seed.ts +0 -3
  20. package/src/dev-assets.ts +4 -7
  21. package/src/dev-cache.ts +140 -36
  22. package/src/dev-lock.ts +8 -7
  23. package/src/dev-purge.ts +120 -0
  24. package/src/dev-queue.ts +31 -6
  25. package/src/dev-render.ts +11 -14
  26. package/src/dev-runtime.ts +62 -15
  27. package/src/dev-storage.ts +8 -2
  28. package/src/dev-sync.ts +37 -2
  29. package/src/document-styles.ts +4 -2
  30. package/src/drift.ts +3 -2
  31. package/src/error-codes.ts +9 -2
  32. package/src/error-contract.ts +5 -5
  33. package/src/errors.ts +1 -29
  34. package/src/flag-reads.ts +2 -2
  35. package/src/generate-write.ts +3 -2
  36. package/src/guards.ts +4 -4
  37. package/src/index.ts +2 -6
  38. package/src/island-bundle.ts +34 -10
  39. package/src/island-routes.ts +7 -1
  40. package/src/island-styles.ts +1 -1
  41. package/src/mcp-errors.ts +2 -2
  42. package/src/metrics-endpoint.ts +0 -2
  43. package/src/output.ts +2 -2
  44. package/src/prerender.ts +34 -20
  45. package/src/runtime-overrides.ts +1 -1
  46. package/src/serve.ts +1 -1
  47. package/src/solid-loader.ts +1 -1
  48. package/src/static-report.ts +41 -3
  49. package/src/style-csp.ts +2 -1
  50. package/src/templates/scaffold-container.ts +12 -0
  51. package/src/templates/scaffold-docs.ts +11 -4
  52. package/src/templates/scaffold-domain-package.ts +3 -1
  53. package/src/templates/scaffold-repo.ts +19 -9
  54. package/src/templates/slice-foundation.ts +3 -5
  55. package/src/test-shards.ts +2 -2
  56. package/src/tsconfig-references.ts +2 -2
  57. package/src/verify-checks.ts +3 -2
  58. package/src/verify-floor.ts +8 -6
  59. package/src/verify-run.ts +2 -2
  60. package/src/verify-step.ts +2 -1
  61. package/src/verify-test-run.ts +2 -2
  62. package/src/workspace-checks.ts +10 -12
  63. package/src/workspace-graph.ts +3 -2
  64. package/src/write-line.ts +7 -1
package/CLAUDE.md CHANGED
@@ -535,6 +535,7 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
535
535
  | `dev-queue.ts` | the db + queue pair alone, and the one place that takes every ambient accessor back |
536
536
  | `dev-runtime.ts` | start the rest on top of it and install the remaining accessors (storage, mail, transport) |
537
537
  | `dev-cache.ts` | which cache tiers this process reads through, and the cross-instance invalidation hop |
538
+ | `dev-purge.ts` | the hourly retention sweep: which framework tables this boot owns, the `purge()` job over them and the `task` that fires it |
538
539
  | `dev-sync.ts` | the `sync` role: its live-query registry, who is dialling it, and the socket it owns |
539
540
  | `runtime-overrides.ts` | the one field a host hands the framework a driver through |
540
541
  | `sync-authenticator.ts` | the app's HTTP authenticator, seen as the sync node's |
@@ -602,6 +603,8 @@ relay draining it.
602
603
  | the durable scheduler | `pgSchedulerState` + `createPgLeaseLeader` in `startRoles` | a watermark forgotten on restart, and every replica its own leader |
603
604
  | the Postgres event bus | `dev-queue.ts` | `step.waitForEvent` forgot every correlation on restart |
604
605
  | the shared idempotency store | `dev-queue.ts` | a retry on another replica charged the card twice |
606
+ | the shared auth limiter | `configureAuthLimiters` in `startServices` | account lockouts counted per POD, so N replicas granted `maxAttempts × N` guesses |
607
+ | the retention sweep | `dev-purge.ts`, declared in `startServices` | three `purgeExpired()` with no caller — every row `x_idempotency`, `x_rate_limit` and `x_auth_*` ever took was kept |
605
608
  | the cache tiers | `dev-cache.ts` | only the CDN tier was registered; memo, LRU and Redis had zero callers |
606
609
  | WebSocket authentication | `dev-sync.ts` | `actorId: null` on every socket — realtime was single-tenant by wiring |
607
610
  | OTLP export | `otlp-export.ts` | the chart set the variable and no code read it |
@@ -769,9 +772,12 @@ Promoting it to `x verify`'s `boundaries` host check is one line in `scripts/ver
769
772
  rule over names. Two stronger rules were measured and rejected: "the read must not be a property
770
773
  initializer" reports six flags, five of which work (`x db --allow-destructive`, `x jobs --queue`);
771
774
  "the summary must match the behaviour" is undecidable. So the flag's summary now says what it does,
772
- and forcing a reload stays what it always was — `@ultimat3/pwa`'s `updateSignal({ reason:
773
- 'security' })`, which `As of 2026-08` has **no runtime caller anywhere**, in that package or
774
- outside it. Wiring the flag means giving that function a caller first.
775
+ and forcing a reload is **not a thing this framework does**, `As of 2026-08`. `updateSignal`
776
+ had no runtime caller for four majors and 9.0.0 deleted it rather than wiring it: `pwa` is tier 4
777
+ and the two runtimes holding both build ids — `http` (2) and `sync` (3) — are below it, so no
778
+ legal import could ever have reached the function. A deploy command has no channel to a running
779
+ client regardless; the plan is `docker compose up` or `helm upgrade`. What ships is notification:
780
+ `useConnection().updateAvailable` from `@ultimat3/realtime`.
775
781
 
776
782
  ## Planned commands are commands
777
783
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "8.0.0",
3
+ "version": "10.0.0",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,30 +37,31 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/core": "^7.28.4",
40
- "@ultimat3/action": "8.0.0",
41
- "@ultimat3/admin": "8.0.0",
42
- "@ultimat3/ai": "8.0.0",
43
- "@ultimat3/cache": "8.0.0",
44
- "@ultimat3/core": "8.0.0",
45
- "@ultimat3/db": "8.0.0",
46
- "@ultimat3/entity": "8.0.0",
47
- "@ultimat3/http": "8.0.0",
48
- "@ultimat3/i18n": "8.0.0",
49
- "@ultimat3/jobs": "8.0.0",
50
- "@ultimat3/mail": "8.0.0",
51
- "@ultimat3/manifest": "8.0.0",
52
- "@ultimat3/mcp": "8.0.0",
53
- "@ultimat3/policy": "8.0.0",
54
- "@ultimat3/pwa": "8.0.0",
55
- "@ultimat3/query": "8.0.0",
56
- "@ultimat3/realtime": "8.0.0",
57
- "@ultimat3/render": "8.0.0",
58
- "@ultimat3/schema": "8.0.0",
59
- "@ultimat3/scraping": "8.0.0",
60
- "@ultimat3/seo": "8.0.0",
61
- "@ultimat3/storage": "8.0.0",
62
- "@ultimat3/testing": "8.0.0",
63
- "@ultimat3/time": "8.0.0",
40
+ "@ultimat3/action": "10.0.0",
41
+ "@ultimat3/admin": "10.0.0",
42
+ "@ultimat3/ai": "10.0.0",
43
+ "@ultimat3/auth": "10.0.0",
44
+ "@ultimat3/cache": "10.0.0",
45
+ "@ultimat3/core": "10.0.0",
46
+ "@ultimat3/db": "10.0.0",
47
+ "@ultimat3/entity": "10.0.0",
48
+ "@ultimat3/http": "10.0.0",
49
+ "@ultimat3/i18n": "10.0.0",
50
+ "@ultimat3/jobs": "10.0.0",
51
+ "@ultimat3/mail": "10.0.0",
52
+ "@ultimat3/manifest": "10.0.0",
53
+ "@ultimat3/mcp": "10.0.0",
54
+ "@ultimat3/policy": "10.0.0",
55
+ "@ultimat3/pwa": "10.0.0",
56
+ "@ultimat3/query": "10.0.0",
57
+ "@ultimat3/realtime": "10.0.0",
58
+ "@ultimat3/render": "10.0.0",
59
+ "@ultimat3/schema": "10.0.0",
60
+ "@ultimat3/scraping": "10.0.0",
61
+ "@ultimat3/seo": "10.0.0",
62
+ "@ultimat3/storage": "10.0.0",
63
+ "@ultimat3/testing": "10.0.0",
64
+ "@ultimat3/time": "10.0.0",
64
65
  "babel-preset-solid": "^1.9.15"
65
66
  }
66
67
  }
package/src/affected.ts CHANGED
@@ -13,7 +13,6 @@
13
13
  // directory is checkout-relative while a caller's scan yields paths relative to its own root.
14
14
  import { join, relative } from 'node:path';
15
15
  import { singleLine, UltimateError } from '@ultimat3/core';
16
- import { docsFor } from './error-codes';
17
16
  import { BadFlagError } from './errors';
18
17
  import type { ExecResult, Runner } from './exec';
19
18
  import { execOutput } from './exec';
@@ -193,7 +192,6 @@ export async function gitRoot(runner: Runner, cwd: string, command: string): Pro
193
192
  code: 'X_CLI_UNEXPECTED',
194
193
  cause: `x ${command} reads its diff from git and "git rev-parse --show-toplevel" exited ${result.code} in ${cwd}: ${singleLine(execOutput(result))}`,
195
194
  fix: `run x ${command} from inside a git checkout — confirm with: git rev-parse --show-toplevel`,
196
- docs: docsFor('X_CLI_UNEXPECTED'),
197
195
  });
198
196
  }
199
197
 
@@ -245,7 +243,6 @@ export async function changedFiles(
245
243
  code: 'X_CLI_UNEXPECTED',
246
244
  cause: `"${failed.command.join(' ')}" exited ${failed.code} in ${options.cwd}: ${singleLine(execOutput(failed))}`,
247
245
  fix: `run it yourself to see why: ${failed.command.join(' ')}`,
248
- docs: docsFor('X_CLI_UNEXPECTED'),
249
246
  });
250
247
  }
251
248
  return [...new Set(runs.flatMap((run) => paths(run.stdout)))].sort();
@@ -14,6 +14,7 @@
14
14
  import { join as joinPath } from 'node:path';
15
15
  // The POSIX variants resolve specifiers against import-graph keys, which are POSIX on every host.
16
16
  import { dirname, join, normalize, relative } from 'node:path/posix';
17
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
17
18
  import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
18
19
  import { checkSurfaceBoundary, importGraph, SURFACES } from '@ultimat3/render';
19
20
  import type { Finding } from './output';
@@ -48,8 +49,6 @@ const CODE_OF: Readonly<Record<BoundaryRule, BoundaryCode>> = {
48
49
  */
49
50
  export const boundaryCodeOf = (rule: BoundaryRule): BoundaryCode => CODE_OF[rule];
50
51
 
51
- const docs = (code: BoundaryCode): string => `https://ultimate.dev/errors/${code}`;
52
-
53
52
  const isRoute = (path: string): boolean => /\/(page|layout|route)\.[cm]?tsx?$/.test(path);
54
53
  const isService = (path: string): boolean => /\/service\.[cm]?ts$/.test(path);
55
54
  const isDbSpecifier = (specifier: string): boolean =>
@@ -128,7 +127,7 @@ const surfaceFindings = (graph: ImportGraph): readonly Finding[] =>
128
127
  code,
129
128
  cause: violation.cause,
130
129
  fix: violation.fix,
131
- docs: docs(code),
130
+ docs: ERROR_DOCS_URL,
132
131
  at: violation.importer,
133
132
  };
134
133
  });
@@ -198,7 +197,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
198
197
  code: 'X_BOUNDARY_ROUTE_TO_DB',
199
198
  cause: `route imports the database ("${specifier}") — routes call actions and queries`,
200
199
  fix: generate('query', file.path, 'then call it from'),
201
- docs: docs('X_BOUNDARY_ROUTE_TO_DB'),
200
+ docs: ERROR_DOCS_URL,
202
201
  at: file.path,
203
202
  });
204
203
  }
@@ -207,7 +206,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
207
206
  code: 'X_BOUNDARY_SERVICE_TO_HTTP',
208
207
  cause: `service imports HTTP ("${specifier}") — a service that knows about requests cannot be reused by a job`,
209
208
  fix: generate('action', file.path, 'read the request there and pass plain values to'),
210
- docs: docs('X_BOUNDARY_SERVICE_TO_HTTP'),
209
+ docs: ERROR_DOCS_URL,
211
210
  at: file.path,
212
211
  });
213
212
  }
package/src/app-env.ts CHANGED
@@ -7,7 +7,12 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
10
- import { checkEnvExample, ENV_EXAMPLE_PATH, renderEnvExample } from '@ultimat3/core';
10
+ import {
11
+ checkEnvExample,
12
+ ENV_EXAMPLE_PATH,
13
+ ERROR_DOCS_URL,
14
+ renderEnvExample,
15
+ } from '@ultimat3/core';
11
16
  import { APP_CONFIG_FILE } from './app-root';
12
17
  import type { Finding } from './output';
13
18
  import { findingFrom } from './output';
@@ -63,7 +68,7 @@ const driftFinding = (cause: string): Finding => ({
63
68
  // The generator, not the assertion: `assertEnvExample`'s own fix is a `Bun.write(…)` call for
64
69
  // an app that has a schema object in scope, and a gate reader has a shell.
65
70
  fix: 'x env example',
66
- docs: 'https://ultimate.dev/errors/X_ENV_EXAMPLE_DRIFT',
71
+ docs: ERROR_DOCS_URL,
67
72
  at: ENV_EXAMPLE_PATH,
68
73
  });
69
74
 
package/src/app-load.ts CHANGED
@@ -11,6 +11,13 @@ import { localeConfig } from '@ultimat3/i18n';
11
11
  import type { ErrorCodeFact } from '@ultimat3/manifest';
12
12
  import { registerQueries } from '@ultimat3/query';
13
13
  import { isRouteConfig, pageComponentOf, registerRoute } from '@ultimat3/render';
14
+ // For the SIDE EFFECT, and it is this module's to hold: importing `@ultimat3/render/server`
15
+ // installs the `.tsx`/`.scss` Bun plugin, a plugin only transforms modules loaded AFTER it, and
16
+ // every app module below is loaded by the dynamic `import()` in this file. Before the render
17
+ // barrel split it came free with the line above; after it, the only other path to `/server` from
18
+ // here is six hops through `error-contract` → `fix-command` → the command registry, which is an
19
+ // accident one refactor away from compiling every app's `.tsx` to `React.createElement`.
20
+ import '@ultimat3/render/server';
14
21
  import { collectDeclaredCodes } from './error-contract';
15
22
  import type { Finding } from './output';
16
23
  import { findingFrom } from './output';
@@ -7,7 +7,6 @@ import { existsSync } from 'node:fs';
7
7
  import { UltimateError } from '@ultimat3/core';
8
8
  import type { CdpLauncherLike, ScrapeDriver } from '@ultimat3/scraping';
9
9
  import { localBrowser } from '@ultimat3/scraping';
10
- import { docsFor } from './error-codes';
11
10
 
12
11
  /**
13
12
  * The one library this works against. Playwright is not an alternative and is not a flag:
@@ -33,7 +32,6 @@ export class ShotBrowserMissingError extends UltimateError {
33
32
  // one literal, and a fix line the gate cannot read is a fix line nothing holds to the
34
33
  // contract. `browser-launcher.test.ts` pins it against the constant instead.
35
34
  fix: 'bun add -d puppeteer-core',
36
- docs: docsFor('X_SHOT_BROWSER_MISSING'),
37
35
  meta: { root: input.root, package: BROWSER_PACKAGE },
38
36
  });
39
37
  }
package/src/budgets.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { existsSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
11
12
  import type { Manifest, RouteFact } from '@ultimat3/manifest';
12
13
  import { formatBytes, parseByteBudget } from '@ultimat3/render';
13
14
  import type { Finding } from './output';
@@ -75,7 +76,7 @@ function unmeasuredFinding(url: string, declared: string, built: boolean): Findi
75
76
  fix: built
76
77
  ? `x build --target static --json # its "unmeasured" list says why ${url} could not be weighed`
77
78
  : 'x build --target static --json && x verify --json',
78
- docs: 'https://ultimate.dev/errors/X_BUDGET_UNMEASURED',
79
+ docs: ERROR_DOCS_URL,
79
80
  at: url,
80
81
  };
81
82
  }
@@ -106,7 +107,7 @@ export function checkBudgets(
106
107
  code: 'X_BUDGET_EXCEEDED',
107
108
  cause: `${route.url} ships ${formatBytes(measured.jsBytes)} of JS over a ${formatBytes(js)} budget via ${chainOf(measured)}`,
108
109
  fix: `x routes --json to see the chain, then move the heavy import behind hydrate: 'interaction'`,
109
- docs: 'https://ultimate.dev/errors/X_BUDGET_EXCEEDED',
110
+ docs: ERROR_DOCS_URL,
110
111
  at: route.url,
111
112
  });
112
113
  }
@@ -115,7 +116,7 @@ export function checkBudgets(
115
116
  code: 'X_BUDGET_EXCEEDED',
116
117
  cause: `${route.url} LCP ${measured.lcpMs}ms over the ${lcp}ms budget`,
117
118
  fix: `raise the budget in defineRoute, or switch render to 'isr' to serve it prebuilt`,
118
- docs: 'https://ultimate.dev/errors/X_BUDGET_EXCEEDED',
119
+ docs: ERROR_DOCS_URL,
119
120
  at: route.url,
120
121
  });
121
122
  }
package/src/cmd-build.ts CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
6
+ import { ERROR_DOCS_URL, frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
7
7
  import { requireAppRoot } from './app-root';
8
8
  import { runVerify } from './cmd-verify';
9
9
  import type { CliCommand, CommandContext } from './command';
@@ -143,7 +143,7 @@ export function buildResult(input: {
143
143
  code: 'X_BUILD_FAILED',
144
144
  cause: `${input.command.join(' ')} exited ${result.code}`,
145
145
  fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
146
- docs: 'https://ultimate.dev/errors/X_BUILD_FAILED',
146
+ docs: ERROR_DOCS_URL,
147
147
  },
148
148
  ],
149
149
  data: {
@@ -3,7 +3,7 @@
3
3
  // `x db branch ls` used to clone a database called `ls`, because the argument was the name.
4
4
  // The facts (what a branch is, per mode) are `db-branch.ts`; the client lifetime is here.
5
5
 
6
- import { nearestName } from '@ultimat3/core';
6
+ import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
7
7
  import { createPostgresClient, type DbClient } from '@ultimat3/db';
8
8
  import type { CommandContext } from './command';
9
9
  import type { BranchRow } from './db-branch';
@@ -215,6 +215,6 @@ function notABranch(services: DevServices, name: string): CommandResult {
215
215
  code: 'X_DB_BRANCH_FAILED',
216
216
  cause: `"${name}" is not a branch of this database, so nothing was dropped (it would be ${target})`,
217
217
  fix: LIST_FIX,
218
- docs: 'https://ultimate.dev/errors/X_DB_BRANCH_FAILED',
218
+ docs: ERROR_DOCS_URL,
219
219
  });
220
220
  }
package/src/cmd-deploy.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // anything that runs containers can execute.
4
4
 
5
5
  import { join } from 'node:path';
6
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
6
7
  import { requireAppRoot } from './app-root';
7
8
  import type { CliCommand, CommandContext } from './command';
8
9
  import { BadFlagError, UnknownCommandError } from './errors';
@@ -31,6 +32,13 @@ import { quoteArg } from './shell-quote';
31
32
  * honours. Both compose definitions carry it — `docker/docker-compose.prod.yml`'s `backfill` and
32
33
  * the one `templates/scaffold-container.ts` scaffolds, which also gates on `migrate` completing.
33
34
  * This paragraph said they "still owe" both for as long as they have had them.
35
+ *
36
+ * COMPOSE-ONLY, and `backfill` is why that has to be written down. `docker/helm`'s `roles:` map is
37
+ * `web|sync|worker|scheduler|replicator` plus a `migrate` Job — there is no `backfill` object in
38
+ * the chart at all — and the helm plan is one `helm upgrade --install`, so this list describes
39
+ * neither the objects a chart deploy creates nor the steps it runs. Every reader of it is therefore
40
+ * inside the compose branch; the SUMMARY reads `plan.steps` instead, or `x deploy --method helm`
41
+ * reports a post-deploy sweep to an operator that nothing will ever run.
34
42
  */
35
43
  export const DEPLOY_ROLES = ['migrate', 'web', 'sync', 'worker', 'scheduler', 'backfill'] as const;
36
44
 
@@ -175,9 +183,12 @@ export const deployCommand: CliCommand = {
175
183
  // `critical: <bool>`, and no file in `packages/` read that field — so the flag changed
176
184
  // nothing about what `x deploy` did, on either method. `flag-reads.ts`'s
177
185
  // `X_CLI_FLAG_UNREAD` passed it, because that gate proves a flag is READ and this one was:
178
- // into a field with no reader. Forcing a reload is `@ultimat3/pwa`'s
179
- // `updateSignal({ reason: 'security' })`, which has no runtime caller either; a flag that
180
- // triggers it is a change in that package, and this was not it.
186
+ // into a field with no reader. It is not coming back: `@ultimat3/pwa`'s
187
+ // `updateSignal({ reason: 'security' })`, the call it was to have triggered, is **deleted**
188
+ // as of 9.0.0 for having had no runtime caller of its own, and nothing in the framework
189
+ // force-navigates a client. A deploy also has no channel to one — the plan is
190
+ // `docker compose up` / `helm upgrade`, and the client's build id is read by `http` (tier 2)
191
+ // and `sync` (tier 3), neither of which may import a tier-4 package to act on it.
181
192
  ],
182
193
  },
183
194
  async run(ctx: CommandContext): Promise<CommandResult> {
@@ -199,11 +210,14 @@ export const deployCommand: CliCommand = {
199
210
  env: { ...plan.env },
200
211
  steps: plan.steps.map((step) => ({ role: step.role, command: step.command.join(' ') })),
201
212
  };
213
+ // The roles THIS plan has, never the compose list: on `--method helm` there is one step,
214
+ // `all`, and naming `backfill` there promises an operator a sweep the chart cannot run.
215
+ const roles = plan.steps.map((step) => step.role).join(',');
202
216
  if (flagBool(ctx.args, 'dry-run')) {
203
217
  return {
204
218
  ok: true,
205
219
  command: 'deploy',
206
- summary: msg('cli.deploy.plan', { images: 1, roles: DEPLOY_ROLES.join(',') }),
220
+ summary: msg('cli.deploy.plan', { images: 1, roles }),
207
221
  data: planJson,
208
222
  lines: plan.steps.map(
209
223
  (step) => ` ${step.role.padEnd(10)} ${stepLine(plan.env, step.command)}`,
@@ -222,7 +236,7 @@ export const deployCommand: CliCommand = {
222
236
  code: 'X_DEPLOY_FAILED',
223
237
  cause: `role "${step.role}" step exited ${result.code}`,
224
238
  fix: `${stepLine(plan.env, step.command)} # run it directly to see the full output`,
225
- docs: 'https://ultimate.dev/errors/X_DEPLOY_FAILED',
239
+ docs: ERROR_DOCS_URL,
226
240
  },
227
241
  ],
228
242
  data: planJson,
@@ -232,7 +246,7 @@ export const deployCommand: CliCommand = {
232
246
  return {
233
247
  ok: true,
234
248
  command: 'deploy',
235
- summary: msg('cli.deploy.plan', { images: 1, roles: DEPLOY_ROLES.join(',') }),
249
+ summary: msg('cli.deploy.plan', { images: 1, roles }),
236
250
  data: planJson,
237
251
  };
238
252
  },
package/src/cmd-docs.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // should not have to guess which of 29 packages holds the answer, and it must never be handed a
5
5
  // URL: `node_modules` already contains every doc, because the published artifact IS the source.
6
6
 
7
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
7
8
  import type { DocEntry, DocHit } from '@ultimat3/manifest';
8
9
  import { nearestTopics, scanInstalledDocs, searchDocs } from '@ultimat3/manifest';
9
10
  import type { CliCommand, CommandContext } from './command';
@@ -22,7 +23,7 @@ const unresolvedFinding = (): Finding => ({
22
23
  code: 'X_CLI_UNEXPECTED',
23
24
  cause: '@ultimat3/core does not resolve from the installed CLI, so no docs could be read',
24
25
  fix: 'bun install && x doctor --json',
25
- docs: 'https://ultimate.dev/errors/X_CLI_UNEXPECTED',
26
+ docs: ERROR_DOCS_URL,
26
27
  at: import.meta.dir,
27
28
  });
28
29
 
package/src/cmd-doctor.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
- import { tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
7
+ import { ERROR_DOCS_URL, tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
8
8
  import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
9
9
  import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
10
10
  import type { CliCommand, CommandContext } from './command';
@@ -49,12 +49,10 @@ export interface DoctorProbe {
49
49
  snapshots(): Promise<readonly Finding[]>;
50
50
  }
51
51
 
52
- const docs = (code: string): string => `https://ultimate.dev/errors/${code}`;
53
-
54
52
  const finding = (code: string, cause: string, fix: string, at?: string): Finding =>
55
53
  at === undefined
56
- ? { code, cause, fix, docs: docs(code) }
57
- : { code, cause, fix, docs: docs(code), at };
54
+ ? { code, cause, fix, docs: ERROR_DOCS_URL }
55
+ : { code, cause, fix, docs: ERROR_DOCS_URL, at };
58
56
 
59
57
  export const OFFLINE_FALLBACK = 'apps/web/app/offline.tsx';
60
58
 
package/src/cmd-env.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  // Bun ships no path-join primitive, and `.env.example` is written app-root-relative.
6
6
  import { join } from 'node:path';
7
- import { checkEnv, ENV_EXAMPLE_PATH, maskedEnvValues } from '@ultimat3/core';
7
+ import { checkEnv, ENV_EXAMPLE_PATH, ERROR_DOCS_URL, maskedEnvValues } from '@ultimat3/core';
8
8
  import { ENV_SCHEMA_EXPORT, envExampleFor, loadEnvSchema } from './app-env';
9
9
  import { APP_CONFIG_FILE, requireAppRoot } from './app-root';
10
10
  import type { CliCommand, CommandContext } from './command';
@@ -54,7 +54,7 @@ async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
54
54
  code: 'X_ENV_MISSING',
55
55
  cause: `${issue.key} is ${issue.reason} (expected ${issue.expected})`,
56
56
  fix: issue.fix,
57
- docs: 'https://ultimate.dev/errors/X_ENV_MISSING',
57
+ docs: ERROR_DOCS_URL,
58
58
  at: ENV_EXAMPLE_PATH,
59
59
  }));
60
60
  return {
package/src/cmd-fix.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // (`docs/architecture/02-boundaries.md`) — a caller runs the printed edit, or the generated
4
4
  // `git mv`, itself.
5
5
 
6
- import { nearestName } from '@ultimat3/core';
6
+ import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
7
7
  import { appImportGraph, readAppSources } from './app-boundaries';
8
8
  import { requireAppRoot } from './app-root';
9
9
  import type { BoundaryCut } from './boundary-cuts';
@@ -18,8 +18,6 @@ export { planBoundaryCuts };
18
18
 
19
19
  export const FIX_SUBCOMMANDS = ['boundary'] as const;
20
20
 
21
- const docsUrl = (code: string): string => `https://ultimate.dev/errors/${code}`;
22
-
23
21
  /**
24
22
  * Accept either an app-root-relative path or a suffix that matches exactly one scanned file —
25
23
  * an agent copying the path out of a `fix:` line has the short form
@@ -81,7 +79,7 @@ const findingForCut = (cut: BoundaryCut): Finding => ({
81
79
  code: cut.code,
82
80
  cause: cut.cause,
83
81
  fix: cut.edit,
84
- docs: docsUrl(cut.code),
82
+ docs: ERROR_DOCS_URL,
85
83
  at: cut.at,
86
84
  });
87
85
 
package/src/cmd-new.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { existsSync } from 'node:fs';
6
6
  import { chmod } from 'node:fs/promises';
7
7
  import { isAbsolute, join, resolve } from 'node:path';
8
- import { renderThrowable } from '@ultimat3/core';
8
+ import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
9
9
  import { dedupe } from './cmd-generate';
10
10
  import type { CliCommand, CommandContext } from './command';
11
11
  import { MissingPositionalError } from './errors';
@@ -195,7 +195,7 @@ export const newCommand: CliCommand = {
195
195
  code: 'X_GENERATE_CONFLICT',
196
196
  cause: `${target} already exists`,
197
197
  fix: `x new ${app.kebab} --force, or choose another name`,
198
- docs: 'https://ultimate.dev/errors/X_GENERATE_CONFLICT',
198
+ docs: ERROR_DOCS_URL,
199
199
  at: target,
200
200
  },
201
201
  ],
package/src/cmd-shot.ts CHANGED
@@ -164,6 +164,17 @@ const intFlag = (
164
164
  fallback,
165
165
  );
166
166
 
167
+ /**
168
+ * How `devServerFor` starts a scratch server. A parameter with a default rather than a direct
169
+ * call, for the reason every `Runner` in this package is one: the failure path below — a boot that
170
+ * throws, and the lock it has to hand back — is otherwise only reachable by breaking a real app.
171
+ */
172
+ export type BootDevServer = (input: {
173
+ readonly root: string;
174
+ readonly port: number;
175
+ readonly env: Readonly<Record<string, string | undefined>>;
176
+ }) => Promise<{ readonly url: string; stop(): Promise<void> }>;
177
+
167
178
  export interface ShotServer {
168
179
  readonly url: string;
169
180
  /** Which server the picture is of. Reported, because the two have different failure modes. */
@@ -272,6 +283,7 @@ export async function devServerFor(
272
283
  root: string,
273
284
  env: Readonly<Record<string, string | undefined>>,
274
285
  port: number,
286
+ boot: BootDevServer = (input) => startDev(input),
275
287
  ): Promise<ShotServer> {
276
288
  const services = resolveServices(root, env);
277
289
  const file = Bun.file(lockPath(services.stateDir));
@@ -281,13 +293,21 @@ export async function devServerFor(
281
293
  return { url: lock.url, origin: 'reused', stop: () => Promise.resolve() };
282
294
  }
283
295
  }
284
- await preflight({
296
+ const { release } = await preflight({
285
297
  stateDir: services.stateDir,
286
298
  port,
287
299
  hostname: DEV_BINDING.hostname,
288
300
  embeddedDb: services.db.mode === 'embedded',
289
301
  });
290
- const dev = await startDev({ root, port, env });
302
+ // The directory is CLAIMED from here down — `preflight` returns holding it, never having merely
303
+ // looked — so a boot that throws has to give it back. `cmd-dev.ts` states the same rule at the
304
+ // same seam. Without it one failed `x shot` refused every later `x dev` and `x shot` on this
305
+ // checkout, naming a pid that had already exited. The original error is re-thrown untouched: a
306
+ // teardown must never replace the failure it is cleaning up after.
307
+ const dev = await boot({ root, port, env }).catch((error: unknown) => {
308
+ release();
309
+ throw error;
310
+ });
291
311
  await writeLock(services.stateDir, {
292
312
  pid: process.pid,
293
313
  port,
package/src/db-finding.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // step that failed. Its own module because `cmd-db.ts` and `cmd-db-branch.ts` both need it and
4
4
  // neither may import the other.
5
5
 
6
- import { renderThrowable } from '@ultimat3/core';
6
+ import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
7
7
  import type { Finding } from './output';
8
8
  import { findingFrom, isUltimateErrorShape } from './output';
9
9
 
@@ -24,5 +24,5 @@ export const stepFinding = (error: unknown, code: string): Finding =>
24
24
  // a TypeError raised while reporting it.
25
25
  cause: renderThrowable(error),
26
26
  fix: 'x doctor --json',
27
- docs: `https://ultimate.dev/errors/${code}`,
27
+ docs: ERROR_DOCS_URL,
28
28
  };
package/src/db-seed.ts CHANGED
@@ -12,7 +12,6 @@ import type { Environment } from '@ultimat3/core';
12
12
  import { UltimateError } from '@ultimat3/core';
13
13
  import type { Driver, Seed, SeedTier } from '@ultimat3/entity';
14
14
  import { isSeed, SEED_TIERS, seedTiersFor } from '@ultimat3/entity';
15
- import { docsFor } from './error-codes';
16
15
  import { BadFlagError } from './errors';
17
16
  import type { Finding, JsonValue } from './output';
18
17
  import { findingFrom } from './output';
@@ -49,7 +48,6 @@ export class SeedUnknownError extends UltimateError {
49
48
  // A dry run, never a bare `x db seed`: the command that answers "which seeds are there" must
50
49
  // not be the command that writes them.
51
50
  fix: 'x db seed --dry-run --json',
52
- docs: docsFor('X_DECLARATION_UNKNOWN'),
53
51
  });
54
52
  }
55
53
  }
@@ -75,7 +73,6 @@ export class SeedEnvironmentError extends UltimateError {
75
73
  code: 'X_SEED_ENVIRONMENT',
76
74
  cause: `seed "${input.seed}" is tier ${input.tier} and ULTIMATE_ENV resolved ${input.environment}, where x db seed runs ${input.tiers.join(', ')} — ULTIMATE_SEED_TIER=${input.tier} says this deploy takes it anyway`,
77
75
  fix: `x db seed ${input.seed} --tier ${input.tier} --json`,
78
- docs: docsFor('X_SEED_ENVIRONMENT'),
79
76
  });
80
77
  }
81
78
  }
package/src/dev-assets.ts CHANGED
@@ -15,8 +15,8 @@ import type { IconPlan } from '@ultimat3/pwa';
15
15
  import { BuiltinImagePipeline, PwaIconMissingError, planIcons } from '@ultimat3/pwa';
16
16
  import type { ImageQuery, ImageTransformDriver } from '@ultimat3/seo';
17
17
  import { builtinImageDriver, DEFAULT_WIDTHS, parseImageQuery } from '@ultimat3/seo';
18
- import type { ImageFormat, ImageTransform, Storage } from '@ultimat3/storage';
19
- import { IMAGE_FORMATS, isTenantScoped, variantKey } from '@ultimat3/storage';
18
+ import type { ImageTransform, Storage, VariantFormat } from '@ultimat3/storage';
19
+ import { isTenantScoped, isVariantFormat, variantKey } from '@ultimat3/storage';
20
20
  import {
21
21
  AUTHORIZED_OBJECT_CACHE,
22
22
  assertReadableKey,
@@ -87,14 +87,11 @@ const imageResponse = (bytes: Uint8Array, contentType: string, cache: CacheHint)
87
87
  const mediaCache = (key: string): CacheHint =>
88
88
  isTenantScoped(key) ? AUTHORIZED_OBJECT_CACHE : IMMUTABLE_IMAGE;
89
89
 
90
- const isImageFormat = (value: string): value is ImageFormat =>
91
- (IMAGE_FORMATS as readonly string[]).includes(value);
92
-
93
90
  /**
94
91
  * `exactOptionalPropertyTypes` makes an explicit `undefined` a different answer from an absent
95
92
  * key, and `variantKey` reads presence — so a spread, never an assignment.
96
93
  */
97
- function storageTransform(query: ImageQuery, format: ImageFormat | undefined): ImageTransform {
94
+ function storageTransform(query: ImageQuery, format: VariantFormat | undefined): ImageTransform {
98
95
  return {
99
96
  ...(query.width === undefined ? {} : { width: query.width }),
100
97
  ...(format === undefined ? {} : { format }),
@@ -117,7 +114,7 @@ async function transformedVariant(
117
114
  // A format storage cannot name has no variant key, so it cannot be cached. The driver refuses
118
115
  // it with core's `X_IMAGE_UNSUPPORTED`; refusing it here too would give one bad URL two codes.
119
116
  const format =
120
- query.format !== undefined && isImageFormat(query.format) ? query.format : undefined;
117
+ query.format !== undefined && isVariantFormat(query.format) ? query.format : undefined;
121
118
  const cacheable = query.format === undefined || format !== undefined;
122
119
  const cached = cacheable ? variantKey(key, storageTransform(query, format)) : undefined;
123
120
  // The SOURCE key decides the posture, not the variant's: `variantKey` keeps the source's prefix,