Skip to content

Commit 1dae9ff

Browse files
joshgodsiffJosh Godsiffclaude
authored
boxcli: fix VS Code binary path resolution on macOS (#2806)
## Summary The WSL fix in #2729 (4ba6fce) broke "Reopen in Devbox shell environment" on macOS. That commit constructs the VS Code binary path as `$VSCODE_CWD/bin/code`. On WSL, `VSCODE_CWD` points to the VS Code installation directory (e.g., `/mnt/c/.../Microsoft VS Code`), so this works correctly. On macOS however, `VSCODE_CWD` is set to the workspace directory, producing a path like `/Users/user/my-project/bin/code` — which doesn't exist: e.g. The debug logs on my machine gives: ``` fork/exec /Users/josh/src/my-project/bin/code: no such file or directory ``` This PR adds an os.Stat check so the constructed path is only used when the binary actually exists there, falling back to the bare command name (resolved via `PATH`) otherwise. The resolution logic is extracted into a `resolveEditorBinary` function with unit tests covering: - `VSCODE_CWD` unset → uses bare name - `VSCODE_CWD` points to a VS Code installation (WSL) → uses full path - `VSCODE_CWD` points to a workspace directory (macOS) → falls back to bare name ## How was it tested? - Unit tests for `resolveEditorBinary` covering all three scenarios - `go test -race -cover ./internal/boxcli/` passes - `golangci-lint run ./internal/boxcli/` passes ## Community Contribution License All community contributions in this pull request are licensed to the project maintainers under the terms of the [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request, I represent that I have the right to license the contributions to the project maintainers under the Apache 2 License as stated in the [Community Contribution License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license). Co-authored-by: Josh Godsiff <josh.godsiff@corro.co> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8e36761 commit 1dae9ff

File tree

2 files changed

+95
-6
lines changed

2 files changed

+95
-6
lines changed

internal/boxcli/integrate.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,7 @@ func runIntegrateVSCodeCmd(cmd *cobra.Command, flags integrateCmdFlags) error {
124124
return err
125125
}
126126
// Open editor with devbox shell environment
127-
cmndName := flags.ideName
128-
cwd, ok := os.LookupEnv("VSCODE_CWD")
129-
if ok {
130-
// Specify full path to avoid running the `code` shell script from VS Code Server, which fails under WSL
131-
cmndName = cwd + "/bin/" + cmndName
132-
}
127+
cmndName := resolveEditorBinary(flags.ideName)
133128
cmnd := exec.Command(cmndName, message.ConfigDir)
134129
cmnd.Env = append(cmnd.Env, envVars...)
135130
var outb, errb bytes.Buffer
@@ -145,6 +140,23 @@ func runIntegrateVSCodeCmd(cmd *cobra.Command, flags integrateCmdFlags) error {
145140
return nil
146141
}
147142

143+
// resolveEditorBinary returns the path to the editor binary. On WSL,
144+
// VSCODE_CWD points to the VS Code installation directory, so we can
145+
// construct an absolute path to avoid the VS Code Server shell script.
146+
// On macOS, VSCODE_CWD is the workspace directory, so the constructed
147+
// path won't exist and we fall back to the bare command name.
148+
func resolveEditorBinary(ideName string) string {
149+
cwd, ok := os.LookupEnv("VSCODE_CWD")
150+
if !ok {
151+
return ideName
152+
}
153+
fullPath := cwd + "/bin/" + ideName
154+
if _, err := os.Stat(fullPath); err == nil {
155+
return fullPath
156+
}
157+
return ideName
158+
}
159+
148160
type debugMode struct {
149161
enabled bool
150162
}

internal/boxcli/integrate_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2024 Jetify Inc. and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package boxcli
5+
6+
import (
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
)
11+
12+
func TestResolveEditorBinary(t *testing.T) {
13+
// Create a temp directory with a bin/code binary to simulate
14+
// a VS Code installation (e.g., WSL's VSCODE_CWD).
15+
installDir := t.TempDir()
16+
binDir := filepath.Join(installDir, "bin")
17+
if err := os.MkdirAll(binDir, 0o755); err != nil {
18+
t.Fatal(err)
19+
}
20+
codePath := filepath.Join(binDir, "code")
21+
if err := os.WriteFile(codePath, []byte("#!/bin/sh\n"), 0o755); err != nil {
22+
t.Fatal(err)
23+
}
24+
25+
// A directory that does NOT contain bin/code, simulating
26+
// macOS where VSCODE_CWD is the workspace directory.
27+
workspaceDir := t.TempDir()
28+
29+
tests := []struct {
30+
name string
31+
vscodeCWD string // empty means unset
32+
ideName string
33+
want string
34+
}{
35+
{
36+
name: "VSCODE_CWD unset falls back to bare name",
37+
ideName: "code",
38+
want: "code",
39+
},
40+
{
41+
name: "VSCODE_CWD with valid binary uses full path",
42+
vscodeCWD: installDir,
43+
ideName: "code",
44+
want: filepath.Join(installDir, "bin", "code"),
45+
},
46+
{
47+
name: "VSCODE_CWD without binary falls back to bare name",
48+
vscodeCWD: workspaceDir,
49+
ideName: "code",
50+
want: "code",
51+
},
52+
{
53+
name: "non-default IDE name resolves correctly",
54+
vscodeCWD: workspaceDir,
55+
ideName: "cursor",
56+
want: "cursor",
57+
},
58+
}
59+
60+
for _, testCase := range tests {
61+
t.Run(testCase.name, func(t *testing.T) {
62+
if testCase.vscodeCWD != "" {
63+
t.Setenv("VSCODE_CWD", testCase.vscodeCWD)
64+
} else {
65+
os.Unsetenv("VSCODE_CWD")
66+
}
67+
68+
got := resolveEditorBinary(testCase.ideName)
69+
if got != testCase.want {
70+
t.Errorf(
71+
"resolveEditorBinary(%q) = %q, want %q",
72+
testCase.ideName, got, testCase.want,
73+
)
74+
}
75+
})
76+
}
77+
}

0 commit comments

Comments
 (0)