Skip to content

feat(cli): machine-output mode — stream discipline, flag propagation, scrubber#1234

Open
John-David Dalton (jdalton) wants to merge 1 commit intomainfrom
fix/no-log-flag-automation
Open

feat(cli): machine-output mode — stream discipline, flag propagation, scrubber#1234
John-David Dalton (jdalton) wants to merge 1 commit intomainfrom
fix/no-log-flag-automation

Conversation

@jdalton
Copy link
Copy Markdown
Contributor

@jdalton John-David Dalton (jdalton) commented Apr 18, 2026

Summary

Three-layer redesign that delivers a pipe-safe --json / --markdown contract across every command and every child process socket-cli spawns. Replaces an earlier narrow-scope attempt (module-level noLogMode + out() wrapper) that only fixed six dry-run formatters and would not survive handoff to child tools.

The problem

socket fix --json --dry-run | jq breaks because dry-run preview text routes to stdout instead of stderr. But that's the tip of the iceberg — the deeper failure modes are:

  1. Child-process stdout contamination: when socket-cli pipes coana/sfw/npm/pnpm/yarn/pip/etc. stdout into its own, any chatter those tools emit lands in the captured payload.
  2. Inconsistent stream discipline in socket-cli itself: status messages, warnings, and previews written to logger.log (stdout) instead of logger.error (stderr).
  3. No coverage for tools that violate the Unix convention (yarn berry/yarn 6 write progress to stdout; synp/gem/cdxgen have no TTY check).

The fix — three composable layers

Layer 1: stream discipline (CLAUDE.md rule + targeted audit)

  • New rule in CLAUDE.md under SHARED STANDARDS: stdout = the data the command was asked to produce; stderr = everything else.
  • Audit: dry-run previews, output-scan-report.mts status lines, coana-fix.mts "Copying fixes result to …" message, cmd-oops.mts dry-run preview all moved from logger.log to logger.error.

Layer 2: per-(tool, subcommand) flag propagation

utils/spawn/machine-mode.mts is 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:

Tool Forwarded flags Env
coana --silent --socket-mode <tempfile> (read file)
sfw Forward to inner PM (transparent)
npm --json --loglevel=error on JSON-aware subcommands NO_COLOR=1
pnpm --reporter=json / --reporter=silent NO_COLOR=1
yarn classic --json --silent NO_COLOR=1
yarn berry (v4) --json where supported YARN_ENABLE_PROGRESS_BARS=0, YARN_ENABLE_INLINE_BUILDS=0, YARN_ENABLE_MESSAGE_NAMES=0, YARN_ENABLE_COLORS=0, YARN_ENABLE_HYPERLINKS=0
yarn 6 / zpm --json on supported cmds; --silent on install+add
vltpkg --view=json (uniform across subcommands)
pip/pip3 -q PIP_NO_COLOR=1
uv --quiet
cargo -q
gem --quiet --no-color
go -json on build/list/test/vet

Universal env vars on every tool: NO_COLOR=1, FORCE_COLOR=0, CLICOLOR_FORCE=0.

Layer 3: output scrubber

utils/output/scrubber.mts is a Node Transform stream that:

  1. Strips BOM and ANSI escapes (OSC + CSI, via ansiRegex() from @socketsecurity/lib/ansi).
  2. Splits input on newlines (handles partial lines across chunks).
  3. Classifies each line:
    • NUL-sentinel-bracketed span → stdout (socket-cli's own payload, zero ambiguity).
    • Valid JSON (JSON.parse succeeds) → stdout.
    • Known noise pattern ([INFO], npm notice, progress glyphs) → stderr.
    • Unknown → stderr (safe default — payload stays clean).
  4. Exposes a tracing mode (SOCKET_SCRUB_TRACE=1) for debugging.
  5. Supports tool-specific adapters (jc-inspired): synp, zpm, gem adapters 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's emitPayload() / 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.mts lets spawn wrappers and output helpers consult the current mode without threading flags through every function signature. Set once at argv-parse time in meowOrExit / 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 --json and --markdown."

What's deleted

  • utils/output/no-log.mts (module-level noLogMode singleton).
  • out() wrapper in utils/dry-run/output.mts (now plain logger.error).
  • setNoLogMode/isNoLogMode callers.
  • no-log.test.mts.

Research provenance

Each child tool's forwarding rule was verified from local source checkouts or installed packages:

  • coana: /Users/.../coana-package-manager/packages/cli/src/index.ts--socket-mode <file> + --silent (Winston silent=true) confirmed.
  • yarn berry: /Users/.../berry/packages/plugin-essentials/sources/commands/*.ts--json per-subcommand matrix confirmed; YARN_ENABLE_* env vars from yarnpkg-core/Configuration.ts.
  • yarn 6 / zpm: /Users/.../zpm/packages/zpm/src/commands/*.rs--json per-subcommand matrix confirmed; no NO_COLOR respect.
  • synp: node_modules/synp@1.9.14/cli/*.js — no flags exist; console.log('Created …') to stdout on success; colored ANSI error on stderr unconditionally.
  • npm/pnpm/pip/uv/cargo/gem/go/vltpkg: public documentation + manual --help inspection.

Test plan

  • pnpm run type clean
  • pnpm run lint clean
  • pnpm run build:cli succeeds
  • pnpm --filter @socketsecurity/cli run test:unit — 5,202 tests pass (74 new, 134 existing assertions updated to reflect stderr routing)
  • CI green

Follow-ups (tracked separately, out of scope)

  • Cross-repo structured NDJSON event contract (cargo's --message-format=json pattern) — per-tool Socket event types.
  • Coana upstream: native --machine-output flag that routes Winston logger to stderr and emits JSON to stdout.
  • zpm upstream: add NO_COLOR/FORCE_COLOR respect.
  • synp upstream: add --quiet and --json.
  • Full socket-cli sweep of the remaining ~500 logger.log call sites for stream-discipline conformance (a lib-side fix to @socketsecurity/lib/logger to route info/step/progress to stderr will automate most of these).

Comment thread packages/cli/src/utils/output/no-log.mts Outdated
John-David Dalton (jdalton) added a commit that referenced this pull request Apr 18, 2026
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.
@jdalton
Copy link
Copy Markdown
Contributor Author

Cursor (@cursor) review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@jdalton
Copy link
Copy Markdown
Contributor Author

Cursor (@cursor) review

Comment thread packages/cli/src/utils/cli/with-subcommands.mts Outdated
@jdalton
Copy link
Copy Markdown
Contributor Author

Cursor (@cursor) review

1 similar comment
@jdalton
Copy link
Copy Markdown
Contributor Author

Cursor (@cursor) review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@jdalton
Copy link
Copy Markdown
Contributor Author

Cursor (@cursor) review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@jdalton
Copy link
Copy Markdown
Contributor Author

Cursor (@cursor) review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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).
@jdalton John-David Dalton (jdalton) changed the title feat(cli): add --no-log flag and keep stdout clean under --json feat(cli): machine-output mode — stream discipline, flag propagation, scrubber Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants