Skip to content

Commit 5803f41

Browse files
fix(cli): Console-dist warning names the remedy for where it runs; README states the Console step and command costs (#22224)
Fixes #22155 Fixes #22164 Clause-②: no One PR for the two-card bundle that edits the same README section. #22156 is serial after this PR and is not addressed here: its README items (the first `curl`, the pnpm floors) and `CONTRIBUTING.md` are untouched. ## What changes ### README, "Hack on the framework" This covers #22164 and the README half of #22155. - The command block gains `pnpm objectui:build` as the Console step, between `pnpm build` and `pnpm dev`. - A paragraph under the block says what the step does: - it builds objectui at the commit pinned in `.objectui-sha` into the gitignored `packages/console/dist`; - it builds from a `../objectui` checkout when one exists, otherwise from a shallow clone into `.cache/`; - it installs objectui's dependencies either way, so it needs network; - without it, `pnpm dev` still serves the REST API, but no Console; - rerun it when `.objectui-sha` moves. This matches `content/docs/getting-started/examples.mdx` and `scripts/build-console.sh`, both read on `origin/main` `c6fe02d7ac`. - A table gives each command's measured cost, quoted as the cards measured it on a fresh clone (4-vCPU container, Node 22.22, pnpm 10.31): | Command | Time | |:---|---:| | `pnpm install` | 16 s | | `pnpm build` | 6 m 54 s (72 turbo tasks) | | `pnpm objectui:build` | 10 m 36 s | | `pnpm test` | 54 m 03 s (135 turbo tasks) | These were not re-measured here. - For a contributor's own change, the section now points to `pnpm turbo run test --affected` and `pnpm --filter @objectstack/PKG test`. A note says that `--affected` compares the branch with the local `main`. - The `pnpm install` line's text is unchanged; only its comment column moved. ### CLI boot warning This is the warning half of #22155, cross-lane to `domain:cli` as declared on the claim. `packages/cli/src/commands/serve.ts` used to print the same remedy wherever it ran: `install @object-ui/console (already built) or run pnpm --filter @object-ui/console build in the objectui workspace`. It now calls `formatConsoleDistMissingWarning(consolePath)`, which is new in `packages/cli/src/utils/console.ts` beside the drift formatters. The function picks the remedy from one signal: whether the nearest `package.json` above the resolved console package declares an `objectui:build` script. - **Framework repo** (the root manifest declares the script). The warning reads `Console dist not found at packages/console/dist`, says `pnpm build` does not produce it, and says to build it with `pnpm objectui:build` in the root directory, which it names. The directory matters because `pnpm dev` runs in `examples/app-showcase`, where the root script does not resolve. - **Anywhere else**. The warning gives the dist path and says the Console ships prebuilt in `@objectstack/console`, a dependency of `@objectstack/cli`, to reinstall at the CLI's version. A scaffolded project depends on `@objectstack/cli` (`packages/create-objectstack/src/templates/blank/package.json`), and the CLI depends on `@objectstack/console` (`packages/cli/package.json`). The resolver reads `@objectstack/console` only. An installed `@object-ui/console` is never consulted. Why this signal (Zone 2, item 3): - The script's presence is exactly the claim the message makes, so the command is named only where it exists. - The `.objectui-sha` pin that the drift guard keys on would also match another repository that carries a pin without the script. - A layout check such as `packages/console` or `scripts/build-console.sh` couples the CLI to this repo's file tree. - The walk stops at the nearest manifest. A project created inside a framework checkout therefore gets the install remedy for the console it actually resolved. The pnpm store layout's intermediate directories carry no manifest, so the walk reaches the project's own. Only the text changes; mount and refuse decisions are untouched. There is a patch changeset for `@objectstack/cli`. The new export is internal. `test/published-subpath-console.pin.test.ts` partitions every export of `utils/console.ts` into public and not-public lists, and it went red until the name was recorded there as `INTERNAL_SINCE_RETIREMENT`. The two "exactly three names" checks still hold it off the published `./console` subpath. The helper's build-root walk is not exported. ### Not done here - **The bare-404 HTTP half of #22155**, which belongs to `domain:cli`. `GET /_console/` on a composition with no Console still answers `404`; this boot measured it at `404`. ## Verification All runs below were on `0ef75bd7d9` unless noted. - **Unit test, both branches with controls** (`packages/cli/test/console-resolve.test.ts`, four new cases): - framework layout: names `pnpm objectui:build` and the root; - control: the same layout without the script gets the install remedy; - installed CLI, hoisted and pnpm-store layouts: names `@objectstack/console`; - a project inside a framework checkout gets the install remedy. Its control removes the project's manifest, and the walk then reaches the outer one and names the build. Every case asserts that `@object-ui/console` is absent. - **Ablations**, each landed on disk via `scripts/ablation-replace.mjs` and restored with blob equal to HEAD and an empty `git diff HEAD`: - the signal forced to `null`: 2 of 4 red (the framework case and the stop-rule control); - the walk made to keep climbing past the nearest manifest: 1 of 4 red (the stop-rule case). - **Live boot, framework context**: `pnpm dev -- --fresh -p 38592` from this worktree, with no `packages/console/dist` and no `../objectui` beside it. The boot printed: ```text ⚠ Console dist not found at packages/console/dist — `pnpm build` does not produce it. Build it with `pnpm objectui:build` in /home/user/objectstack-issue-22155 (uses a ../objectui checkout if present, else clones objectui at the pinned commit — needs network). ``` `/api/v1/health` answered 200 and `/_console/` answered 404. The non-framework branch was verified by the unit test only; no scaffolded project was booted. - **`--affected`**, verified with `pnpm turbo run test --affected --dry-run=json` (turbo 2.11.5): - `TURBO_SCM_BASE=origin/main` before the change: 0 packages; - after the change: 8 packages, namely `//`, `@objectstack/cli` and its six dependents `dogfood`, `downstream-contract` and `example-{crm,multi-package,showcase,todo}`; - against this checkout's stale local `main` (`9cc2c7954a`, 108 commits behind): 81 packages. That is why the README says a stale `main` widens the set. - **`@objectstack/cli`**: - `build`: exit 0; - `typecheck`, including `check:test-typecheck`: exit 0; - `vitest run --project unit`: 263 files and 3878 tests passed; - `--project integration`, run locally because `serve.ts` is on the boot path: 93 files, 897 tests passed and 2 skipped. - **Gates**: `node scripts/pm/dispatch-gates.mjs --commands` over this diff derived 68 commands. That is the dispatch's 53 plus 15 that the changeset and test files bring in. All 68 exited 0. `--ran` reconciliation: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. - **Lint**, a proven narrowing: eslint `--no-inline-config --format json` on the 4 changed TS files reported 4 files, 0 errors and 0 warnings. `--print-config` shows each file is in the config's population. `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project` or `projectService`), so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is left to CI. ## Acceptance notes - `os serve --help` still describes `--[no-]ui` as `Enable the bundled Console portal at /_console/ when @object-ui/console is installed`. That is the same wrong package name, at `serve.ts:1198` and mirrored in `packages/cli/README.md:244`. It is outside this claim's file surface, so it is reported to the PM rather than changed here. - `packages/spec/scripts/publish-smoke-boot-failure.test.ts` and a comment in `scripts/publish-smoke.sh` quote the old warning line. Those quotes are verbatim records of past CI runs, kept unchanged on purpose. The smoke's boot-failure predicate does not key on this line, and neither does the new wording. --- _Generated by [Claude Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 15cd10f commit 5803f41

6 files changed

Lines changed: 238 additions & 9 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): the "Console dist not found" boot warning names the remedy that works where the server runs (#22155)
6+
7+
When the Console package the server resolves has no built `dist/`, the boot warning used to say `install @object-ui/console (already built) or run pnpm --filter @object-ui/console build in the objectui workspace`, wherever it ran. The first remedy works nowhere, because the resolver never looks for an installed `@object-ui/console`. The second needs an objectui checkout, which a fresh clone of the framework repository does not have; there the Console is built by a root script.
8+
9+
The warning now names one remedy. It picks it by asking whether the nearest `package.json` above the resolved console package declares an `objectui:build` script:
10+
11+
- **In the framework repository** (the root manifest declares it), the warning gives the dist path, says that `pnpm build` does not produce it, and names `pnpm objectui:build` with the directory to run it in.
12+
- **Anywhere else**, such as a project that installed `@objectstack/cli`, the warning gives the dist path and says the Console ships prebuilt in `@objectstack/console`, a dependency of `@objectstack/cli`. The remedy is to reinstall that package at the CLI's version.
13+
14+
Only the text changes. The Console mounts, or is skipped, exactly as before.

‎README.md‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,40 @@ to run the whole loop end-to-end.
222222
```bash
223223
git clone https://github.com/objectstack-ai/objectstack.git
224224
cd objectstack
225-
pnpm install # Node 22+, pnpm 10 (corepack enable)
226-
pnpm build # build all packages
227-
pnpm dev # showcase example: REST + Console on :3000
228-
pnpm test # run the test suite
225+
pnpm install # Node 22+, pnpm 10 (corepack enable)
226+
pnpm build # build all packages
227+
pnpm objectui:build # build the Console SPA (not part of pnpm build)
228+
pnpm dev # showcase example: REST + Console on :3000
229229
```
230230

231+
`pnpm objectui:build` builds [objectui](https://github.com/objectstack-ai/objectui)
232+
at the commit pinned in `.objectui-sha` into `packages/console/dist`
233+
(gitignored). It builds from a `../objectui` checkout if one sits next to this
234+
repo, otherwise from a shallow clone of objectui into `.cache/`; either way it
235+
installs objectui's dependencies, so it needs network. Skip it and `pnpm dev`
236+
still serves the REST API, without the Console. Rerun it when `.objectui-sha`
237+
moves.
238+
239+
A first run is slow, not stuck. Measured once on a fresh clone of `main`
240+
(4-vCPU container, Node 22.22, pnpm 10.31):
241+
242+
| Command | Time | |
243+
|:---|---:|:---|
244+
| `pnpm install` | 16 s | |
245+
| `pnpm build` | 6 m 54 s | 72 turbo tasks |
246+
| `pnpm objectui:build` | 10 m 36 s | clones objectui and builds the Console |
247+
| `pnpm test` | 54 m 03 s | the full suite: 135 turbo tasks |
248+
249+
For your own change, test what it reaches instead of the full suite:
250+
251+
```bash
252+
pnpm turbo run test --affected # packages your branch changes, and their dependents
253+
pnpm --filter @objectstack/<pkg> test # one package
254+
```
255+
256+
`--affected` compares your branch with your local `main`, so a stale `main`
257+
widens the set; keep it current, or set `TURBO_SCM_BASE=origin/main`.
258+
231259
Other examples: `pnpm dev:crm`, `pnpm dev:todo`. Docs site: `pnpm docs:dev`.
232260
[AGENTS.md](./AGENTS.md) is the working rulebook for both humans and agents;
233261
[CONTRIBUTING.md](./CONTRIBUTING.md) covers the workflow.

‎packages/cli/src/commands/serve.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ import {
153153
decideConsoleMount,
154154
formatConsoleShaDriftWarning,
155155
formatConsoleShaDriftRefusal,
156+
formatConsoleDistMissingWarning,
156157
createConsoleStaticPlugin,
157158
createRuntimeAssetsPlugin,
158159
type ConsoleShaDrift,
@@ -5066,7 +5067,7 @@ export default class Serve extends Command {
50665067
} else if (refusedForDrift && consoleDrift) {
50675068
console.error(chalk.red(formatConsoleShaDriftRefusal(consoleDrift)));
50685069
} else {
5069-
console.warn(chalk.yellow(` ⚠ Console dist not found — install \`@object-ui/console\` (already built) or run \`pnpm --filter @object-ui/console build\` in the objectui workspace`));
5070+
console.warn(chalk.yellow(formatConsoleDistMissingWarning(consolePath)));
50705071
}
50715072
}
50725073
}

‎packages/cli/src/utils/console.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,75 @@ export function hasConsoleDist(consolePath: string): boolean {
294294
return fs.existsSync(path.join(consolePath, 'dist', 'index.html'));
295295
}
296296

297+
// ─── "No dist" remedy — named for where the server runs ─────────────
298+
299+
/** The framework repo's root script that builds `packages/console/dist`. */
300+
const CONSOLE_BUILD_SCRIPT = 'objectui:build';
301+
302+
/**
303+
* The directory whose `package.json` declares `objectui:build` for this
304+
* console package, or `null`.
305+
*
306+
* Only the NEAREST `package.json` above `consolePath` is asked. In the
307+
* framework repo that is the root manifest (`packages/` carries none), which
308+
* declares the script. An installed `@objectstack/console` stops at the
309+
* consuming project's own manifest — including the pnpm store layout, whose
310+
* intermediate directories carry none — so a project that merely sits inside
311+
* a framework checkout is never told to run a script that does not build the
312+
* console it actually resolved. The signal is the script itself rather than
313+
* the repo's layout or its `.objectui-sha` pin, so the remedy is named only
314+
* where it exists.
315+
*/
316+
function findConsoleBuildRoot(consolePath: string): string | null {
317+
let dir = path.dirname(consolePath);
318+
for (let depth = 0; depth < 8; depth++) {
319+
const pkgPath = path.join(dir, 'package.json');
320+
if (fs.existsSync(pkgPath)) {
321+
try {
322+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
323+
return typeof pkg?.scripts?.[CONSOLE_BUILD_SCRIPT] === 'string' ? dir : null;
324+
} catch {
325+
return null; // unreadable manifest — name the install remedy instead
326+
}
327+
}
328+
const parent = path.dirname(dir);
329+
if (parent === dir) break;
330+
dir = parent;
331+
}
332+
return null;
333+
}
334+
335+
/**
336+
* The boot warning for a resolved console package with no built `dist/`.
337+
*
338+
* Two places reach it, and each gets the remedy that works there:
339+
*
340+
* - the framework repo, where `packages/console/dist` is a gitignored build
341+
* that `pnpm build` never produces — `pnpm objectui:build`, run at the
342+
* root it was found under (it is a root script, so it does not resolve
343+
* from the example directory `pnpm dev` runs in);
344+
* - anywhere else, where the Console arrives prebuilt as
345+
* `@objectstack/console`, a dependency of `@objectstack/cli` — so the
346+
* remedy is that package install. (An installed `@object-ui/console`,
347+
* which this text used to name, is never consulted: that name is matched
348+
* only as the workspace package of a sibling `../objectui` checkout.)
349+
*/
350+
export function formatConsoleDistMissingWarning(consolePath: string): string {
351+
const dist = path.join(consolePath, 'dist');
352+
const buildRoot = findConsoleBuildRoot(consolePath);
353+
if (buildRoot) {
354+
return (
355+
` ⚠ Console dist not found at ${path.relative(buildRoot, dist)} — \`pnpm build\` does not produce it. ` +
356+
`Build it with \`pnpm ${CONSOLE_BUILD_SCRIPT}\` in ${buildRoot} ` +
357+
`(uses a ../objectui checkout if present, else clones objectui at the pinned commit — needs network).`
358+
);
359+
}
360+
return (
361+
` ⚠ Console dist not found at ${dist} — the Console ships prebuilt in \`${CONSOLE_PACKAGE}\`, ` +
362+
`a dependency of \`@objectstack/cli\`; reinstall it at the same version as the CLI.`
363+
);
364+
}
365+
297366
// ─── objectui-SHA Drift Guard (dev monorepo only) ───────────────────
298367

299368
/**

‎packages/cli/test/console-resolve.test.ts‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
isConsoleVersionCompatible,
2121
detectConsoleShaDrift,
2222
formatConsoleShaDriftWarning,
23+
formatConsoleDistMissingWarning,
2324
} from '../src/utils/console.js';
2425

2526
// resolveConsolePath() also discovers the real, version-locked workspace
@@ -223,3 +224,108 @@ describe('detectConsoleShaDrift', () => {
223224
expect(warnings).toEqual([]);
224225
});
225226
});
227+
228+
describe('formatConsoleDistMissingWarning — the remedy named for where the server runs', () => {
229+
/** The framework repo's remedy, and the one the warning used to name everywhere. */
230+
const BUILD_REMEDY = 'pnpm objectui:build';
231+
const RETIRED_REMEDY = '@object-ui/console';
232+
233+
function writeJson(file: string, value: unknown): void {
234+
fs.mkdirSync(path.dirname(file), { recursive: true });
235+
fs.writeFileSync(file, JSON.stringify(value));
236+
}
237+
238+
/** A console package with a manifest and no `dist/` — what the warning is printed for. */
239+
function writeUnbuiltConsole(dir: string): string {
240+
writeJson(path.join(dir, 'package.json'), { name: '@objectstack/console', version: '1.0.0' });
241+
return dir;
242+
}
243+
244+
function tmpRoot(tag: string): string {
245+
return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), `os-console-remedy-${tag}-`)));
246+
}
247+
248+
/**
249+
* The framework repo's shape: the root manifest declares the build script
250+
* (or, for the control, does not), and `packages/console` is the workspace
251+
* package `@objectstack/cli` resolves.
252+
*/
253+
function makeFrameworkTree(withScript: boolean): { root: string; consoleDir: string } {
254+
const root = tmpRoot('repo');
255+
writeJson(path.join(root, 'package.json'), {
256+
name: 'framework-root',
257+
private: true,
258+
scripts: withScript
259+
? { build: 'turbo run build', 'objectui:build': 'bash scripts/build-console.sh' }
260+
: { build: 'turbo run build' },
261+
});
262+
const consoleDir = writeUnbuiltConsole(path.join(root, 'packages', 'console'));
263+
return { root, consoleDir };
264+
}
265+
266+
it('names `pnpm objectui:build` and the root to run it in, inside the framework repo', () => {
267+
const { root, consoleDir } = makeFrameworkTree(true);
268+
269+
const message = formatConsoleDistMissingWarning(consoleDir);
270+
expect(message).toContain(BUILD_REMEDY);
271+
expect(message).toContain(` in ${root} `);
272+
expect(message).toContain(path.join('packages', 'console', 'dist'));
273+
expect(message).not.toContain('@objectstack/console');
274+
expect(message).not.toContain(RETIRED_REMEDY);
275+
});
276+
277+
it('control: the same layout without the script gets the install remedy — the script is the signal, not the layout', () => {
278+
const { consoleDir } = makeFrameworkTree(false);
279+
280+
const message = formatConsoleDistMissingWarning(consoleDir);
281+
expect(message).not.toContain(BUILD_REMEDY);
282+
expect(message).toContain('@objectstack/console');
283+
});
284+
285+
it('names the `@objectstack/console` install in a project that installed the CLI (hoisted and pnpm-store layouts)', () => {
286+
const project = tmpRoot('app');
287+
writeJson(path.join(project, 'package.json'), {
288+
name: 'consumer-app',
289+
scripts: { dev: 'objectstack dev' },
290+
});
291+
for (const consoleDir of [
292+
writeUnbuiltConsole(path.join(project, 'node_modules', '@objectstack', 'console')),
293+
writeUnbuiltConsole(
294+
path.join(
295+
project, 'node_modules', '.pnpm', '@objectstack+console@1.0.0',
296+
'node_modules', '@objectstack', 'console',
297+
),
298+
),
299+
]) {
300+
const message = formatConsoleDistMissingWarning(consoleDir);
301+
expect(message).toContain('@objectstack/console');
302+
expect(message).toContain(path.join(consoleDir, 'dist'));
303+
expect(message).not.toContain(BUILD_REMEDY);
304+
expect(message).not.toContain(RETIRED_REMEDY);
305+
}
306+
});
307+
308+
it('stops at the consuming project\'s own manifest, even inside a framework checkout', () => {
309+
// A project created inside a directory whose manifest declares the script:
310+
// the console it resolved is ITS install, which `objectui:build` does not
311+
// build, so the install remedy is the true one.
312+
const outer = tmpRoot('outer');
313+
writeJson(path.join(outer, 'package.json'), {
314+
name: 'framework-root',
315+
scripts: { 'objectui:build': 'bash scripts/build-console.sh' },
316+
});
317+
const project = path.join(outer, 'scratch', 'my-app');
318+
const consoleDir = writeUnbuiltConsole(
319+
path.join(project, 'node_modules', '@objectstack', 'console'),
320+
);
321+
writeJson(path.join(project, 'package.json'), { name: 'my-app' });
322+
expect(formatConsoleDistMissingWarning(consoleDir)).not.toContain(BUILD_REMEDY);
323+
324+
// Control: without the project's own manifest the walk reaches the outer
325+
// one — so the stop above is what kept the answer right.
326+
fs.rmSync(path.join(project, 'package.json'));
327+
const climbed = formatConsoleDistMissingWarning(consoleDir);
328+
expect(climbed).toContain(BUILD_REMEDY);
329+
expect(climbed).toContain(` in ${outer} `);
330+
});
331+
});

