Skip to content

feat(cli): detect piped stdout and output content-only for inspect#551

Draft
fran wants to merge 1 commit intoendorhq:mainfrom
fran:5/detect-stdout-piped
Draft

feat(cli): detect piped stdout and output content-only for inspect#551
fran wants to merge 1 commit intoendorhq:mainfrom
fran:5/detect-stdout-piped

Conversation

@fran
Copy link
Contributor

@fran fran commented Mar 4, 2026

When stdout is piped or redirected (e.g. rover inspect 10 | cat or rover inspect --file=docs.md 6 > out.txt), the CLI now outputs only the raw content: no Rover header, no Details/Workspace sections, no box around file content, no tips. In an interactive terminal, behavior is unchanged (full formatted output).

Closes #456.

Changes

  • Added packages/cli/src/utils/stdout.ts with isStdoutTTY() and isStdoutPiped() for stdout TTY detection
  • Skip Rover header in the program preAction hook when !isStdoutTTY() (in addition to JSON and MCP mode)
  • In inspect, when stdout is not a TTY: output only the raw content of the requested or default iteration file(s) via console.log; no showTitle, showProperties, showList, showFile, or showTips
  • Added unit tests that mock isStdoutTTY to assert content-only output when piped and no "Details" / "Workspace" / box-drawing characters

Notes

  • --json and --raw-file behavior is unchanged
  • To verify: run node packages/cli/dist/index.mjs inspect <taskId> | cat after pnpm build to ensure the local CLI is used (global rover may be an older version)

- Add packages/cli/src/utils/stdout.ts with isStdoutTTY/isStdoutPiped
- Skip Rover header in program when stdout is not a TTY
- In inspect, output only raw file content when stdout is piped (no Details, Workspace, boxes, tips)
- Add tests for piped stdout behavior in inspect.test.ts

Fixes endorhq#456
@fran fran marked this pull request as draft March 4, 2026 16:39
@fran
Copy link
Contributor Author

fran commented Mar 4, 2026

hey @ereslibre - I have what I think it is a good question here. Maybe the approach I'm following it is not the correct because when I tested locally I get using something like
ROVER_AGENT_IMAGE=ghcr.io/endorhq/rover/agent-dev:wip2 rover inspect 10 | cat
I get it does not work but when I use
node packages/cli/dist/index.mjs inspect 10 | cat
it works.

I would like to know if use process.stdout.isTTY does it make sense because when it is run inside a container I get TTY is false.
Maybe I'm missing something

Copy link
Collaborator

@ereslibre ereslibre left a comment

Choose a reason for hiding this comment

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

Thanks for working on this!

Maybe the approach I'm following it is not the correct because when I tested locally I get using something like ROVER_AGENT_IMAGE=ghcr.io/endorhq/rover/agent-dev:wip2 rover inspect 10 | cat I get it does not work but when I use node packages/cli/dist/index.mjs inspect 10 | cat it works.

This is probably because you are not installing the built version, so that when you run rover, it gets executed from your $PATH, and is the old rover version (not the one you built).

I usually do: alias rover=~/projects/endorhq/rover/packages/cli/dist/index.js, so that it's easier to also alias to another path if I'm in a specific git worktree etc, but this might vary depending on your workflow :)

I would like to know if use process.stdout.isTTY does it make sense because when it is run inside a container I get TTY is false.

This is expected, docker/podman by default don't allocate TTY's; it's needed to provide the -t flag so that they allocate them:

❯ docker run --rm node:latest -e "console.log(process.stdout.isTTY)"
undefined
❯ docker run --rm -t node:latest -e "console.log(process.stdout.isTTY)"
true

However, for the logic we are working on in this changeset, we are not yet calling docker/podman, we are checking if there's a TTY for the user console, and adapting our own output depending on that.

The TTY detection for inspect works well, but the approach can be generalized so that every command can benefit from it consistently, rather than each command implementing its own ad-hoc branching logic.

One possible approach would be:

  1. Make the display utilities TTY-aware

Instead of each command wrapping every showTitle/showProperties/showList/showFile/showTips call in if (isStdoutTTY()) { ... } blocks, the display functions themselves should become no-ops when stdout is piped. This way, existing and future commands get correct piped behavior for free — decorations simply don't render.

For example, in packages/core/src/display/:

  export const showTitle = (title: string): void => {
    if (!process.stdout.isTTY) return;
    // ... existing implementation
  };

Apply the same to showProperties, showList, showFile, showTips, showDiagram, and showRoverHeader.

  1. Suppress tips in exitWithError/exitWithSuccess when piped

The exit utilities already skip tips in JSON mode — add the same guard for piped stdout so that tips are never leaked into piped output regardless of which command calls them.

  1. Each command defines its "piped content" explicitly

With display utilities silenced automatically, commands only need a small block for their piped-specific output — the meaningful content that downstream consumers actually want. The pattern becomes:

  if (isJsonMode()) {
    // Structured JSON — already handled
  } else if (!isStdoutTTY()) {
    // Content-only: output just what matters for piping
    console.log(rawContent);
  } else {
    // Interactive TTY: full formatted output with showTitle, showProperties, etc.
    // (these calls are safe in all paths since they'll no-op when piped,
    //  but grouping them here keeps intent clear)
  }

This is a lightweight three-tier pattern that's easy to follow in every command.

  1. Move isStdoutTTY into the shared CLI context

Rather than importing from utils/stdout.ts in each command, integrate it into the CLI context alongside isJsonMode() — e.g. expose an isPipedMode() from context.ts that returns !isJsonMode() && !isStdoutTTY(). This gives commands a single check for "should I output content-only?", and keeps the mode logic centralized.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Another interesting command to adapt would be diff.ts.

The idea is that if I do rover diff <taskId>, I get a decorated output, but if I do rover diff <taskId> | cat I get only the patch itself.

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.

feat: detect when stdout is being piped to another program and write just content in that case

2 participants