package windsurf import ( "fmt" "os" "path/filepath" "runtime" "strings" ) // ErrBinaryNotFound is returned when no Windsurf LS binary can be located // via any configured source (env, explicit config, or platform candidates). var ErrBinaryNotFound = fmt.Errorf("windsurf: language server binary not found") // binaryStatFn reports whether the given path exists and is executable for // the current platform. It is a package variable so tests can replace it // with a map-backed implementation that does not touch the filesystem. var binaryStatFn = defaultBinaryStat // userHomeFn returns the user's home directory. Replaced in tests. var userHomeFn = defaultUserHome func defaultBinaryStat(path string) bool { info, err := os.Stat(path) if err != nil || info.IsDir() { return false } if runtime.GOOS == "windows" { // Windows ignores the Unix execute bit — rely on the .exe suffix. return strings.HasSuffix(strings.ToLower(path), ".exe") } return info.Mode()&0o111 != 0 } func defaultUserHome() string { if dir, err := os.UserHomeDir(); err == nil { return dir } return "" } // DiscoverBinary resolves the Windsurf LS binary path for the current // platform. Resolution order: // // 1. LS_BINARY_PATH environment variable (explicit override — user intent // wins even if the path doesn't exist, so we can surface a clear error) // 2. cfg.Binary (explicit config override) // 3. Platform-specific candidate list (official install locations) // // Returns ErrBinaryNotFound when none of the sources yield an executable // file; the error message directs the user to LS_BINARY_PATH or ls_mode=docker. func DiscoverBinary(cfg LSPoolConfig) (string, error) { return discoverBinaryFor(DetectPlatform(), os.Getenv("LS_BINARY_PATH"), cfg.Binary) } func discoverBinaryFor(p Platform, envPath, cfgPath string) (string, error) { if envPath != "" { return validateBinaryPath(envPath, p, "LS_BINARY_PATH") } if cfgPath != "" { return validateBinaryPath(cfgPath, p, "cfg.Binary") } candidates, err := platformCandidates(p) if err != nil { return "", err } for _, path := range candidates { if binaryStatFn(path) { return path, nil } } return "", fmt.Errorf("%w for %s; searched %d paths (%s); set LS_BINARY_PATH or use ls_mode=docker", ErrBinaryNotFound, p, len(candidates), strings.Join(candidates, ", ")) } func validateBinaryPath(path string, p Platform, source string) (string, error) { if binaryStatFn(path) { return path, nil } hint := "file does not exist or is not executable" if p.OS == "windows" && !strings.HasSuffix(strings.ToLower(path), ".exe") { hint = "Windows LS binaries must end in .exe" } else if p.OS != "windows" { hint += " (try chmod +x)" } return "", fmt.Errorf("%w: %s=%q — %s; set LS_BINARY_PATH to a valid path or use ls_mode=docker", ErrBinaryNotFound, source, path, hint) } // platformCandidates returns the ordered list of paths where the official // Windsurf LS binary may be installed on the given platform. Paths are // ordered by preference — the first existing+executable path wins. func platformCandidates(p Platform) ([]string, error) { filename, err := BinaryFilename(p) if err != nil { return nil, err } switch p.OS { case "darwin": return darwinCandidates(filename), nil case "linux": return linuxCandidates(filename), nil case "windows": return windowsCandidates(filename), nil } // BinaryFilename would have errored first, so this is defensive. return nil, fmt.Errorf("%w: %s (no candidate list)", ErrUnsupportedPlatform, p) } func darwinCandidates(filename string) []string { const bundleSubpath = "Contents/Resources/app/extensions/windsurf/bin" candidates := []string{ filepath.Join("/Applications/Windsurf.app", bundleSubpath, filename), } if home := userHomeFn(); home != "" { candidates = append(candidates, filepath.Join(home, "Applications/Windsurf.app", bundleSubpath, filename), ) } // Legacy sub2api install (pre-cross-platform). candidates = append(candidates, filepath.Join("/opt/windsurf", filename)) return candidates } func linuxCandidates(filename string) []string { candidates := []string{ // Legacy sub2api install (pre-cross-platform) — matches existing deployments. filepath.Join("/opt/windsurf", filename), // Official Debian/RPM install locations. filepath.Join("/usr/share/windsurf/resources/app/extensions/windsurf/bin", filename), filepath.Join("/usr/lib/windsurf/resources/app/extensions/windsurf/bin", filename), } // User-local install (Flatpak/AppImage unpacked). if home := userHomeFn(); home != "" { candidates = append(candidates, filepath.Join(home, ".local/share/windsurf/resources/app/extensions/windsurf/bin", filename), ) } return candidates } func windowsCandidates(filename string) []string { // Split the install subpath into its components so filepath.Join produces // a platform-native path on whichever OS this runs (Windows '\\', Unix '/'). // This matters for tests running on non-Windows builders. installSubpath := []string{"resources", "app", "extensions", "windsurf", "bin", filename} var candidates []string if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" { candidates = append(candidates, filepath.Join(append([]string{localAppData, "Programs", "Windsurf"}, installSubpath...)...), ) } if programFiles := os.Getenv("PROGRAMFILES"); programFiles != "" { candidates = append(candidates, filepath.Join(append([]string{programFiles, "Windsurf"}, installSubpath...)...), ) } return candidates }