@ultimat3/cli 18.0.0 → 19.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
@@ -4,7 +4,7 @@ Tier 5. May import tiers 0–4. Nothing imports this except `create-ultimate`.
4
4
 
5
5
  | Rule | Detail |
6
6
  |---|---|
7
- | Entry | `src/bin.ts` (`#!/usr/bin/env bun`) — argv, stdout, exit code only |
7
+ | Entry | `src/bin.ts` (`#!/usr/bin/env bun`) — argv, stdout, exit code only. Before `dispatch`, `local-cli.ts` decides whether THIS process is the app's own CLI: a global `x` inside an app re-executes `node_modules/@ultimat3/cli/src/bin.ts` when that is a different realpath, because a second module instance of `@ultimat3/entity` is an empty registry (a zero-entity manifest, green, 2026-09-05). Same realpath (a workspace symlink), no app, a compiled binary, or `ULTIMATE_KEEP_GLOBAL_CLI` set: no hand-over |
8
8
  | stdout | `write-line.ts`'s `writeLine` — synchronous fd 1, never `process.stdout.write`, which truncates at the 64KB pipe buffer when `process.exit` follows. Exported, because `create-ultimate`'s entry point needs the same one |
9
9
  | stderr | `write-line.ts`'s `writeErrorLine` — the same loop on fd 2, for a line that is not the command's answer. A `CommandResult` declaring `stream: 'stderr'` is routed there by `dispatch.ts`'s `sinkFor`, and `x mcp serve --transport stdio` is the one case: its fd 1 carries JSON-RPC frames, so the `✓ mcp stdio serving 13 tools` line rendered after the loop was a malformed frame. Neither renderer carries `stream`, exactly like `hold` |
10
10
  | Boot logs under `--json` | `dispatch.ts` calls core's `setLogStream('stderr')` when `args.json` is set, once, for all thirty commands. `x db migrate --json` printed the boot logger's `ultimate migrate applied` and then the command's own object, so `json.load` raised on the second document. A server's stdout stays its log stream; this is the CLI process only |
@@ -329,26 +329,120 @@ to know about everything — so the join is here, and it is the same rule
329
329
  | `e2e-evaluate.ts` | the closure→string crossing, which is the only lossy edge in the adapter |
330
330
  | `e2e-errors.ts` | one constructor per refusal |
331
331
  | `e2e-dom-fixture.ts` | a document small enough to hold in a test and real enough to RUN the expressions above |
332
-
333
- **Absent by default, and that is a requirement rather than a state.** CI has no Chrome. Nothing here
334
- runs until `installE2eDriver` is called, so `hasE2eDriver()` still answers `false` and the gate's
335
- `e2e` step still refuses instead of passing over a browser it does not have.
332
+ | `cdp-browser.ts` | the two doors: `openE2eBrowserIfAvailable()` (undefined when there is no browser) and `openE2eBrowser()` (refuses by name), and the close that undoes both halves |
333
+ | `cdp-launch.ts` | which Chrome, and starting it — the candidate list, the flags, and the endpoint read off its stderr |
334
+ | `cdp-connection.ts` | CDP over Bun's own `WebSocket`: request framing, reply correlation by `id`, one-shot event waiters, the per-call deadline |
335
+ | `cdp-e2e-page.ts` | `E2eBrowserPage`'s five methods over an attached, flattened session |
336
+ | `cdp-errors.ts` | one constructor per way the browser half refuses |
337
+
338
+ **Absent by default, and that is a requirement rather than a state.** Nothing here runs until
339
+ `installE2eDriver` is called, so `hasE2eDriver()` still answers `false` and the gate's `e2e` step
340
+ still refuses instead of passing over a browser it does not have. This paragraph also said "CI has
341
+ no Chrome" until 2026-08-27, and that is false and was the reason issue #390's fourth requirement
342
+ — a real browser check — was recorded as out of reach: GitHub-hosted `ubuntu-latest` ships one at
343
+ `/usr/bin/google-chrome`, preinstalled, with no download step and no new dependency.
344
+
345
+ **The browser is RAW CDP over Bun's own `WebSocket`, and carries no dependency.**
346
+ `packages/scraping/src/cdp-port.ts` declares a ~25-method port because `ScrapePage` is a full
347
+ scraping surface and its intended implementation is `puppeteer-core`. `E2eBrowserPage` is FIVE
348
+ methods, and CDP's wire format is one JSON object with an `id` — so the whole thing an e2e driver
349
+ needs is four small modules, which is why `x test e2e` needs nothing installed that `bun install`
350
+ did not already put there. `e2e/cdp-browser.e2e.test.ts` drives a real Chrome against a real
351
+ `Bun.serve` and asserts all five methods; `openE2eBrowserIfAvailable()` answering `undefined` is
352
+ what makes it a SKIP on a laptop without one rather than a red step.
353
+
354
+ **The load EVENT is the completion signal, never `Page.navigate`'s reply.** Measured on Chrome 150:
355
+ a navigation that swaps the render process — `about:blank` → `http://localhost:<port>/`, the most
356
+ ordinary one there is — loads the page, hits the server and answers a later `Runtime.evaluate` from
357
+ the new document, and the navigate frame **never comes back at all**. A driver that awaited the
358
+ reply waited out its full deadline on every first navigation. So `cdpConnect().once()` registers a
359
+ `Page.loadEventFired` waiter BEFORE the send, and the reply is raced against it — still read, but
360
+ only for `errorText`, which is the one place a refused navigation is named.
361
+
362
+ **A CDP call is deadlined and a close settles every call in flight.** Without that, a suite whose
363
+ browser died waits out one full deadline per call and reports a timeout, where the true fault is a
364
+ dead browser. The four codes are four repairs, which is why they are not one:
365
+ `X_CDP_BROWSER_MISSING` (install one), `X_CDP_LAUNCH_FAILED` (read the browser's own stderr, which
366
+ the cause carries), `X_CDP_CALL_FAILED` (look at the page), `X_CDP_TIMEOUT` (raise the deadline).
336
367
 
