Files
steipete--codexbar/Sources/CodexBarCore/RemoteSessionFetcher.swift
T
2026-07-13 12:22:33 +08:00

192 lines
7.8 KiB
Swift

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 {
public static func hosts(from data: Data, excludingLocalHost localHost: String? = nil) -> [String] {
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [] }
let peers: [[String: Any]] = if let dictionary = root["Peer"] as? [String: [String: Any]] {
Array(dictionary.values)
} else if let array = root["Peer"] as? [[String: Any]] {
array
} else {
[]
}
let selfStatus = root["Self"] as? [String: Any]
let localLabels = Set([
localHost,
selfStatus?["DNSName"] as? String,
selfStatus?["HostName"] as? String,
].compactMap(self.firstDNSLabel).map { $0.lowercased() })
var seen = Set<String>()
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()
}
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]
{
guard let tailscale = self.tailscaleBinary(environment: environment),
let result = try? await SubprocessRunner.run(
binary: tailscale,
arguments: ["status", "--json"],
environment: environment,
timeout: 5,
label: "Tailscale session host discovery"),
let data = result.stdout.data(using: .utf8)
else { return [] }
return TailscaleStatusParser.hosts(from: data, excludingLocalHost: localHost)
}
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)
}
}
private func tailscaleBinary(environment: [String: String]) -> String? {
self.findExecutable("tailscale", environment: environment) ?? {
let bundled = "/Applications/Tailscale.app/Contents/MacOS/Tailscale"
return FileManager.default.isExecutableFile(atPath: bundled) ? bundled : nil
}()
}
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<String>()
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: "'\\''") + "'"
}
}