Skip to content

fix(cli): make the two invocation failures that read as a crashed boot say so - #10181

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-10111-cli-entry-and-flag-failure-loudness
Aug 20, 2026
Merged

fix(cli): make the two invocation failures that read as a crashed boot say so#10181
os-elon merged 2 commits into
mainfrom
claude/issue-10111-cli-entry-and-flag-failure-loudness

Conversation

@os-elon

@os-elonos-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10111

Two ways to invoke this CLI wrong both presented as a crashed boot, and the measured cost was a runner spending a boot cycle debugging the application instead of the invocation.

Re-measured on current main before editing

The card is stamped at 4a7b3604c; main is at 2d3860df9. Both premises still hold there — measured by running origin/main's own src/index.ts and bin/run-dev.js through tsx, in a worktree with the dependency closure built:

origin/main barrel as the process entry
exit=0 stdout=0 bytes stderr=0 bytes
origin/main shim, `dev --no-ui`
exit=2 stdout=0 bytes stderr=99 lines
line 1: Error: Nonexistent flag: --no-ui
then: a stack trace, then the USAGE / ARGUMENTS / FLAGS dump

After

$ node packages/cli/dist/index.js ; echo $?
objectstack: NOT A CLI ENTRY POINT — …/packages/cli/dist/index.js only re-exports the command classes, so running it starts nothing and exits.
objectstack: the CLI entry point is …/packages/cli/bin/run.js (installed as `objectstack` / `os`) — e.g. `node …/packages/cli/bin/run.js dev`.
1
$ node packages/cli/bin/run.js dev --no-ui ; echo $?
objectstack: INVOCATION ERROR — Nonexistent flag: --no-ui. The command never ran: nothing was started and nothing is listening. Invoked as: objectstack dev --no-ui
› Error: Nonexistent flag: --no-ui
› See more help with --help
… (oclif's usage dump, unchanged, after the line)
2

The loud line is written beforehandle() runs, so it is first in the stream. That also makes it the part most likely to survive a truncated log: handle() ends in process.exit, and writes to a piped stderr are asynchronous.

Three files carry it

  • src/utils/invocation.ts — new; the two judgments, importing nothing but node: builtins because the bin shims reach it from the failure path.
  • src/index.ts — the barrel now detects that it is the process entry, names bin/run.js on stderr and sets process.exitCode = 1. process.exitCode rather than process.exit(): nothing runs after it, and the write must be allowed to drain.
  • bin/run.js / bin/run-dev.jsexecute() from @oclif/core 4.13.3 inlined verbatim apart from one added call, because execute hands the error straight to handle() and there is no seam between them. The helper is imported lazily, inside the catch: a static import of ../dist/ would make an unbuilt tree fail with Cannot find module …/dist/utils/invocation.js instead of oclif's "command not found", which is the signature scripts/cli-build-prerequisite.mjs classifies for every gate that shells out to this CLI.

Not the repo's prevailing entry-guard idiom, on purpose

#10086 measured the invokedDirectly guard in ~8 spellings across scripts/, all of them a variant of resolve(argv[1]) === fileURLToPath(import.meta.url), and every one goes silently inert — exit 0, no output — when reached through a symlink. That is the same defect this PR removes, so isProcessEntry compares realpathSync on both sides (the shape #10086 recommends and #10084 pinned) and also covers the directory form, where node DIR leaves argv[1] naming the directory rather than the index it resolved to. Both legs are measured:

node LINK (LINK is a symlink to dist/index.js) → exit 1, same first line
node packages/cli/dist (the directory, not the file) → exit 1, same first line

The naive spelling answers false for both, which is the silent no-op. Nothing in this PR repairs scripts/#10086 remains open for that.

Flag surface: unchanged

os dev --no-ui is still rejected. serve declares ui with allowNo: true and dev does not; reconciling those two is a flag-surface decision and stays out of this card by its own fence. What changed is only what the CLI says when it rejects an invocation, and the success path is pinned alongside (--version still exits 0, version on stdout, nothing added to stderr).

Verification

Gates run locally at e72a2d266, working tree clean (the union re-derived from the real diff with node scripts/pm/dispatch-gates.mjs, no paths passed):

gateverdict line
pnpm check:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new
node scripts/docs-audit/check-affected-docs.mjsexit 0
pnpm check:changeset-gate-self-tests✓ check-empty-changeset --self-test: 118 assertions (+ siblings)
pnpm check:objectui-changeset✓ objectui-range --self-test: all checks passed
node scripts/check-adr-0087-registration.mjs✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)
node scripts/check-changeset-no-major.mjs✓ This diff introduces no major bump.
node scripts/check-empty-changeset.mjs✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)
pnpm check:engine-double-contractcheck-engine-double-contract: OK — 329 pinned
pnpm check:where-matcher✓ where-matcher conformance holds: 264 matcher(s) … none new
pnpm check:query-options-erasure✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) … none new
pnpm check:type-check-coveragecheck-type-check-coverage: OK — 64/77 workspace packages type-checked
pnpm check:type-check-debt--re-measure: OK — 33 ledger entr(ies) re-measured in 363.6s, 1924 raw tsc error(s) total, none above its recorded number — run against the FULL workspace closure built first (turbo run build: 70 tasks successful). This one mattered: @objectstack/cli's TEST_DEBT entry is 146 recorded exactly, no bootstrap margin, and the new test/invocation-loudness.e2e.test.ts lands in exactly the test/ tree that entry covers. It adds zero errors — no relative imports (so no TS2835) and no implicitly-typed parameters.
pnpm check:nul-bytesOK (scanned 6070 text file(s) … no raw ASCII control bytes)
pnpm check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared
pnpm check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned
pnpm --filter @objectstack/cli typechecktsc --noEmit — exit 0
pnpm --filter @objectstack/cli testTest Files 137 passed (137) · Tests 1498 passed (1498) — exit 0; 137/1498 against the 135/1470 this package’s vitest config records, i.e. the two new files and their 28 tests

