Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/26035-console-drift-remedy-console-build.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 4 additions & 3 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
23 changes: 11 additions & 12 deletions packages/cli/src/utils/console.sha-drift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down
60 changes: 32 additions & 28 deletions packages/cli/src/utils/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<framework>/.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 /
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
Expand All @@ -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'.`
);
}

Expand All @@ -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)}`;
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/console-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading