From 9aefb25e8c4b43a7f4fbe868926924e060d18d20 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:02:19 +0000 Subject: [PATCH] fix(cli): the Console drift remedy names `pnpm console:build`, not the retired `objectui:build` The objectui pin and its `objectui:*` scripts retired with #25745, but the drift warning, the `os dev` refusal and its 503 body still told the reader to run `pnpm objectui:build` (and mentioned `pnpm objectui:refresh`). Both now name `pnpm console:build`; the refresh line is dropped. The drift path itself stays: the framework repo can no longer reach it (no pin, and build-dist.sh writes no stamp), but a tree carrying its own `.objectui-sha` above a stamped dist still can, and that tree is not readable from here. The two tests that pin the remedy string move with it, and the docblocks around the guard (console.ts, serve.ts, the public console.ts face) stop describing the pin as current. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01CBGRCbYq9F1ceLwZ2YCbWE --- ...6035-console-drift-remedy-console-build.md | 13 ++++ packages/cli/src/commands/serve.ts | 7 ++- packages/cli/src/console.ts | 7 ++- .../cli/src/utils/console.sha-drift.test.ts | 23 ++++--- packages/cli/src/utils/console.ts | 60 ++++++++++--------- packages/cli/test/console-resolve.test.ts | 2 +- 6 files changed, 65 insertions(+), 47 deletions(-) create mode 100644 .changeset/26035-console-drift-remedy-console-build.md diff --git a/.changeset/26035-console-drift-remedy-console-build.md b/.changeset/26035-console-drift-remedy-console-build.md new file mode 100644 index 00000000000..d13b5d3374a --- /dev/null +++ b/.changeset/26035-console-drift-remedy-console-build.md @@ -0,0 +1,13 @@ +--- +'@objectstack/cli': patch +--- + +fix(cli): the Console drift warning and refusal name `pnpm console:build` instead of the retired `pnpm objectui:build` + +Clause-②: no +Metadata-protocol: none + +When the Console build being served was stamped from a different objectui commit than the `.objectui-sha` pin above it, `os serve` printed a warning and `os dev` refused to mount the Console (and answered `503` at `/_console/`). Both told you to run `pnpm objectui:build`, and the refusal also mentioned `pnpm objectui:refresh`. Neither script exists any more: the objectui pin and its scripts were retired when the Console moved into this workspace, and the Console is now built with `pnpm console:build`. + +- **Now.** The warning ends with `rebuild it with 'pnpm console:build'.`, and the refusal (terminal and `503` body) says `Rebuild the console:` followed by `pnpm console:build`. The `objectui:refresh` line is gone. The SHAs, the `OS_ALLOW_CONSOLE_DRIFT=1` escape hatch and the status codes are unchanged. +- **Who sees it.** Only a tree that still carries its own `.objectui-sha` above a stamped Console build. The framework repository carries no pin, and `pnpm console:build` writes no stamp, so a framework checkout never reaches this message. A published install carries no pin either. diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 603bdb76c10..80458419a16 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -5285,12 +5285,13 @@ export default class Serve extends Command { // ── Console portal ────────────────────────────────────────── // The opinionated, fork-ready runtime console (`@objectstack/console`, - // the build of objectstack-ai/objectui's console app this framework - // vendors at its `.objectui-sha` pin) mounts under `/_console/`. When + // the build of this workspace's `ui/apps/console` that + // `pnpm console:build` vendors) mounts under `/_console/`. When // present, it owns the root `/` redirect (preferred default UI). It is // optional — we only mount it when the package resolves and a // pre-built `dist/` is present, and — in dev — only when that build - // matches the repo's objectui pin (#7752). Every other boot that wants + // matches any objectui pin found above it (#7752; the framework repo + // itself carries none since #25745). Every other boot that wants // a Console gets a responder on those same routes instead, so // `/_console/` answers `503` naming the remedy its boot message prints // rather than a bare 404 (#22420): the drift refusal; a package with no diff --git a/packages/cli/src/console.ts b/packages/cli/src/console.ts index fd83ba39cda..9dfc94a2435 100644 --- a/packages/cli/src/console.ts +++ b/packages/cli/src/console.ts @@ -63,8 +63,9 @@ * answers 1,640 hits from the same index in the same session, so objectui * is genuinely indexed and its zero is a measurement rather than a silence. * 5. The PINNED sibling checkout — the reading AGENTS.md prescribes before a - * removal ships, taken at `.objectui-sha` (`a472b07167a3`) rather than at - * whatever that checkout's HEAD happens to be. The specifier + * removal ships, taken at the objectui pin this repository then committed + * (`.objectui-sha`, `a472b07167a3`; the pin retired with #25745) rather + * than at whatever that checkout's HEAD happens to be. The specifier * `cli/console` does not occur in objectui at that sha, and none of the * ten occurs as an identifier, except `CONSOLE_PATH` twice — both inside * comment prose in one browser test, neither an import. Its control: 545 @@ -73,7 +74,7 @@ * earlier draft of this docblock justified the exclusion on "the one * sibling checkout reachable from the implementing container", and that * was retired for being unverifiable from anywhere else. A sha this - * repository itself pins is re-runnable by anyone who checks objectui out. + * repository itself pinned is re-runnable by anyone who checks objectui out. * * ⚠️ `cloud` is NOT MEASURED, which is a different thing from zero. The same * control run against it — `repo:objectstack-ai/cloud objectstack` — answers 0 diff --git a/packages/cli/src/utils/console.sha-drift.test.ts b/packages/cli/src/utils/console.sha-drift.test.ts index 7bf5a50e697..032d746f972 100644 --- a/packages/cli/src/utils/console.sha-drift.test.ts +++ b/packages/cli/src/utils/console.sha-drift.test.ts @@ -3,9 +3,9 @@ /** * objectui-SHA drift guard — the seat on the boot path itself (#7752). * - * `pnpm check:console-sha` guards the root `pnpm dev` / `dev:showcase` / - * `dev:crm` / `dev:todo` scripts. Every other way to boot reaches the server - * without passing it: `objectstack dev` run inside an example dir, an + * `pnpm check:console-sha` guarded the root `pnpm dev` / `dev:showcase` / + * `dev:crm` / `dev:todo` scripts until it retired with the framework's objectui + * pin (#25745). Every other way to boot reached the server without passing it: `objectstack dev` run inside an example dir, an * example's own `dev` script (`objectstack dev --seed-admin`), a * `.claude/launch.json` config driving `pnpm exec objectstack dev`. A QA sweep * booted that way and spent the run measuring a console two days behind the @@ -97,7 +97,7 @@ describe('decideConsoleMount', () => { it('keeps "no dist at all" distinct from "refused for drift"', () => { // Both leave the Console unmounted, but they need different messages — - // "run objectui:build" vs the existing "console dist not found" hint. + // "rebuild, the dist is not the pin" vs the "console dist not found" hint. expect(decideConsoleMount({ hasDist: false, drift: null, isDev: true, env: {} })).toEqual({ mount: false, refusedForDrift: false, @@ -106,17 +106,16 @@ describe('decideConsoleMount', () => { }); describe('drift messages', () => { - it('names the rebuild, never the pin bump — refresh would move the pin instead', () => { + it('names the rebuild script that exists, never a retired pin script', () => { for (const message of [ formatConsoleShaDriftWarning(drift), formatConsoleShaDriftRefusal(drift), ]) { - expect(message).toContain('pnpm objectui:build'); + expect(message).toContain('pnpm console:build'); + // The `objectui:*` pin scripts retired with the pin (#25745); a remedy + // naming one sends the reader to a script `pnpm` cannot find. + expect(message).not.toMatch(/objectui:(build|refresh|bump|clean)/); } - // `objectui:refresh` appears only as the explicitly-labelled wrong turn. - expect(formatConsoleShaDriftRefusal(drift)).toContain( - "(Use 'pnpm objectui:refresh' only when you intend to move the pin", - ); }); it('shows both SHAs and the escape hatch, so the boot log is self-explaining', () => { @@ -189,14 +188,14 @@ describe('what a dev server that refused a drifted Console answers', () => { request = await serve(createConsoleShaDriftRefusalPlugin(drift)); }); - it.each(CONSOLE_REQUESTS)('GET %s answers 503 naming `pnpm objectui:build`, not the SPA', async (p) => { + it.each(CONSOLE_REQUESTS)('GET %s answers 503 naming `pnpm console:build`, not the SPA', async (p) => { const res = await request(p); expect(res.status).toBe(503); expect(res.headers.get('content-type')).toMatch(/^text\/plain/); expect(res.headers.get('cache-control')).toBe('no-store'); const body = await res.text(); expect(body.split('\n')[0]).toBe(HEADLINE); - expect(body).toContain('pnpm objectui:build'); + expect(body).toContain('pnpm console:build'); expect(body).toContain(PIN.slice(0, 12)); expect(body).toContain(STAMP.slice(0, 12)); expect(body).toContain(DRIFT_OVERRIDE_ENV); diff --git a/packages/cli/src/utils/console.ts b/packages/cli/src/utils/console.ts index 2cb1aa00857..d9b267389c8 100644 --- a/packages/cli/src/utils/console.ts +++ b/packages/cli/src/utils/console.ts @@ -21,8 +21,8 @@ * Resolution strategy, in priority order: * * 1. `@objectstack/console` — the framework-vendored, version-locked - * build. Shipped as a dist-only npm package frozen at the objectui - * SHA recorded in `/.objectui-sha`. This is what a + * build. Shipped as a dist-only npm package built from this + * workspace's `ui/apps/console` (`pnpm console:build`). This is what a * fresh `@objectstack/cli` install gets: the CLI declares this * package as a dependency and both ship at one version from the * Changesets `fixed` group, so no app installs it by hand. Cloud / @@ -408,20 +408,26 @@ export function formatConsoleDistMissingWarning(consolePath: string | null): str /** * The vendored console `dist/` is a locally-built, gitignored artifact - * rebuilt only by `scripts/build-console.sh` (pnpm objectui:build / - * objectui:refresh / release) — never by `turbo run build`. When a - * developer pulls a branch that bumps the framework's `.objectui-sha` pin - * without re-running `pnpm objectui:build`, the dist stays frozen at the - * previous objectui commit and would be served silently. The npm-major - * version guard above can't catch this: the objectui SHA moves underneath a - * single `@objectstack/console` version. + * rebuilt only by `pnpm console:build` (and release) — never by + * `turbo run build`. The guard below was built for the two-repo era: the + * framework pinned objectui by SHA in a committed `.objectui-sha`, the old + * build script stamped the built SHA into `dist/.objectui-sha`, and a pull + * that moved the pin without a rebuild left the dist frozen at the previous + * objectui commit, served silently. The npm-major version guard above can't + * catch that: the objectui SHA moves underneath a single + * `@objectstack/console` version. * - * build-console.sh stamps the built SHA into `dist/.objectui-sha`. Here we - * compare it against the framework's committed `.objectui-sha` pin, located - * by walking up from the resolved console package. This is inherently a - * monorepo/dev concern: a published install ships no `.objectui-sha` pin - * (so the stamped dist is authoritative), and the sibling-repo dev fallback - * writes no stamp — both cases are skipped silently. + * Since #25745 the framework repo carries no pin (the Console builds from + * `ui/apps/console` in this workspace) and `packages/console/build-dist.sh` + * writes no stamp and replaces the whole dist, so this guard can no longer + * fire there. It is kept because a tree that still carries its own + * `.objectui-sha` above the console package (the objectos overlay named in + * this file's header) with a stamped dist reaches it, and that tree is not + * readable from this repository. Here we compare the stamp against the + * nearest `.objectui-sha`, found by walking up from the resolved console + * package. A published install ships no pin (so the stamped dist is + * authoritative), and the sibling-repo dev fallback writes no stamp — both + * cases are skipped silently. */ function findObjectuiPin(startDir: string): { pin: string; file: string } | null { let dir = startDir; @@ -469,7 +475,7 @@ export function detectConsoleShaDrift(consoleDir: string): ConsoleShaDrift | nul } catch { return null; // unreadable stamp — fail open } - // Unstamped dist: can't prove drift; `pnpm check:console-sha` surfaces it. + // Unstamped dist (every `pnpm console:build` since #25745): can't prove drift. if (!stamp || stamp === found.pin) return null; return { stamp, pin: found.pin, pinFile: found.file }; @@ -481,7 +487,7 @@ export function formatConsoleShaDriftWarning(drift: ConsoleShaDrift): string { ` ⚠ Console version drift: serving @objectstack/console built from objectui@${drift.stamp.slice(0, 12)}, ` + `but ${drift.pinFile} pins objectui@${drift.pin.slice(0, 12)}. ` + `packages/console/dist is a gitignored local build that 'turbo run build' does not refresh — ` + - `rebuild it with 'pnpm objectui:build'.` + `rebuild it with 'pnpm console:build'.` ); } @@ -506,18 +512,16 @@ function describeConsoleShaDrift(drift: ConsoleShaDrift, hostPaths: boolean): st ` packages/console/dist is a gitignored local build that 'turbo run build' does NOT refresh,\n` + ` so this server would serve a Console SPA the repo no longer pins — and anything you\n` + ` observed in it would describe a different objectui commit.\n\n` + - ` Rebuild the console at the pinned SHA:\n\n` + - ` pnpm objectui:build\n\n` + - ` (Use 'pnpm objectui:refresh' only when you intend to move the pin to your local ../objectui HEAD.)\n` + + ` Rebuild the console:\n\n` + + ` pnpm console:build\n\n` + ` To boot anyway with the stale bundle: ${DRIFT_OVERRIDE_ENV}=1 — the API still serves either way.\n` ); } /** - * The refusal block — `os dev` declines to mount a drifted console. Mirrors - * the remediation of `scripts/check-console-sha.mjs` verbatim (rebuild at the - * *pinned* SHA with `pnpm objectui:build`; `objectui:refresh` would re-bump - * the pin to the local ../objectui HEAD, which is the opposite of the fix). + * The refusal block — `os dev` declines to mount a drifted console. The remedy + * is the rebuild, `pnpm console:build`, the same script the not-built remedy + * names ({@link describeConsoleDistMissing}). */ export function formatConsoleShaDriftRefusal(drift: ConsoleShaDrift): string { return `\n ✗ Console version drift — refusing to serve /_console in dev.\n\n${describeConsoleShaDrift(drift, true)}`; @@ -534,10 +538,10 @@ function driftOverridden(env: NodeJS.ProcessEnv): boolean { /** * Decide whether the Console SPA may mount, given what resolution found. * - * The drift guard has two seats and this is the second one. The first — - * `pnpm check:console-sha` — is wired into the root `pnpm dev` / - * `dev:showcase` / `dev:crm` / `dev:todo` scripts, and every boot that does - * not go through those scripts misses it: `objectstack dev` run directly in + * The drift guard had two seats and this is the one that remains. The other — + * `pnpm check:console-sha`, wired into the root `pnpm dev` scripts — retired + * with the framework's pin (#25745). Even while it ran, every boot that did + * not go through those scripts missed it: `objectstack dev` run directly in * an example dir, an example's own `dev` script, a `.claude/launch.json` * config, `pnpm exec objectstack dev`. That is how a QA sweep came to measure * a console two days behind the pin (#7752) — the pin had moved, the local diff --git a/packages/cli/test/console-resolve.test.ts b/packages/cli/test/console-resolve.test.ts index be25dfa10f6..2264435437e 100644 --- a/packages/cli/test/console-resolve.test.ts +++ b/packages/cli/test/console-resolve.test.ts @@ -196,7 +196,7 @@ describe('detectConsoleShaDrift', () => { expect(warnings).toHaveLength(1); expect(warnings[0]).toContain(SHA_A.slice(0, 12)); expect(warnings[0]).toContain(SHA_B.slice(0, 12)); - expect(warnings[0]).toContain('objectui:build'); + expect(warnings[0]).toContain('pnpm console:build'); }); it('is silent when the dist stamp matches the pin', () => {