import Foundation public struct RemoteSessionHostResult: Equatable, Sendable, Identifiable { public let host: String public let sessions: [AgentSession] public let error: String? public var id: String { self.host } public var isReachable: Bool { self.error == nil } public init(host: String, sessions: [AgentSession], error: String?) { self.host = host self.sessions = sessions self.error = error } } public enum TailscaleStatusParser { /// Parses hosts from `tailscale status --json` output. /// /// Returns `nil` when `data` is not recognizable Tailscale status JSON — a failed, wrong, or /// non-Tailscale `tailscale` binary — so callers can fall through to the next candidate. Returns a /// possibly-empty list for a valid status that simply has no eligible peers (a real answer, stop). package static func parseHosts(from data: Data, excludingLocalHost localHost: String? = nil) -> [String]? { guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } if let rawBackendState = root["BackendState"] { guard let backendState = rawBackendState as? String, backendState.caseInsensitiveCompare("Running") == .orderedSame else { return nil } } let selfStatus: [String: Any]? if let rawSelf = root["Self"] { guard let parsedSelf = rawSelf as? [String: Any] else { return nil } selfStatus = parsedSelf } else { selfStatus = nil } let peers: [[String: Any]] let hasPeerShape: Bool switch root["Peer"] { case let dictionary as [String: [String: Any]]: peers = Array(dictionary.values) hasPeerShape = true case let array as [[String: Any]]: peers = array hasPeerShape = true case is NSNull: peers = [] hasPeerShape = true case nil: peers = [] hasPeerShape = false default: return nil } guard selfStatus != nil || hasPeerShape else { return nil } let localLabels = Set([ localHost, selfStatus?["DNSName"] as? String, selfStatus?["HostName"] as? String, ].compactMap(self.firstDNSLabel).map { $0.lowercased() }) var seen = Set() return peers.compactMap { peer in guard peer["Online"] as? Bool == true, let operatingSystem = peer["OS"] as? String, operatingSystem == "macOS" || operatingSystem == "linux", let label = self.firstDNSLabel(peer["DNSName"] as? String) else { return nil } let normalized = label.lowercased() guard !localLabels.contains(normalized), seen.insert(normalized).inserted else { return nil } return label }.sorted() } /// Convenience returning `[]` for unparseable output. Prefer `parseHosts` when the caller needs to /// distinguish a failed probe from an empty tailnet. public static func hosts(from data: Data, excludingLocalHost localHost: String? = nil) -> [String] { self.parseHosts(from: data, excludingLocalHost: localHost) ?? [] } private static func firstDNSLabel(_ value: String?) -> String? { guard let value else { return nil } let trimmed = value.trimmingCharacters(in: CharacterSet(charactersIn: ".")) guard let label = trimmed.split(separator: ".").first, !label.isEmpty else { return nil } return String(label) } } public struct RemoteSessionFetcher: Sendable { public static let bundledCLIFallback = "/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI" public init() {} public func discoveredHosts( environment: [String: String] = ProcessInfo.processInfo.environment, localHost: String = ProcessInfo.processInfo.hostName) async -> [String] { let probeEnvironment = Self.tailscaleCLIEnvironment(from: environment) let candidates = Self.tailscaleBinaryCandidates(path: environment["PATH"]) .filter { FileManager.default.isExecutableFile(atPath: $0) } return await Self.firstDiscoveredHosts(candidates: candidates, localHost: localHost) { binary in guard let result = try? await SubprocessRunner.run( binary: binary, arguments: ["status", "--json"], environment: probeEnvironment, timeout: 5, label: "Tailscale session host discovery") else { return nil } return Data(result.stdout.utf8) } } /// Runs `tailscale status --json` on each candidate in order, falling through to the next when a /// candidate fails (`run` returns nil), returns invalid status JSON, or reports an inactive backend. /// Returns the first candidate's parsed hosts (possibly empty), or `[]` if none succeed. This keeps /// the app-binary fallback working even when an earlier — but non-functional — `tailscale` variant /// is installed (e.g. an open-source/Homebrew CLI that isn't the active client). package static func firstDiscoveredHosts( candidates: [String], localHost: String?, run: (String) async -> Data?) async -> [String] { for binary in candidates { guard let data = await run(binary), let hosts = TailscaleStatusParser.parseHosts(from: data, excludingLocalHost: localHost) else { continue } return hosts } return [] } public func fetch( hosts: [String], environment: [String: String] = ProcessInfo.processInfo.environment) async -> [RemoteSessionHostResult] { let normalizedHosts = Self.sanitizedHosts(hosts) return await withTaskGroup( of: RemoteSessionHostResult.self, returning: [RemoteSessionHostResult].self) { group in for host in normalizedHosts { group.addTask { await self.fetch(host: host, environment: environment) } } var results: [RemoteSessionHostResult] = [] for await result in group { results.append(result) } return results .sorted { lhs, rhs in lhs.host.localizedCaseInsensitiveCompare(rhs.host) == .orderedAscending } } } public func focus( sessionID: String, host: String, environment: [String: String] = ProcessInfo.processInfo.environment) async { guard let host = Self.sanitizedHosts([host]).first else { return } guard let ssh = self.findExecutable("ssh", environment: environment) ?? (["/usr/bin/ssh", "/bin/ssh"].first { FileManager.default.isExecutableFile(atPath: $0) }) else { return } let command = "codexbar sessions focus \(Self.shellQuote(sessionID)) || " + "\(Self.shellQuote(Self.bundledCLIFallback)) sessions focus \(Self.shellQuote(sessionID))" _ = try? await SubprocessRunner.run( binary: ssh, arguments: ["-o", "BatchMode=yes", "-o", "ConnectTimeout=3", host, "sh", "-lc", Self.shellQuote(command)], environment: environment, timeout: 5, acceptsNonZeroExit: true, label: "focus remote agent session") } private func fetch(host: String, environment: [String: String]) async -> RemoteSessionHostResult { guard let ssh = self.findExecutable("ssh", environment: environment) ?? (["/usr/bin/ssh", "/bin/ssh"].first { FileManager.default.isExecutableFile(atPath: $0) }) else { return RemoteSessionHostResult(host: host, sessions: [], error: "ssh not found") } let command = "codexbar sessions --json || " + "\(Self.shellQuote(Self.bundledCLIFallback)) sessions --json" do { let result = try await SubprocessRunner.run( binary: ssh, arguments: [ "-o", "BatchMode=yes", "-o", "ConnectTimeout=3", host, "sh", "-lc", Self.shellQuote(command), ], environment: environment, timeout: 5, label: "fetch remote agent sessions") let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 var sessions = try decoder.decode([AgentSession].self, from: Data(result.stdout.utf8)) for index in sessions.indices { sessions[index].host = host } return RemoteSessionHostResult(host: host, sessions: sessions, error: nil) } catch { return RemoteSessionHostResult(host: host, sessions: [], error: error.localizedDescription) } } /// Ordered candidate paths for the `tailscale` CLI, most-preferred first. /// /// The macOS app ships its CLI as a thin `/bin/sh` wrapper (usually /// `/usr/local/bin/tailscale`) around the app's dual-mode binary. We prefer the /// wrapper, but a GUI-launched CodexBar inherits a minimal `PATH` (`/usr/bin:/bin`) /// that omits the standard CLI locations, so we also probe them explicitly before /// falling back to the app binary itself. package static func tailscaleBinaryCandidates(path: String?) -> [String] { let pathDirs = path?.split(separator: ":").map(String.init) ?? [] var seen = Set() var candidates = (pathDirs + ["/usr/local/bin", "/opt/homebrew/bin"]) .filter { seen.insert($0).inserted } .map { $0 + "/tailscale" } // Last resort: the dual-mode app binary. Must be run via // `tailscaleCLIEnvironment(from:)` so it stays in CLI mode. candidates.append("/Applications/Tailscale.app/Contents/MacOS/Tailscale") return candidates } /// Environment that keeps the dual-mode Tailscale app binary in CLI mode. /// /// With no shell/terminal marker present the binary boots the full menu-bar GUI /// (SkyLight/WindowServer, status icon) instead of running the CLI: it never emits /// JSON, the probe times out, and the Tailscale icon flickers on every refresh. A /// set `TERM` or `SHLVL` forces CLI mode (argv[0] casing and `XPC_SERVICE_NAME` do /// not). `SHLVL` is what the app's own `/bin/sh` CLI wrapper injects, so we mirror it here. /// /// Applied to every probe, not just the app-binary fallback: it is redundant but harmless for the /// CLI wrapper (itself a `/bin/sh` script that already exports `SHLVL`), and injecting it /// unconditionally keeps CLI mode guaranteed regardless of which binary `tailscaleBinary` resolves. /// An existing `TERM`/`SHLVL` (real terminal context) is left untouched. package static func tailscaleCLIEnvironment(from environment: [String: String]) -> [String: String] { guard environment["TERM"] == nil, environment["SHLVL"] == nil else { return environment } var environment = environment environment["SHLVL"] = "1" return environment } private func findExecutable(_ name: String, environment: [String: String]) -> String? { let path = environment["PATH"] ?? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" return path.split(separator: ":") .map { String($0) + "/" + name } .first { FileManager.default.isExecutableFile(atPath: $0) } } public static func sanitizedHosts(_ hosts: [String]) -> [String] { var seen = Set() return hosts.compactMap { rawHost in let host = rawHost.trimmingCharacters(in: .whitespacesAndNewlines) let hasUnsafeScalar = host.unicodeScalars.contains { scalar in CharacterSet.controlCharacters.contains(scalar) || CharacterSet.whitespacesAndNewlines.contains(scalar) } guard !host.isEmpty, !host.hasPrefix("-"), !hasUnsafeScalar, seen.insert(host.lowercased()).inserted else { return nil } return host } } private static func shellQuote(_ value: String) -> String { "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'" } }