@@ -3,6 +3,7 @@ package open
33import (
44 "errors"
55 "fmt"
6+ "os"
67 "os/exec"
78 "strings"
89 "time"
@@ -16,6 +17,7 @@ import (
1617 "github.com/brevdev/brev-cli/pkg/cmd/util"
1718 "github.com/brevdev/brev-cli/pkg/entity"
1819 breverrors "github.com/brevdev/brev-cli/pkg/errors"
20+ "github.com/brevdev/brev-cli/pkg/files"
1921 "github.com/brevdev/brev-cli/pkg/store"
2022 "github.com/brevdev/brev-cli/pkg/terminal"
2123 uutil "github.com/brevdev/brev-cli/pkg/util"
@@ -27,9 +29,14 @@ import (
2729 "github.com/spf13/cobra"
2830)
2931
32+ const (
33+ EditorVSCode = "code"
34+ EditorCursor = "cursor"
35+ )
36+
3037var (
31- openLong = "[command in beta] This will open VS Code SSH-ed in to your workspace. You must have 'code' installed in your path."
32- openExample = "brev open workspace_id_or_name\n brev open my-app\n brev open h9fp5vxwe "
38+ openLong = "[command in beta] This will open VS Code or Cursor SSH-ed in to your workspace. You must have the editor installed in your path."
39+ openExample = "brev open workspace_id_or_name\n brev open my-app\n brev open my-app code \n brev open my-app cursor \n brev open --set-default cursor "
3340)
3441
3542type OpenStore interface {
@@ -46,10 +53,10 @@ type OpenStore interface {
4653}
4754
4855func NewCmdOpen (t * terminal.Terminal , store OpenStore , noLoginStartStore OpenStore ) * cobra.Command {
49- var openWithCursor bool
5056 var waitForSetupToFinish bool
5157 var directory string
5258 var host bool
59+ var setDefault string
5360
5461 cmd := & cobra.Command {
5562 Annotations : map [string ]string {"ssh" : "" },
@@ -58,30 +65,85 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto
5865 Short : "[beta] open VSCode or Cursor to your workspace" ,
5966 Long : openLong ,
6067 Example : openExample ,
61- Args : cmderrors .TransformToValidationError (cobra .ExactArgs ( 1 )),
68+ Args : cmderrors .TransformToValidationError (cobra .RangeArgs ( 1 , 2 )),
6269 ValidArgsFunction : completions .GetAllWorkspaceNameCompletionHandler (noLoginStartStore , t ),
6370 RunE : func (cmd * cobra.Command , args []string ) error {
71+ if setDefault != "" {
72+ return handleSetDefault (t , store , setDefault )
73+ }
74+
6475 setupDoneString := "------ Git repo cloned ------"
6576 if waitForSetupToFinish {
6677 setupDoneString = "------ Done running execs ------"
6778 }
68- err := runOpenCommand (t , store , args [0 ], setupDoneString , directory , host )
79+
80+ editorType , err := determineEditorType (args , store )
81+ if err != nil {
82+ return breverrors .WrapAndTrace (err )
83+ }
84+
85+ err = runOpenCommand (t , store , args [0 ], setupDoneString , directory , host , editorType )
6986 if err != nil {
7087 return breverrors .WrapAndTrace (err )
7188 }
7289 return nil
7390 },
7491 }
75- cmd .Flags ().BoolVarP (& openWithCursor , "cursor" , "c" , false , "open cursor instead of VS Code" )
7692 cmd .Flags ().BoolVarP (& host , "host" , "" , false , "ssh into the host machine instead of the container" )
7793 cmd .Flags ().BoolVarP (& waitForSetupToFinish , "wait" , "w" , false , "wait for setup to finish" )
7894 cmd .Flags ().StringVarP (& directory , "dir" , "d" , "" , "directory to open" )
95+ cmd .Flags ().StringVar (& setDefault , "set-default" , "" , "set default editor (code or cursor)" )
7996
8097 return cmd
8198}
8299
100+ func handleSetDefault (t * terminal.Terminal , store OpenStore , editorType string ) error {
101+ if editorType != EditorVSCode && editorType != EditorCursor {
102+ return fmt .Errorf ("invalid editor type: %s. Must be 'code' or 'cursor'" , editorType )
103+ }
104+
105+ homeDir , err := os .UserHomeDir ()
106+ if err != nil {
107+ return breverrors .WrapAndTrace (err )
108+ }
109+
110+ settings := & files.PersonalSettings {
111+ DefaultEditor : editorType ,
112+ }
113+
114+ err = files .WritePersonalSettings (files .AppFs , homeDir , settings )
115+ if err != nil {
116+ return breverrors .WrapAndTrace (err )
117+ }
118+
119+ t .Vprintf (t .Green ("Default editor set to %s\n " ), editorType )
120+ return nil
121+ }
122+
123+ func determineEditorType (args []string , store OpenStore ) (string , error ) {
124+ if len (args ) == 2 {
125+ editorType := args [1 ]
126+ if editorType != EditorVSCode && editorType != EditorCursor {
127+ return "" , fmt .Errorf ("invalid editor type: %s. Must be 'code' or 'cursor'" , editorType )
128+ }
129+ return editorType , nil
130+ }
131+
132+ homeDir , err := os .UserHomeDir ()
133+ if err != nil {
134+ return EditorVSCode , nil
135+ }
136+
137+ settings , err := files .ReadPersonalSettings (files .AppFs , homeDir )
138+ if err != nil {
139+ return EditorVSCode , nil
140+ }
141+
142+ return settings .DefaultEditor , nil
143+ }
144+
83145// Fetch workspace info, then open code editor
84- func runOpenCommand (t * terminal.Terminal , tstore OpenStore , wsIDOrName string , setupDoneString string , directory string , host bool ) error { //nolint:funlen // define brev command
146+ func runOpenCommand (t * terminal.Terminal , tstore OpenStore , wsIDOrName string , setupDoneString string , directory string , host bool , editorType string ) error { //nolint:funlen // define brev command
85147 // todo check if workspace is stopped and start if it if it is stopped
86148 fmt .Println ("finding your instance..." )
87149 res := refresh .RunRefreshAsync (tstore )
@@ -132,7 +194,7 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
132194 // legacy environments wont support this and cause errrors,
133195 // but we don't want to block the user from using vscode
134196 _ = writeconnectionevent .WriteWCEOnEnv (tstore , string (localIdentifier ))
135- err = openVsCodeWithSSH (t , string (localIdentifier ), projPath , tstore , setupDoneString )
197+ err = openEditorWithSSH (t , string (localIdentifier ), projPath , tstore , setupDoneString , editorType )
136198 if err != nil {
137199 if strings .Contains (err .Error (), `"code": executable file not found in $PATH` ) {
138200 errMsg := "code\" : executable file not found in $PATH\n \n add 'code' to your $PATH to open VS Code from the terminal\n \t export PATH=\" /Applications/Visual Studio Code.app/Contents/Resources/app/bin:$PATH\" "
@@ -148,6 +210,20 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
148210 }
149211 return errors .New (errMsg )
150212 }
213+ if strings .Contains (err .Error (), `"cursor": executable file not found in $PATH` ) {
214+ errMsg := "cursor\" : executable file not found in $PATH\n \n add 'cursor' to your $PATH to open Cursor from the terminal"
215+ _ , errStore := tstore .UpdateUser (
216+ workspace .CreatedByUserID ,
217+ & entity.UpdateUser {
218+ OnboardingData : map [string ]interface {}{
219+ "pathErrorTS" : time .Now ().UTC ().Unix (),
220+ },
221+ })
222+ if errStore != nil {
223+ return errors .New (errMsg + "\n " + errStore .Error ())
224+ }
225+ return errors .New (errMsg )
226+ }
151227 return breverrors .WrapAndTrace (err )
152228 }
153229 // Call analytics for open
@@ -232,13 +308,37 @@ func tryToInstallExtensions(
232308 }
233309}
234310
311+ func tryToInstallCursorExtensions (
312+ t * terminal.Terminal ,
313+ extIDs []string ,
314+ ) {
315+ for _ , extID := range extIDs {
316+ extInstalled , err0 := uutil .IsCursorExtensionInstalled (extID )
317+ if ! extInstalled {
318+ err1 := uutil .InstallCursorExtension (extID )
319+ isRemoteInstalled , err2 := uutil .IsCursorExtensionInstalled (extID )
320+ if ! isRemoteInstalled {
321+ err := multierror .Append (err0 , err1 , err2 )
322+ t .Print (t .Red ("Couldn't install the necessary Cursor extension automatically.\n Error: " + err .Error ()))
323+ t .Print ("\t Please install Cursor and the following Cursor extension: " + t .Yellow (extID ) + ".\n " )
324+ _ = terminal .PromptGetInput (terminal.PromptContent {
325+ Label : "Hit enter when finished:" ,
326+ ErrorMsg : "error" ,
327+ AllowEmpty : true ,
328+ })
329+ }
330+ }
331+ }
332+ }
333+
235334// Opens code editor. Attempts to install code in path if not installed already
236- func openVsCodeWithSSH (
335+ func openEditorWithSSH (
237336 t * terminal.Terminal ,
238337 sshAlias string ,
239338 path string ,
240339 tstore OpenStore ,
241340 _ string ,
341+ editorType string ,
242342) error {
243343 // infinite for loop:
244344 res := refresh .RunRefreshAsync (tstore )
@@ -255,14 +355,22 @@ func openVsCodeWithSSH(
255355 }
256356
257357 // todo: add it here
258- s .Suffix = " Instance is ready. Opening VS Code 🤙"
358+ editorName := "VS Code"
359+ if editorType == EditorCursor {
360+ editorName = "Cursor"
361+ }
362+ s .Suffix = fmt .Sprintf (" Instance is ready. Opening %s 🤙" , editorName )
259363 time .Sleep (250 * time .Millisecond )
260364 s .Stop ()
261365 t .Vprintf ("\n " )
262366
263- tryToInstallExtensions (t , []string {"ms-vscode-remote.remote-ssh" , "ms-toolsai.jupyter-keymap" , "ms-python.python" })
264-
265- err = openVsCode (sshAlias , path , tstore )
367+ if editorType == EditorCursor {
368+ tryToInstallCursorExtensions (t , []string {"ms-vscode-remote.remote-ssh" , "ms-toolsai.jupyter-keymap" , "ms-python.python" })
369+ err = openCursor (sshAlias , path , tstore )
370+ } else {
371+ tryToInstallExtensions (t , []string {"ms-vscode-remote.remote-ssh" , "ms-toolsai.jupyter-keymap" , "ms-python.python" })
372+ err = openVsCode (sshAlias , path , tstore )
373+ }
266374 if err != nil {
267375 return breverrors .WrapAndTrace (err )
268376 }
@@ -289,7 +397,11 @@ func openVsCodeWithSSH(
289397 if strings .Contains (err .Error (), "you are in a remote brev instance;" ) {
290398 return breverrors .WrapAndTrace (err )
291399 }
292- return breverrors .WrapAndTrace (fmt .Errorf (t .Red ("couldn't open VSCode, try adding it to PATH (you can do this in VSCode by running CMD-SHIFT-P and typing 'install code in path')\n " )))
400+ editorName := "VSCode"
401+ if editorType == EditorCursor {
402+ editorName = "Cursor"
403+ }
404+ return breverrors .WrapAndTrace (fmt .Errorf (t .Red ("couldn't open %s, try adding it to PATH\n " ), editorName ))
293405 } else {
294406 return nil
295407 }
@@ -341,3 +453,21 @@ func openVsCode(sshAlias string, path string, store OpenStore) error {
341453 }
342454 return nil
343455}
456+
457+ func openCursor (sshAlias string , path string , store OpenStore ) error {
458+ cursorString := fmt .Sprintf ("vscode-remote://ssh-remote+%s%s" , sshAlias , path )
459+ cursorString = shellescape .QuoteCommand ([]string {cursorString })
460+
461+ windowsPaths := getWindowsCursorPaths (store )
462+ _ , err := uutil .TryRunCursorCommand ([]string {"--folder-uri" , cursorString }, windowsPaths ... )
463+ if err != nil {
464+ return breverrors .WrapAndTrace (err )
465+ }
466+ return nil
467+ }
468+
469+ func getWindowsCursorPaths (store vscodePathStore ) []string {
470+ wd , _ := store .GetWindowsDir ()
471+ paths := append ([]string {}, fmt .Sprintf ("%s/AppData/Local/Programs/Cursor/Cursor.exe" , wd ), fmt .Sprintf ("%s/AppData/Local/Programs/Cursor/bin/cursor" , wd ))
472+ return paths
473+ }
0 commit comments