import CodexBarCore import Darwin import Foundation struct ClaudeLoginRunner { static let loginArguments = ["auth", "login", "--claudeai"] private static let successMarkers = ["Successfully logged in", "Login successful", "Logged in successfully"] enum Phase { case requesting case waitingBrowser } struct Result { enum Outcome { case success case timedOut case failed(status: Int32) case missingBinary case launchFailed(String) } let outcome: Outcome let output: String let authLink: String? } static func run( timeout: TimeInterval = 120, binary: String = "claude", environment: [String: String]? = nil, onPhaseChange: @escaping @Sendable (Phase) -> Void) async -> Result { await Task(priority: .userInitiated) { onPhaseChange(.requesting) do { let runResult = try self.runPTY( timeout: timeout, binary: binary, environment: environment, onPhaseChange: onPhaseChange) let link = self.firstLink(in: runResult.output) switch runResult.completion { case .processExited(status: 0): return Result(outcome: .success, output: runResult.output, authLink: link) case let .processExited(status): return Result(outcome: .failed(status: status), output: runResult.output, authLink: link) case .outputCondition where self.successMarkers.contains(where: runResult.output.contains): return Result(outcome: .success, output: runResult.output, authLink: link) case .outputCondition, .idleTimeout, .deadlineExceeded: return Result(outcome: .timedOut, output: runResult.output, authLink: link) } } catch LoginError.binaryNotFound { return Result(outcome: .missingBinary, output: "", authLink: nil) } catch let LoginError.timedOut(text) { return Result(outcome: .timedOut, output: text, authLink: self.firstLink(in: text)) } catch { return Result(outcome: .launchFailed(error.localizedDescription), output: "", authLink: nil) } }.value } // MARK: - PTY runner private enum LoginError: Error { case binaryNotFound case timedOut(text: String) case launchFailed(String) } private struct PTYRunResult { let output: String let completion: TTYCommandRunner.Result.Completion } private static func runPTY( timeout: TimeInterval, binary: String, environment: [String: String]?, onPhaseChange: @escaping @Sendable (Phase) -> Void) throws -> PTYRunResult { let runner = TTYCommandRunner() var options = TTYCommandRunner.Options(rows: 50, cols: 160, timeout: timeout) options.extraArgs = self.loginArguments options.baseEnvironment = environment options.stopOnURL = false // keep running until CLI confirms options.stopOnSubstrings = self.successMarkers options.sendOnSubstrings = ["press ENTER to open in browser": "\r"] options.settleAfterStop = 0.35 options.returnOnEmptyProcessExit = true do { let result = try runner.run( binary: binary, send: "", options: options, onURLDetected: { onPhaseChange(.waitingBrowser) }) return PTYRunResult(output: result.text, completion: result.completion) } catch TTYCommandRunner.Error.binaryNotFound { throw LoginError.binaryNotFound } catch TTYCommandRunner.Error.timedOut { throw LoginError.timedOut(text: "") } catch let TTYCommandRunner.Error.launchFailed(msg) { throw LoginError.launchFailed(msg) } catch { throw LoginError.launchFailed(error.localizedDescription) } } private static func firstLink(in text: String) -> String? { let pattern = #"https?://[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } let nsRange = NSRange(text.startIndex..\"'").contains(last) { url.unicodeScalars.removeLast() } return url } }