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

96 lines
3.5 KiB
Swift

import AppKit
import CodexBarCore
import Foundation
enum GeminiLoginRunner {
private static let geminiConfigDir = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gemini")
private static let credentialsFile = "oauth_creds.json"
private static func clearCredentials() {
let fm = FileManager.default
let filesToDelete = [credentialsFile, "google_accounts.json"]
for file in filesToDelete {
let path = self.geminiConfigDir.appendingPathComponent(file)
try? fm.removeItem(at: path)
}
}
struct Result {
enum Outcome {
case success
case missingBinary
case launchFailed(String)
}
let outcome: Outcome
}
static func run(onCredentialsCreated: (@Sendable () -> Void)? = nil) async -> Result {
await Task(priority: .userInitiated) {
let env = ProcessInfo.processInfo.environment
guard let binary = BinaryLocator.resolveGeminiBinary(
env: env,
loginPATH: LoginShellPathCache.shared.current)
else {
return Result(outcome: .missingBinary)
}
// Clear existing credentials before auth (enables clean account switch)
Self.clearCredentials()
// Start watching for credentials file to be created
if let callback = onCredentialsCreated {
Self.watchForCredentials(callback: callback)
}
// Create a temporary shell script that runs gemini (auto-prompts for auth when no creds)
let scriptContent = """
#!/bin/bash
cd ~
"\(binary)"
"""
let tempDir = FileManager.default.temporaryDirectory
let scriptURL = tempDir.appendingPathComponent("gemini_login_\(UUID().uuidString).command")
do {
try scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path)
let config = NSWorkspace.OpenConfiguration()
config.activates = true
try await NSWorkspace.shared.open(scriptURL, configuration: config)
// Clean up script after Terminal has time to read it
let scriptPath = scriptURL.path
DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
try? FileManager.default.removeItem(atPath: scriptPath)
}
return Result(outcome: .success)
} catch {
return Result(outcome: .launchFailed(error.localizedDescription))
}
}.value
}
/// Watch for credentials file to be created, then call callback once
private static func watchForCredentials(callback: @escaping @Sendable () -> Void, timeout: TimeInterval = 300) {
let credsPath = self.geminiConfigDir.appendingPathComponent(self.credentialsFile).path
DispatchQueue.global(qos: .utility).async {
let startTime = Date()
while Date().timeIntervalSince(startTime) < timeout {
if FileManager.default.fileExists(atPath: credsPath) {
// Small delay to ensure file is fully written
Thread.sleep(forTimeInterval: 0.5)
callback()
return
}
Thread.sleep(forTimeInterval: 1.0)
}
}
}
}