fix(cli): honor --json contract on all error paths - #26
Conversation
Adversarial review verdict: REQUEST_CHANGES (do not merge)(Posted as a comment — GH blocks a formal request-changes review from the PR author account.) The core fix is correct in isolation — verified before/after that Two blocking issues: 1. Regression: reintroduces the internal-stack leak that #23 already fixed
This PR's
Fix: log the stack at 2. Branch conflicts with current main
(Suite note: full-suite |
Fatal CLI errors emitted nothing machine-parseable on stdout even when the
caller passed --json, so consumers parsing `<cmd> --json` could not detect or
read failures (they only got a plaintext line on stderr).
Route fatal errors through a new emitCliError() helper. The human-readable
diagnostic is always written to stderr (`Error: <msg>`), preserving existing
stderr-reading behavior; when --json is present it additionally emits a
machine-parseable { ok: false, error, message } object on stdout (matching the
{ ok: true, ... } success contract). The internal stack stays behind debug
logging. The shared helper is also used by the test bin wrapper so it mirrors
the real entrypoint.
Adds regression tests covering error paths under --json (machine-parseable
stdout) and the non-json path (empty stdout, plaintext stderr).
782ec9f to
a5ca0d1
Compare
Bug
--jsoncontract violated on all error paths. With--json, error paths emitted empty stdout and a plaintext[ERROR] CLI error {...}line plusError: ..on stderr (exit 1). Consumers parsing<cmd> --jsoncould neither detect nor read failures.Repro (before):
knowledge db stats --json,knowledge project-panel --json,knowledge providers check --json,knowledge add --json(no args) — all print nothing to stdout.Root cause
The top-level
import.meta.maincatch handler insrc/cli.tsunconditionally wrote the failure tostderr(log('error', 'CLI error', ...)+console.error("Error: ..")) and never consulted the--jsonflag, so stdout stayed empty on every thrown error regardless of--json.Fix
Introduce
emitCliError(error, argv):--jsonis present, emit a machine-parseable{ ok: false, error, message }object on stdout (mirroring the existing{ ok: true, ... }success contract via the sharedoutput()helper).1in both modes.The exported helper is reused by the test bin wrapper so it mirrors the real entrypoint (no duplicated, drift-prone catch logic). No version bump, no publish;
prepublishOnlyrebuilds thebin/artifacts at publish time.Test
Adds regression tests in
tests/cli.test.ts:--json error paths emit a machine-parseable object on stdout— coversadd,providers check, unknown command, andget(missing--id); asserts non-empty stdout that parses to{ ok: false, error: <string> }.non-json error paths keep plaintext stderr and empty stdout.Both fail before the fix and pass after.
Verification
bunx tsc -p tsconfig.build.json --noEmitclean.bun test tests/cli.test.ts: branch 44 pass / 6 fail vs origin/main baseline 42 pass / 6 fail — the 2 new tests pass and the same 6 pre-existing, environment-dependent failures (real-HOME auth/legacy-migration state, slow-box timeouts) occur on both, so no new failures introduced.🤖 Generated with Claude Code