feat(cli): detect piped stdout and output content-only for inspect#551
feat(cli): detect piped stdout and output content-only for inspect#551fran wants to merge 1 commit intoendorhq:mainfrom
Conversation
- 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
|
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 I would like to know if use |
ereslibre
left a comment
There was a problem hiding this comment.
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 | catI get it does not work but when I usenode packages/cli/dist/index.mjs inspect 10 | catit 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:
- 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.
- Suppress tips in
exitWithError/exitWithSuccesswhen 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.
- 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.
- Move
isStdoutTTYinto 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.
There was a problem hiding this comment.
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.
When stdout is piped or redirected (e.g.
rover inspect 10 | catorrover 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
packages/cli/src/utils/stdout.tswithisStdoutTTY()andisStdoutPiped()for stdout TTY detection!isStdoutTTY()(in addition to JSON and MCP mode)inspect, when stdout is not a TTY: output only the raw content of the requested or default iteration file(s) viaconsole.log; noshowTitle,showProperties,showList,showFile, orshowTipsisStdoutTTYto assert content-only output when piped and no "Details" / "Workspace" / box-drawing charactersNotes
--jsonand--raw-filebehavior is unchangednode packages/cli/dist/index.mjs inspect <taskId> | catafterpnpm buildto ensure the local CLI is used (globalrovermay be an older version)