Skip to content

Commit 5b555fb

Browse files
Add tmux support to brev open command (#243)
* Add tmux support to brev open command - Add EditorTmux constant and update all validation functions - Implement openTmux() with session management (brev-<workspace> naming) - Add ensureTmuxInstalled() for automatic tmux installation - Follow same patterns as VSCode/Cursor/Windsurf implementations - Support session reconnection and creation with proper working directory - Handle tmux-specific error cases and installation requirements - Use interactive SSH execution for seamless tmux session access Co-Authored-By: Alec Fong <[email protected]> * Simplify tmux session name to 'brev' for consistency - Change session name from 'brev-<workspace>' to just 'brev' - Ensures users always connect to same session regardless of workspace name changes - Addresses PR feedback from @theFong Co-Authored-By: Alec Fong <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alec Fong <[email protected]>
1 parent b603505 commit 5b555fb

File tree

1 file changed

+69
-8
lines changed

1 file changed

+69
-8
lines changed

pkg/cmd/open/open.go

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ const (
3333
EditorVSCode = "code"
3434
EditorCursor = "cursor"
3535
EditorWindsurf = "windsurf"
36+
EditorTmux = "tmux"
3637
)
3738

3839
var (
39-
openLong = "[command in beta] This will open VS Code, Cursor, or Windsurf SSH-ed in to your instance. You must have the editor installed in your path."
40-
openExample = "brev open instance_id_or_name\nbrev open instance\nbrev open instance code\nbrev open instance cursor\nbrev open instance windsurf\nbrev open --set-default cursor\nbrev open --set-default windsurf"
40+
openLong = "[command in beta] This will open VS Code, Cursor, Windsurf, or tmux SSH-ed in to your instance. You must have the editor installed in your path."
41+
openExample = "brev open instance_id_or_name\nbrev open instance\nbrev open instance code\nbrev open instance cursor\nbrev open instance windsurf\nbrev open instance tmux\nbrev open --set-default cursor\nbrev open --set-default windsurf\nbrev open --set-default tmux"
4142
)
4243

4344
type OpenStore interface {
@@ -63,7 +64,7 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto
6364
Annotations: map[string]string{"ssh": ""},
6465
Use: "open",
6566
DisableFlagsInUseLine: true,
66-
Short: "[beta] open VSCode, Cursor, or Windsurf to your instance",
67+
Short: "[beta] open VSCode, Cursor, Windsurf, or tmux to your instance",
6768
Long: openLong,
6869
Example: openExample,
6970
Args: cmderrors.TransformToValidationError(func(cmd *cobra.Command, args []string) error {
@@ -99,14 +100,14 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto
99100
cmd.Flags().BoolVarP(&host, "host", "", false, "ssh into the host machine instead of the container")
100101
cmd.Flags().BoolVarP(&waitForSetupToFinish, "wait", "w", false, "wait for setup to finish")
101102
cmd.Flags().StringVarP(&directory, "dir", "d", "", "directory to open")
102-
cmd.Flags().StringVar(&setDefault, "set-default", "", "set default editor (code, cursor, or windsurf)")
103+
cmd.Flags().StringVar(&setDefault, "set-default", "", "set default editor (code, cursor, windsurf, or tmux)")
103104

104105
return cmd
105106
}
106107

107108
func handleSetDefault(t *terminal.Terminal, editorType string) error {
108-
if editorType != EditorVSCode && editorType != EditorCursor && editorType != EditorWindsurf {
109-
return fmt.Errorf("invalid editor type: %s. Must be 'code', 'cursor', or 'windsurf'", editorType)
109+
if editorType != EditorVSCode && editorType != EditorCursor && editorType != EditorWindsurf && editorType != EditorTmux {
110+
return fmt.Errorf("invalid editor type: %s. Must be 'code', 'cursor', 'windsurf', or 'tmux'", editorType)
110111
}
111112

112113
homeDir, err := os.UserHomeDir()
@@ -130,8 +131,8 @@ func handleSetDefault(t *terminal.Terminal, editorType string) error {
130131
func determineEditorType(args []string) (string, error) {
131132
if len(args) == 2 {
132133
editorType := args[1]
133-
if editorType != EditorVSCode && editorType != EditorCursor && editorType != EditorWindsurf {
134-
return "", fmt.Errorf("invalid editor type: %s. Must be 'code', 'cursor', or 'windsurf'", editorType)
134+
if editorType != EditorVSCode && editorType != EditorCursor && editorType != EditorWindsurf && editorType != EditorTmux {
135+
return "", fmt.Errorf("invalid editor type: %s. Must be 'code', 'cursor', 'windsurf', or 'tmux'", editorType)
135136
}
136137
return editorType, nil
137138
}
@@ -215,6 +216,10 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
215216
errMsg := "windsurf\": executable file not found in $PATH\n\nadd 'windsurf' to your $PATH to open Windsurf from the terminal"
216217
return handlePathError(tstore, workspace, errMsg)
217218
}
219+
if strings.Contains(err.Error(), `tmux: command not found`) {
220+
errMsg := "tmux not found on remote instance. This will be installed automatically."
221+
return handlePathError(tstore, workspace, errMsg)
222+
}
218223
return breverrors.WrapAndTrace(err)
219224
}
220225
// Call analytics for open
@@ -352,6 +357,8 @@ func getEditorName(editorType string) string {
352357
return "Cursor"
353358
case EditorWindsurf:
354359
return "Windsurf"
360+
case EditorTmux:
361+
return "tmux"
355362
default:
356363
return "VSCode"
357364
}
@@ -380,6 +387,8 @@ func openEditorByType(t *terminal.Terminal, editorType string, sshAlias string,
380387
case EditorWindsurf:
381388
tryToInstallWindsurfExtensions(t, extensions)
382389
return openWindsurf(sshAlias, path, tstore)
390+
case EditorTmux:
391+
return openTmux(sshAlias, path, tstore)
383392
default:
384393
tryToInstallExtensions(t, extensions)
385394
return openVsCode(sshAlias, path, tstore)
@@ -526,3 +535,55 @@ func getWindowsWindsurfPaths(store vscodePathStore) []string {
526535
paths := append([]string{}, fmt.Sprintf("%s/AppData/Local/Programs/Windsurf/Windsurf.exe", wd), fmt.Sprintf("%s/AppData/Local/Programs/Windsurf/bin/windsurf", wd))
527536
return paths
528537
}
538+
539+
func openTmux(sshAlias string, path string, store OpenStore) error {
540+
_ = store // unused parameter required by interface
541+
err := ensureTmuxInstalled(sshAlias)
542+
if err != nil {
543+
return breverrors.WrapAndTrace(err)
544+
}
545+
546+
sessionName := "brev"
547+
548+
checkCmd := fmt.Sprintf("ssh %s 'tmux has-session -t %s 2>/dev/null'", sshAlias, sessionName)
549+
checkExec := exec.Command("bash", "-c", checkCmd) // #nosec G204
550+
err = checkExec.Run()
551+
552+
var tmuxCmd string
553+
if err == nil {
554+
tmuxCmd = fmt.Sprintf("ssh -t %s 'tmux attach-session -t %s'", sshAlias, sessionName)
555+
} else {
556+
tmuxCmd = fmt.Sprintf("ssh -t %s 'cd %s && tmux new-session -s %s'", sshAlias, path, sessionName)
557+
}
558+
559+
sshCmd := exec.Command("bash", "-c", tmuxCmd) // #nosec G204
560+
sshCmd.Stderr = os.Stderr
561+
sshCmd.Stdout = os.Stdout
562+
sshCmd.Stdin = os.Stdin
563+
564+
err = sshCmd.Run()
565+
if err != nil {
566+
return breverrors.WrapAndTrace(err)
567+
}
568+
return nil
569+
}
570+
571+
func ensureTmuxInstalled(sshAlias string) error {
572+
checkCmd := fmt.Sprintf("ssh %s 'which tmux >/dev/null 2>&1'", sshAlias)
573+
checkExec := exec.Command("bash", "-c", checkCmd) // #nosec G204
574+
err := checkExec.Run()
575+
if err == nil {
576+
return nil
577+
}
578+
579+
installCmd := fmt.Sprintf("ssh %s 'sudo apt-get update && sudo apt-get install -y tmux'", sshAlias)
580+
installExec := exec.Command("bash", "-c", installCmd) // #nosec G204
581+
installExec.Stderr = os.Stderr
582+
installExec.Stdout = os.Stdout
583+
584+
err = installExec.Run()
585+
if err != nil {
586+
return breverrors.WrapAndTrace(err)
587+
}
588+
return nil
589+
}

0 commit comments

Comments
 (0)