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', () => {