Filed unassigned, recording only — measured while running the phase-1 reachability probe for #14974 (os lint --eval --json has no JSON face for an uncaught throw). Different mode, different exit, different helper, so it is recorded separately rather than folded in.
The measurement
Driven on origin/main at ed9d87653eb through the published entry point packages/cli/bin/run.js (not the bin/run-dev.js shim), NO_COLOR=1, stdout and stderr captured to separate files, exit code read before any pipe.
$ os lint ./nope-does-not-exist.ts --json
exit 1 · stdout 286 bytes · stderr 0 bytes
stdout, verbatim:
✗ Config file not found: /abs/path/nope-does-not-exist.ts
Hint: Run this command from a directory with objectstack.config.ts
Or specify the path: objectstack COMMAND path/to/config.ts
JSON.parse on that stdout throws. The identical shape reproduces on os validate ./nope-does-not-exist.ts --json and os build ./nope-does-not-exist.ts --json — exit 1, stdout 286 bytes, not JSON, stderr empty in all three.
Where it comes from
packages/cli/src/utils/config.ts, resolveConfigPath() at L47:
- L51-54 emit the message through
printError and console.log — both write to stdout
- L55 calls
process.exit(1) directly
So this path never throws. Every command's catch-all --json error exit sits downstream of a throw; with no throw, no catch runs and no payload is emitted. resolveConfigPath has no --json awareness at all — the flag is not in scope there.
The same helper does it a second time at L72-75, for the auto-detect miss (No objectstack.config.{ts,js,mjs} found in current directory).
Why this is a stronger shape than a missing payload
⚠️ This is not "no JSON document on stdout". It puts human text on stdout — the exact stream --json reserves for the machine. The consumer gets 286 bytes that parse as nothing, on the channel it was told to read.
Blast radius
9 command modules both declare a json flag and reach loadConfig / resolveConfigPath:
validate.ts · info.ts · diff.ts · lint.ts · migrate/meta.ts · compile.ts · verify.ts · i18n/check.ts · i18n/extract.ts
Three of those faces were driven (lint, validate, plus build, which reaches the helper as well); the rest is the static reading, ⛔ not measured.
Not already covered
packages/cli/test/json-stdout-purity.e2e.test.ts pins "stdout is exactly one JSON document", but its family is DISCOVERED as the commands that call bootSchemaStack. These commands reach the config helper without booting a kernel, so that pin never sees this path.
packages/cli/src/utils/schema-migration-plugins.ts L189 already records that the helper process.exit(1)s, and deliberately avoids calling it for that reason. The --json consequence of the same behaviour is recorded nowhere.
⛔ What is NOT claimed
No route is proposed. Whether the helper should throw a typed error, accept a json option, or be replaced at each call site is a design question this card does not settle — and any answer touches 9 published --json faces at once.
Refs
Filed unassigned, recording only — measured while running the phase-1 reachability probe for #14974 (
os lint --eval --jsonhas no JSON face for an uncaught throw). Different mode, different exit, different helper, so it is recorded separately rather than folded in.The measurement
Driven on
origin/mainated9d87653ebthrough the published entry pointpackages/cli/bin/run.js(not thebin/run-dev.jsshim),NO_COLOR=1, stdout and stderr captured to separate files, exit code read before any pipe.JSON.parseon that stdout throws. The identical shape reproduces onos validate ./nope-does-not-exist.ts --jsonandos build ./nope-does-not-exist.ts --json— exit 1, stdout 286 bytes, not JSON, stderr empty in all three.Where it comes from
packages/cli/src/utils/config.ts,resolveConfigPath()at L47:printErrorandconsole.log— both write to stdoutprocess.exit(1)directlySo this path never throws. Every command's catch-all
--jsonerror exit sits downstream of a throw; with no throw, no catch runs and no payload is emitted.resolveConfigPathhas no--jsonawareness at all — the flag is not in scope there.The same helper does it a second time at L72-75, for the auto-detect miss (
No objectstack.config.{ts,js,mjs} found in current directory).Why this is a stronger shape than a missing payload
--jsonreserves for the machine. The consumer gets 286 bytes that parse as nothing, on the channel it was told to read.Blast radius
9 command modules both declare a
jsonflag and reachloadConfig/resolveConfigPath:validate.ts·info.ts·diff.ts·lint.ts·migrate/meta.ts·compile.ts·verify.ts·i18n/check.ts·i18n/extract.tsThree of those faces were driven (
lint,validate, plusbuild, which reaches the helper as well); the rest is the static reading, ⛔ not measured.Not already covered
packages/cli/test/json-stdout-purity.e2e.test.tspins "stdout is exactly one JSON document", but its family is DISCOVERED as the commands that callbootSchemaStack. These commands reach the config helper without booting a kernel, so that pin never sees this path.packages/cli/src/utils/schema-migration-plugins.tsL189 already records that the helperprocess.exit(1)s, and deliberately avoids calling it for that reason. The--jsonconsequence of the same behaviour is recorded nowhere.⛔ What is NOT claimed
No route is proposed. Whether the helper should throw a typed error, accept a
jsonoption, or be replaced at each call site is a design question this card does not settle — and any answer touches 9 published--jsonfaces at once.Refs
os lint --eval --jsonhas no JSON face for an uncaught throw —runEvalis dispatched above thetry, so the error escapes as oclif's human output #14974 — the phase-1 probe this was measured beside; that one is about--evalmode and stands on its own termspackages/cli/src/utils/config.tsL47-76