Skip to content
5 changes: 3 additions & 2 deletions Sources/CodexBarClaudeWebProbe/main.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import CodexBarCore
import Foundation

@main
enum CodexBarClaudeWebProbe {
private static let defaultEndpoints: [String] = [
"https://claude.ai/api/organizations",
Expand All @@ -19,7 +18,7 @@ enum CodexBarClaudeWebProbe {
"https://claude.ai/settings/usage",
]

static func main() async {
static func run() async {
let args = CommandLine.arguments.dropFirst()
let endpoints = args.isEmpty ? Self.defaultEndpoints : Array(args)
let includePreview = ProcessInfo.processInfo.environment["CLAUDE_WEB_PROBE_PREVIEW"] == "1"
Expand Down Expand Up @@ -61,3 +60,5 @@ enum CodexBarClaudeWebProbe {
print("")
}
}
await CodexBarClaudeWebProbe.run()

97 changes: 85 additions & 12 deletions Sources/CodexBarCore/PathEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public enum BinaryLocator {
loginPATH: loginPATH,
commandV: commandV,
aliasResolver: aliasResolver,
useShellCache: true,
fileManager: fileManager,
home: home)
}
Expand All @@ -72,6 +73,7 @@ public enum BinaryLocator {
loginPATH: loginPATH,
commandV: commandV,
aliasResolver: aliasResolver,
useShellCache: true,
fileManager: fileManager,
home: home)
}
Expand All @@ -92,6 +94,7 @@ public enum BinaryLocator {
loginPATH: loginPATH,
commandV: commandV,
aliasResolver: aliasResolver,
useShellCache: true,
fileManager: fileManager,
home: home)
}
Expand All @@ -112,6 +115,7 @@ public enum BinaryLocator {
loginPATH: loginPATH,
commandV: commandV,
aliasResolver: aliasResolver,
useShellCache: true,
fileManager: fileManager,
home: home)
}
Expand All @@ -124,6 +128,7 @@ public enum BinaryLocator {
loginPATH: [String]?,
commandV: (String, String?, TimeInterval, FileManager) -> String?,
aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String?,
useShellCache: Bool,
fileManager: FileManager,
home: String) -> String?
{
Expand All @@ -150,18 +155,48 @@ public enum BinaryLocator {
return pathHit
}

// 4) Interactive login shell lookup (captures nvm/fnm/mise paths from .zshrc/.bashrc)
if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager),
fileManager.isExecutableFile(atPath: shellHit)
{
return shellHit
}

// 4b) Alias fallback (login shell); only attempt after all standard lookups fail.
if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home),
fileManager.isExecutableFile(atPath: aliasHit)
{
return aliasHit
// 4) Interactive login shell lookup — use cache to avoid repeated shell spawns
// (shell auto-start hooks like zellij create new daemon instances on each invocation).
// Cache is bypassed when custom commandV/aliasResolver closures are injected (e.g. tests)
// to prevent a cached result from one call context affecting another.
if useShellCache {
let cached = BinaryResolutionCache.shared.cachedResult(for: name)
if let cached {
if let path = cached.path {
if fileManager.isExecutableFile(atPath: path) {
return path
}
// Cached path is no longer executable (e.g. CLI uninstalled/moved) — retry
BinaryResolutionCache.shared.invalidate(name)
} else {
// Cached nil = confirmed not found; skip shell spawns
return nil
}
}
// Not yet cached (or just invalidated) — run the shell lookups once and cache the result
var resolved: String? = nil
if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager),
fileManager.isExecutableFile(atPath: shellHit)
{
resolved = shellHit
} else if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home),
fileManager.isExecutableFile(atPath: aliasHit)
{
resolved = aliasHit
}
BinaryResolutionCache.shared.store(path: resolved, for: name)
if let resolved { return resolved }
} else {
if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager),
fileManager.isExecutableFile(atPath: shellHit)
{
return shellHit
}
if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home),
fileManager.isExecutableFile(atPath: aliasHit)
{
return aliasHit
}
}

// 5) Minimal fallback
Expand Down Expand Up @@ -440,6 +475,44 @@ enum LoginShellPathCapturer {
}
}

/// Caches the results of interactive-shell binary lookups (steps 4/4b of resolveBinary).
/// Prevents repeated login-shell spawns — which can trigger auto-start hooks like zellij —
/// on every refresh cycle when a binary is not found on the standard PATH.
final class BinaryResolutionCache: @unchecked Sendable {
static let shared = BinaryResolutionCache()

private let lock = NSLock()
// nil value = binary confirmed not found; missing key = not yet looked up
private var cache: [String: String?] = [:]

func cachedResult(for name: String) -> (found: Bool, path: String?)? {
lock.lock()
defer { lock.unlock() }
guard cache.keys.contains(name) else { return nil }
return (found: cache[name] != nil, path: cache[name] ?? nil)
}

func store(path: String?, for name: String) {
lock.lock()
defer { lock.unlock() }
cache[name] = path
}

/// Resets a single entry (e.g., when a binary is installed/uninstalled at runtime).
func invalidate(_ name: String) {
lock.lock()
defer { lock.unlock() }
cache.removeValue(forKey: name)
}

/// Resets all cached entries.
func invalidateAll() {
lock.lock()
defer { lock.unlock() }
cache.removeAll()
}
}

public final class LoginShellPathCache: @unchecked Sendable {
public static let shared = LoginShellPathCache()

Expand Down