‎packages/cli/test/published-subpath-console.pin.test.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ const RETIRED_RUNTIME_NAMES = [
168168
'isConsoleVersionCompatible',
169169
];
170170

171+
/**
172+
* Exports `utils/console.ts` gained after the retirement above, internal from
173+
* the day they landed. Listed so the partition below still accounts for every
174+
* export of that module; the two "exactly three names" checks are what keep
175+
* them off the published subpath. Publishing one is the same deliberate act as
176+
* re-admitting a retired name.
177+
*/
178+
const INTERNAL_SINCE_RETIREMENT = ['formatConsoleDistMissingWarning'];
179+
171180
/**
172181
* Every subpath the published package resolves, in full. A subpath removed here
173182
* is a consumer broken in the exact shape of #15325 and #13662; a subpath added
@@ -682,16 +691,18 @@ describe('the public surface is exactly three names', () => {
682691

683692
it('retires exactly the names that are NOT public — the two lists partition the module, with nothing dropped', () => {
684693
// The census is taken from the packed INTERNAL module rather than from a
685-
// constant here, so a 14th export added to `utils/console.ts` lands in
686-
// neither list and fails this — instead of silently being neither published
687-
// nor recorded as retired.
694+
// constant here, so a new export added to `utils/console.ts` lands in no
695+
// list and fails this — instead of silently being neither published nor
696+
// recorded as internal.
688697
const internal = declaredExports(join(installedRoot, 'dist', 'utils', 'console.d.ts'));
689698
// ⛔ First: the census is only a partition if every export was NAMEABLE. An
690699
// export form this walk cannot name is missing from both lists, and the
691700
// equality below would still hold — green, over a surface read short.
692701
expectEveryExportNamed('the packed internal module `.d.ts`', internal);
693702
expect(internal.starReExports).toBe(0);
694-
expect(internal.names).toEqual([...PUBLIC_SURFACE, ...RETIRED_FROM_THIS_SUBPATH].sort());
703+
expect(internal.names).toEqual(
704+
[...PUBLIC_SURFACE, ...RETIRED_FROM_THIS_SUBPATH, ...INTERNAL_SINCE_RETIREMENT].sort(),
705+
);
695706
});
696707
});
697708

0 commit comments

Comments
 (0)