99 lines · 3.2 KB · Swift
RawDownload
#if canImport(Darwin)import Darwin#elseimport Glibc#endifimport 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,                    extraArgs: ["--allowed-tools", "", "--version"],                    initialDelay: 0.0)).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) -> String? {        let proc = Process()        proc.executableURL = URL(fileURLWithPath: path)        proc.arguments = args        let out = Pipe()        proc.standardOutput = out        proc.standardError = Pipe()        proc.standardInput = nil         let exitSemaphore = DispatchSemaphore(value: 0)        proc.terminationHandler = { _ in            exitSemaphore.signal()        }         do {            try proc.run()        } catch {            return nil        }         let didExit = exitSemaphore.wait(timeout: .now() + timeout) == .success        if !didExit, !Self.forceExit(proc, exitSemaphore: exitSemaphore) {            return nil        }         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    }     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    }}