feat(cli): machine-output mode — stream discipline, flag propagation, scrubber#1234
feat(cli): machine-output mode — stream discipline, flag propagation, scrubber#1234John-David Dalton (jdalton) wants to merge 1 commit intomainfrom
Conversation
Addresses Cursor bugbot feedback on PR #1234. The helper was exported but never imported in any production source — only in its own test file. Production uses isNoLogMode() directly, and the flag-checking logic already lives inline in meowOrExit. Delete the helper and its dedicated tests.
|
Cursor (@cursor) review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e04fa30. Configure here.
|
Cursor (@cursor) review |
|
Cursor (@cursor) review |
1 similar comment
|
Cursor (@cursor) review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ed83f91. Configure here.
|
Cursor (@cursor) review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ed83f91. Configure here.
|
Cursor (@cursor) review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ed83f91. Configure here.
… scrubber
Three-layer redesign that delivers pipe-safe --json/--dry-run while
scaling to every command and child process socket-cli spawns.
Layer 1 — stream discipline (CLAUDE.md SHARED STANDARDS):
- stdout carries ONLY the data a command was asked to produce.
- stderr carries everything else: progress, spinners, status, warnings,
errors, dry-run previews, context, banners, prompts.
- Under --json/--markdown, stdout MUST parse as the declared format.
- Targeted audit of output formatters: dry-run previews, scan-report
status lines ("Writing json report to …"), coana-fix "Copying …"
message, oops dry-run preview all moved from stdout to stderr.
Layer 2 — per-(tool, subcommand) flag propagation
(utils/spawn/machine-mode.mts):
- coana: --silent + --socket-mode <tempfile>; read file.
- sfw: transparent — forward to inner PM (npm/pnpm/yarn/yarn-berry/zpm/pip).
- npm: --json + --loglevel=error on JSON-aware subcommands.
- pnpm: --reporter=json or --reporter=silent.
- yarn classic: --json --silent.
- yarn berry: --json where supported + full YARN_ENABLE_* env quieting.
- yarn 6 / zpm: --json on supported cmds; --silent on install+add.
- vltpkg: --view=json uniformly.
- pip/pip3/uv/cargo/gem/go: per-tool quiet/json flags.
- Universal env injection: NO_COLOR, FORCE_COLOR=0, CLICOLOR_FORCE=0.
Layer 3 — output scrubber (utils/output/scrubber.mts):
- Transform stream that catches the residue from tools that don't cooperate
(synp "Created …" line, zpm ANSI leaks, gem progress dots, unknown
binaries). NDJSON-aware line classifier:
1. NUL sentinel spans (socket-cli's own output) → stdout.
2. JSON.parse succeeds → stdout.
3. Known noise patterns (log prefixes, status glyphs) → stderr.
4. Unknown → stderr (safe default).
- Uses ansiRegex() from @socketsecurity/lib/ansi for OSC+CSI stripping.
- Tracing via SOCKET_SCRUB_TRACE=1 for debugging.
- Tool-specific adapters (synp, zpm, gem) plug into the classifier
registry — small, inspectable, jc-style.
Layer 4 — sentinel wrapping (utils/output/emit-payload.mts):
- emitPayload() wraps the primary payload in NUL-bracketed sentinels
under machine mode so the scrubber extracts it unambiguously.
- NUL never appears in legitimate JSON/Markdown, so sentinels are
zero-ambiguity.
Ambient mode context (utils/output/ambient-mode.mts):
- meowWithSubcommands / meowOrExit call setMachineOutputMode() once per
invocation with the parsed flags, resetting prior state so sequential
vitest cases don't leak mode.
- Spawn wrappers consult getMachineOutputMode() to decide whether to
apply flag forwarding and scrubbing.
Flag rename:
- --no-log → --quiet. Reframed description: "Route non-essential output
(status, progress, warnings) to stderr so stdout carries only the
payload. Implied by --json and --markdown."
Deletions:
- utils/output/no-log.mts + setNoLogMode/isNoLogMode module-level state.
- out() wrapper in utils/dry-run/output.mts — plain logger.error now.
Tests:
- 74 new unit tests across mode, emit-payload, scrubber, pipe-safety,
machine-mode.
- 134 existing test assertions updated to expect dry-run text on stderr
(mockLogger.error) rather than stdout (mockLogger.log).
- All 5,202 unit tests pass; type-check and lint clean.
Follow-ups tracked separately:
- Cross-repo: structured NDJSON event contract ("reason" field per
cargo's --message-format=json).
- Upstream: coana native --machine-output that routes logger to stderr;
zpm NO_COLOR respect; synp --quiet/--json flags.
- Full socket-cli-wide sweep of the remaining ~500 logger.log call sites
for stream-discipline conformance (lib-side fix to @socketsecurity/lib
will automate most of this).
e656d9b to
b20ec1e
Compare
Summary
Three-layer redesign that delivers a pipe-safe
--json/--markdowncontract across every command and every child process socket-cli spawns. Replaces an earlier narrow-scope attempt (module-levelnoLogMode+out()wrapper) that only fixed six dry-run formatters and would not survive handoff to child tools.The problem
socket fix --json --dry-run | jqbreaks because dry-run preview text routes to stdout instead of stderr. But that's the tip of the iceberg — the deeper failure modes are:logger.log(stdout) instead oflogger.error(stderr).The fix — three composable layers
Layer 1: stream discipline (CLAUDE.md rule + targeted audit)
CLAUDE.mdunder SHARED STANDARDS: stdout = the data the command was asked to produce; stderr = everything else.output-scan-report.mtsstatus lines,coana-fix.mts"Copying fixes result to …" message,cmd-oops.mtsdry-run preview all moved fromlogger.logtologger.error.Layer 2: per-(tool, subcommand) flag propagation
utils/spawn/machine-mode.mtsis a single source of truth for what flags and env vars each child tool needs under machine mode. Verified from each tool's own source:--silent --socket-mode <tempfile>(read file)--json --loglevel=erroron JSON-aware subcommandsNO_COLOR=1--reporter=json/--reporter=silentNO_COLOR=1--json --silentNO_COLOR=1--jsonwhere supportedYARN_ENABLE_PROGRESS_BARS=0,YARN_ENABLE_INLINE_BUILDS=0,YARN_ENABLE_MESSAGE_NAMES=0,YARN_ENABLE_COLORS=0,YARN_ENABLE_HYPERLINKS=0--jsonon supported cmds;--silenton install+add--view=json(uniform across subcommands)-qPIP_NO_COLOR=1--quiet-q--quiet --no-color-jsonon build/list/test/vetUniversal env vars on every tool:
NO_COLOR=1,FORCE_COLOR=0,CLICOLOR_FORCE=0.Layer 3: output scrubber
utils/output/scrubber.mtsis a Node Transform stream that:ansiRegex()from@socketsecurity/lib/ansi).JSON.parsesucceeds) → stdout.[INFO],npm notice, progress glyphs) → stderr.SOCKET_SCRUB_TRACE=1) for debugging.synp,zpm,gemadapters ship with this PR; each ~30 LOC.The scrubber is bounded (100 MB buffer cap; fallback to streaming passthrough if exceeded).
Layer 4: sentinel wrapping for our own payload
utils/output/emit-payload.mts'semitPayload()/emitJsonPayload()wraps socket-cli's own JSON/Markdown output in NUL-bracketed sentinels under machine mode. NUL bytes never appear in legitimate JSON or Markdown, so extraction is unambiguous.Plus: ambient mode context
utils/output/ambient-mode.mtslets spawn wrappers and output helpers consult the current mode without threading flags through every function signature. Set once at argv-parse time inmeowOrExit/meowWithSubcommands; reset between invocations to keep vitest cases isolated.Flag rename
--no-log→--quiet. New description: "Route non-essential output (status, progress, warnings) to stderr so stdout carries only the payload. Implied by--jsonand--markdown."What's deleted
utils/output/no-log.mts(module-levelnoLogModesingleton).out()wrapper inutils/dry-run/output.mts(now plainlogger.error).setNoLogMode/isNoLogModecallers.no-log.test.mts.Research provenance
Each child tool's forwarding rule was verified from local source checkouts or installed packages:
/Users/.../coana-package-manager/packages/cli/src/index.ts—--socket-mode <file>+--silent(Winstonsilent=true) confirmed./Users/.../berry/packages/plugin-essentials/sources/commands/*.ts—--jsonper-subcommand matrix confirmed;YARN_ENABLE_*env vars fromyarnpkg-core/Configuration.ts./Users/.../zpm/packages/zpm/src/commands/*.rs—--jsonper-subcommand matrix confirmed; no NO_COLOR respect.node_modules/synp@1.9.14/cli/*.js— no flags exist;console.log('Created …')to stdout on success; colored ANSI error on stderr unconditionally.--helpinspection.Test plan
pnpm run typecleanpnpm run lintcleanpnpm run build:clisucceedspnpm --filter @socketsecurity/cli run test:unit— 5,202 tests pass (74 new, 134 existing assertions updated to reflect stderr routing)Follow-ups (tracked separately, out of scope)
--message-format=jsonpattern) — per-tool Socket event types.--machine-outputflag that routes Winston logger to stderr and emits JSON to stdout.NO_COLOR/FORCE_COLORrespect.--quietand--json.logger.logcall sites for stream-discipline conformance (a lib-side fix to@socketsecurity/lib/loggerto routeinfo/step/progressto stderr will automate most of these).