337
368
  **`evaluate` is the edge that cannot be lossless.** `PageLike.evaluate` takes a closure and every
338
369
  browser port in this framework takes a string, so what crosses is `Function.prototype.toString()`
339
370
  and nothing else. A zero-parameter closure naming only page globals is supported; a native or bound
340
371
  function, a declared parameter and a method shorthand are refused STATICALLY, before a byte leaves;
341
372
  a binding the page does not have comes back named, from the page's own `ReferenceError`. Measured on
342
- Bun 1.4.0 and load-bearing: **Bun's transpiler folds `wanted === 3` to `!0` before `toString()` ever
373
+ Bun 1.3.14 and 1.4.0 alike — re-measured on both when the repo moved back to the 1.3 series, because a version-stamped claim that names one runtime is unread evidence on the other — and load-bearing: **Bun's transpiler folds `wanted === 3` to `!0` before `toString()` ever
343
374
  runs**, so a captured PRIMITIVE can vanish from the source and never fail at all, while a captured
344
375
  reference always survives as its name. No static rule in this process can see the difference — which
345
376
  is why the refusal is raised from the page's answer rather than from a scan of the source.
346
377
 
347
- **Three of `E2eFixtures`' four members refuse, deliberately.** `offline()` and `online()` need a CDP
348
- method for the browser's own network state and `CdpPageLike` declares none; `update()` needs a second
349
- build served under a new id, which is a fact about the server. A fixture that silently no-opped would
350
- make the assertion after it read as proof — `offline()` followed by "the fallback rendered" is the
351
- app's ONLINE page passing an offline test.
378
+ **One of `E2eFixtures`' four members still refuses, and it is the one that is not a port gap.**
379
+ `update()` needs a second build served under a new immutable build id, which is a fact about the
380
+ SERVER, and no page port has ever been able to speak for one. `offline()`/`online()` FORWARD — to
381
+ `E2eBrowserPage.offline`, which `cdp-e2e-page.ts` implements as
382
+ `Network.emulateNetworkConditions` and `@ultimat3/scraping` implements through
383
+ `CdpPageLike.setOfflineMode`. They refused until 2026-08-27 on a reason the tree contradicted on
384
+ the day it was written. A fixture that silently no-opped would make the assertion after it read as
385
+ proof — `offline()` followed by "the fallback rendered" is the app's ONLINE page passing an offline
386
+ test — so an `E2eBrowserPage` that declares no `offline` still gets the refusal, now naming the
387
+ method the double is missing rather than a capability the framework does not have.
388
+
389
+ ## The service worker is emitted here, because the emitter needs facts only a build has
390
+
391
+ `@ultimat3/pwa` shipped `generateServiceWorker`, `buildPrecacheManifest`, `offlineFallbackSource`,
392
+ `backgroundSyncSource` and `pushSource` since it existed, and every one had **zero callers** outside
393
+ its own package. So `pwa.offline`, `pwa.backgroundSync`, `pwa.push` and every route's own `offline:`
394
+ were declarations with no build behind them, and no Ultimate app worked offline however its config
395
+ was written (#390). `sw-artifacts.ts` is the caller.
396
+
397
+ **Why here and not beside the manifest.** `loadPwaArtifacts(root)` needs a root and a config file;
398
+ the worker needs the ROUTE TABLE and the ISLAND BUNDLE as well — facts only a booted app and a
399
+ finished build have. Splitting them keeps `loadPwaArtifacts` callable before either exists, which
400
+ `x doctor` and the icon writer rely on. The route table is `describeRoutes()`, the one projection
401
+ `x.manifest.json`, `/_x`, the sitemap and `sw.js` are all built from, so a route added to the app
402
+ cannot be missing from the precache manifest.
403
+
404
+ | Surface | What it does with the worker |
405
+ |---|---|
406
+ | `cmd-dev.ts` | mounts `/sw.js` and `/x-sw-register.js`; built ONCE at boot and deliberately not rebuilt on the watcher tick — a worker that changes under a page it already controls is the update path, and re-emitting one per keystroke exercises it on every save |
407
+ | `serve.ts` | the same two routes in the container, from the same call |
408
+ | `prerender.ts` | writes both as FILES into the export — a static host runs no route table, so a `<script src="/x-sw-register.js">` in every document is a 404 unless the bytes are in the artifact |
409
+
410
+ **Registration is an EXTERNAL script, never inline**, and that is a CSP fact rather than a
411
+ preference: `startWeb` computes a `script-src` sha256 per inline script, so an unhashed one is
412
+ blocked in the container while passing report-only under `x dev` — which is how the hydration
413
+ runtime shipped broken once already.
414
+
415
+ **`sw.js` is served `no-store` with `Service-Worker-Allowed: /`.** A cached `sw.js` is a worker that
416
+ cannot be replaced: the browser re-fetches it to decide whether an update exists, and an
417
+ intermediary answering the old bytes pins every client to the deploy that shipped them. Without the
418
+ header the browser refuses to let a worker served from `/` control `/` — the failure `assertScope`
419
+ cannot see, because the scope a REGISTRATION asks for has to be allowed by the script's own response
420
+ and not only by its path.
421
+
422
+ **`api/` and `shared/` never cross.** An API response is a JSON document whose freshness is the
423
+ app's business, and precaching one serves a stale answer to a client that had a network; `shared/`
424
+ is not a URL at all. The filter is a `flatMap` rather than `filter().map()` because the predicate
425
+ does not narrow `surface` for the map that follows it, and a cast would hide the day a fifth surface
426
+ arrives.
427
+
428
+ **`pwa.push` is read and still wires nothing, and it says so.** `generateServiceWorker` emits a push
429
+ handler only when a VAPID key comes with the capability, there is no `pwa.vapid` config key, and it
430
+ drops the handler in SILENCE otherwise. `pushWarning` is this module's own finding, reported through
431
+ `x build --json`'s `serviceWorkerWarnings` — `jobs.driver`'s shape one package over, refused the same way.
432
+
433
+ **The browser check is what let any of this ship.** #390's fourth requirement was *"a real browser
434
+ check that the emitted worker installs, activates and serves the fallback offline. Until it exists,
435
+ do not ship the worker"* — a bad `sw.js` is sticky in a way a manifest is not.
436
+ `e2e/service-worker.e2e.test.ts` registers the emitted file in a real Chrome, waits for it to take
437
+ control, takes the network away, and asserts that a runtime route with nothing cached renders the
438
+ offline document.
439
+
440
+ **And it found the driver bug first.** `E2eFixtures.offline()` did not take the SERVICE WORKER
441
+ offline: a worker fetches on its own CDP target, the condition was only ever set on the page's, and
442
+ a `networkFirst` route the cache had never seen still answered from the network. So an offline
443
+ assertion made on a PWA tested nothing. `cdp-e2e-page.ts` now auto-attaches worker targets and
444
+ carries the condition onto each, including one that attaches AFTER `offline(true)` — the ordinary
445
+ case for a PWA.
352
446
 
353
447
  ## The `errors` step enforces the error contract
354
448
 
package/README.md CHANGED
@@ -34,6 +34,25 @@ Everything in [CLI reference](../../wiki/CLI-Reference.md)'s planned table is al
34
34
  and exits `X_NOT_IMPLEMENTED` with a `fix:` naming the closest shipped command — "not built yet"
35
35
  and "not a command" are different facts.
36
36
 
37
+ ## Which `x` runs
38
+
39
+ **The app's own.** `x` is a workspace dependency, and `bunx x` / the `package.json` scripts resolve
40
+ `node_modules/.bin/x`. A **globally** installed `x` — `bun link` of a checkout, `bun add -g` — is
41
+ a second copy of every `@ultimat3/*` package, and a second copy of `@ultimat3/entity` is a second,
42
+ **empty** registry: the app's entities register into the instance under its `node_modules`, and
43
+ only the CLI under that same `node_modules` can see them. Measured 2026-09-05, in an app run with a
44
+ linked checkout's `x`: `x entities list` answered `0 entities`, `x policy list` answered
45
+ `0 permission(s), 0 role(s)`, and `x manifest` wrote a manifest with **zero entities and zero
46
+ actions** — exit 0, green — which `x db gen` then read as "drop every table".
47
+
48
+ So a global `x` inside an app **hands over** to `node_modules/@ultimat3/cli/src/bin.ts` when that
49
+ is a different file (`local-cli.ts`), prints one line on stderr saying so, and exits with the
50
+ child's code; fd 1 is the child's alone, so a `--json` consumer sees one document. A workspace
51
+ symlink resolves to the same file and is not handed over (both tracked apps, and every scaffold
52
+ CI installs), and a compiled `x` keeps itself — its own path is not one `realpath` can resolve.
53
+ `ULTIMATE_KEEP_GLOBAL_CLI=1` keeps the CLI that was invoked, for the one deliberate case: running a
54
+ checkout's `x` against an app pinned to an older release to see what the next one would say.
55
+
37
56
  ## The output contract
38
57
 
39
58
  Every command returns one `CommandResult`; the human renderer and the JSON renderer are
@@ -95,6 +114,11 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
95
114
  | `cmd-*.ts` | one command group each |
96
115
  | `templates/` | scaffolding as typed string modules, not copied fixtures |
97
116
  | `app-load.ts` | import an app's modules so the framework registries hold it |
117
+ | `app-mcp.ts` | the app's own MCP endpoint: `apps/<app>/mcp.ts` exports `mcp`, and both boots mount `POST config.ai.mcp.path` through this one call |
118
+ | `app-runtime.ts` | the app's `RuntimeOverrides`: `apps/<app>/runtime.ts` exports `runtime`, read by `x dev` and by `runRole` when its caller passed none |
119
+ | `local-cli.ts` | which `x` runs: a global CLI inside an app hands over to the app's own, because a second module instance is an empty registry |
120
+ | `measurement-actor.ts` | the actor a weigh-and-discard render runs as — every permission, never served |
121
+ | `dev-live-feed.ts` | what feeds the sync node this process booted: the in-process row observer under the embedded database, the WAL decoder with a real one, nothing without the role — `live=` on the ready line |
98
122
  | `app-manifest.ts` | `x.manifest.json`, projected by `@ultimat3/manifest` |
99
123
  | `app-openapi.ts` | `openapi.json`, projected by `@ultimat3/action` |
100
124
  | `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "18.0.0",
3
+ "version": "19.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,34 +37,34 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/core": "^7.28.4",
40
- "@ultimat3/action": "18.0.0",
41
- "@ultimat3/admin": "18.0.0",
42
- "@ultimat3/ai": "18.0.0",
43
- "@ultimat3/auth": "18.0.0",
44
- "@ultimat3/cache": "18.0.0",
45
- "@ultimat3/core": "18.0.0",
46
- "@ultimat3/db": "18.0.0",
47
- "@ultimat3/entity": "18.0.0",
48
- "@ultimat3/flags": "18.0.0",
49
- "@ultimat3/http": "18.0.0",
50
- "@ultimat3/i18n": "18.0.0",
51
- "@ultimat3/jobs": "18.0.0",
52
- "@ultimat3/mail": "18.0.0",
53
- "@ultimat3/manifest": "18.0.0",
54
- "@ultimat3/mcp": "18.0.0",
55
- "@ultimat3/money": "18.0.0",
56
- "@ultimat3/notify": "18.0.0",
57
- "@ultimat3/policy": "18.0.0",
58
- "@ultimat3/pwa": "18.0.0",
59
- "@ultimat3/query": "18.0.0",
60
- "@ultimat3/realtime": "18.0.0",
61
- "@ultimat3/render": "18.0.0",
62
- "@ultimat3/schema": "18.0.0",
63
- "@ultimat3/scraping": "18.0.0",
64
- "@ultimat3/seo": "18.0.0",
65
- "@ultimat3/storage": "18.0.0",
66
- "@ultimat3/testing": "18.0.0",
67
- "@ultimat3/time": "18.0.0",
40
+ "@ultimat3/action": "19.1.0",
41
+ "@ultimat3/admin": "19.1.0",
42
+ "@ultimat3/ai": "19.1.0",
43
+ "@ultimat3/auth": "19.1.0",
44
+ "@ultimat3/cache": "19.1.0",
45
+ "@ultimat3/core": "19.1.0",
46
+ "@ultimat3/db": "19.1.0",
47
+ "@ultimat3/entity": "19.1.0",
48
+ "@ultimat3/flags": "19.1.0",
49
+ "@ultimat3/http": "19.1.0",
50
+ "@ultimat3/i18n": "19.1.0",
51
+ "@ultimat3/jobs": "19.1.0",
52
+ "@ultimat3/mail": "19.1.0",
53
+ "@ultimat3/manifest": "19.1.0",
54
+ "@ultimat3/mcp": "19.1.0",
55
+ "@ultimat3/money": "19.1.0",
56
+ "@ultimat3/notify": "19.1.0",
57
+ "@ultimat3/policy": "19.1.0",
58
+ "@ultimat3/pwa": "19.1.0",
59
+ "@ultimat3/query": "19.1.0",
60
+ "@ultimat3/realtime": "19.1.0",
61
+ "@ultimat3/render": "19.1.0",
62
+ "@ultimat3/schema": "19.1.0",
63
+ "@ultimat3/scraping": "19.1.0",
64
+ "@ultimat3/seo": "19.1.0",
65
+ "@ultimat3/storage": "19.1.0",
66
+ "@ultimat3/testing": "19.1.0",
67
+ "@ultimat3/time": "19.1.0",
68
68
  "babel-preset-solid": "^1.9.15"
69
69
  }
70
70
  }
package/src/app-mcp.ts ADDED
@@ -0,0 +1,152 @@
1
+ // The app's own MCP endpoint, mounted by the web role. `defineAppMcp` built `mcp.route` — a
2
+ // `POST` handler with token auth and per-class rate limits — and `app.config.ts` declared
3
+ // `ai: { mcp: { expose: true, path: '/mcp' } }` by DEFAULT, and nothing between the two served it:
4
+ // neither `x dev` nor `runRole` mounted the route, so `POST /mcp` answered `X_ROUTE_NOT_FOUND` in
5
+ // every app ever scaffolded (measured 2026-09-05). The contract is one file: `apps/<app>/mcp.ts`
6
+ // exports `mcp`, an `AppMcp`; this module finds it, and both boots mount what it carries.
7
+
8
+ // why: a directory's existence — `Bun.file().exists()` answers for files, and `apps/` is a directory.
9
+ import { existsSync } from 'node:fs';
10
+ // why: Bun exposes no path-join primitive; the config file and each candidate are joined to root.
11
+ import { join } from 'node:path';
12
+ import { logger } from '@ultimat3/core';
13
+ import type { Route } from '@ultimat3/http';
14
+ import { type AppMcp, McpAppUnmountedError } from '@ultimat3/mcp';
15
+ import { APP_CONFIG_EXPORT } from './app-auth';
16
+ import { APP_CONFIG_FILE } from './app-root';
17
+
18
+ /** The one file an app writes, per app directory. */
19
+ export const APP_MCP_GLOB = 'apps/*/mcp.ts';
20
+ /** The export that file makes — an `AppMcp`, the value `defineAppMcp` returns. */
21
+ export const APP_MCP_EXPORT = 'mcp';
22
+ /** What the boot line and `/_x` call the route. */
23
+ export const APP_MCP_ROUTE_NAME = 'mcp';
24
+
25
+ export interface AppMcpMount {
26
+ /** `[]` when `expose` is false, when nothing exports `mcp`, or when the export has no route. */
27
+ readonly routes: readonly Route[];
28
+ /** `POST <path>` when mounted, else `null` — the boot line prints it. */
29
+ readonly path: string | null;
30
+ /** Set exactly when `expose` is true and `routes` is empty: the reason, as an instruction. */
31
+ readonly warning: McpAppUnmountedError | undefined;
32
+ }
33
+
34
+ interface ExposeDeclaration {
35
+ readonly expose: boolean;
36
+ readonly path: string;
37
+ }
38
+
39
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
40
+ typeof value === 'object' && value !== null;
41
+
42
+ /**
43
+ * `config.ai.mcp`, off the app's own resolved config — the exported `config` is what
44
+ * `defineConfig` returned, so both keys are present and defaulted. Read the same way
45
+ * `loadSignInPath` reads `auth.signInPath`: the config file is imported, never re-parsed.
46
+ * An app with no config file has nothing exposed and nothing to warn about.
47
+ */
48
+ async function exposeDeclaration(root: string): Promise<ExposeDeclaration | undefined> {
49
+ const configPath = join(root, APP_CONFIG_FILE);
50
+ if (!(await Bun.file(configPath).exists())) return undefined;
51
+ const module = (await import(configPath)) as Record<string, unknown>;
52
+ const config = module[APP_CONFIG_EXPORT];
53
+ if (!isRecord(config) || !isRecord(config['ai']) || !isRecord(config['ai']['mcp'])) {
54
+ return undefined;
55
+ }
56
+ const mcp = config['ai']['mcp'];
57
+ const path = mcp['path'];
58
+ return {
59
+ expose: mcp['expose'] === true,
60
+ path: typeof path === 'string' && path.startsWith('/') ? path : '/mcp',
61
+ };
62
+ }
63
+
64
+ const isAppMcp = (value: unknown): value is AppMcp =>
65
+ isRecord(value) && 'server' in value && 'tools' in value && 'route' in value;
66
+
67
+ /** Every `apps/<app>/mcp.ts`, app-root-relative and sorted, so two apps answer in one order. */
68
+ async function candidates(root: string): Promise<readonly string[]> {
69
+ // A root with no `apps/` is an app with no MCP file, never a boot failure — the scan's ENOENT
70
+ // is answered as "none", and the warning below says which file to write.
71
+ if (!existsSync(join(root, 'apps'))) return [];
72
+ const files: string[] = [];
73
+ for await (const file of new Bun.Glob(APP_MCP_GLOB).scan({ cwd: root })) files.push(file);
74
+ return files.sort();
75
+ }
76
+
77
+ /**
78
+ * The route to mount, or the reason there is none. Pure over the filesystem it is pointed at;
79
+ * `mountAppMcp` below is the one place the warning becomes a log line.
80
+ *
81
+ * `meta.auth: 'public'` and `enforcedBy: 'handler'` — the http pipeline must not pre-judge:
82
+ * `mcp.route.handle` is the one evaluation, and it reads `Authorization: Bearer` through the
83
+ * `resolveToken` the app gave `defineAppMcp`, then decides per tool through the same policy every
84
+ * other surface evaluates. A pipeline `auth: 'required'` would demand a session cookie an agent
85
+ * does not have and answer 401 before the token was ever read.
86
+ */
87
+ export async function appMcpMount(root: string): Promise<AppMcpMount> {
88
+ const declared = await exposeDeclaration(root);
89
+ if (declared === undefined || !declared.expose)
90
+ return { routes: [], path: null, warning: undefined };
91
+ const files = await candidates(root);
92
+ const fallbackFile = 'apps/web/mcp.ts';
93
+ if (files.length === 0) {
94
+ return {
95
+ routes: [],
96
+ path: null,
97
+ warning: new McpAppUnmountedError({
98
+ reason: 'missing',
99
+ path: declared.path,
100
+ file: fallbackFile,
101
+ }),
102
+ };
103
+ }
104
+ for (const file of files) {
105
+ const module = (await import(join(root, file))) as Record<string, unknown>;
106
+ const exported = module[APP_MCP_EXPORT];
107
+ if (!isAppMcp(exported)) continue;
108
+ const route = exported.route;
109
+ if (route === undefined) {
110
+ return {
111
+ routes: [],
112
+ path: null,
113
+ warning: new McpAppUnmountedError({ reason: 'no-route', path: declared.path, file }),
114
+ };
115
+ }
116
+ return {
117
+ routes: [
118
+ {
119
+ method: 'POST',
120
+ path: declared.path,
121
+ handler: (request) => route.handle(request.raw),
122
+ meta: { name: APP_MCP_ROUTE_NAME, auth: 'public', enforcedBy: 'handler' },
123
+ },
124
+ ],
125
+ path: declared.path,
126
+ warning: undefined,
127
+ };
128
+ }
129
+ return {
130
+ routes: [],
131
+ path: null,
132
+ warning: new McpAppUnmountedError({
133
+ reason: 'missing',
134
+ path: declared.path,
135
+ file: files[0] ?? fallbackFile,
136
+ }),
137
+ };
138
+ }
139
+
140
+ /**
141
+ * The boot's call: the routes to spread into the table, with the warning already logged ONCE and
142
+ * the mount announced. Both `x dev` and `runRole` go through here, so a developer's terminal and a
143
+ * container's log say the same thing about the same endpoint.
144
+ */
145
+ export async function mountAppMcp(root: string): Promise<AppMcpMount> {
146
+ const mount = await appMcpMount(root);
147
+ if (mount.warning !== undefined) {
148
+ logger.warn(`${mount.warning.code}: ${mount.warning.cause} — fix: ${mount.warning.fix}`);
149
+ }
150
+ if (mount.path !== null) logger.info('app mcp mounted', { method: 'POST', path: mount.path });
151
+ return mount;
152
+ }
package/src/app-root.ts CHANGED
@@ -12,12 +12,21 @@ export const MANIFEST_FILE = 'x.manifest.json';
12
12
  * through 2026-08-27 while `x test` spent `bun test --isolate` — a flag Bun introduced in
13
13
  * **1.3.13** — so a user on a Bun this file declared supported got an unknown-flag failure out of
14
14
  * the gate's dominant step, with `x doctor` reporting the runtime as fine. `--parallel` arrived in
15
- * the same release and is emitted now.
15
+ * the same release and is emitted now, so the floor may never fall below that patch.
16
16
  *
17
17
  * `1.4.0` rather than `1.3.13` because a floor is a claim about a runtime somebody TESTED: CI pins
18
18
  * `1.4.x`, both images build on `oven/bun:1.4-*`, and the per-worker database rests on
19
19
  * `BUN_TEST_WORKER_ID`'s numbering, probed on 1.4.0 and on nothing older. `scripts/bun-pin.test.ts`
20
20
  * holds this to the same series as every other pin.
21
+ *
22
+ * **Lowering it to 1.3.14 was tried on 2026-08-27 and refused**, and the argument for trying was
23
+ * sound — `--isolate` and `--parallel` are 1.3.13 features, no package here calls a 1.4-only API
24
+ * (`bun run typecheck` is clean against `@types/bun@1.3.14`), and `>=1.4.0` therefore bars Bun 1.3
25
+ * users for a capability the framework does not use. What refused it is a Bun 1.3.14 defect, not
26
+ * the paperwork: a service shutdown against a destroyed database never resolves there
27
+ * (`queue.stop()`, reproduced by `dev-runtime.live.test.ts`), so an app on a runtime this line
28
+ * declared supported would hang on graceful shutdown the moment its database went away. The full
29
+ * measurement is in `.github/actions/setup/action.yml`; read it before lowering this.
21
30
  */
22
31
  export const REQUIRED_BUN = '1.4.0';
23
32
 
@@ -0,0 +1,39 @@
1
+ // The app's `RuntimeOverrides`, found rather than handed over. `runRole({ runtime })` has taken
2
+ // them since the seam existed, and the scaffolded `apps/web/server.ts` passes none — so an app's
3
+ // own middleware reached no process the framework boots, and `x dev` had no parameter to reach at
4
+ // all (measured 2026-09-05: `x dev` passed only the read-replica override). The contract is one
5
+ // file: `apps/<app>/runtime.ts` exports `runtime`, a `RuntimeOverrides`; `x dev` reads it, and
6
+ // `runRole` reads it when its caller passed nothing, so the two boots compose the same chain.
7
+
8
+ // why: a directory's existence — `Bun.file().exists()` answers for files, and `apps/` is a directory.
9
+ import { existsSync } from 'node:fs';
10
+ // why: Bun exposes no path-join primitive; each candidate is joined to the app root.
11
+ import { join } from 'node:path';
12
+ import type { RuntimeOverrides } from './runtime-overrides';
13
+
14
+ /** The one file an app writes, per app directory. */
15
+ export const APP_RUNTIME_GLOB = 'apps/*/runtime.ts';
16
+ /** The export that file makes — a `RuntimeOverrides`. */
17
+ export const APP_RUNTIME_EXPORT = 'runtime';
18
+
19
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
20
+ typeof value === 'object' && value !== null;
21
+
22
+ /**
23
+ * The first `apps/<app>/runtime.ts` exporting `runtime`, or `undefined` when no app declares one.
24
+ * Sorted, so two apps answer in one order; the object is handed on as declared, because every key
25
+ * of `RuntimeOverrides` already means "replace the resolved default" wherever a boot reads it.
26
+ */
27
+ export async function loadAppRuntime(root: string): Promise<RuntimeOverrides | undefined> {
28
+ // A root with no `apps/` — a bare test fixture, a directory that does not exist — is no app
29
+ // declaring an override, never a boot failure: the scan's ENOENT is answered as "none".
30
+ if (!existsSync(join(root, 'apps'))) return undefined;
31
+ const files: string[] = [];
32
+ for await (const file of new Bun.Glob(APP_RUNTIME_GLOB).scan({ cwd: root })) files.push(file);
33
+ for (const file of files.sort()) {
34
+ const module = (await import(join(root, file))) as Record<string, unknown>;
35
+ const exported = module[APP_RUNTIME_EXPORT];
36
+ if (isRecord(exported)) return exported as RuntimeOverrides;
37
+ }
38
+ return undefined;
39
+ }
package/src/bin.ts CHANGED
@@ -3,12 +3,30 @@
3
3
  // dispatch.ts, so the whole CLI is testable without spawning a process.
4
4
 
5
5
  import { dispatch } from './dispatch';
6
+ import { resolveLocalCli } from './local-cli';
6
7
  // The writes themselves are `write-line.ts`: `create-ultimate`'s entry point needs the identical
7
8
  // one, and a second copy of a note about pipe truncation is a second copy that drifts. Two sinks,
8
9
  // because fd 1 is not always this process's to write on — `x mcp serve --transport stdio` hands it
9
10
  // to the protocol, and `dispatch` addresses that result to the second.
10
11
  import { writeErrorLine, writeLine } from './write-line';
11
12
 
13
+ // An app's own CLI sees the app's entity registry; a global one does not. Hand over before
14
+ // deciding anything — see local-cli.ts for the zero-entity manifest this prevents. On fd 2, so a
15
+ // `--json` consumer reading fd 1 sees exactly the child's one document. `process.execPath` is the
16
+ // Bun that is already running, never a `PATH` lookup: the child's `import.meta.path` resolves to
17
+ // the app's file, so its own `resolveLocalCli` answers "same file" and the chain stops at one hop.
18
+ const local = resolveLocalCli({ cwd: process.cwd(), selfPath: import.meta.path, env: Bun.env });
19
+ if (local !== undefined) {
20
+ writeErrorLine(`x: using the app's own @ultimat3/cli at ${local}`);
21
+ const child = Bun.spawn([process.execPath, local, ...Bun.argv.slice(2)], {
22
+ stdin: 'inherit',
23
+ stdout: 'inherit',
24
+ stderr: 'inherit',
25
+ env: Bun.env,
26
+ });
27
+ process.exit(await child.exited);
28
+ }
29
+
12
30
  const code = await dispatch({
13
31
  argv: Bun.argv.slice(2),
14
32
  cwd: process.cwd(),
package/src/budgets.ts CHANGED
@@ -111,7 +111,7 @@ export function checkBudgets(
111
111
  if (js !== null && measured.jsBytes > js) {
112
112
  findings.push({
113
113
  code: 'X_BUDGET_EXCEEDED',
114
- cause: `${route.url} ships ${formatBytes(measured.jsBytes)} of JS over a ${formatBytes(js)} budget via ${chainOf(measured)}`,
114
+ cause: `${route.url} ships ${formatBytes(measured.jsBytes)} of JS (minified, uncompressed) over a ${formatBytes(js)} budget via ${chainOf(measured)}`,
115
115
  fix: `x routes --json to see the chain, then move the heavy import behind hydrate: 'interaction'`,
116
116
  docs: ERROR_DOCS_URL,
117
117
  at: route.url,
@@ -0,0 +1,100 @@
1
+ // One responsibility: compose the three halves — find a browser, connect to it, attach a page —
2
+ // into the one object `installE2eDriver({ page })` takes, plus the way to shut it down.
3
+ //
4
+ // **Absent is a SKIP, never a failure, and that is a requirement rather than a state.** A CI box
5
+ // with no Chrome must not turn the `e2e` step red for a reason unrelated to the change, which is
6
+ // the rule `packages/cli/CLAUDE.md` already states about `x shot`, `x pr` and `x ci`.
7
+ // `openE2eBrowserIfAvailable` is that door; `openE2eBrowser` refuses by name for a caller that has
8
+ // already decided a browser is required.
9
+
10
+ import { finiteCount } from '@ultimat3/core';
11
+ import type { CdpConnection } from './cdp-connection';
12
+ import { cdpConnect } from './cdp-connection';
13
+ import { cdpE2ePage } from './cdp-e2e-page';
14
+ import { CdpBrowserMissingError } from './cdp-errors';
15
+ import type { LaunchedBrowser } from './cdp-launch';
16
+ import { CHROME_CANDIDATES, findChrome, launchChrome } from './cdp-launch';
17
+ import type { E2eBrowserPage } from './e2e-page';
18
+
19
+ /** How long a launch, a connect or a single CDP call may take. One number, three deadlines. */
20
+ export const DEFAULT_CDP_TIMEOUT_MS = 30_000;
21
+
22
+ export interface E2eBrowser {
23
+ readonly page: E2eBrowserPage;
24
+ /** Idempotent, and it closes both halves: the CDP socket, then the process and its profile. */
25
+ close(): void;
26
+ }
27
+
28
+ export interface OpenE2eBrowserOptions {
29
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
30
+ readonly timeoutMs?: number | undefined;
31
+ }
32
+
33
+ /**
34
+ * Screened HERE, before a browser exists, and not where it lands. It becomes three deadlines — the
35
+ * launch, the handshake and every CDP call — and `Number(process.env.E2E_TIMEOUT ?? '')` is `NaN`
36
+ * for an unset variable and is not nullish, so `??` keeps it: a `setTimeout` given `NaN` fires at
37
+ * 1ms in this Bun, which makes every call report `X_CDP_TIMEOUT` against a browser that was
38
+ * answering. A misdiagnosis reported as a test failure is worse than the failure.
39
+ */
40
+ const budget = (options: OpenE2eBrowserOptions): number =>
41
+ finiteCount('openE2eBrowser', 'timeoutMs', options.timeoutMs ?? DEFAULT_CDP_TIMEOUT_MS);
42
+
43
+ const compose = (
44
+ launched: LaunchedBrowser,
45
+ connection: CdpConnection,
46
+ page: E2eBrowserPage,
47
+ ): E2eBrowser => ({
48
+ page,
49
+ close(): void {
50
+ // The socket first: closing the process out from under an open connection makes every
51
+ // in-flight call report "the browser closed the CDP connection", which is true and useless.
52
+ connection.close();
53
+ launched.close();
54
+ },
55
+ });
56
+
57
+ /**
58
+ * Launch a browser and attach one page to it. Refuses with `X_CDP_BROWSER_MISSING` when there is
59
+ * nothing to launch — the caller that wants a skip asks `openE2eBrowserIfAvailable` instead.
60
+ */
61
+ export async function openE2eBrowser(options: OpenE2eBrowserOptions = {}): Promise<E2eBrowser> {
62
+ const timeoutMs = budget(options);
63
+ const executable = await findChrome(options.env ?? process.env);
64
+ if (executable === undefined) throw new CdpBrowserMissingError({ tried: CHROME_CANDIDATES });
65
+ return openLaunched(executable, timeoutMs);
66
+ }
67
+
68
+ /** `undefined` when this machine has no browser. Every other failure still throws. */
69
+ export async function openE2eBrowserIfAvailable(
70
+ options: OpenE2eBrowserOptions = {},
71
+ ): Promise<E2eBrowser | undefined> {
72
+ const timeoutMs = budget(options);
73
+ const executable = await findChrome(options.env ?? process.env);
74
+ if (executable === undefined) return undefined;
75
+ return openLaunched(executable, timeoutMs);
76
+ }
77
+
78
+ /**
79
+ * The half both doors share. Each step undoes the ones before it on the way out: a Chrome that
80
+ * launched and then refused the CDP handshake would otherwise be left running, holding its profile
81
+ * directory, for the rest of the test process — one leaked browser per failing suite.
82
+ */
83
+ async function openLaunched(executable: string, timeoutMs: number): Promise<E2eBrowser> {
84
+ const launched = await launchChrome({ executable, timeoutMs });
85
+ let connection: CdpConnection;
86
+ try {
87
+ connection = await cdpConnect({ endpoint: launched.endpoint, timeoutMs });
88
+ } catch (error) {
89
+ launched.close();
90
+ throw error;
91
+ }
92
+ try {
93
+ const page = await cdpE2ePage({ connection, loadTimeoutMs: timeoutMs });
94
+ return compose(launched, connection, page);
95
+ } catch (error) {
96
+ connection.close();
97
+ launched.close();
98
+ throw error;
99
+ }
100
+ }