Skip to content

Commit 6aece4f

Browse files
theFongclaude
andauthored
feat(cli): add pipe support to stop, start, delete, and ls commands (#283)
* feat(cli): add pipe support to stop, start, delete, and ls commands 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 * refactor: extract helper functions to reduce cognitive complexity - Extract trackLsAnalytics() in ls.go - Extract runBatchStart() and runSingleStart() in start.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract piping utilities to shared pkg/cmd/util/piping.go - 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 * fix: address PR review comments for piping-composability - 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 * feat: add exit code support for partial batch failures - 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 * feat: add exit code support for partial batch failures in stop * fix: only pipe output to stdout on full success 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. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ca0ca77 commit 6aece4f

5 files changed

Lines changed: 404 additions & 82 deletions

File tree

pkg/cmd/delete/delete.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package delete
33
import (
44
_ "embed"
55
"fmt"
6+
"os"
67
"strings"
78

89
"github.com/brevdev/brev-cli/pkg/cmd/completions"
@@ -18,7 +19,7 @@ import (
1819
var (
1920
//go:embed doc.md
2021
deleteLong string
21-
deleteExample = "brev delete <ws_name>"
22+
deleteExample = "brev delete <ws_name>...\necho instance-name | brev delete"
2223
)
2324

2425
type DeleteStore interface {
@@ -37,27 +38,41 @@ func NewCmdDelete(t *terminal.Terminal, loginDeleteStore DeleteStore, noLoginDel
3738
Example: deleteExample,
3839
ValidArgsFunction: completions.GetAllWorkspaceNameCompletionHandler(noLoginDeleteStore, t),
3940
RunE: func(cmd *cobra.Command, args []string) error {
41+
piped := util.IsStdoutPiped()
42+
names, err := util.GetInstanceNames(args)
43+
if err != nil {
44+
return breverrors.WrapAndTrace(err)
45+
}
4046
var allError error
41-
for _, workspace := range args {
42-
err := deleteWorkspace(workspace, t, loginDeleteStore)
47+
var deletedNames []string
48+
for _, workspace := range names {
49+
err := deleteWorkspace(workspace, t, loginDeleteStore, piped)
4350
if err != nil {
4451
allError = multierror.Append(allError, err)
52+
} else {
53+
deletedNames = append(deletedNames, workspace)
4554
}
4655
}
4756
if allError != nil {
4857
return breverrors.WrapAndTrace(allError)
4958
}
59+
// Only output names for piping if all succeeded
60+
if piped {
61+
for _, name := range deletedNames {
62+
fmt.Println(name)
63+
}
64+
}
5065
return nil
5166
},
5267
}
5368

5469
return cmd
5570
}
5671

57-
func deleteWorkspace(workspaceName string, t *terminal.Terminal, deleteStore DeleteStore) error {
72+
func deleteWorkspace(workspaceName string, t *terminal.Terminal, deleteStore DeleteStore, piped bool) error {
5873
workspace, err := util.GetUserWorkspaceByNameOrIDErr(deleteStore, workspaceName)
5974
if err != nil {
60-
err1 := handleAdminUser(err, deleteStore)
75+
err1 := handleAdminUser(err, deleteStore, piped)
6176
if err1 != nil {
6277
return breverrors.WrapAndTrace(err1)
6378
}
@@ -75,12 +90,14 @@ func deleteWorkspace(workspaceName string, t *terminal.Terminal, deleteStore Del
7590
return breverrors.WrapAndTrace(err)
7691
}
7792

78-
t.Vprintf("Deleting instance %s. This can take a few minutes. Run 'brev ls' to check status\n", deletedWorkspace.Name)
93+
if !piped {
94+
t.Vprintf("Deleting instance %s. This can take a few minutes. Run 'brev ls' to check status\n", deletedWorkspace.Name)
95+
}
7996

8097
return nil
8198
}
8299

83-
func handleAdminUser(err error, deleteStore DeleteStore) error {
100+
func handleAdminUser(err error, deleteStore DeleteStore, piped bool) error {
84101
if strings.Contains(err.Error(), "not found") {
85102
user, err1 := deleteStore.GetCurrentUser()
86103
if err1 != nil {
@@ -89,7 +106,9 @@ func handleAdminUser(err error, deleteStore DeleteStore) error {
89106
if user.GlobalUserType != "Admin" {
90107
return breverrors.WrapAndTrace(err)
91108
}
92-
fmt.Println("attempting to delete an instance you don't own as admin")
109+
if !piped {
110+
fmt.Fprintln(os.Stderr, "attempting to delete an instance you don't own as admin")
111+
}
93112
return nil
94113
}
95114

0 commit comments

Comments
 (0)