diff --git a/internal/boxcli/integrate.go b/internal/boxcli/integrate.go index d5ee89d2b5d..4b64c5b34f1 100644 --- a/internal/boxcli/integrate.go +++ b/internal/boxcli/integrate.go @@ -124,12 +124,7 @@ func runIntegrateVSCodeCmd(cmd *cobra.Command, flags integrateCmdFlags) error { return err } // Open editor with devbox shell environment - cmndName := flags.ideName - cwd, ok := os.LookupEnv("VSCODE_CWD") - if ok { - // Specify full path to avoid running the `code` shell script from VS Code Server, which fails under WSL - cmndName = cwd + "/bin/" + cmndName - } + cmndName := resolveEditorBinary(flags.ideName) cmnd := exec.Command(cmndName, message.ConfigDir) cmnd.Env = append(cmnd.Env, envVars...) var outb, errb bytes.Buffer @@ -145,6 +140,23 @@ func runIntegrateVSCodeCmd(cmd *cobra.Command, flags integrateCmdFlags) error { return nil } +// resolveEditorBinary returns the path to the editor binary. On WSL, +// VSCODE_CWD points to the VS Code installation directory, so we can +// construct an absolute path to avoid the VS Code Server shell script. +// On macOS, VSCODE_CWD is the workspace directory, so the constructed +// path won't exist and we fall back to the bare command name. +func resolveEditorBinary(ideName string) string { + cwd, ok := os.LookupEnv("VSCODE_CWD") + if !ok { + return ideName + } + fullPath := cwd + "/bin/" + ideName + if _, err := os.Stat(fullPath); err == nil { + return fullPath + } + return ideName +} + type debugMode struct { enabled bool } diff --git a/internal/boxcli/integrate_test.go b/internal/boxcli/integrate_test.go new file mode 100644 index 00000000000..1cc006af9a3 --- /dev/null +++ b/internal/boxcli/integrate_test.go @@ -0,0 +1,77 @@ +// Copyright 2024 Jetify Inc. and contributors. All rights reserved. +// Use of this source code is governed by the license in the LICENSE file. + +package boxcli + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveEditorBinary(t *testing.T) { + // Create a temp directory with a bin/code binary to simulate + // a VS Code installation (e.g., WSL's VSCODE_CWD). + installDir := t.TempDir() + binDir := filepath.Join(installDir, "bin") + if err := os.MkdirAll(binDir, 0o755); err != nil { + t.Fatal(err) + } + codePath := filepath.Join(binDir, "code") + if err := os.WriteFile(codePath, []byte("#!/bin/sh\n"), 0o755); err != nil { + t.Fatal(err) + } + + // A directory that does NOT contain bin/code, simulating + // macOS where VSCODE_CWD is the workspace directory. + workspaceDir := t.TempDir() + + tests := []struct { + name string + vscodeCWD string // empty means unset + ideName string + want string + }{ + { + name: "VSCODE_CWD unset falls back to bare name", + ideName: "code", + want: "code", + }, + { + name: "VSCODE_CWD with valid binary uses full path", + vscodeCWD: installDir, + ideName: "code", + want: filepath.Join(installDir, "bin", "code"), + }, + { + name: "VSCODE_CWD without binary falls back to bare name", + vscodeCWD: workspaceDir, + ideName: "code", + want: "code", + }, + { + name: "non-default IDE name resolves correctly", + vscodeCWD: workspaceDir, + ideName: "cursor", + want: "cursor", + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + if testCase.vscodeCWD != "" { + t.Setenv("VSCODE_CWD", testCase.vscodeCWD) + } else { + os.Unsetenv("VSCODE_CWD") + } + + got := resolveEditorBinary(testCase.ideName) + if got != testCase.want { + t.Errorf( + "resolveEditorBinary(%q) = %q, want %q", + testCase.ideName, got, testCase.want, + ) + } + }) + } +}