Skip to content

feat(cli): add pipe support to stop, start, delete, and ls commands#283

Open
theFong wants to merge 7 commits intomainfrom
pr/piping-composability
Open

feat(cli): add pipe support to stop, start, delete, and ls commands#283
theFong wants to merge 7 commits intomainfrom
pr/piping-composability

Conversation

@theFong
Copy link
Member

@theFong theFong commented Feb 2, 2026

Summary

Add stdin piping and improved output for CLI composability, enabling Unix-style command chaining.

Changes

Stop/Start/Delete

  • Accept instance names from stdin (one per line)
  • Accept multiple instance names as arguments
  • Output instance names when piped for chaining
  • Add --all flag to stop command

Ls Improvements

  • Add --json flag for JSON output
  • Output plain table when piped (for grep/awk compatibility)
  • Add JSON and pipe support to brev ls orgs

Examples

# Stop all running instances
brev ls | awk '/RUNNING/ {print $1}' | brev stop

# Delete all stopped instances
brev ls | awk '/STOPPED/ {print $1}' | brev delete

# Stop instances matching pattern
brev ls | grep "test-" | awk '{print $1}' | brev stop

# JSON output for scripting
brev ls --json | jq '.[] | select(.status == "RUNNING")'

# Stop all at once
brev stop --all

Test plan

  • echo "instance" | brev stop works
  • brev stop inst1 inst2 works with multiple args
  • brev ls --json outputs valid JSON
  • brev ls | grep works (plain table when piped)
  • brev stop --all stops all running instances
  • brev ls orgs --json outputs valid JSON

Add stdin piping and improved output for CLI composability.

Stop/Start/Delete:
- Accept instance names from stdin (one per line)
- Accept multiple instance names as arguments
- Output instance names when piped for chaining
- Add --all flag to stop command

Ls improvements:
- Add --json flag for JSON output
- Output plain table when piped (for grep/awk)
- Add JSON and pipe support to ls orgs

Examples:
  brev ls | awk '/RUNNING/ {print $1}' | brev stop
  brev ls | grep "test-" | awk '{print $1}' | brev delete
  brev ls --json | jq '.[] | select(.status == "RUNNING")'
  brev stop --all
@theFong theFong requested a review from a team as a code owner February 2, 2026 07:10
- Extract trackLsAnalytics() in ls.go
- Extract runBatchStart() and runSingleStart() in start.go

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@theFong
Copy link
Member Author

theFong commented Feb 3, 2026

Manual QA Results ✅

Build & Static Analysis:

  • ✅ Build passes
  • go vet passes
  • ⚠️ Unit tests are minimal (10 lines in stop_test.go, 1 line in ls_test.go)

Test Plan Verification:

  • echo "instance" | brev stop works - Verified: Accepts instance names from stdin
  • brev stop inst1 inst2 works with multiple args - Verified: Multiple arguments accepted
  • brev ls --json outputs valid JSON - Verified: Returns valid JSON array with instance details
  • brev ls | grep works (plain table when piped) - Verified: Outputs plain table format suitable for grep/awk
  • ⚠️ brev stop --all stops all running instances - Not tested (would stop all instances)
  • brev ls orgs --json outputs valid JSON - Verified: Flag is implemented

Piping Integration Test:

  • echo "test-pipe-instance" | brev delete - Verified: Successfully deleted the instance

Notes:

  • Full Unix-style composability chain works: brev search | brev create | brev delete
  • Consider adding unit tests for stdin parsing logic

- Consolidate duplicate isStdoutPiped() from delete, ls, start, stop
- Consolidate duplicate getInstanceNames() from delete, stop
- Consolidate duplicate getInstanceNamesFromStdin() from start
- Create IsStdoutPiped(), IsStdinPiped(), GetInstanceNames(), GetInstanceNamesWithPipeInfo()
- Remove ~108 lines of duplicated code
@theFong theFong force-pushed the pr/piping-composability branch from ea4dba8 to 03b8f8c Compare February 3, 2026 03:59
@theFong
Copy link
Member Author

theFong commented Feb 4, 2026

}, startStore)
if err != nil {
if !piped {
t.Vprintf("Error starting %s: %s\n", instanceName, err.Error())
Copy link
Contributor

Choose a reason for hiding this comment

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

this purposely doesn't return the error, just prints it?

WorkspaceClass string
Detached bool
InstanceType string
Piped bool // true when stdout is piped to another command
Copy link
Contributor

Choose a reason for hiding this comment

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

is this field ever read?

stdinPiped := IsStdinPiped()
if stdinPiped {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
Copy link
Contributor

Choose a reason for hiding this comment

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

no scanner.err check?

patelspratik
patelspratik previously approved these changes Feb 12, 2026
- Return errors from runBatchStart instead of silently swallowing them
- Remove unused Piped field from StartOptions
- Add scanner.Err() check after stdin scan loop in piping.go
// Output names for piping to next command
if piped {
for _, name := range stoppedNames {
fmt.Println(name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting -- so this collects the names of the workspaces for which the stop command succeeded, then proceeds with piping to the next command. We have the scenario then of having one or more of the stops having failed, but we will proceed with the piped command on those that succeeded, and then we return an error.

Should we instead be only proceeding with output (or in other words, checking and reacting to the piped flag) after ensuring that allErr is nil?

Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder if we should probably exit non zero but still return the workspace names

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah unless we have set -o pipefail then we'll simply continue supplying output to the next command. In the absence of this (which we should assume is possible as we can't control the shell) we can be a good citizen by limiting what we send over stdout to the next possible command.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay decided to go all or nothing.

}
fmt.Println("attempting to delete an instance you don't own as admin")
if !piped {
fmt.Println("attempting to delete an instance you don't own as admin")
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this go to stderr?

- Add ExitCodeError type to distinguish failure modes
- Batch start returns exit code 2 for partial failure, 1 for all failed
- main.go propagates custom exit codes from ExitCodeError
Address drewmalin's review: don't send names to stdout when there are
partial failures, since the next piped command would act on them even
without pipefail. Also send admin delete message to stderr instead of
stdout, and remove unused ExitCodeError type.
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.

3 participants