import Foundation public enum ProviderVersionDetector { public static func codexVersion() -> String? { guard let path = TTYCommandRunner.which("codex") else { return nil } let candidates = [ ["--version"], ["version"], ["-v"], ] for args in candidates { if let version = Self.run(path: path, args: args) { return version } } return nil } public static func geminiVersion() -> String? { let env = ProcessInfo.processInfo.environment guard let path = BinaryLocator.resolveGeminiBinary(env: env, loginPATH: nil) ?? TTYCommandRunner.which("gemini") else { return nil } let candidates = [ ["--version"], ["-v"], ] for args in candidates { if let version = Self.run(path: path, args: args) { return version } } return nil } private static func run(path: String, args: [String]) -> String? { let proc = Process() proc.executableURL = URL(fileURLWithPath: path) proc.arguments = args let out = Pipe() proc.standardOutput = out proc.standardError = Pipe() do { try proc.run() } catch { return nil } let deadline = Date().addingTimeInterval(2.0) while proc.isRunning, Date() < deadline { usleep(50000) } if proc.isRunning { proc.terminate() let killDeadline = Date().addingTimeInterval(0.5) while proc.isRunning, Date() < killDeadline { usleep(20000) } if proc.isRunning { kill(proc.processIdentifier, SIGKILL) } } let data = out.fileHandleForReading.readDataToEndOfFile() guard proc.terminationStatus == 0, let text = String(data: data, encoding: .utf8)? .split(whereSeparator: \.isNewline).first else { return nil } let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? nil : trimmed }}
import Foundation
public enum ProviderVersionDetector {
public static func codexVersion() -> String? {
guard let path = TTYCommandRunner.which("codex") else { return nil }
let candidates = [
["--version"],
["version"],
["-v"],
]
for args in candidates {
if let version = Self.run(path: path, args: args) { return version }
}
return nil
}
public static func geminiVersion() -> String? {
let env = ProcessInfo.processInfo.environment
guard let path = BinaryLocator.resolveGeminiBinary(env: env, loginPATH: nil)
?? TTYCommandRunner.which("gemini") else { return nil }
let candidates = [
["--version"],
["-v"],
]
for args in candidates {
if let version = Self.run(path: path, args: args) { return version }
}
return nil
}
private static func run(path: String, args: [String]) -> String? {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: path)
proc.arguments = args
let out = Pipe()
proc.standardOutput = out
proc.standardError = Pipe()
do {
try proc.run()
} catch {
return nil
}
let deadline = Date().addingTimeInterval(2.0)
while proc.isRunning, Date() < deadline {
usleep(50000)
}
if proc.isRunning {
proc.terminate()
let killDeadline = Date().addingTimeInterval(0.5)
while proc.isRunning, Date() < killDeadline {
usleep(20000)
}
if proc.isRunning {
kill(proc.processIdentifier, SIGKILL)
}
}
let data = out.fileHandleForReading.readDataToEndOfFile()
guard proc.terminationStatus == 0,
let text = String(data: data, encoding: .utf8)?
.split(whereSeparator: \.isNewline).first
else { return nil }
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}