@ultimat3/cli 17.0.0 → 19.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.
- package/CLAUDE.md +194 -17
- package/package.json +30 -30
- package/src/app-root.ts +22 -1
- package/src/cdp-browser.ts +100 -0
- package/src/cdp-connection.ts +211 -0
- package/src/cdp-e2e-page.ts +209 -0
- package/src/cdp-errors.ts +56 -0
- package/src/cdp-launch.ts +130 -0
- package/src/cmd-dev.ts +28 -2
- package/src/cmd-doctor.ts +1 -1
- package/src/cmd-test.ts +20 -9
- package/src/compile-externals.ts +11 -4
- package/src/db-accept-created.ts +207 -0
- package/src/db-generate.ts +18 -1
- package/src/db-subscribes.ts +81 -0
- package/src/db-ungeneratable.ts +14 -2
- package/src/dev-assets.ts +19 -54
- package/src/dev-notify-retention.ts +69 -0
- package/src/dev-purge.ts +47 -2
- package/src/dev-render.ts +20 -4
- package/src/dev-replicator.ts +19 -1
- package/src/dev-runtime.ts +12 -3
- package/src/dev-services.ts +8 -0
- package/src/e2e-driver.ts +35 -17
- package/src/e2e-page.ts +15 -3
- package/src/error-codes.ts +20 -0
- package/src/icon-assets.ts +74 -0
- package/src/index.ts +46 -4
- package/src/island-harness-script.ts +8 -1
- package/src/island-shot.ts +37 -4
- package/src/island-verdict.ts +16 -4
- package/src/mcp-errors.ts +15 -0
- package/src/messages.ts +5 -1
- package/src/prerender.ts +41 -1
- package/src/pwa-artifacts.ts +230 -0
- package/src/serve.ts +24 -1
- package/src/static-report.ts +46 -3
- package/src/sw-artifacts.ts +162 -0
- package/src/sw-routes.ts +53 -0
- package/src/templates/naming.ts +11 -0
- package/src/templates/scaffold-app.ts +58 -7
- package/src/templates/scaffold-repo.ts +17 -1
- package/src/test-shards.ts +150 -116
- package/src/ts-scan.ts +6 -1
- package/src/verify-test-run.ts +31 -46
package/CLAUDE.md
CHANGED
|
@@ -8,6 +8,7 @@ Tier 5. May import tiers 0–4. Nothing imports this except `create-ultimate`.
|
|
|
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 |
|
|
11
|
+
| Test execution | `test-shards.ts`'s `testArgs` — ONE `bun test --parallel=N`, never N processes this repo packs itself. It did pack them, largest-first greedy over file SIZE, and the packer was deleted for buying **nothing**: four interleaved runs each on the 1296-file unit corpus gave 58.2/60.0/65.0/66.5s hand-packed against 54.5/57.8/61.7/64.5s under `--parallel=8`, within noise, because both are work-bound — 436.7s of file time is a 54.6s floor on 8 workers and the slowest single file is 20.5s. A greedy pack of 1296 small items lands near-optimal by accident. `--timings` is refused on the same evidence (#342). `--parallel` implies `--isolate`, so the per-file module registry is unchanged, and the per-worker database is too: `@ultimat3/testing`'s `workerId` already read `BUN_TEST_WORKER_ID`, which Bun sets 1..N. `ULTIMATE_TEST_WORKER` is set only for a single-shard `x test --worker I` rerun, which is one process |
|
|
11
12
|
| Numeric flags | `flag-number.ts` — one reader for `--port` / `--workers` / `--shard`. A bare `Number.parseInt` accepts `4abc` and answers `NaN`, which turned three checks into ones that cannot fail |
|
|
12
13
|
| Shell quoting | `shell-quote.ts`'s `quoteArg` — every value the CLI pastes into a `fix:` or a reproduce line, `exec.ts`'s missing-program refusal and `test-shards.ts`'s reproduce command both. A name holding a space or a `;` interpolated bare is an instruction that runs something else |
|
|
13
14
|
| Missing positionals | `MissingPositionalError`, never `BadFlagError` (names a flag that does not exist) and never `UnknownCommandError` (says a known command form is not one). Its `example` is a REAL invocation — `x g route <name>` in a shell is a redirect |
|
|
@@ -273,11 +274,28 @@ image of the wrong thing.
|
|
|
273
274
|
`page.pageErrors()` are bounded rings over the whole SESSION, so a shared one files state A's
|
|
274
275
|
console errors under state B — and per-state attribution is the half of the artifact that gates.
|
|
275
276
|
|
|
276
|
-
**The picture is the
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
**The picture is the CROP TARGET, `As of 2026-08-26`** — the readiness probe's own box, which is
|
|
278
|
+
the selector the manifest declared or the island's host element. Measured on `examples/dummy`
|
|
279
|
+
before it: 720x560 for a component whose box the verdict reported, in the same run, as 688x104.
|
|
280
|
+
`CaptureClip` had been on the port since #336 and `island-shot.ts` passed none, and this paragraph
|
|
281
|
+
said the port "takes no clip rectangle" — so did the verdict's own `blind` list, which is a blind
|
|
282
|
+
spot naming a capability the tool has, the same lie as one hiding a gap. The state's `viewport` is
|
|
283
|
+
still what the page is LAID OUT in, so there is still one browser per declared viewport, memoised;
|
|
284
|
+
it is no longer what the picture is.
|
|
285
|
+
|
|
286
|
+
**The clip is translated, not copied.** `getBoundingClientRect()` answers VIEWPORT coordinates and
|
|
287
|
+
a capture clip is in PAGE coordinates; they agree only at the origin, which is the one case a
|
|
288
|
+
harness happens to be in and is a rule nothing enforces. So the probe returns `scroll` beside `box`
|
|
289
|
+
and `clipFor` adds them — a component below the fold would otherwise crop a band it is not in, with
|
|
290
|
+
a picture that looks like a picture and nothing anywhere to report it. `box` keeps meaning the DOM's
|
|
291
|
+
own answer, because that is what the verdict publishes.
|
|
292
|
+
|
|
293
|
+
**Both themes are photographed by emulating the PREFERENCE.** `page.colorScheme(target.theme)`
|
|
294
|
+
before the navigation, so the first paint already has it. The harness's `data-theme` attribute stays
|
|
295
|
+
— it is right for a component that READS a theme it does not own — but it is the OUTCOME of a theme
|
|
296
|
+
decision, and a component that resolves `'system'` itself deletes it on mount: `x shot --island`
|
|
297
|
+
reported four pictures and wrote two, byte-identical, same md5 (#338). Re-setting the attribute
|
|
298
|
+
after readiness is not the repair; it photographs a state the component would never reach.
|
|
281
299
|
|
|
282
300
|
**`loadApp` does not import a states file**, for the reason it does not import an island: it
|
|
283
301
|
registers no primitive, and importing it would put `@ultimat3/testing` in the server module graph of
|
|
@@ -311,26 +329,120 @@ to know about everything — so the join is here, and it is the same rule
|
|
|
311
329
|
| `e2e-evaluate.ts` | the closure→string crossing, which is the only lossy edge in the adapter |
|
|
312
330
|
| `e2e-errors.ts` | one constructor per refusal |
|
|
313
331
|
| `e2e-dom-fixture.ts` | a document small enough to hold in a test and real enough to RUN the expressions above |
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
`e2e`
|
|
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).
|
|
318
367
|
|
|
319
368
|
**`evaluate` is the edge that cannot be lossless.** `PageLike.evaluate` takes a closure and every
|
|
320
369
|
browser port in this framework takes a string, so what crosses is `Function.prototype.toString()`
|
|
321
370
|
and nothing else. A zero-parameter closure naming only page globals is supported; a native or bound
|
|
322
371
|
function, a declared parameter and a method shorthand are refused STATICALLY, before a byte leaves;
|
|
323
372
|
a binding the page does not have comes back named, from the page's own `ReferenceError`. Measured on
|
|
324
|
-
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
|
|
325
374
|
runs**, so a captured PRIMITIVE can vanish from the source and never fail at all, while a captured
|
|
326
375
|
reference always survives as its name. No static rule in this process can see the difference — which
|
|
327
376
|
is why the refusal is raised from the page's answer rather than from a scan of the source.
|
|
328
377
|
|
|
329
|
-
**
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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.
|
|
334
446
|
|
|
335
447
|
## The `errors` step enforces the error contract
|
|
336
448
|
|
|
@@ -573,6 +685,8 @@ regex and `+` is a quantifier — `n1` is what actually selects these tests.
|
|
|
573
685
|
| `db-branch.ts` | what a branch IS: the closed verb set, the name it takes on disk and in `pg_database`, and list/create/drop per mode |
|
|
574
686
|
| `cmd-db-branch.ts` | `x db branch`'s wiring alone — which verb, which refusal, and the one connection an external clone runs on |
|
|
575
687
|
| `db-finding.ts` | one thrown value → one `Finding`, shared by `cmd-db.ts` and `cmd-db-branch.ts` |
|
|
688
|
+
| `db-accept-created.ts` | `acceptCreatedTables`: the post-migrate report minus the tables the applied migrations' own SQL creates — the half `@ultimat3/db`'s `unexpectedTable` names |
|
|
689
|
+
| `db-subscribes.ts` | `replicaIdentityTables`: the tables `x db gen` grants `REPLICA IDENTITY FULL`, read off each live query's declared `subscribes:` — and `X_QUERY_SUBSCRIBES_UNKNOWN` for a name no entity's table matches |
|
|
576
690
|
| `drift.ts` | `checkSourceDrift`: the `.hash` sidecar the `drift` step compares, no database needed |
|
|
577
691
|
| `schema-diff.ts` | what two GENERATED snapshots disagree about, as data — the pure half |
|
|
578
692
|
| `schema-drift.ts` | `checkMigrationDrift`: entity declarations against the newest `.snapshot.json`, and the composition the `drift` step and `x doctor` both read |
|
|
@@ -674,6 +788,47 @@ is only the channel each has. `ROLE=migrate` logged and exited 0 until it did no
|
|
|
674
788
|
whose only signal is the exit code reported success over a schema nobody can reconstruct, which is
|
|
675
789
|
the failure the post-migrate check exists to catch.
|
|
676
790
|
|
|
791
|
+
**`x db gen` emits `REPLICA IDENTITY FULL`, and the set is DECLARED rather than derived**,
|
|
792
|
+
`As of 2026-08-26` (#357). `@ultimat3/realtime` refuses a live subscription to a table without it —
|
|
793
|
+
logical replication carries no old row on an UPDATE, so no patch can be computed — and for two
|
|
794
|
+
years nothing in the framework emitted one. It could not be derived, and that is the load-bearing
|
|
795
|
+
fact: the relation name lives inside the query's `sql:` callback, which no generator can invoke
|
|
796
|
+
without valid input (`describeSql` says so itself — "`null` when no sample input was supplied").
|
|
797
|
+
So a live query DECLARES it (`subscribes:`, `@ultimat3/query`), the declaration is machine-checked
|
|
798
|
+
against the resolved `shape.entity` on the first subscribe (`X_QUERY_SUBSCRIBES_DRIFT`), and
|
|
799
|
+
`db-subscribes.ts` reads it off `describeQueries()` — the same source `frameworkSources` copies onto
|
|
800
|
+
`QueryFact.subscribes`, one hop earlier, because building the manifest here would re-load the app
|
|
801
|
+
and demand a `package.json` that `x db gen` has never needed.
|
|
802
|
+
|
|
803
|
+
**The third `subscribes:` refusal is this package's, because no other tier can ask it.**
|
|
804
|
+
`@ultimat3/db` keeps only the declared names an entity's table matches and DROPS the rest — it has
|
|
805
|
+
no way to tell a typo from a table another migration owns — and `@ultimat3/query` holds no table
|
|
806
|
+
catalog at all. So `subscribes: ['posts', 'user']` granted the identity to `posts`, dropped `user`
|
|
807
|
+
in silence, and read as granted. `X_QUERY_SUBSCRIBES_UNKNOWN` refuses it BEFORE anything is
|
|
808
|
+
written, naming the query and offering the tables the app does declare. It is checked after
|
|
809
|
+
`loadApp`'s findings, never before: a module that would not import leaves the registry short, and
|
|
810
|
+
every name whose entity lives in it would then look like a typo.
|
|
811
|
+
|
|
812
|
+
**And it accepts a table the migrations it just applied demonstrably created**, `As of 2026-08-26`
|
|
813
|
+
(issue #345). A snapshot records only what ENTITIES declare, so a table created by a HAND-WRITTEN
|
|
814
|
+
migration reached no sidecar and was `unexpected-table` on every deploy forever — with a `fix:`
|
|
815
|
+
that generated an empty migration, because `x db gen` diffs the entity registry against the newest
|
|
816
|
+
snapshot and the table is on neither side. `@ultimat3/db` fixed the wording; `acceptCreatedTables`
|
|
817
|
+
(`db-accept-created.ts`) is the half that file's `unexpectedTable` names, and it is composed around
|
|
818
|
+
`checkDrift` inside `runMigrations`, so `x db migrate` and `ROLE=migrate` accept the same set.
|
|
819
|
+
**Only `unexpected-table`, and only for a name a migration's SQL creates** — which is what keeps it
|
|
820
|
+
an acceptance rather than the check switched off: a table absent from the snapshot produces exactly
|
|
821
|
+
one difference (`diffSchema` reports it and never compares its columns), and a table nobody
|
|
822
|
+
declared and no migration created is still reported, cause and `fix:` intact. The evidence is the
|
|
823
|
+
applied list itself: `migrate()` runs first, so every file on disk has been applied by the time the
|
|
824
|
+
question is asked. The verb phrase is read ANCHORED off the raw statement, which is the whole
|
|
825
|
+
protection — a `create table` can only be at position 0 by being one, so `values ('create table
|
|
826
|
+
ghost')` opens with `insert` and a comment-only chunk is not a statement at all. A `stripSqlNoise`
|
|
827
|
+
pass was written first and deleted: it could not change one answer, and a defence that cannot fail
|
|
828
|
+
is one nobody can test. Everything the anchor admits and the name grammar does not — a comment
|
|
829
|
+
between the keywords, a `temp` table, a qualifier naming a schema `checkDrift` never introspected —
|
|
830
|
+
contributes nothing, which reports drift that could have been accepted and never the reverse.
|
|
831
|
+
|
|
677
832
|
**The `drift` step asks a third thing, off the same directory and with no database either: is every
|
|
678
833
|
destructive statement declared?** `db-destructive.ts` reads each committed migration through
|
|
679
834
|
`migrations.ts` — the reader `x db migrate` applies from, because a rail checking a list the
|
|
@@ -751,8 +906,15 @@ decisions behind that shape:
|
|
|
751
906
|
Regenerating is exactly what *discards* these statements, so the command every other db code
|
|
752
907
|
answers with is the one this one must not lead with — `X_MIGRATION_UNGENERATABLE`'s `CLI_FIXES` row
|
|
753
908
|
is `x verify --only drift`, and the re-declare branch (an enum is a text column plus a check
|
|
754
|
-
invariant) rides behind an em-dash because it is available for some of the statements and
|
|
755
|
-
|
|
909
|
+
invariant) rides behind an em-dash because it is available for some of the statements and not all.
|
|
910
|
+
**`REPLICA IDENTITY FULL` was the statement with no second branch, and stopped being one on
|
|
911
|
+
2026-08-26** (#357): a live query declares the relations it is patched from (`subscribes:`),
|
|
912
|
+
`db-subscribes.ts` reads them off the same registry the manifest is projected from, `x db gen`
|
|
913
|
+
emits the ALTER and `@ultimat3/db` records it on the snapshot so it is emitted once. The re-declare
|
|
914
|
+
branch covers it now: declare `subscribes:` and regenerate. A statement already committed is a
|
|
915
|
+
different question and still counts — `GENERATABLE_FORMS` (`@ultimat3/db`) matches a leading verb
|
|
916
|
+
phrase and does not carry this one, measured at 7 found / 7 declared on `examples/dummy`'s
|
|
917
|
+
`0001_init.sql`, `As of 2026-08-26` — so the marker branch remains the only remedy for SQL on disk.
|
|
756
918
|
|
|
757
919
|
**`x db gen` reports what it could not write, and exits 0.** `GeneratedMigration.unrendered` reached
|
|
758
920
|
the committed `.sql` as a `-- UNRENDERED` comment and nothing else read it; `db-generate.ts` now
|
|
@@ -813,6 +975,7 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
|
|
|
813
975
|
| `dev-runtime.ts` | start the rest on top of it and install the remaining accessors (storage, mail, transport) |
|
|
814
976
|
| `dev-cache.ts` | which cache tiers this process reads through, and the cross-instance invalidation hop |
|
|
815
977
|
| `dev-purge.ts` | the hourly retention sweep: which framework tables this boot owns, the `purge()` job over them and the `task` that fires it |
|
|
978
|
+
| `dev-notify-retention.ts` | `notify.inboxReadRetentionMs` / `inboxUnreadRetentionMs` off the app's own `app.config.ts` — the sibling of `loadSignInPath` and `loadCacheTiers`, because `startServices` holds no `AppConfig` |
|
|
816
979
|
| `dev-sync.ts` | the `sync` role: its live-query registry, who is dialling it, and the socket it owns |
|
|
817
980
|
| `runtime-overrides.ts` | the one field a host hands the framework a driver through |
|
|
818
981
|
| `sync-authenticator.ts` | the app's HTTP authenticator, seen as the sync node's |
|
|
@@ -824,6 +987,7 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
|
|
|
824
987
|
| `favicon.ts` | `/favicon.ico`: the app's own file, and the bytes the framework answers with when there is none |
|
|
825
988
|
| `dev-hooks.ts` | the pipeline's `authorize` seam, decided from the app's own `Policy` objects |
|
|
826
989
|
| `dev-replica.ts` | which boot gets a standby, and the one middleware frame that opens the read scope |
|
|
990
|
+
| `dev-replicator.ts` | the `replicator` role: the feed selected, locked and pumped — and `replicatedRelations()`, the entity TABLES it filters on |
|
|
827
991
|
| `dev-roles.ts` | `--role` selection plus start/stop for `web`, `sync`, `worker`, `scheduler` |
|
|
828
992
|
| `dev-dashboard.ts` | the `DevSources` hooks only this process can answer, and the two CLI panels |
|
|
829
993
|
| `dev-traces.ts` | core's spans → the `/_x` timeline's request traces |
|
|
@@ -912,6 +1076,19 @@ The per-TENANT subscription cap is deliberately unset, and **both halves of it a
|
|
|
912
1076
|
nothing — and no default is defensible when one tenant is a person and the next is five thousand
|
|
913
1077
|
seats. The per-socket 128 stands because a socket is one browser tab.
|
|
914
1078
|
|
|
1079
|
+
**The change feed is filtered by TABLE, never by entity name**, `As of 2026-08-26`.
|
|
1080
|
+
`replicatedRelations()` (`dev-replicator.ts`) is the one projection, and both of its readers are
|
|
1081
|
+
catalog readers: `PgReplicationStream` keeps a change only when `#entities.has(relation.name)` and a
|
|
1082
|
+
pgoutput Relation message names the table, while `warnPartialIdentity` matches the same list against
|
|
1083
|
+
`pg_class.relname`. An entity NAME is the framework's own registry key — a cache tag, a policy and
|
|
1084
|
+
`x entities describe` are all keyed by it — and `entity('user', { table: 'users' })` makes the two
|
|
1085
|
+
different strings. It passed `.name`, so a renamed table matched on neither side: **every change
|
|
1086
|
+
skipped** and a replica-identity warning that could never fire, with no error anywhere. Invisible to
|
|
1087
|
+
every fixture in the tree, because `table` defaults to the name verbatim and all six entities in
|
|
1088
|
+
`examples/dummy` have `name === table` — `dev-replicator.test.ts` uses `billingAccount` on
|
|
1089
|
+
`billing_accounts` for exactly that reason, and proves the value through the real call chain:
|
|
1090
|
+
`assertIdentifier` refuses `billingAccount` before any connection and accepts `billing_accounts`.
|
|
1091
|
+
|
|
915
1092
|
`trustProxy` is read from `TRUSTED_PROXY_HOPS` in `startWeb`, the way `PORT` and `ROLE` are read: it
|
|
916
1093
|
is a fact about the deployment, not an app config choice, and one image runs behind an ingress in
|
|
917
1094
|
one cluster and behind nothing on a laptop. Without it `ctx.ip` is the ingress's socket address on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "19.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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"LICENSE"
|
|
29
29
|
],
|
|
30
30
|
"engines": {
|
|
31
|
-
"bun": ">=1.
|
|
31
|
+
"bun": ">=1.4.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
@@ -37,34 +37,34 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/core": "^7.28.4",
|
|
40
|
-
"@ultimat3/action": "
|
|
41
|
-
"@ultimat3/admin": "
|
|
42
|
-
"@ultimat3/ai": "
|
|
43
|
-
"@ultimat3/auth": "
|
|
44
|
-
"@ultimat3/cache": "
|
|
45
|
-
"@ultimat3/core": "
|
|
46
|
-
"@ultimat3/db": "
|
|
47
|
-
"@ultimat3/entity": "
|
|
48
|
-
"@ultimat3/flags": "
|
|
49
|
-
"@ultimat3/http": "
|
|
50
|
-
"@ultimat3/i18n": "
|
|
51
|
-
"@ultimat3/jobs": "
|
|
52
|
-
"@ultimat3/mail": "
|
|
53
|
-
"@ultimat3/manifest": "
|
|
54
|
-
"@ultimat3/mcp": "
|
|
55
|
-
"@ultimat3/money": "
|
|
56
|
-
"@ultimat3/notify": "
|
|
57
|
-
"@ultimat3/policy": "
|
|
58
|
-
"@ultimat3/pwa": "
|
|
59
|
-
"@ultimat3/query": "
|
|
60
|
-
"@ultimat3/realtime": "
|
|
61
|
-
"@ultimat3/render": "
|
|
62
|
-
"@ultimat3/schema": "
|
|
63
|
-
"@ultimat3/scraping": "
|
|
64
|
-
"@ultimat3/seo": "
|
|
65
|
-
"@ultimat3/storage": "
|
|
66
|
-
"@ultimat3/testing": "
|
|
67
|
-
"@ultimat3/time": "
|
|
40
|
+
"@ultimat3/action": "19.0.0",
|
|
41
|
+
"@ultimat3/admin": "19.0.0",
|
|
42
|
+
"@ultimat3/ai": "19.0.0",
|
|
43
|
+
"@ultimat3/auth": "19.0.0",
|
|
44
|
+
"@ultimat3/cache": "19.0.0",
|
|
45
|
+
"@ultimat3/core": "19.0.0",
|
|
46
|
+
"@ultimat3/db": "19.0.0",
|
|
47
|
+
"@ultimat3/entity": "19.0.0",
|
|
48
|
+
"@ultimat3/flags": "19.0.0",
|
|
49
|
+
"@ultimat3/http": "19.0.0",
|
|
50
|
+
"@ultimat3/i18n": "19.0.0",
|
|
51
|
+
"@ultimat3/jobs": "19.0.0",
|
|
52
|
+
"@ultimat3/mail": "19.0.0",
|
|
53
|
+
"@ultimat3/manifest": "19.0.0",
|
|
54
|
+
"@ultimat3/mcp": "19.0.0",
|
|
55
|
+
"@ultimat3/money": "19.0.0",
|
|
56
|
+
"@ultimat3/notify": "19.0.0",
|
|
57
|
+
"@ultimat3/policy": "19.0.0",
|
|
58
|
+
"@ultimat3/pwa": "19.0.0",
|
|
59
|
+
"@ultimat3/query": "19.0.0",
|
|
60
|
+
"@ultimat3/realtime": "19.0.0",
|
|
61
|
+
"@ultimat3/render": "19.0.0",
|
|
62
|
+
"@ultimat3/schema": "19.0.0",
|
|
63
|
+
"@ultimat3/scraping": "19.0.0",
|
|
64
|
+
"@ultimat3/seo": "19.0.0",
|
|
65
|
+
"@ultimat3/storage": "19.0.0",
|
|
66
|
+
"@ultimat3/testing": "19.0.0",
|
|
67
|
+
"@ultimat3/time": "19.0.0",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/app-root.ts
CHANGED
|
@@ -7,7 +7,28 @@ import { BunVersionError, NotInAppError } from './errors';
|
|
|
7
7
|
|
|
8
8
|
export const APP_CONFIG_FILE = 'app.config.ts';
|
|
9
9
|
export const MANIFEST_FILE = 'x.manifest.json';
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The floor the shipped `x` enforces, and it must not sit below what `x` EMITS. It said `1.3.0`
|
|
12
|
+
* through 2026-08-27 while `x test` spent `bun test --isolate` — a flag Bun introduced in
|
|
13
|
+
* **1.3.13** — so a user on a Bun this file declared supported got an unknown-flag failure out of
|
|
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, so the floor may never fall below that patch.
|
|
16
|
+
*
|
|
17
|
+
* `1.4.0` rather than `1.3.13` because a floor is a claim about a runtime somebody TESTED: CI pins
|
|
18
|
+
* `1.4.x`, both images build on `oven/bun:1.4-*`, and the per-worker database rests on
|
|
19
|
+
* `BUN_TEST_WORKER_ID`'s numbering, probed on 1.4.0 and on nothing older. `scripts/bun-pin.test.ts`
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
export const REQUIRED_BUN = '1.4.0';
|
|
11
32
|
|
|
12
33
|
export interface AppRoot {
|
|
13
34
|
readonly dir: string;
|
|
@@ -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
|
+
}
|