diff --git a/Sources/CodexBarCore/PathEnvironment.swift b/Sources/CodexBarCore/PathEnvironment.swift index ece93eda2..2706170b7 100644 --- a/Sources/CodexBarCore/PathEnvironment.swift +++ b/Sources/CodexBarCore/PathEnvironment.swift @@ -52,10 +52,22 @@ public enum BinaryLocator { loginPATH: loginPATH, commandV: commandV, aliasResolver: aliasResolver, + wellKnownPaths: self.claudeWellKnownPaths(home: home), fileManager: fileManager, home: home) } + /// Well-known installation paths for the Claude CLI binary. + /// Covers the macOS Terminal installer (cmux.app), ~/.claude/bin, and Homebrew. + static func claudeWellKnownPaths(home: String) -> [String] { + [ + "/Applications/cmux.app/Contents/Resources/bin/claude", + "\(home)/.claude/bin/claude", + "/usr/local/bin/claude", + "/opt/homebrew/bin/claude", + ] + } + public static func resolveCodexBinary( env: [String: String] = ProcessInfo.processInfo.environment, loginPATH: [String]? = LoginShellPathCache.shared.current, @@ -124,6 +136,7 @@ public enum BinaryLocator { loginPATH: [String]?, commandV: (String, String?, TimeInterval, FileManager) -> String?, aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String?, + wellKnownPaths: [String] = [], fileManager: FileManager, home: String) -> String? { @@ -164,7 +177,14 @@ public enum BinaryLocator { return aliasHit } - // 5) Minimal fallback + // 5) Well-known installation paths (e.g. cmux.app bundle, ~/.claude/bin) + // macOS apps launched from Finder may not inherit the user's shell PATH, + // so check common install locations that the shell-based lookups above may miss. + for candidate in wellKnownPaths where fileManager.isExecutableFile(atPath: candidate) { + return candidate + } + + // 6) Minimal fallback let fallback = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"] if let pathHit = self.find(name, in: fallback, fileManager: fileManager) { return pathHit diff --git a/Tests/CodexBarTests/PathBuilderTests.swift b/Tests/CodexBarTests/PathBuilderTests.swift index d40bc2345..b7787a3e7 100644 --- a/Tests/CodexBarTests/PathBuilderTests.swift +++ b/Tests/CodexBarTests/PathBuilderTests.swift @@ -210,6 +210,56 @@ struct PathBuilderTests { #expect(resolved == aliasPath) } + @Test + func `resolves claude from well-known cmux path when shell lookups fail`() { + let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude" + let fm = MockFileManager(executables: [cmuxPath]) + let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil } + let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil } + + let resolved = BinaryLocator.resolveClaudeBinary( + env: ["SHELL": "/bin/zsh"], + loginPATH: nil, + commandV: commandV, + aliasResolver: aliasResolver, + fileManager: fm, + home: "/Users/test") + #expect(resolved == cmuxPath) + } + + @Test + func `resolves claude from well-known home dir path`() { + let homePath = "/Users/test/.claude/bin/claude" + let fm = MockFileManager(executables: [homePath]) + let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil } + let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil } + + let resolved = BinaryLocator.resolveClaudeBinary( + env: ["SHELL": "/bin/zsh"], + loginPATH: nil, + commandV: commandV, + aliasResolver: aliasResolver, + fileManager: fm, + home: "/Users/test") + #expect(resolved == homePath) + } + + @Test + func `prefers shell PATH over well-known paths`() { + let shellPath = "/custom/bin/claude" + let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude" + let fm = MockFileManager(executables: [shellPath, cmuxPath]) + let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in shellPath } + + let resolved = BinaryLocator.resolveClaudeBinary( + env: ["SHELL": "/bin/zsh"], + loginPATH: nil, + commandV: commandV, + fileManager: fm, + home: "/Users/test") + #expect(resolved == shellPath) + } + @Test func `skips alias when command V resolves`() { let path = "/shell/bin/claude"