Skip to content

[finding] resolveConfigPath prints human text to STDOUT then process.exit(1)s — 9 commands' --json face emits unparseable bytes when the config file is missing #15547

Description

@os-litant

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.logboth 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions