chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#endif
|
||||
import Foundation
|
||||
|
||||
public enum ProviderVersionDetector {
|
||||
public static func claudeVersion() -> String? {
|
||||
guard let path = TTYCommandRunner.which("claude") else { return nil }
|
||||
do {
|
||||
let out = try TTYCommandRunner().run(
|
||||
binary: path,
|
||||
send: "",
|
||||
options: TTYCommandRunner.Options(
|
||||
timeout: 5.0,
|
||||
// `--version` alone makes Claude Code print and exit before it
|
||||
// initializes its credential subsystem. Passing `--allowed-tools`
|
||||
// (even empty) makes it treat the invocation as a real session and
|
||||
// read the OAuth token from the macOS keychain ("Claude Code-credentials"),
|
||||
// which spawns `/usr/bin/security` and triggers a keychain prompt on
|
||||
// every probe when no ~/.claude/.credentials.json / env token exists.
|
||||
extraArgs: ["--version"],
|
||||
initialDelay: 0.0,
|
||||
useClaudeProbeWorkingDirectory: true)).text
|
||||
let trimmed = TextParsing.stripANSICodes(out).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
static func run(
|
||||
path: String,
|
||||
args: [String],
|
||||
timeout: TimeInterval = 2.0,
|
||||
environment: [String: String]? = nil,
|
||||
mergeStandardError: Bool = false) -> String?
|
||||
{
|
||||
let proc = Process()
|
||||
proc.executableURL = URL(fileURLWithPath: path)
|
||||
proc.arguments = args
|
||||
proc.environment = environment
|
||||
let out = Pipe()
|
||||
proc.standardOutput = out
|
||||
proc.standardError = mergeStandardError ? out : FileHandle.nullDevice
|
||||
proc.standardInput = nil
|
||||
let outputCapture = ProcessPipeCapture(pipe: out)
|
||||
outputCapture.start()
|
||||
|
||||
let exitSemaphore = DispatchSemaphore(value: 0)
|
||||
proc.terminationHandler = { _ in
|
||||
exitSemaphore.signal()
|
||||
}
|
||||
|
||||
do {
|
||||
try proc.run()
|
||||
} catch {
|
||||
outputCapture.stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
let didExit = exitSemaphore.wait(timeout: .now() + timeout) == .success
|
||||
if !didExit, !Self.forceExit(proc, exitSemaphore: exitSemaphore) {
|
||||
outputCapture.stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
let data = outputCapture.finishSynchronously(timeout: 0.25)
|
||||
guard proc.terminationStatus == 0,
|
||||
let text = ProcessPipeCapture.decodeUTF8(data)
|
||||
.split(whereSeparator: \.isNewline).first
|
||||
else { return nil }
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
private static func forceExit(_ proc: Process, exitSemaphore: DispatchSemaphore) -> Bool {
|
||||
guard proc.isRunning else { return true }
|
||||
|
||||
proc.terminate()
|
||||
if exitSemaphore.wait(timeout: .now() + 0.5) == .success {
|
||||
return true
|
||||
}
|
||||
|
||||
guard proc.isRunning else { return true }
|
||||
kill(proc.processIdentifier, SIGKILL)
|
||||
return exitSemaphore.wait(timeout: .now() + 1.0) == .success
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user