Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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