Skip to content

Commit ca0ca77

Browse files
theFongclaude
andauthored
feat(shell,open): add piping, multi-instance, and cross-platform support (#282)
* feat(shell,open): add piping, multi-instance, and cross-platform support Enhance brev shell and brev open commands with composability features. Shell improvements: - Add -c flag to run commands non-interactively - Support @filepath syntax to run local scripts remotely - Accept multiple instances or pipe from stdin - Output instance names for chaining Open improvements: - Support multiple instances (opens each in separate window) - Add terminal and tmux editor options - Add Terminal.app support on macOS - Fix WSL exec format error on Windows - Accept instances from stdin for piping Examples: brev shell my-instance -c "nvidia-smi" brev shell my-instance -c @setup.sh brev create my-instance | brev shell -c "nvidia-smi" brev open my-instance tmux brev create my-cluster --count 3 | brev open cursor * style: fix lint issues in open.go and util.go - Fix gofumpt formatting - Wrap external package errors with breverrors.WrapAndTrace Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(prd): document shell and open enhancements Add implemented features for brev shell and brev open: Shell enhancements: - Non-interactive -c flag for scripted execution - @filepath syntax to run local scripts remotely - Multi-instance support (run on multiple instances) - Stdin piping from brev create/ls - Output instance names for command chaining Open enhancements: - Multiple editor options (vscode, cursor, vim, terminal, tmux) - Multi-instance support (opens each in separate window) - Cross-platform fixes (macOS Terminal.app, WSL) - Stdin piping from brev create Also expands skills documentation explaining why they matter for agentic use cases. * docs(prd): clarify shell vs shell -c in pipeable commands table - shell: interactive, no stdin/stdout - shell -c: accepts instance names from stdin, outputs command stdout/stderr * feat: add brev exec command, keep shell interactive only Create new 'brev exec' command for non-interactive command execution: - Run commands on one or more instances - Support @filepath syntax to run local scripts remotely - Accept instance names from stdin for piping - Output instance names for command chaining Simplify 'brev shell' to be interactive only: - Remove -c flag (use 'brev exec' instead) - Single instance argument required - Keep --host flag for host SSH Update PRD to document the new command separation. * refactor: improve open.go error handling and consolidate editor commands open.go: - Add 'terminal' to valid editors in error message - Use multierror for better multi-instance error aggregation - Add scanner error check for stdin reading util.go: - Extract common runEditorCommand helper for WSL compatibility - Simplify runVsCodeCommand, runCursorCommand, runWindsurfCommand * feat(open): output instance names to stdout when piped Enable chaining with brev open by outputting instance names: brev create my-gpu | brev open cursor | brev exec 'pip install torch' Only outputs when stdout is piped, keeping interactive use clean. * docs(prd): add 'Why Now' section on coding agents and CLIs Coding agents prefer CLIs: text-native, self-documenting, composable, and already learned from training data. Positions Brev CLI as the default for autonomous GPU workflows. * refactor: deduplicate SSH/polling utils and fix bounds check Address PR review comments: - Extract waitForSSHToBeAvailable, pollUntil, startWorkspaceIfStopped into shared pkg/cmd/util/ssh.go to eliminate duplication between exec.go and shell.go - Add bounds check on strings.Split before accessing index 1 in waitForSSHToBeAvailable (prevents potential panic) - Add 10-minute timeout to pollUntil to prevent infinite hangs - Clean up openTerminalWithTmux to use _ in function signature * style: fix gofumpt lint in openTerminalWithTmux --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 272bf99 commit ca0ca77

8 files changed

Lines changed: 1028 additions & 156 deletions

File tree

docs/PRD-composable-cli.md

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# PRD: Composable & Agentic Brev CLI
2+
3+
## Vision
4+
5+
Make the Brev CLI idiomatic, programmable, and agent-friendly. Users and AI agents should be able to compose commands using standard Unix patterns (`|`, `grep`, `awk`, `jq`) while also having structured output options for programmatic access.
6+
7+
## Why Now
8+
9+
Coding agents (Claude Code, Cursor, Cline, Aider, OpenCode, Clawdbot) are becoming the primary interface between developers and their tools. These agents prefer CLIs over APIs:
10+
11+
- **Text-native** - LLMs think in text; pipes and grep are natural
12+
- **Self-documenting** - `--help` and tab completion beat reading API docs
13+
- **Composable** - Chain steps: `brev search | brev create | brev exec "setup.sh"`
14+
- **Learned from training** - Agents already know Unix conventions from GitHub/Stack Overflow
15+
16+
Most GPU clouds have dashboards and APIs, but weak CLIs. A composable Brev CLI becomes the default for autonomous GPU workflows.
17+
18+
## Goals
19+
20+
1. **Unix Idiomatic** - Commands work naturally with pipes and standard tools
21+
2. **Programmable** - JSON output mode for all commands that return data
22+
3. **Agentic** - Claude Code skills can orchestrate complex workflows
23+
4. **Composable** - Output of one command feeds into input of another
24+
25+
## Design Principles
26+
27+
### Pipe Detection
28+
- Commands detect when stdout is piped (`os.Stdout.Stat()`)
29+
- Piped output: clean table format (no colors, no help text)
30+
- Interactive output: colored, with contextual help
31+
32+
### Input Handling
33+
- Commands accept arguments directly OR from stdin
34+
- Stdin is read line-by-line when piped
35+
- First column of table input is parsed as the primary identifier
36+
37+
### Output Formats
38+
| Mode | Trigger | Format |
39+
|------|---------|--------|
40+
| Interactive | TTY | Colored table + help text |
41+
| Piped | `cmd \| ...` | Plain table (greppable) |
42+
| JSON | `--json` | Structured JSON array |
43+
44+
### Data Passthrough
45+
- Filter flags (e.g., `--min-disk`) should propagate through pipes
46+
- Table output includes computed fields (e.g., `TARGET_DISK`)
47+
- JSON output includes all relevant fields
48+
49+
## Implemented Features
50+
51+
### Pipeable Commands
52+
| Command | Stdin | Stdout (piped) | Status |
53+
|---------|-------|----------------|--------|
54+
| `brev ls` | - | Plain table ||
55+
| `brev ls orgs` | - | Plain table ||
56+
| `brev search` | - | Plain table w/ TARGET_DISK ||
57+
| `brev stop` | Instance names | Instance names ||
58+
| `brev start` | Instance names | Instance names ||
59+
| `brev delete` | Instance names | Instance names ||
60+
| `brev create` | Instance types (table or JSON) | Instance names ||
61+
| `brev shell` | - | - (interactive) ||
62+
| `brev exec` | Instance names | Command stdout/stderr ||
63+
| `brev open` | Instance names | Instance names ||
64+
65+
### Exec Command (`brev exec`)
66+
67+
Non-interactive command execution for scripted and agentic workflows.
68+
69+
**Run commands directly**:
70+
```bash
71+
brev exec my-gpu "nvidia-smi"
72+
brev exec my-gpu "python train.py && echo done"
73+
```
74+
75+
**Run local scripts remotely** (`@filepath` syntax):
76+
```bash
77+
brev exec my-gpu @setup.sh # Runs local setup.sh on remote
78+
brev exec my-gpu @scripts/deploy.sh # Relative paths supported
79+
```
80+
81+
**Multi-instance support**:
82+
```bash
83+
# Run on multiple instances
84+
brev exec gpu-1 gpu-2 gpu-3 "nvidia-smi"
85+
86+
# Pipe from create
87+
brev create my-cluster --count 3 | brev exec "nvidia-smi"
88+
89+
# Chain with other commands
90+
brev ls | grep RUNNING | brev exec "df -h"
91+
```
92+
93+
**Output for chaining**:
94+
Outputs instance names after execution completes, enabling pipelines:
95+
```bash
96+
brev create my-gpu | brev exec "pip install torch" | brev exec "python train.py"
97+
```
98+
99+
### Shell Command (`brev shell`)
100+
101+
Interactive SSH session to an instance. Use `brev exec` for non-interactive commands.
102+
103+
```bash
104+
brev shell my-gpu # Interactive shell
105+
brev shell $(brev create my-gpu) # Create and connect
106+
brev shell my-gpu --host # SSH to host instead of container
107+
```
108+
109+
### Open Enhancements (`brev open`)
110+
111+
Open instances in editors/terminals with multi-instance and cross-platform support.
112+
113+
**Editor options**:
114+
```bash
115+
brev open my-gpu vscode # VS Code (default)
116+
brev open my-gpu cursor # Cursor
117+
brev open my-gpu vim # Vim over SSH
118+
brev open my-gpu terminal # Terminal/SSH session
119+
brev open my-gpu tmux # Tmux session
120+
```
121+
122+
**Multi-instance support**:
123+
```bash
124+
# Open multiple instances (each in separate window)
125+
brev open gpu-1 gpu-2 gpu-3 cursor
126+
127+
# Pipe from create
128+
brev create my-cluster --count 3 | brev open cursor
129+
```
130+
131+
**Output for chaining**:
132+
Outputs instance names when piped, enabling pipelines:
133+
```bash
134+
# Create, open in editor, then run setup
135+
brev create my-gpu | brev open cursor | brev exec "pip install -r requirements.txt"
136+
```
137+
138+
**Cross-platform support**:
139+
- macOS: Terminal.app, iTerm2
140+
- Linux: Default terminal emulator
141+
- Windows/WSL: Fixed exec format errors
142+
143+
### Search Filters
144+
```bash
145+
brev search --gpu-name H100 # Filter by GPU
146+
brev search --min-vram 40 # Min VRAM per GPU
147+
brev search --min-total-vram 80 # Min total VRAM
148+
brev search --min-disk 500 # Min disk size (GB)
149+
brev search --max-boot-time 5 # Max boot time (minutes)
150+
brev search --stoppable # Can stop/restart
151+
brev search --rebootable # Can reboot
152+
brev search --flex-ports # Configurable firewall
153+
```
154+
155+
### JSON Mode
156+
```bash
157+
brev ls --json
158+
brev ls orgs --json
159+
brev search --json
160+
```
161+
162+
## Example Workflows
163+
164+
### Filter and Create
165+
```bash
166+
# Find stoppable H100s with 500GB disk, create first match
167+
brev search --min-disk 500 --stoppable | grep H100 | head -1 | brev create --name my-gpu
168+
```
169+
170+
### Batch Operations
171+
```bash
172+
# Stop all running instances
173+
brev ls | grep RUNNING | awk '{print $1}' | brev stop
174+
175+
# Delete all stopped instances
176+
brev ls | grep STOPPED | awk '{print $1}' | brev delete
177+
```
178+
179+
### Chained Lifecycle
180+
```bash
181+
# Create, use, cleanup
182+
brev search --gpu-name A100 | head -1 | brev create --name job-1 | brev exec "python train.py" && brev delete job-1
183+
```
184+
185+
### JSON Processing
186+
```bash
187+
# Get cheapest H100 with jq
188+
brev search --json | jq '[.[] | select(.gpu_name == "H100")] | sort_by(.price_per_hour) | .[0]'
189+
```
190+
191+
## Claude Code Integration
192+
193+
### Why Skills Matter
194+
195+
The composable CLI is necessary but not sufficient for agentic use. Skills bridge the gap between:
196+
197+
1. **Raw CLI** - Powerful but requires knowing exact flags and syntax
198+
2. **Natural Language** - How users actually describe intent
199+
200+
Without skills, an agent must:
201+
- Know that `--min-total-vram` exists (not `--vram`, `--gpu-memory`, etc.)
202+
- Remember flag combinations for common tasks
203+
- Handle error messages and retry logic
204+
- Understand which commands can be piped together
205+
206+
Skills encode this domain knowledge, turning "spin up a cheap GPU for testing" into the correct `brev search --stoppable --sort price | head -1 | brev create` pipeline.
207+
208+
### Skill Capabilities
209+
210+
The `/brev-cli` skill provides:
211+
212+
**Natural Language → CLI Translation**
213+
- "Create an A100 instance for ML training" → selects appropriate flags
214+
- "Find GPUs with 40GB VRAM under $2/hr" → `--min-total-vram 40` + price filter
215+
- "Stop all my running instances" → `brev ls | grep RUNNING | ... | brev stop`
216+
217+
**Context-Aware Defaults**
218+
- Knows common GPU requirements for ML workloads
219+
- Suggests `--stoppable` for dev instances (cost savings)
220+
- Recommends disk sizes based on use case
221+
222+
**Error Recovery**
223+
- Retries with fallback instance types on capacity errors
224+
- Suggests alternatives when requested GPU unavailable
225+
- Handles "instance already exists" gracefully
226+
227+
**Workflow Orchestration**
228+
- Multi-step operations (create → wait → execute → cleanup)
229+
- Monitors instance health during long-running jobs
230+
- Streams logs and captures results
231+
232+
### Agentic Patterns
233+
234+
With composable CLI + skills, agents can autonomously:
235+
236+
1. **Provision** - Search, filter, and create instances matching workload requirements
237+
2. **Deploy** - Stream code/data to instances via pipeable `cp`
238+
3. **Execute** - Run workloads via `brev exec`, capture output
239+
4. **Monitor** - Poll status via `brev ls --json`, stream logs
240+
5. **Scale** - Spin up parallel instances, distribute work
241+
6. **Cleanup** - Stop/delete instances, manage costs
242+
243+
### Example: Autonomous Training Job
244+
245+
```
246+
User: "Train my model on an H100, save checkpoints every hour"
247+
248+
Agent:
249+
1. brev search --gpu-name H100 --stoppable --min-disk 500 | head -1 | brev create --name training-job
250+
2. brev wait training-job --state ready
251+
3. tar czf - ./src | brev cp - training-job:/app/
252+
4. brev exec training-job "cd /app && python train.py --checkpoint-interval 3600"
253+
5. brev cp training-job:/app/checkpoints - | tar xzf - -C ./results/
254+
6. brev delete training-job
255+
```
256+
257+
The skill handles the translation, error recovery, and orchestration—the composable CLI makes each step possible.
258+
259+
## Future Considerations
260+
261+
### Planned
262+
263+
#### `brev logs` - Stream/tail instance logs
264+
```bash
265+
brev logs my-gpu # Follow logs
266+
brev logs my-gpu --since 5m # Last 5 minutes
267+
brev logs my-gpu | grep ERROR # Filter logs
268+
```
269+
270+
#### `brev wait` - Block until instance reaches state
271+
```bash
272+
brev create --name my-gpu ... && brev wait my-gpu --state ready
273+
brev stop my-gpu && brev wait my-gpu --state stopped
274+
```
275+
276+
#### `brev cp` - Pipeable file copy (stdin/stdout)
277+
278+
Stream data directly through stdin/stdout without intermediate files. Uses `-` to indicate stdin/stdout (standard Unix convention).
279+
280+
**Current behavior** (requires temp files):
281+
```bash
282+
brev cp local.tar.gz my-gpu:/data/
283+
brev cp my-gpu:/results/output.csv ./output.csv
284+
```
285+
286+
**Proposed pipeable behavior**:
287+
```bash
288+
# Stream archive directly to instance
289+
tar czf - ./data | brev cp - my-gpu:/data/archive.tar.gz
290+
291+
# Pipe file content to instance
292+
cat model.pt | brev cp - my-gpu:/models/model.pt
293+
294+
# Stream from instance and process locally
295+
brev cp my-gpu:/results/output.csv - | grep "success" > filtered.csv
296+
297+
# Transfer between instances without local storage
298+
brev cp gpu-1:/checkpoint.pt - | brev cp - gpu-2:/checkpoint.pt
299+
```
300+
301+
**Agentic use cases**:
302+
```bash
303+
# Agent streams training data, captures results
304+
cat dataset.jsonl | brev exec my-gpu "python train.py" > results.log
305+
306+
# Agent deploys code without temp files
307+
tar czf - ./src | brev cp - my-gpu:/app/src.tar.gz
308+
309+
# Agent extracts specific results
310+
brev cp my-gpu:/logs/metrics.json - | jq '.accuracy'
311+
```

pkg/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/brevdev/brev-cli/pkg/cmd/copy"
1313
"github.com/brevdev/brev-cli/pkg/cmd/delete"
1414
"github.com/brevdev/brev-cli/pkg/cmd/envvars"
15+
"github.com/brevdev/brev-cli/pkg/cmd/exec"
1516
"github.com/brevdev/brev-cli/pkg/cmd/fu"
1617
"github.com/brevdev/brev-cli/pkg/cmd/gpucreate"
1718
"github.com/brevdev/brev-cli/pkg/cmd/gpusearch"
@@ -276,6 +277,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
276277
cmd.AddCommand(configureenvvars.NewCmdConfigureEnvVars(t, loginCmdStore))
277278
cmd.AddCommand(importideconfig.NewCmdImportIDEConfig(t, noLoginCmdStore))
278279
cmd.AddCommand(shell.NewCmdShell(t, loginCmdStore, noLoginCmdStore))
280+
cmd.AddCommand(exec.NewCmdExec(t, loginCmdStore, noLoginCmdStore))
279281
cmd.AddCommand(copy.NewCmdCopy(t, loginCmdStore, noLoginCmdStore))
280282
cmd.AddCommand(open.NewCmdOpen(t, loginCmdStore, noLoginCmdStore))
281283
cmd.AddCommand(ollama.NewCmdOllama(t, loginCmdStore))

pkg/cmd/cmderrors/cmderrors.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func DisplayAndHandleError(err error) {
3939
case *breverrors.NvidiaMigrationError:
4040
// Handle nvidia migration error
4141
if nvErr, ok := errors.Cause(err).(*breverrors.NvidiaMigrationError); ok {
42-
fmt.Println("\n This account has been migrated to NVIDIA Auth. Attempting to log in with NVIDIA account...")
42+
fmt.Fprintln(os.Stderr, "\n This account has been migrated to NVIDIA Auth. Attempting to log in with NVIDIA account...")
4343
brevBin, err1 := os.Executable()
4444
if err1 == nil {
4545
cmd := exec.Command(brevBin, "login", "--auth", "nvidia") // #nosec G204
@@ -68,9 +68,9 @@ func DisplayAndHandleError(err error) {
6868
}
6969
}
7070
if featureflag.Debug() || featureflag.IsDev() {
71-
fmt.Println(err)
71+
fmt.Fprintln(os.Stderr, err)
7272
} else {
73-
fmt.Println(prettyErr)
73+
fmt.Fprintln(os.Stderr, prettyErr)
7474
}
7575
}
7676
}

0 commit comments

Comments
 (0)