Declared deviation — pnpm lint was narrowed. The repo-wide lint is a shared-container cost this branch could not justify holding the verify lock for; ESLint ran over the six changed files instead, with --format json to prove coverage rather than trusting a zero-match exit 0: 6 files linted, 0 errors, 0 warnings, 0 suppressed. The full farm runs in CI regardless.

Declared deviation — one file outside the dispatched surface. The claim named bin/run.js, bin/run-dev.js, src/index.ts, test/** and .changeset/**. src/utils/invocation.ts is new: the alternative was spelling the same predicate twice, in two files, which is exactly the duplication #10086 measured going wrong eight different ways. It is also the only placement that ships — files never names bin/, and npm packs only the bin target itself, so a helper next to the shims would be absent from the published package.

Out-of-scope finding filed, not fixed here:#10185packages/cli/tsup.config.ts describes a build nothing runs, and clean: true means running it would delete dist/commands/**.


Generated by Claude Code

…t say so (#10111)
`node packages/cli/dist/index.js` ran the re-export barrel to completion,
printed nothing and exited 0; `os dev --no-ui` answered with a usage dump whose
first sentence scrolls past in a background log. Both present as a server that
booted and died, which sends the reader to debug the application instead of the
invocation.
The barrel now detects that it is the process entry and names `bin/run.js` on
stderr with a non-zero exit, and both bin shims put one unmistakable line ahead
of oclif's error-plus-usage output when the invocation itself was rejected.
The entry predicate is deliberately NOT the repo's prevailing `invokedDirectly`
spelling: #10086 measured that every one of those goes silently inert through a
symlink, which is the same defect being fixed here.
No flag surface changes: `os dev --no-ui` is still rejected, only legibly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
The e2e legs spawn real child processes, because `process.exitCode` inside a
vitest worker is not an exit status, and they assert the FIRST line of stderr —
"somewhere in the output" is the property the usage dump already had.
The unit legs pin the entry predicate through a symlink and through `node <dir>`,
which is where every `invokedDirectly` spelling measured in #10086 answers false
and goes silently inert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 14 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1packageMentionDocs.

Which tree this was computed on

This run read content/docs from adf1f2296830f90acc2f14196a340658cea87eee — the merge of head e72a2d2665e8235eb8d402d291aca4773412486e into base 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin adf1f2296830f90acc2f14196a340658cea87eee && git checkout adf1f2296830f90acc2f14196a340658cea87eee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1 e72a2d2665e8235eb8d402d291aca4773412486e && git checkout -B drift-repro 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1 && git merge --no-ff e72a2d2665e8235eb8d402d291aca4773412486e
node scripts/docs-audit/affected-docs.mjs --json 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
@os-elon
os-elon marked this pull request as ready for review August 20, 2026 13:13
@os-elon
os-elon added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 8d1fa00Aug 20, 2026
31 checks passed
@os-elon
os-elon deleted the claude/issue-10111-cli-entry-and-flag-failure-loudness branch August 20, 2026 13:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-elon@claude