Skip to content

Commit aef82b4

Browse files
committed
feat: add brev open codex for remote Codex CLI sessions
1 parent 782baf9 commit aef82b4

2 files changed

Lines changed: 120 additions & 7 deletions

File tree

pkg/cmd/open/open.go

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const (
4040
EditorTerminal = "terminal"
4141
EditorTmux = "tmux"
4242
EditorClaude = "claude"
43+
EditorCodex = "codex"
4344
)
4445

4546
var (
@@ -52,6 +53,7 @@ Supported editors:
5253
terminal - Opens a new terminal window with SSH
5354
tmux - Opens a new terminal window with SSH + tmux session
5455
claude - Claude Code in a tmux session (auto-installs, auto-authenticates)
56+
codex - Codex CLI in a tmux session (auto-installs, auto-authenticates)
5557
5658
Terminal support by platform:
5759
macOS: Terminal.app
@@ -105,7 +107,11 @@ You must have the editor installed in your path.`
105107
106108
# Pass flags through to Claude Code (use -- to separate brev flags from claude flags)
107109
brev open my-instance claude -- --model opus --allowedTools computer
108-
brev open my-instance claude -- -p "fix the tests"`
110+
brev open my-instance claude -- -p "fix the tests"
111+
112+
# Open Codex CLI on a remote instance (installs if needed, auto-authenticates with OPENAI_API_KEY)
113+
brev open my-instance codex
114+
brev open my-instance codex -- --model o3`
109115
)
110116

111117
type OpenStore interface {
@@ -151,7 +157,7 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto
151157

152158
// Validate editor flag if provided
153159
if editor != "" && !isEditorType(editor) {
154-
return breverrors.NewValidationError(fmt.Sprintf("invalid editor: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', or 'claude'", editor))
160+
return breverrors.NewValidationError(fmt.Sprintf("invalid editor: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', 'claude', or 'codex'", editor))
155161
}
156162

157163
// Get instance names and editor type from args or stdin
@@ -194,15 +200,15 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto
194200
cmd.Flags().BoolVarP(&host, "host", "", false, "ssh into the host machine instead of the container")
195201
cmd.Flags().BoolVarP(&waitForSetupToFinish, "wait", "w", false, "wait for setup to finish")
196202
cmd.Flags().StringVarP(&directory, "dir", "d", "", "directory to open")
197-
cmd.Flags().StringVar(&setDefault, "set-default", "", "set default editor (code, cursor, windsurf, terminal, tmux, or claude)")
198-
cmd.Flags().StringVarP(&editor, "editor", "e", "", "editor to use (code, cursor, windsurf, terminal, tmux, or claude)")
203+
cmd.Flags().StringVar(&setDefault, "set-default", "", "set default editor (code, cursor, windsurf, terminal, tmux, claude, or codex)")
204+
cmd.Flags().StringVarP(&editor, "editor", "e", "", "editor to use (code, cursor, windsurf, terminal, tmux, claude, or codex)")
199205

200206
return cmd
201207
}
202208

203209
// isEditorType checks if a string is a valid editor type
204210
func isEditorType(s string) bool {
205-
return s == EditorVSCode || s == EditorCursor || s == EditorWindsurf || s == EditorTerminal || s == EditorTmux || s == EditorClaude
211+
return s == EditorVSCode || s == EditorCursor || s == EditorWindsurf || s == EditorTerminal || s == EditorTmux || s == EditorClaude || s == EditorCodex
206212
}
207213

208214
// isPiped returns true if stdout is piped to another command
@@ -277,7 +283,7 @@ func getInstanceNamesAndEditor(args []string, editorFlag string) ([]string, stri
277283

278284
func handleSetDefault(t *terminal.Terminal, editorType string) error {
279285
if !isEditorType(editorType) {
280-
return fmt.Errorf("invalid editor type: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', or 'claude'", editorType)
286+
return fmt.Errorf("invalid editor type: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', 'claude', or 'codex'", editorType)
281287
}
282288

283289
homeDir, err := os.UserHomeDir()
@@ -382,6 +388,9 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
382388
if strings.Contains(err.Error(), "failed to install Claude Code") {
383389
return breverrors.WrapAndTrace(err)
384390
}
391+
if strings.Contains(err.Error(), "failed to install Codex") {
392+
return breverrors.WrapAndTrace(err)
393+
}
385394
return breverrors.WrapAndTrace(err)
386395
}
387396
// Call analytics for open
@@ -555,6 +564,8 @@ func getEditorName(editorType string) string {
555564
return "tmux"
556565
case EditorClaude:
557566
return "Claude Code"
567+
case EditorCodex:
568+
return "Codex"
558569
default:
559570
return "VSCode"
560571
}
@@ -589,6 +600,8 @@ func openEditorByType(t *terminal.Terminal, editorType string, sshAlias string,
589600
return openTerminalWithTmux(sshAlias, path, tstore)
590601
case EditorClaude:
591602
return openClaude(t, sshAlias, path, editorArgs)
603+
case EditorCodex:
604+
return openCodex(t, sshAlias, path, editorArgs)
592605
default:
593606
tryToInstallExtensions(t, extensions)
594607
return openVsCode(sshAlias, path, tstore)
@@ -962,3 +975,102 @@ func ensureClaudeInstalled(t *terminal.Terminal, sshAlias string) error {
962975
t.Vprintf("%s", t.Green("Claude Code installed successfully\n"))
963976
return nil
964977
}
978+
979+
func openCodex(t *terminal.Terminal, sshAlias string, path string, codexArgs []string) error {
980+
// Ensure tmux is available on remote
981+
err := ensureTmuxInstalled(sshAlias)
982+
if err != nil {
983+
return breverrors.WrapAndTrace(fmt.Errorf("tmux: command not found"))
984+
}
985+
986+
// Install Codex remotely if not present
987+
err = ensureCodexInstalled(t, sshAlias)
988+
if err != nil {
989+
return breverrors.WrapAndTrace(err)
990+
}
991+
992+
// Auto-authenticate: only forward a key if the remote is not already logged in
993+
apiKey := resolveCodexAPIKey(t, sshAlias)
994+
995+
sessionName := "codex"
996+
997+
var envExport string
998+
if apiKey != "" {
999+
envExport = fmt.Sprintf("export OPENAI_API_KEY=%s; ", shellescape.Quote(apiKey))
1000+
}
1001+
1002+
// Build the codex command with any extra flags
1003+
codexCmd := "codex"
1004+
if len(codexArgs) > 0 {
1005+
codexCmd = "codex " + strings.Join(codexArgs, " ")
1006+
}
1007+
1008+
// Prepend installer paths, set env if needed, then attach-or-create tmux session
1009+
remoteScript := fmt.Sprintf(
1010+
`export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"; %stmux has-session -t %s 2>/dev/null && tmux attach-session -t %s || (cd %s && tmux new-session -s %s %s)`,
1011+
envExport, sessionName, sessionName, shellescape.Quote(path), sessionName, shellescape.Quote(codexCmd),
1012+
)
1013+
1014+
// Run SSH inline in the current terminal (interactive, with TTY)
1015+
sshCmd := exec.Command("ssh", "-t", sshAlias, remoteScript) // #nosec G204
1016+
sshCmd.Stdin = os.Stdin
1017+
sshCmd.Stdout = os.Stdout
1018+
sshCmd.Stderr = os.Stderr
1019+
1020+
err = sshCmd.Run()
1021+
if err != nil {
1022+
return breverrors.WrapAndTrace(err)
1023+
}
1024+
return nil
1025+
}
1026+
1027+
// resolveCodexAPIKey returns an API key to forward to the remote, or "" if
1028+
// the remote is already authenticated or no local key can be found.
1029+
func resolveCodexAPIKey(t *terminal.Terminal, sshAlias string) string {
1030+
// Check if remote already has OPENAI_API_KEY set
1031+
if isRemoteCodexAuthenticated(sshAlias) {
1032+
return ""
1033+
}
1034+
1035+
// Check local OPENAI_API_KEY env var
1036+
if key := os.Getenv("OPENAI_API_KEY"); key != "" {
1037+
t.Vprintf("%s", t.Green("Forwarding OPENAI_API_KEY to remote instance\n"))
1038+
return key
1039+
}
1040+
1041+
return ""
1042+
}
1043+
1044+
// isRemoteCodexAuthenticated checks whether the remote already has
1045+
// OPENAI_API_KEY set in the shell.
1046+
func isRemoteCodexAuthenticated(sshAlias string) bool {
1047+
checkCmd := exec.Command(
1048+
"ssh", sshAlias,
1049+
`printenv OPENAI_API_KEY >/dev/null 2>&1`,
1050+
) // #nosec G204
1051+
return checkCmd.Run() == nil
1052+
}
1053+
1054+
func ensureCodexInstalled(t *terminal.Terminal, sshAlias string) error {
1055+
checkCmd := fmt.Sprintf(
1056+
"ssh %s 'export PATH=\"$HOME/.local/bin:$HOME/.npm-global/bin:$PATH\"; which codex >/dev/null 2>&1'",
1057+
sshAlias,
1058+
)
1059+
checkExec := exec.Command("bash", "-c", checkCmd) // #nosec G204
1060+
err := checkExec.Run()
1061+
if err == nil {
1062+
return nil // already installed
1063+
}
1064+
1065+
t.Vprintf("Installing Codex CLI on remote instance...\n")
1066+
1067+
installCmd := fmt.Sprintf("ssh %s 'npm install -g @openai/codex 2>/dev/null || sudo npm install -g @openai/codex'", sshAlias)
1068+
installExec := exec.Command("bash", "-c", installCmd) // #nosec G204
1069+
output, err := installExec.CombinedOutput()
1070+
if err != nil {
1071+
return fmt.Errorf("failed to install Codex: %s\n%s", err, string(output))
1072+
}
1073+
1074+
t.Vprintf("%s", t.Green("Codex CLI installed successfully\n"))
1075+
return nil
1076+
}

pkg/cmd/open/open_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
)
66

77
func TestIsEditorType(t *testing.T) {
8-
valid := []string{"code", "cursor", "windsurf", "terminal", "tmux", "claude"}
8+
valid := []string{"code", "cursor", "windsurf", "terminal", "tmux", "claude", "codex"}
99
for _, v := range valid {
1010
if !isEditorType(v) {
1111
t.Errorf("expected %q to be valid editor type", v)
@@ -31,6 +31,7 @@ func TestGetEditorName(t *testing.T) {
3131
{"terminal", "Terminal"},
3232
{"tmux", "tmux"},
3333
{"claude", "Claude Code"},
34+
{"codex", "Codex"},
3435
{"unknown", "VSCode"},
3536
}
3637
for _, tt := range tests {

0 commit comments

Comments
 (0)