import CodexBarCore import Commander #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #endif import Foundation extension CodexBarCLI { static func decodeProvider(from values: ParsedValues, config: CodexBarConfig) -> ProviderSelection { let rawOverride = values.options["provider"]?.last return Self.providerSelection(rawOverride: rawOverride, enabled: config.enabledProviders()) } static func providerSelection(rawOverride: String?, enabled: [UsageProvider]) -> ProviderSelection { if let rawOverride, let parsed = ProviderSelection(argument: rawOverride) { return parsed } if enabled.count == 2 { let enabledSet = Set(enabled) let primary = Set(ProviderDescriptorRegistry.all.filter(\ .metadata.isPrimaryProvider).map(\ .id)) if !primary.isEmpty, enabledSet == primary { return .both } return .custom(enabled) } if enabled.count >= 3 { return .custom(enabled) } if let first = enabled.first { return ProviderSelection(provider: first) } return .custom([]) } static func decodeFormat(from values: ParsedValues) -> OutputFormat { if let raw = values.options["format"]?.last, let parsed = OutputFormat(argument: raw) { return parsed } if values.flags.contains("jsonShortcut") || values.flags.contains("json") || values.flags.contains("jsonOnly") { return .json } return .text } static func decodeTokenAccountSelection(from values: ParsedValues) throws -> TokenAccountCLISelection { let label = values.options["account"]?.last let rawIndex = values.options["accountIndex"]?.last var index: Int? if let rawIndex { guard let parsed = Int(rawIndex), parsed > 0 else { throw CLIArgumentError("--account-index must be a positive integer.") } index = parsed - 1 } let allAccounts = values.flags.contains("allAccounts") return TokenAccountCLISelection(label: label, index: index, allAccounts: allAccounts) } static func shouldUseColor(noColor: Bool, format: OutputFormat) -> Bool { guard format == .text else { return false } if noColor { return false } let env = ProcessInfo.processInfo.environment if env["TERM"]?.lowercased() == "dumb" { return false } return isatty(STDOUT_FILENO) == 1 } static func detectVersion(for provider: UsageProvider, browserDetection: BrowserDetection) -> String? { ProviderDescriptorRegistry.descriptor(for: provider).cli.versionDetector?(browserDetection) } static func normalizeVersion(raw: String?) -> String? { guard let raw, !raw.isEmpty else { return nil } if let match = raw.range(of: #"(\d+(?:\.\d+)+)"#, options: .regularExpression) { return String(raw[match]).trimmingCharacters(in: .whitespacesAndNewlines) } return raw.trimmingCharacters(in: .whitespacesAndNewlines) } static func makeHeader(provider: UsageProvider, version: String?, source: String) -> String { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName if let version, !version.isEmpty { return "\(name) \(version) (\(source))" } return "\(name) (\(source))" } static func printFetchAttempts(provider: UsageProvider, attempts: [ProviderFetchAttempt]) { guard !attempts.isEmpty else { return } self.writeStderr("[\(provider.rawValue)] fetch strategies:\n") for attempt in attempts { let kindLabel = Self.fetchKindLabel(attempt.kind) var line = " - \(attempt.strategyID) (\(kindLabel))" line += attempt.wasAvailable ? " available" : " unavailable" if let error = attempt.errorDescription, !error.isEmpty { line += " error=\(error)" } self.writeStderr("\(line)\n") } } static func usageTextNotes( provider: UsageProvider, sourceMode: ProviderSourceMode, resolvedSourceLabel: String) -> [String] { guard provider == .kilo, sourceMode == .auto, resolvedSourceLabel.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "cli" else { return [] } return ["Using CLI fallback"] } static func kiloAutoFallbackSummary( provider: UsageProvider, sourceMode: ProviderSourceMode, attempts: [ProviderFetchAttempt]) -> String? { guard provider == .kilo, sourceMode == .auto, !attempts.isEmpty else { return nil } let parts = attempts.map { attempt in let label = Self.fetchKindLabel(attempt.kind) let message = attempt.errorDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !message.isEmpty { return "\(label): \(message)" } return "\(label): \(attempt.wasAvailable ? "success" : "unavailable")" } guard !parts.isEmpty else { return nil } return "Kilo auto fallback attempts: " + parts.joined(separator: " -> ") } private static func fetchKindLabel(_ kind: ProviderFetchKind) -> String { switch kind { case .cli: "cli" case .web: "web" case .oauth: "oauth" case .apiToken: "api" case .localProbe: "local" case .webDashboard: "web" } } static func fetchStatus(for provider: UsageProvider) async -> ProviderStatusPayload? { let urlString = ProviderDescriptorRegistry.descriptor(for: provider).metadata.statusPageURL guard let urlString, let baseURL = URL(string: urlString) else { return nil } do { return try await StatusFetcher.fetch(from: baseURL) } catch { return ProviderStatusPayload( indicator: .unknown, description: error.localizedDescription, updatedAt: nil, url: urlString) } } static func resetTimeDisplayStyleFromDefaults() -> ResetTimeDisplayStyle { let domains = [ "com.steipete.codexbar", "com.steipete.codexbar.debug", ] for domain in domains { if let value = UserDefaults(suiteName: domain)?.object(forKey: "resetTimesShowAbsolute") as? Bool { return value ? .absolute : .countdown } } let fallback = UserDefaults.standard.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false return fallback ? .absolute : .countdown } static func weeklyProgressWorkDaysFromDefaults() -> Int? { let domains = [ "com.steipete.codexbar", "com.steipete.codexbar.debug", ] for domain in domains { if let value = UserDefaults(suiteName: domain)?.object(forKey: "weeklyProgressWorkDays") as? Int { return value } } return UserDefaults.standard.object(forKey: "weeklyProgressWorkDays") as? Int } static func fetchProviderUsage( provider: UsageProvider, context: ProviderFetchContext) async -> ProviderFetchOutcome { let descriptor = ProviderDescriptorRegistry.descriptor(for: provider) if !descriptor.fetchPlan.sourceModes.contains(context.sourceMode) { let error = SourceSelectionError.unsupported( provider: descriptor.cli.name, source: context.sourceMode) return ProviderFetchOutcome(result: .failure(error), attempts: []) } return await descriptor.fetchOutcome(context: context) } private enum SourceSelectionError: LocalizedError { case unsupported(provider: String, source: ProviderSourceMode) var errorDescription: String? { switch self { case let .unsupported(provider, source): "Source '\(source.rawValue)' is not supported for \(provider)." } } } static func loadOpenAIDashboardIfAvailable( usage: UsageSnapshot, sourceLabel: String, context: ProviderFetchContext) -> OpenAIDashboardSnapshot? { guard let cache = OpenAIDashboardCacheStore.load() else { return nil } let snapshot: OpenAIDashboardSnapshot = if cache.snapshot.dailyBreakdown.isEmpty, !cache.snapshot.creditEvents.isEmpty { OpenAIDashboardSnapshot( signedInEmail: cache.snapshot.signedInEmail, codeReviewRemainingPercent: cache.snapshot.codeReviewRemainingPercent, codeReviewLimit: cache.snapshot.codeReviewLimit, creditEvents: cache.snapshot.creditEvents, dailyBreakdown: OpenAIDashboardSnapshot.makeDailyBreakdown( from: cache.snapshot.creditEvents, maxDays: 30), usageBreakdown: cache.snapshot.usageBreakdown, creditsPurchaseURL: cache.snapshot.creditsPurchaseURL, updatedAt: cache.snapshot.updatedAt) } else { cache.snapshot } let input = CodexCLIDashboardAuthorityContext.makeCachedDashboardInput( dashboard: snapshot, cachedAccountEmail: cache.accountEmail, usage: usage, sourceLabel: sourceLabel, context: context) let decision = CodexDashboardAuthority.evaluate(input) if decision.allowedEffects.contains(.cachedDashboardReuse) { return snapshot } if decision.cleanup.contains(.dashboardCache) { OpenAIDashboardCacheStore.clear() } return nil } static func decodeWebTimeout(from values: ParsedValues) throws -> TimeInterval? { guard let raw = values.options["webTimeout"]?.last else { return nil } guard let seconds = Double(raw), seconds.isFinite, seconds >= 0, seconds <= TimeInterval(Int64.max) else { throw CLIArgumentError("--web-timeout must be a finite, nonnegative number within the supported range.") } return seconds } static func decodeSourceMode(from values: ParsedValues) -> ProviderSourceMode? { if values.flags.contains("web") { return .web } guard let raw = values.options["source"]?.last?.lowercased() else { return nil } return ProviderSourceMode(rawValue: raw) } static func renderOpenAIWebDashboardText(_ dash: OpenAIDashboardSnapshot) -> String { var lines: [String] = [] if let email = dash.signedInEmail, !email.isEmpty { lines.append("Web session: \(email)") } if let remaining = dash.codeReviewRemainingPercent { let percent = Int(remaining.rounded()) if let limit = dash.codeReviewLimit, let reset = UsageFormatter.resetLine(for: limit, style: .countdown) { lines.append("Code review: \(percent)% remaining (\(reset))") } else { lines.append("Code review: \(percent)% remaining") } } if let first = dash.creditEvents.first { let day = first.date.formatted(date: .abbreviated, time: .omitted) lines.append("Web history: \(dash.creditEvents.count) events (latest \(day))") } else { lines.append("Web history: none") } return lines.joined(separator: "\n") } static func mapError(_ error: Error) -> ExitCode { switch error { case TTYCommandRunner.Error.binaryNotFound, CodexStatusProbeError.codexNotInstalled, ClaudeUsageError.claudeNotInstalled, GeminiStatusProbeError.geminiNotInstalled: ExitCode(2) case CodexStatusProbeError.timedOut, TTYCommandRunner.Error.timedOut, GeminiStatusProbeError.timedOut, ClaudeWebFetchStrategyError.timedOut, CostUsageError.timedOut: ExitCode(4) case ClaudeUsageError.parseFailed, ClaudeUsageError.oauthFailed, CostUsageError.unsupportedProvider, UsageError.decodeFailed, UsageError.noRateLimitsFound, GeminiStatusProbeError.parseFailed, GeminiStatusProbeError.consumerTierDeprecated: ExitCode(3) default: .failure } } static func printAntigravityPlanInfo(_ info: AntigravityPlanInfoSummary) { let fields: [(String, String?)] = [ ("planName", info.planName), ("planDisplayName", info.planDisplayName), ("displayName", info.displayName), ("productName", info.productName), ("planShortName", info.planShortName), ] self.writeStderr("Antigravity plan info:\n") for (label, value) in fields { guard let value, !value.isEmpty else { continue } self.writeStderr(" \(label): \(value)\n") } } static func loadConfig(output: CLIOutputPreferences) -> CodexBarConfig { let store = CodexBarConfigStore() do { if let existing = try store.load() { return existing } return CodexBarConfig.makeDefault() } catch { if output.usesJSONOutput { let payload = ProviderPayload( providerID: "cli", account: nil, version: nil, source: "cli", status: nil, usage: nil, credits: nil, antigravityPlanInfo: nil, openaiDashboard: nil, error: self.makeErrorPayload(code: .failure, message: error.localizedDescription, kind: .config)) self.printJSON([payload], pretty: output.pretty) } else { self.writeStderr("Error: \(error.localizedDescription)\n") } Self.platformExit(ExitCode.failure.rawValue) } } } struct CLIArgumentError: LocalizedError { let message: String init(_ message: String) { self.message = message } var errorDescription: String? { self.message } } #if DEBUG extension CodexBarCLI { static func _usageSignatureForTesting() -> CommandSignature { CommandSignature.describe(UsageOptions()) } static func _costSignatureForTesting() -> CommandSignature { CommandSignature.describe(CostOptions()) } static func _cacheSignatureForTesting() -> CommandSignature { CommandSignature.describe(CacheOptions()) } static func _diagnoseSignatureForTesting() -> CommandSignature { CommandSignature.describe(DiagnoseOptions()) } static func _configSetAPIKeySignatureForTesting() -> CommandSignature { CommandSignature.describe(ConfigSetAPIKeyOptions()) } static func _configProviderToggleSignatureForTesting() -> CommandSignature { CommandSignature.describe(ConfigProviderToggleOptions()) } static func _decodeFormatForTesting(from values: ParsedValues) -> OutputFormat { self.decodeFormat(from: values) } static func _decodeWebTimeoutForTesting(from values: ParsedValues) throws -> TimeInterval? { try self.decodeWebTimeout(from: values) } static func _decodeSourceModeForTesting(from values: ParsedValues) -> ProviderSourceMode? { self.decodeSourceMode(from: values) } } #endif