@ultimat3/cli 10.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -193,6 +193,7 @@ cast: a `null` where an id was expected would otherwise become a mutation agains
193
193
  | `fix-imports.ts` | which of those factories a file can call that it did not declare — one relative specifier, one file read |
194
194
  | `error-contract.ts` | the rules, the two checks that turn them into findings, and `collectDeclaredCodes` |
195
195
  | `fix-command.ts` | resolving an `x <command>` a `fix:` cites against the registry |
196
+ | `fix-path.ts` | resolving a PATH or a glob a `fix:` cites against the root the gate is running in |
196
197
  | `source-files.ts` | which files are shipped source — shared with `filesize`, never a second list |
197
198
 
198
199
  **A `fix:` may not cite a command this build does not ship.** Six shipped fix lines named
@@ -201,6 +202,26 @@ one passed — the text rule checks that a fix NAMES a command, never that the r
201
202
  `fix-command.ts` resolves the citation, and a PLANNED command fails too: `x logs` parses, `x help`
202
203
  lists it, and running it hands the reader `X_NOT_IMPLEMENTED` instead of the fix.
203
204
 
205
+ **A `fix:` may not cite a file this repo does not have, either.** That was the other half, and
206
+ nothing resolved it: `X_UI_RUNTIME_MISSING` told its reader to paste a line no generator ever wrote,
207
+ through every gate since it shipped (#274, #246). A file token is one of the four things that make a
208
+ fix an instruction at all (`COMMAND_TOKENS`), so `fix-path.ts` is built from the SAME extension list
209
+ — a token that satisfies the instruction rule is exactly the token this one has to resolve, and two
210
+ lists would be a citation the second rule cannot see. `X_ERROR_FIX_PATH_MISSING` is its own code:
211
+ `X_ERROR_FIX_INVALID` means the fix is not an instruction, this one means it is one and points at
212
+ nothing, and the repairs differ.
213
+
214
+ It is narrow so a finding never has to be argued with — three shapes are not judged at all, because
215
+ each resolves against something other than the root the gate is running in: a scoped specifier
216
+ (`@ultimat3/ui/global.scss`, which resolves through `node_modules`), a dot-relative path
217
+ (`./global.scss`, which resolves against the reader's own file) and any path whose **parent
218
+ directory** this root does not have (`src/errors.ts`, `apps/web/server.ts`,
219
+ `packages/i18n/catalogs/en.json` — all three name a directory a generated app has and this repo does
220
+ not). What is left is the citation a root really can answer: a directory that exists, named as
221
+ holding a file it does not hold. A glob must match at least one file. Measured over all three roots
222
+ the gate runs in — the framework, `examples/dummy`, `dummy/social-media-clone` — **117 path citations
223
+ read, 0 findings**, so it enforces outright with no pin table.
224
+
204
225
  The rule is **conditional, and that is load-bearing**: *if* a fix cites `x <command>`, it must
205
226
  resolve. It does not require every fix to name one — `set OTEL_EXPORTER_OTLP_ENDPOINT=…` and
206
227
  `counter('orders_total', { maxSeries: 4000 })` are executable and correctly cite nothing, and a
@@ -235,6 +256,33 @@ that named the code in its `<PKG>_BORROWED_ERROR_CODES`. The docs check reads it
235
256
  framework's own `framework.manifest.json`, because a second scanner over a narrower file set is a
236
257
  manifest that claims completeness it does not have.
237
258
 
259
+ **A `code:` is a literal, a module-scope const in the same file, or a finding** — `As of 2026-08-23`,
260
+ and until then it was a literal or silence. `scanCodes` matched `code\s*[:=]\s*'X_…'`, so
261
+ `const STALE = 'X_DOC_PACKAGE_GRAPH_STALE'` followed by `code: STALE` — the DRY thing to write, and
262
+ what `scripts/package-map-graph.ts` really wrote — was a declaration to nobody: no manifest row, no
263
+ row demanded on `wiki/Error-Codes.md`, no entry for `bun run gate-codes`, and `x errors explain`
264
+ answering `X_ERROR_CODE_UNKNOWN` for a code the build throws. Silent, and in the **permissive**
265
+ direction: the DRYer the author, the less the gate saw (#277).
266
+
267
+ `scanCodeDeclarations` is that one pass, and it returns both halves. It resolves the identifier
268
+ against the module-scope consts of the **same file** — anchored at column 0, which is what makes it
269
+ module scope without a parser — and reports every name it cannot resolve as `X_ERROR_CODE_UNRESOLVED`
270
+ rather than skipping it, which is the whole point: a scanner that reads only what it likes enforces
271
+ only what it sees. `scanCodes` is its `.sites`, so the manifest, the docs check, `bun run gate-codes`
272
+ and `x errors explain` (through `scanCodeFixSites`, which resolves the same way) cannot see different
273
+ sets. Cross-file resolution was **refused** even though `fix-imports.ts` already does the harder
274
+ version for `fix:`: it would make the scan async for every caller, and the finding is the better
275
+ answer anyway — one file holds both the code and its only spelling.
276
+
277
+ Three shapes are deliberately not judged, each measured over the framework and both tracked apps
278
+ before the rule shipped. A name that resolves to something that is **not** a code is an answer, not
279
+ a gap (`const STATUS_NOT_FOUND = 404` in `@ultimat3/realtime`'s NATS fake, the one live instance). A
280
+ **table read** is not judged — `SEO_ERROR_CODES.metaMissing` is how `@ultimat3/seo` and
281
+ `@ultimat3/ui` raise all 18 of their codes, and the registry those literals live in already declares
282
+ them. A **lowercase** name is not judged: 164 sit at a `code:` position in this tree and every one is
283
+ a type annotation (`readonly code: string`) or a re-raise (`code: opts.code`). Measured on all three
284
+ roots: **0 findings**, so it enforces outright with no pin table.
285
+
238
286
  An empty `fix`, or a `fix` that says `check` / `make sure` / `try` / `see the docs` and names no
239
287
  command, call or file path, is `X_ERROR_FIX_INVALID`. A declared code the host's error reference
240
288
  does not name is `X_ERROR_CODE_UNDOCUMENTED` — `wiki/Error-Codes.md` here, nothing in a generated
@@ -542,7 +590,9 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
542
590
  | `otlp-export.ts` | the exporters `OTEL_EXPORTER_OTLP_ENDPOINT` switches on, and their drain hooks |
543
591
  | `dev-render.ts` | one HTTP route per registered `route`, through render's own mode function |
544
592
  | `style-csp.ts` | the `style-src` sha256 of every inline `<style>` the web role serves |
593
+ | `script-csp.ts` | the `script-src` sha256 of every inline `<script>` it serves — the hydration runtime, from `@ultimat3/render`'s own `HYDRATE_RUNTIME_BODIES` |
545
594
  | `dev-assets.ts` | the image pipeline's only HTTP surface: `/icons/*` and `/media/*` |
595
+ | `favicon.ts` | `/favicon.ico`: the app's own file, and the bytes the framework answers with when there is none |
546
596
  | `dev-hooks.ts` | the pipeline's `authorize` seam, decided from the app's own `Policy` objects |
547
597
  | `dev-roles.ts` | `--role` selection plus start/stop for `web`, `sync`, `worker`, `scheduler` |
548
598
  | `dev-dashboard.ts` | the `DevSources` hooks only this process can answer, and the two CLI panels |
@@ -654,6 +704,23 @@ missing.
654
704
  | `prerender.ts` | build first, write the chunks into the export, then measure |
655
705
  | `budgets.ts` | `measureDocumentJs` weighs `data-x-entry` as well as `<script src>` |
656
706
 
707
+ **A stats row is keyed by the route's DECLARED path, and holds its heaviest page**
708
+ (`As of 2026-08-23`). `checkBudgets` looks a route up by `route.url` off the manifest, which is the
709
+ pattern (`/blog/:slug`), and `prerenderSite` pushed `artifact.path` — the filled one
710
+ (`/blog/hello`). So no dynamic static route had ever been weighed: each was `X_BUDGET_UNMEASURED`
711
+ and `X_BUDGET_EXCEEDED` could not fire for the whole class. The heaviest page and not the first,
712
+ because a budget is a ceiling and the page that breaks it is the one the route answers for; the
713
+ report's `emitted` list still names every filled path.
714
+
715
+ **Both halves of the CSP are computed at boot, `As of 2026-08-23`.** `style-csp.ts` was alone, and
716
+ the hydration runtime is emitted INLINE in every document carrying an island — so with
717
+ `script-src 'self' 'wasm-unsafe-eval'` no island booted in any container. `x dev` sends the policy
718
+ report-only (`dev: true`), which is exactly why nobody saw it: the page hydrated on a laptop and
719
+ never in the image. `startWeb` extends both directives; the property is pinned end to end by
720
+ `dev-roles-script-csp.test.ts`, which parses a served document, hashes every executable inline
721
+ script in it and asserts the response's own `script-src` names each one — with `dev: false`, the
722
+ only mode in which the policy can block anything.
723
+
657
724
  **One `Bun.build` per island, never one call with N entry points**, and `splitting: false`. The
658
725
  island's `src` is a string, so no import edge reaches it and the page's graph stays the page's
659
726
  (axiom 6) — a shared chunk would put that number back behind a graph walk, and the budget compares
@@ -719,6 +786,21 @@ width is not one of the eight. Anything outside it is still served; only the `pu
719
786
  no caller gains a new 4xx. `?q=` is deliberately still unbounded here — the closed set for quality
720
787
  is `@ultimat3/seo`'s to declare, not this file's.
721
788
 
789
+ **`/favicon.ico` is a mechanism, not a scaffolded file.** Every browser requests it unprompted, the
790
+ scaffold wrote none and neither served surface mounted a route, so a permanent 404 sat in the console
791
+ of every app the framework produces — noise that trains the reader to ignore console errors, which is
792
+ the opposite of what `--json` and an executable `fix:` are for (#272). Two rungs and one path: the
793
+ app's own `apps/web/site/favicon.ico` wins, and `favicon.ts` answers a 32x32 PNG encoded through
794
+ `@ultimat3/core`'s own pipeline when there is none — the same encoder `x new`'s icon goes through, so
795
+ there is no second image format in the tree and no base64 blob nobody can verify. It is deliberately
796
+ NOT derived from `ICON_SOURCE`: resizing the install icon needs `@ultimat3/pwa`'s pipeline and would
797
+ make the answer depend on a file that may be absent, which is a third rung under a mechanism that has
798
+ exactly two. The file is read per REQUEST, so dropping one into a running `x dev` takes effect
799
+ without a restart. It mounts through `assetRoutes`, which is the one route set `serve.ts` and
800
+ `cmd-dev.ts` both compose — a favicon added to one of them alone is a 404 that comes back in
801
+ production only — and `prerenderSite` writes the same bytes into the static export, because an
802
+ artifact served with no process behind it has to carry every byte the browser will ask for.
803
+
722
804
  `ICON_SOURCE` lives here, not in `cmd-doctor.ts`, because this is the module that reads it: the
723
805
  diagnostic checks what `x dev` serves, so one constant cannot pass the check and serve nothing.
724
806
  It is a **PNG** — core decodes PNG and JPEG only, and the SVG this used to name could never
@@ -745,6 +827,22 @@ watcher — *after* the drain, so an in-flight request still has the database it
745
827
  Ctrl-C is therefore the same three phases production runs, not a kill that leaves `.x/pgdata`
746
828
  locked by a process that no longer exists.
747
829
 
830
+ **The release runs INSIDE the drain's own deadline, `As of 2026-08-23`, and it is the same
831
+ deadline.** `drain()` ABANDONS a hook that overruns `ShutdownReason.deadlineAt` — the process is
832
+ meant to exit without it — and `release` re-enters the very same teardown one call later:
833
+ `app.stop()` → `startRoles().stop()` → `worker.stop()`, memoised in the package that owns it, so
834
+ awaiting it is awaiting the promise the drain just walked away from. Unbounded, that hung past
835
+ `terminationGracePeriodSeconds` and the kubelet SIGKILLed a process that had already drained
836
+ cleanly. The budget is the hook's own `reason.deadlineAt`, not a stopwatch of ours, so there is one
837
+ number and not two; an overrun is logged as `X_SHUTDOWN_TIMEOUT` and a REJECTION still rejects,
838
+ because `dispatch` awaits the hold inside its own `try`.
839
+
840
+ **`options.exit` has exactly one caller: `runRole` in `serve.ts`.** `bin.ts` ends in
841
+ `process.exit(code)`, so `x dev` and `x mcp` need nothing; `apps/web/server.ts` — which is what a
842
+ container runs — has no such line, and one non-unref'd interval anywhere in the app then holds an
843
+ event loop with nothing left to do. A function rather than a boolean because `process.exit` inside
844
+ a library is untestable, and the caller is the one that knows.
845
+
748
846
  Commands: `bun test`, `bunx tsc --noEmit -p tsconfig.json`.
749
847
 
750
848
  ## A declared flag with no reader is a promise `x help` makes and nothing keeps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "10.0.0",
3
+ "version": "11.1.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,31 +37,33 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/core": "^7.28.4",
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",
40
+ "@ultimat3/action": "11.1.0",
41
+ "@ultimat3/admin": "11.1.0",
42
+ "@ultimat3/ai": "11.1.0",
43
+ "@ultimat3/auth": "11.1.0",
44
+ "@ultimat3/cache": "11.1.0",
45
+ "@ultimat3/core": "11.1.0",
46
+ "@ultimat3/db": "11.1.0",
47
+ "@ultimat3/entity": "11.1.0",
48
+ "@ultimat3/flags": "11.1.0",
49
+ "@ultimat3/http": "11.1.0",
50
+ "@ultimat3/i18n": "11.1.0",
51
+ "@ultimat3/jobs": "11.1.0",
52
+ "@ultimat3/mail": "11.1.0",
53
+ "@ultimat3/manifest": "11.1.0",
54
+ "@ultimat3/mcp": "11.1.0",
55
+ "@ultimat3/money": "11.1.0",
56
+ "@ultimat3/policy": "11.1.0",
57
+ "@ultimat3/pwa": "11.1.0",
58
+ "@ultimat3/query": "11.1.0",
59
+ "@ultimat3/realtime": "11.1.0",
60
+ "@ultimat3/render": "11.1.0",
61
+ "@ultimat3/schema": "11.1.0",
62
+ "@ultimat3/scraping": "11.1.0",
63
+ "@ultimat3/seo": "11.1.0",
64
+ "@ultimat3/storage": "11.1.0",
65
+ "@ultimat3/testing": "11.1.0",
66
+ "@ultimat3/time": "11.1.0",
65
67
  "babel-preset-solid": "^1.9.15"
66
68
  }
67
69
  }
package/src/budgets.ts CHANGED
@@ -16,6 +16,12 @@ import type { Finding } from './output';
16
16
  export const BUILD_STATS_FILE = join('.x', 'build-stats.json');
17
17
 
18
18
  export interface RouteStats {
19
+ /**
20
+ * The route's DECLARED path — `/blog/:slug`, never `/blog/hello`. `checkBudgets` looks a row up
21
+ * by `route.url` off the manifest, which is the pattern, so a row keyed by a filled path is a
22
+ * row nothing can find: every dynamic static route read as `X_BUDGET_UNMEASURED`. A route that
23
+ * prerenders many pages contributes ONE row, holding its heaviest.
24
+ */
19
25
  readonly path: string;
20
26
  readonly jsBytes: number;
21
27
  /**
@@ -27,6 +27,7 @@ import type { CommandResult } from './output';
27
27
  import { findingFrom } from './output';
28
28
  import type { ParsedArgs } from './parse';
29
29
  import { flagBool, flagString } from './parse';
30
+ import { quoteArg } from './shell-quote';
30
31
 
31
32
  /** Which of the four questions an invocation asked. `pass` is `<name>` and `--all` both. */
32
33
  type BackfillShape = 'list' | 'pending' | 'pass';
@@ -73,11 +74,23 @@ function readShape(args: ParsedArgs): { readonly shape: BackfillShape; readonly
73
74
  // `--name` is a FILTER under `--list` and a target everywhere else, so it selects a shape only
74
75
  // where `--list` is absent. Two spellings of one target are still two, and are refused.
75
76
  const target = list ? undefined : (positional ?? named);
77
+ // Under `--list` the positional is DROPPED by the line above, and until 2026-08 nothing said so:
78
+ // `x db backfill cleanup --list` printed the whole ledger and reported `ok: true` while the
79
+ // operator had named one sweep. `--name` is the filter's one spelling here, so this is a refusal
80
+ // and not a second reading of the argument — the same rule every other shape in this function
81
+ // follows, and the same argv one flag over (`--pending cleanup`) has always been refused.
82
+ if (list && positional !== undefined) {
83
+ return refuseShape(
84
+ 'name',
85
+ `x db backfill --list filters the ledger with --name, so the positional "${positional}" selects nothing — it is a pass target in every other shape`,
86
+ `x db backfill --list --name ${quoteArg(positional)} --json`,
87
+ );
88
+ }
76
89
  if (!list && positional !== undefined && named !== undefined) {
77
90
  return refuseShape(
78
91
  'name',
79
92
  `x db backfill names two backfills ("${positional}" and "${named}") — a pass sweeps the positional or --name, never both`,
80
- `x db backfill ${positional} --write --json`,
93
+ `x db backfill ${quoteArg(positional)} --write --json`,
81
94
  );
82
95
  }
83
96
  const asked = [
package/src/cmd-dev.ts CHANGED
@@ -199,6 +199,9 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
199
199
  // declaration, and `x dev` and `serve.ts` must not be able to disagree about where the app's
200
200
  // sign-in page is.
201
201
  signInPath: await loadSignInPath(options.root),
202
+ // The same seam `serve.ts` passes: the app's own error page is a FILE, so the root is what
203
+ // `startWeb` needs to find one.
204
+ root: options.root,
202
205
  // The one document this process serves that the app did not write; `startRoles` covers the
203
206
  // app's own surfaces itself. `x dev` sends the policy report-only, so an uncovered `<style>`
204
207
  // here is a console report rather than a blank page — which is how this reached production.
package/src/cmd-new.ts CHANGED
@@ -8,7 +8,8 @@ import { isAbsolute, join, resolve } from 'node:path';
8
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
- import { MissingPositionalError } from './errors';
11
+ import { invocationOf } from './command';
12
+ import { AppNameIsPathError, MissingPositionalError } from './errors';
12
13
  import type { Runner } from './exec';
13
14
  import { msg } from './messages';
14
15
  import type { CommandResult } from './output';
@@ -116,7 +117,8 @@ export interface WrittenApp {
116
117
  * a migration whose snapshot never existed is what made the app's first two database commands
117
118
  * refuse each other — `x db migrate` naming `x db gen`, and `x db gen` refusing a sidecar version
118
119
  * control never had. The consequence is deliberate: `x verify`'s `drift` step is red on a pristine
119
- * scaffold until `x db gen "initial"` runs, which is what `cli.new.done` tells the author to do.
120
+ * scaffold until `x db gen "initial"` runs — which `bin/setup` does, and which is what
121
+ * `cli.new.done` tells the author to run.
120
122
  */
121
123
  export async function writeNewApp(target: string, options: NewAppOptions): Promise<WrittenApp> {
122
124
  const files = planNewApp(options);
@@ -130,6 +132,24 @@ function parentDir(cwd: string, dirFlag: string | undefined): string {
130
132
  return isAbsolute(dirFlag) ? dirFlag : join(cwd, dirFlag);
131
133
  }
132
134
 
135
+ /**
136
+ * The `--dir`/name split of a positional that is really a path, or `undefined` when it is a name.
137
+ *
138
+ * Separator-agnostic: a Windows path pasted into a shell here is the same mistake, and `\` is not
139
+ * a character any app name may hold either.
140
+ */
141
+ export function appNamePath(raw: string): { parent: string; base: string } | undefined {
142
+ if (!/[\\/]/.test(raw)) return undefined;
143
+ const trimmed = raw.replace(/[\\/]+$/, '');
144
+ const segments = trimmed.split(/[\\/]+/).filter((segment) => segment.length > 0);
145
+ const base = segments.at(-1) ?? 'myapp';
146
+ const parent = trimmed.slice(0, trimmed.lastIndexOf(base)).replace(/[\\/]+$/, '');
147
+ // `x new ./shop` and `x new shop/` both name the directory the caller is already in; `/shop`
148
+ // names the root, which is a directory and not "here".
149
+ if (parent !== '' && parent !== '.') return { parent, base };
150
+ return { parent: /^[\\/]/.test(trimmed) ? '/' : '.', base };
151
+ }
152
+
133
153
  export const newCommand: CliCommand = {
134
154
  spec: {
135
155
  name: 'new',
@@ -143,7 +163,7 @@ export const newCommand: CliCommand = {
143
163
  {
144
164
  // The summary carries the default and the negation because the page has to answer "which
145
165
  // one do I get if I type neither": the usage line offered `--no-example`, this table said
146
- // `--example`, and `default: true` is a field only `--json` renders. 134 files against 107.
166
+ // `--example`, and `default: true` is a field only `--json` renders. 136 files against 109.
147
167
  name: 'example',
148
168
  type: 'boolean',
149
169
  summary: 'include the example feature slice (default: on; --no-example for an empty app/)',
@@ -168,9 +188,19 @@ export const newCommand: CliCommand = {
168
188
  throw new MissingPositionalError({
169
189
  command: 'new',
170
190
  positional: 'name',
171
- example: 'x new myapp',
191
+ // Never the literal `x new`: this command is also the whole of `bunx create-ultimate`,
192
+ // which runs BEFORE `x` exists — so the one instruction the reader was given named a
193
+ // binary they had not installed yet.
194
+ example: `${invocationOf(ctx, 'new')} myapp`,
172
195
  });
173
196
  }
197
+ // Before `names()`, which is what makes this reachable at all: it slugifies a path into one
198
+ // kebab-case directory name, so `/srv/apps/shop` becomes `srv-apps-shop` inside the cwd and
199
+ // the scaffold lands somewhere nobody asked for. `--dir` is the flag that takes a path.
200
+ const path = appNamePath(raw);
201
+ if (path !== undefined) {
202
+ throw new AppNameIsPathError({ name: raw, invocation: invocationOf(ctx, 'new'), ...path });
203
+ }
174
204
  const app = names(raw);
175
205
  const target = resolve(parentDir(ctx.cwd, flagString(ctx.args, 'dir')), app.kebab);
176
206
  const options: NewAppOptions = { name: raw, example: ctx.args.flags.get('example') !== false };
package/src/command.ts CHANGED
@@ -13,8 +13,20 @@ export interface CommandContext {
13
13
  readonly runner: Runner;
14
14
  readonly env: Readonly<Record<string, string | undefined>>;
15
15
  readonly bunVersion: string;
16
+ /**
17
+ * How this process was invoked, up to and including the subcommand — `x new`, or
18
+ * `bunx create-ultimate` when `create-ultimate` is the entry point. Read by a `fix:` line, which
19
+ * is a command the reader is meant to RUN: `create-ultimate`'s whole reason to exist is running
20
+ * before `x` is installed, so `x new myapp` was an instruction nobody in that process could
21
+ * follow. Absent means the default below — a caller that builds a context by hand owes nothing.
22
+ */
23
+ readonly invocation?: string;
16
24
  }
17
25
 
26
+ /** What `ctx.invocation` means when nobody said: the binary, then the subcommand they typed. */
27
+ export const invocationOf = (ctx: CommandContext, command: string): string =>
28
+ ctx.invocation ?? `x ${command}`;
29
+
18
30
  export interface CliCommand {
19
31
  readonly spec: CommandSpec;
20
32
  run(ctx: CommandContext): Promise<CommandResult>;
package/src/dev-assets.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  authorizeStorageRead,
24
24
  STORAGE_READ_PERMISSION,
25
25
  } from './dev-storage';
26
+ import { faviconRoute } from './favicon';
26
27
 
27
28
  /**
28
29
  * The one source image every generated icon derives from. `x new` scaffolds it, `x doctor` checks
@@ -254,6 +255,11 @@ export function assetRoutes(options: AssetRoutesOptions): readonly Route[] {
254
255
  handler: async (request: UltimateRequest, ctx: RequestContext): Promise<Response> =>
255
256
  mediaResponse(request, ctx, options.storage, options.images),
256
257
  });
258
+ // Mounted here rather than in `serve.ts` and `cmd-dev.ts` separately: this is the one route set
259
+ // both served surfaces already compose, and a favicon that answers on a laptop and 404s in the
260
+ // container is the dev/prod difference this package's own rule forbids. `favicon.ts` owns what
261
+ // the answer IS — this file only says the app's asset surface is where it hangs.
262
+ routes.push(faviconRoute(options.root));
257
263
 
258
264
  return routes;
259
265
  }
package/src/dev-hooks.ts CHANGED
@@ -41,14 +41,22 @@ export interface DevHookOptions {
41
41
  * that starts a web role — `serve.ts` included — carry a diagnostic only one of them installs.
42
42
  */
43
43
  readonly devNotices?: ServerHooks['devNotices'];
44
+ /**
45
+ * The app's own error page for a status, read off its disk. Passed for `devNotices`' reason: a
46
+ * hook that reached for the app root itself would make every host that starts a web role carry a
47
+ * path only the one that knows the root can supply.
48
+ */
49
+ readonly errorPage?: ServerHooks['errorPage'];
44
50
  }
45
51
 
46
52
  export function devHooks(options: DevHookOptions = {}): ServerHooks {
47
53
  const authenticate = configuredAuthenticator();
48
54
  const devNotices = options.devNotices;
55
+ const errorPage = options.errorPage;
49
56
  return {
50
57
  ...(authenticate === undefined ? {} : { authenticate }),
51
58
  ...(devNotices === undefined ? {} : { devNotices }),
59
+ ...(errorPage === undefined ? {} : { errorPage }),
52
60
  authorize: (route, _request, ctx): AuthzDecision => {
53
61
  // An action route never arrives here: it carries `enforcedBy: 'handler'`, so the pipeline
54
62
  // never asks. `invoke` is its one evaluation, and the only one holding the row a row-level
package/src/dev-purge.ts CHANGED
@@ -112,9 +112,15 @@ function declareSweep(): void {
112
112
  * background work at all, and this is one more thing it does not do.
113
113
  */
114
114
  export function installRetentionSweep(stores: RetentionStores): () => void {
115
- installed = retentionTargets(stores);
115
+ const mine = retentionTargets(stores);
116
+ installed = mine;
116
117
  declareSweep();
117
118
  return () => {
118
- installed = [];
119
+ // Only if the slot is still OURS. Two runtimes can share one process — a test harness, or
120
+ // `x shot`'s scratch server beside the one it photographs — and a boot that installed after
121
+ // this one owns the slot now: emptying it there would leave the LIVE boot's hourly sweep
122
+ // deleting nothing, forever, with no error anywhere. Same rule, and the same comment, as
123
+ // `installedRevalidator` in `@ultimat3/render`'s ISR controller.
124
+ if (installed === mine) installed = [];
119
125
  };
120
126
  }
package/src/dev-render.ts CHANGED
@@ -183,11 +183,14 @@ async function resultFor(
183
183
  return { status: 200, headers: staticHeaders(contentHash(body), options.buildId), body };
184
184
  }
185
185
  case 'isr': {
186
- // `isrKey(url)`, never `url.pathname`: the query is part of what was rendered — this
186
+ // `isrKey(url, locale)`, never `url.pathname`: the query is part of what was rendered — this
187
187
  // route's own `meta` reads `data.url` — so two URLs differing only in their query are two
188
188
  // documents. Keyed on the pathname alone, the first render answered every later query
189
189
  // string (#171). Render owns the derivation so no second caller can invent another.
190
- const served = await isr.serve(isrKey(url), () =>
190
+ // The locale is the second dimension and it is `ctx.locale`, the answer the `locale` stage
191
+ // already negotiated for THIS request — never `currentLocale()`, which would read the same
192
+ // value through an ambient store the key does not need.
193
+ const served = await isr.serve(isrKey(url, ctx.locale), () =>
191
194
  documentFrom(entry, request, data, options),
192
195
  );
193
196
  return served.result;
package/src/dev-roles.ts CHANGED
@@ -26,9 +26,11 @@ import { startReplicator } from './dev-replicator';
26
26
  import type { RunningServices } from './dev-runtime';
27
27
  import type { Env } from './dev-services';
28
28
  import { startSync } from './dev-sync';
29
+ import { errorPageHook } from './error-pages';
29
30
  import { BadFlagError, PortInvalidError, RuntimeDriverSplitError } from './errors';
30
31
  import { DEFAULT_METRICS_PORT, startMetricsEndpoint } from './metrics-endpoint';
31
32
  import type { RuntimeOverrides } from './runtime-overrides';
33
+ import { inlineScriptSources } from './script-csp';
32
34
  import { inlineStyleSources } from './style-csp';
33
35
 
34
36
  /** The roles `x dev` starts when `--role` names none, in boot order. */
@@ -63,6 +65,16 @@ export interface StartRolesOptions {
63
65
  * `startRoles` takes plain values — a test starts a web role with no `app.config.ts` at all.
64
66
  */
65
67
  readonly signInPath?: string | null;
68
+ /**
69
+ * The app root, for the one seam that is a FILE and not a value: `apps/web/site/errors/404.html`
70
+ * and its siblings. Bound HERE rather than passed by each caller, because `x dev` and `serve.ts`
71
+ * both boot through this function and an override wired at one of them alone is a page that
72
+ * appears in dev and not in production — `/favicon.ico`'s rule, one seam over.
73
+ *
74
+ * Optional for the reason `signInPath` is: `startRoles` takes plain values, and a test starts a
75
+ * web role with no app on disk at all. Absent, every error page is the framework's.
76
+ */
77
+ readonly root?: string;
66
78
  /**
67
79
  * Inline `<style>` bodies this process serves that the app's own surfaces do not account for —
68
80
  * `/_x`'s shell. The surfaces themselves are read from the stylesheet registry here rather than
@@ -235,7 +247,10 @@ function startWeb(options: StartRolesOptions): ServerHandle {
235
247
  return createServer({
236
248
  routes: options.routes,
237
249
  role: 'web',
238
- hooks: devHooks(options.devNotices === undefined ? {} : { devNotices: options.devNotices }),
250
+ hooks: devHooks({
251
+ ...(options.devNotices === undefined ? {} : { devNotices: options.devNotices }),
252
+ ...(options.root === undefined ? {} : { errorPage: errorPageHook(options.root) }),
253
+ }),
239
254
  // Both seams `createServer` already had and `startRoles` passed neither of, so an app's own
240
255
  // middleware could not reach the pipeline any process the framework boots actually runs.
241
256
  ...(options.overrides?.middleware === undefined
@@ -260,8 +275,17 @@ function startWeb(options: StartRolesOptions): ServerHandle {
260
275
  // Hashes, never `'unsafe-inline'`: a `render: 'static'` page is a file on disk, so
261
276
  // nothing can stamp a per-response nonce into it, but its body is fixed and a hash is a
262
277
  // function of that body. Read after `loadApp` — importing the app IS what registered them.
278
+ // BOTH directives, and the script half is the one that was missing: the hydration runtime
279
+ // is emitted inline in every document that carries an island, so `script-src 'self'` meant
280
+ // no island booted anywhere the policy is enforced — which is every container, and never
281
+ // `x dev`, where it is report-only.
263
282
  security: {
264
- csp: { extend: { 'style-src': inlineStyleSources(options.inlineStyles ?? []) } },
283
+ csp: {
284
+ extend: {
285
+ 'style-src': inlineStyleSources(options.inlineStyles ?? []),
286
+ 'script-src': inlineScriptSources(),
287
+ },
288
+ },
265
289
  },
266
290
  }),
267
291
  }).start();
package/src/dispatch.ts CHANGED
@@ -31,6 +31,13 @@ export interface DispatchOptions {
31
31
  * it cannot produce. `bin.ts` passes the real fd 2.
32
32
  */
33
33
  readonly writeError?: (line: string) => void;
34
+ /**
35
+ * How the caller invoked this process, up to and including the subcommand — passed straight to
36
+ * `CommandContext.invocation` and read by the `fix:` lines that quote a whole command back.
37
+ * `create-ultimate` is the one caller that supplies it, because it is the one entry point whose
38
+ * name is not `x`.
39
+ */
40
+ readonly invocation?: string;
34
41
  }
35
42
 
36
43
  /**
@@ -122,6 +129,7 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
122
129
  runner: options.runner ?? exec,
123
130
  env: options.env,
124
131
  bunVersion: options.bunVersion,
132
+ ...(options.invocation === undefined ? {} : { invocation: options.invocation }),
125
133
  };
126
134
 
127
135
  try {
@@ -44,12 +44,22 @@ export const CATALOG_PACKAGES = [
44
44
  '@ultimat3/ui',
45
45
  ] as const;
46
46
 
47
+ /**
48
+ * The two packages the catalog may import WITHOUT `@ultimat3/cli` declaring them: they reach for a
49
+ * JSX runtime an app has and a bare CLI process does not, so a hard dependency would make the CLI
50
+ * uninstallable where the codes are merely absent today. Every other entry above is a real runtime
51
+ * import and must be a declared dependency — `error-catalog.test.ts` holds the list to exactly that,
52
+ * because an undeclared one resolves through workspace symlinks here and through nothing in an
53
+ * installed app, where `x errors explain X_FLAG_EXPIRED` then refuses a code the wiki promises.
54
+ */
55
+ export const CATALOG_OPTIONAL_HOSTS: readonly string[] = ['@ultimat3/admin', '@ultimat3/ui'];
56
+
47
57
  export interface ErrorCatalog {
48
58
  /** Packages whose codes are now registered. */
49
59
  readonly loaded: readonly string[];
50
60
  /**
51
61
  * Packages this process could not *resolve*, so their codes are absent from the answer. The one
52
- * tolerated case is the optional host: `@ultimat3/ui` and `@ultimat3/admin` reach for a JSX
62
+ * tolerated case is the optional host: `CATALOG_OPTIONAL_HOSTS` reach for a JSX
53
63
  * runtime an app has and a bare CLI process does not, and a list silently missing their codes is
54
64
  * worse than one that says which packages are missing. A package that resolved and then threw is
55
65
  * a defect, not a host gap, and goes to `failed`.
@@ -21,8 +21,15 @@ export const CLI_OWNED_ERROR_CODES = [
21
21
  'X_JOB_UNKNOWN',
22
22
  'X_FIX_TARGET_UNKNOWN',
23
23
  'X_ERROR_FIX_INVALID',
24
+ // The second half of the same contract: X_ERROR_FIX_INVALID means the fix is not an instruction,
25
+ // this one means it is one and cites a file that is not there. Two conditions, two repairs.
26
+ 'X_ERROR_FIX_PATH_MISSING',
24
27
  'X_ERROR_CODE_UNDOCUMENTED',
25
28
  'X_ERROR_CODE_UNREGISTERED',
29
+ // The third: a `code:` the scan cannot read at all. `const STALE = 'X_…'` in another file and
30
+ // `code: STALE` here is invisible to every reader of the code set, and silence there is
31
+ // permissive — the DRYer the author, the less the gate sees (#277).
32
+ 'X_ERROR_CODE_UNRESOLVED',
26
33
  // Reported as `Finding`s rather than thrown, and unregistered until now because of it — so
27
34
  // `x errors explain X_TYPECHECK_FAILED` refused a code `x verify` had just printed. A finding
28
35
  // carries an `X_*` code to the same reader a throw does; the registry is what makes that code
@@ -46,6 +53,11 @@ export const CLI_OWNED_ERROR_CODES = [
46
53
  'X_STORAGE_SECRET_DEV',
47
54
  'X_MANIFEST_STALE',
48
55
  'X_BUDGET_UNMEASURED',
56
+ // The other half of #271, and the half no runtime can raise: a route reads a live hook and boots
57
+ // no module in a browser, so its rows have nowhere to arrive and the page renders its loading
58
+ // branch forever, at 200. Only this package can see it — `@ultimat3/realtime` cannot see a route
59
+ // and `@ultimat3/render` may not import realtime.
60
+ 'X_LIVE_ROUTE_NO_ISLAND',
49
61
  'X_BUILD_FAILED',
50
62
  'X_BUILD_ENTRY_MISSING',
51
63
  'X_DEPLOY_FAILED',
@@ -160,8 +172,10 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
160
172
  X_JOB_UNKNOWN: 'the queue holds no job with this id',
161
173
  X_FIX_TARGET_UNKNOWN: 'the named file is not one of the app source files',
162
174
  X_ERROR_FIX_INVALID: "an error's fix line is not a runnable instruction",
175
+ X_ERROR_FIX_PATH_MISSING: "an error's fix line cites a file this repository does not have",
163
176
  X_ERROR_CODE_UNDOCUMENTED: 'a shipped error code has no row in the error reference',
164
177
  X_ERROR_CODE_UNREGISTERED: 'the error reference documents a code no package registers',
178
+ X_ERROR_CODE_UNRESOLVED: 'an error code is written as a name this repository cannot resolve',
165
179
  X_STORAGE_UNWRITABLE: 'the storage disk this process needs cannot be written to',
166
180
  X_STORAGE_SECRET_DEV: 'upload grants would be signed with the shipped development key',
167
181
  X_CLI_UNEXPECTED: 'the CLI itself failed',
@@ -175,6 +189,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
175
189
  X_RELEASE_VERSION_SKEW: 'a workspace is not at the lockstep version',
176
190
  X_MANIFEST_STALE: 'openapi.json is stale',
177
191
  X_BUDGET_UNMEASURED: 'a route declares a budget the build never measured',
192
+ X_LIVE_ROUTE_NO_ISLAND: 'a route reads live rows and boots nothing that could receive them',
178
193
  X_BUILD_FAILED: 'x build failed',
179
194
  X_BUILD_ENTRY_MISSING: "the build target's entry file is not in the app",
180
195
  X_DEPLOY_FAILED: 'a deploy step failed',