745 lines
29 KiB
Swift
745 lines
29 KiB
Swift
import CodexBarCore
|
|
import Commander
|
|
import Foundation
|
|
|
|
struct UsageCommandContext {
|
|
let format: OutputFormat
|
|
let includeCredits: Bool
|
|
let sourceModeOverride: ProviderSourceMode?
|
|
let antigravityPlanDebug: Bool
|
|
let augmentDebug: Bool
|
|
let webDebugDumpHTML: Bool
|
|
let webTimeout: TimeInterval
|
|
let verbose: Bool
|
|
let useColor: Bool
|
|
let resetStyle: ResetTimeDisplayStyle
|
|
let weeklyWorkDays: Int?
|
|
let jsonOnly: Bool
|
|
let includeAllCodexAccounts: Bool
|
|
let fetcher: UsageFetcher
|
|
let claudeFetcher: ClaudeUsageFetcher
|
|
let browserDetection: BrowserDetection
|
|
/// True for long-lived hosts (`codexbar serve`) that keep warm provider
|
|
/// helper sessions (such as the managed Antigravity `agy` process) alive
|
|
/// between fetches instead of resetting after each one-shot fetch.
|
|
var persistCLISessions: Bool = false
|
|
var persistentCLISessionIdleWindow: TimeInterval?
|
|
var cardsLayout: Bool = false
|
|
}
|
|
|
|
struct UsageCommandOutput {
|
|
var sections: [String] = []
|
|
var payload: [ProviderPayload] = []
|
|
var cards: [CLICardModel] = []
|
|
var cardFailures: [CLICardFailure] = []
|
|
var exitCode: ExitCode = .success
|
|
}
|
|
|
|
private struct UsageSuccessRenderInput {
|
|
let provider: UsageProvider
|
|
let accountLabel: String?
|
|
let cacheAccountKey: String?
|
|
let version: String?
|
|
let source: String
|
|
let status: ProviderStatusPayload?
|
|
let usage: UsageSnapshot
|
|
let credits: CreditsSnapshot?
|
|
let antigravityPlanInfo: AntigravityPlanInfoSummary?
|
|
let dashboard: OpenAIDashboardSnapshot?
|
|
let effectiveSourceMode: ProviderSourceMode
|
|
let command: UsageCommandContext
|
|
let notes: [String]
|
|
}
|
|
|
|
extension UsageCommandOutput {
|
|
mutating func merge(_ other: UsageCommandOutput) {
|
|
self.sections.append(contentsOf: other.sections)
|
|
self.payload.append(contentsOf: other.payload)
|
|
self.cards.append(contentsOf: other.cards)
|
|
self.cardFailures.append(contentsOf: other.cardFailures)
|
|
if other.exitCode != .success {
|
|
self.exitCode = other.exitCode
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CodexBarCLI {
|
|
static func runUsage(_ values: ParsedValues) async {
|
|
let output = CLIOutputPreferences.from(values: values)
|
|
let config = Self.loadConfig(output: output)
|
|
let provider = Self.decodeProvider(from: values, config: config)
|
|
let format = output.format
|
|
let includeCredits = format == .json ? true : !values.flags.contains("noCredits")
|
|
let includeStatus = values.flags.contains("status")
|
|
let sourceModeRaw = values.options["source"]?.last
|
|
let parsedSourceMode = Self.decodeSourceMode(from: values)
|
|
if sourceModeRaw != nil, parsedSourceMode == nil {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Error: --source must be auto|web|cli|oauth|api.",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
let antigravityPlanDebug = values.flags.contains("antigravityPlanDebug")
|
|
let augmentDebug = values.flags.contains("augmentDebug")
|
|
let webDebugDumpHTML = values.flags.contains("webDebugDumpHtml")
|
|
let webTimeout: TimeInterval
|
|
do {
|
|
webTimeout = try Self.decodeWebTimeout(from: values) ?? 60
|
|
} catch {
|
|
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .args)
|
|
}
|
|
let verbose = values.flags.contains("verbose")
|
|
let noColor = values.flags.contains("noColor")
|
|
let useColor = Self.shouldUseColor(noColor: noColor, format: format)
|
|
let resetStyle = Self.resetTimeDisplayStyleFromDefaults()
|
|
let weeklyWorkDays = Self.weeklyProgressWorkDaysFromDefaults()
|
|
let providerList = provider.asList
|
|
|
|
let tokenSelection: TokenAccountCLISelection
|
|
do {
|
|
tokenSelection = try Self.decodeTokenAccountSelection(from: values)
|
|
} catch {
|
|
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .args)
|
|
}
|
|
|
|
if tokenSelection.allAccounts, tokenSelection.label != nil || tokenSelection.index != nil {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Error: --all-accounts cannot be combined with --account or --account-index.",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
|
|
if tokenSelection.usesOverride {
|
|
guard providerList.count == 1 else {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Error: account selection requires a single provider.",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
let supportsAllCodexAccounts = providerList[0] == .codex
|
|
&& tokenSelection.allAccounts
|
|
&& tokenSelection.label == nil
|
|
&& tokenSelection.index == nil
|
|
guard supportsAllCodexAccounts || TokenAccountSupportCatalog.support(for: providerList[0]) != nil else {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Error: \(providerList[0].rawValue) does not support token accounts.",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
}
|
|
|
|
let browserDetection = BrowserDetection()
|
|
let fetcher = UsageFetcher()
|
|
let claudeFetcher = ClaudeUsageFetcher(browserDetection: browserDetection)
|
|
let tokenContext: TokenAccountCLIContext
|
|
do {
|
|
tokenContext = try TokenAccountCLIContext(
|
|
selection: tokenSelection,
|
|
config: config,
|
|
verbose: verbose)
|
|
} catch {
|
|
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config)
|
|
}
|
|
|
|
var sections: [String] = []
|
|
var payload: [ProviderPayload] = []
|
|
var exitCode: ExitCode = .success
|
|
let command = UsageCommandContext(
|
|
format: format,
|
|
includeCredits: includeCredits,
|
|
sourceModeOverride: parsedSourceMode,
|
|
antigravityPlanDebug: antigravityPlanDebug,
|
|
augmentDebug: augmentDebug,
|
|
webDebugDumpHTML: webDebugDumpHTML,
|
|
webTimeout: webTimeout,
|
|
verbose: verbose,
|
|
useColor: useColor,
|
|
resetStyle: resetStyle,
|
|
weeklyWorkDays: weeklyWorkDays,
|
|
jsonOnly: output.jsonOnly,
|
|
includeAllCodexAccounts: tokenSelection.allAccounts && providerList == [.codex],
|
|
fetcher: fetcher,
|
|
claudeFetcher: claudeFetcher,
|
|
browserDetection: browserDetection)
|
|
|
|
for p in providerList {
|
|
let status = includeStatus ? await Self.fetchStatus(for: p) : nil
|
|
// CLI usage should not clear Keychain cooldowns or attempt interactive Keychain prompts.
|
|
let output = await ProviderInteractionContext.$current.withValue(.background) {
|
|
await Self.fetchUsageOutputs(
|
|
provider: p,
|
|
status: status,
|
|
tokenContext: tokenContext,
|
|
command: command)
|
|
}
|
|
if output.exitCode != .success {
|
|
exitCode = output.exitCode
|
|
}
|
|
sections.append(contentsOf: output.sections)
|
|
payload.append(contentsOf: output.payload)
|
|
}
|
|
|
|
switch format {
|
|
case .text:
|
|
if !sections.isEmpty {
|
|
print(sections.joined(separator: "\n\n"))
|
|
}
|
|
case .json:
|
|
Self.printJSON(payload, pretty: output.pretty)
|
|
}
|
|
|
|
Self.exit(code: exitCode, output: output, kind: exitCode == .success ? .runtime : .provider)
|
|
}
|
|
|
|
static func fetchUsageOutputs(
|
|
provider: UsageProvider,
|
|
status: ProviderStatusPayload?,
|
|
tokenContext: TokenAccountCLIContext,
|
|
command: UsageCommandContext) async -> UsageCommandOutput
|
|
{
|
|
if provider == .codex, command.includeAllCodexAccounts {
|
|
var output = UsageCommandOutput()
|
|
let accounts = tokenContext.visibleCodexAccounts().visibleAccounts
|
|
let selections: [CodexVisibleAccount?] = accounts.isEmpty ? [nil] : accounts.map { Optional($0) }
|
|
for visibleAccount in selections {
|
|
let result = await Self.fetchUsageOutput(
|
|
provider: provider,
|
|
account: nil,
|
|
codexVisibleAccount: visibleAccount,
|
|
status: status,
|
|
tokenContext: tokenContext,
|
|
command: command)
|
|
output.merge(result)
|
|
}
|
|
return output
|
|
}
|
|
|
|
let accounts: [ProviderTokenAccount]
|
|
do {
|
|
accounts = try tokenContext.resolvedAccounts(for: provider)
|
|
} catch {
|
|
return Self.usageOutputForAccountResolutionError(
|
|
provider: provider,
|
|
status: status,
|
|
command: command,
|
|
error: error)
|
|
}
|
|
|
|
let selections = Self.accountSelections(from: accounts)
|
|
var output = UsageCommandOutput()
|
|
for account in selections {
|
|
let result = await Self.fetchUsageOutput(
|
|
provider: provider,
|
|
account: account,
|
|
status: status,
|
|
tokenContext: tokenContext,
|
|
command: command)
|
|
output.merge(result)
|
|
}
|
|
return output
|
|
}
|
|
|
|
private static func accountSelections(from accounts: [ProviderTokenAccount]) -> [ProviderTokenAccount?] {
|
|
if accounts.isEmpty { return [nil] }
|
|
return accounts.map { Optional($0) }
|
|
}
|
|
|
|
private static func usageOutputForAccountResolutionError(
|
|
provider: UsageProvider,
|
|
status: ProviderStatusPayload?,
|
|
command: UsageCommandContext,
|
|
error: Error) -> UsageCommandOutput
|
|
{
|
|
var output = UsageCommandOutput()
|
|
output.exitCode = .failure
|
|
if command.format == .json {
|
|
output.payload.append(Self.makeProviderErrorPayload(
|
|
provider: provider,
|
|
account: nil,
|
|
source: command.sourceModeOverride?.rawValue ?? "auto",
|
|
status: status,
|
|
error: error,
|
|
kind: .provider))
|
|
} else if command.cardsLayout {
|
|
output.cardFailures.append(CLICardFailure(
|
|
provider: provider,
|
|
accountLabel: nil,
|
|
message: error.localizedDescription))
|
|
} else if !command.jsonOnly {
|
|
Self.writeStderr("Error: \(error.localizedDescription)\n")
|
|
}
|
|
return output
|
|
}
|
|
|
|
// swiftlint:disable:next function_parameter_count
|
|
private static func makeUsagePayload(
|
|
provider: UsageProvider,
|
|
accountLabel: String?,
|
|
cacheAccountKey: String?,
|
|
version: String?,
|
|
source: String,
|
|
status: ProviderStatusPayload?,
|
|
usage: UsageSnapshot,
|
|
credits: CreditsSnapshot?,
|
|
antigravityPlanInfo: AntigravityPlanInfoSummary?,
|
|
dashboard: OpenAIDashboardSnapshot?,
|
|
weeklyWorkDays: Int?) -> ProviderPayload
|
|
{
|
|
ProviderPayload(
|
|
provider: provider,
|
|
account: accountLabel,
|
|
cacheAccountKey: cacheAccountKey,
|
|
version: version,
|
|
source: source,
|
|
status: status,
|
|
usage: usage,
|
|
credits: credits,
|
|
antigravityPlanInfo: antigravityPlanInfo,
|
|
openaiDashboard: dashboard,
|
|
error: nil,
|
|
pace: CLIRenderer.providerPacePayload(provider: provider, snapshot: usage, weeklyWorkDays: weeklyWorkDays))
|
|
}
|
|
|
|
private static func appendSuccessRenderOutput(
|
|
_ input: UsageSuccessRenderInput,
|
|
output: inout UsageCommandOutput)
|
|
{
|
|
switch input.command.format {
|
|
case .text:
|
|
if input.command.cardsLayout {
|
|
output.cards.append(CLICardsRenderer.makeCard(CLICardBuildInput(
|
|
provider: input.provider,
|
|
snapshot: input.usage,
|
|
credits: input.credits,
|
|
source: input.source,
|
|
status: input.status,
|
|
notes: input.notes,
|
|
useColor: input.command.useColor,
|
|
resetStyle: input.command.resetStyle,
|
|
weeklyWorkDays: input.command.weeklyWorkDays,
|
|
now: Date())))
|
|
} else {
|
|
var text = CLIRenderer.renderText(
|
|
provider: input.provider,
|
|
snapshot: input.usage,
|
|
credits: input.credits,
|
|
context: RenderContext(
|
|
header: Self.makeHeader(
|
|
provider: input.provider,
|
|
version: input.version,
|
|
source: input.source),
|
|
status: input.status,
|
|
useColor: input.command.useColor,
|
|
resetStyle: input.command.resetStyle,
|
|
weeklyWorkDays: input.command.weeklyWorkDays,
|
|
notes: input.notes))
|
|
if let dashboard = input.dashboard, input.provider == .codex, input.effectiveSourceMode.usesWeb {
|
|
text += "\n" + Self.renderOpenAIWebDashboardText(dashboard)
|
|
}
|
|
output.sections.append(text)
|
|
}
|
|
case .json:
|
|
output.payload.append(self.makeUsagePayload(
|
|
provider: input.provider,
|
|
accountLabel: input.accountLabel,
|
|
cacheAccountKey: input.cacheAccountKey,
|
|
version: input.version,
|
|
source: input.source,
|
|
status: input.status,
|
|
usage: input.usage,
|
|
credits: input.credits,
|
|
antigravityPlanInfo: input.antigravityPlanInfo,
|
|
dashboard: input.dashboard,
|
|
weeklyWorkDays: input.command.weeklyWorkDays))
|
|
}
|
|
}
|
|
|
|
private static func fetchUsageOutput(
|
|
provider: UsageProvider,
|
|
account: ProviderTokenAccount?,
|
|
codexVisibleAccount: CodexVisibleAccount? = nil,
|
|
status: ProviderStatusPayload?,
|
|
tokenContext: TokenAccountCLIContext,
|
|
command: UsageCommandContext) async -> UsageCommandOutput
|
|
{
|
|
var output = UsageCommandOutput()
|
|
let env = tokenContext.environment(
|
|
base: ProcessInfo.processInfo.environment,
|
|
provider: provider,
|
|
account: account,
|
|
codexActiveSourceOverride: codexVisibleAccount?.selectionSource)
|
|
let settings = tokenContext.settingsSnapshot(
|
|
for: provider,
|
|
account: account,
|
|
codexActiveSourceOverride: codexVisibleAccount?.selectionSource)
|
|
let configSource = tokenContext.preferredSourceMode(for: provider)
|
|
let baseSource = command.sourceModeOverride ?? configSource
|
|
let effectiveSourceMode = tokenContext.effectiveSourceMode(
|
|
base: baseSource,
|
|
provider: provider,
|
|
account: account)
|
|
let cacheAccountKey = Self.usageCacheAccountKey(
|
|
provider: provider,
|
|
account: account,
|
|
codexVisibleAccount: codexVisibleAccount)
|
|
|
|
#if !os(macOS)
|
|
if Self.sourceModeRequiresWebSupport(
|
|
effectiveSourceMode,
|
|
provider: provider,
|
|
environment: env,
|
|
settings: settings)
|
|
{
|
|
return Self.webSourceUnsupportedOutput(
|
|
provider: provider,
|
|
account: (
|
|
label: account?.label ?? codexVisibleAccount?.menuDisplayName,
|
|
cacheKey: cacheAccountKey),
|
|
source: effectiveSourceMode.rawValue,
|
|
status: status,
|
|
command: command)
|
|
}
|
|
#endif
|
|
|
|
let fetchContext = ProviderFetchContext(
|
|
runtime: .cli,
|
|
sourceMode: effectiveSourceMode,
|
|
includeCredits: command.includeCredits,
|
|
webTimeout: command.webTimeout,
|
|
webDebugDumpHTML: command.webDebugDumpHTML,
|
|
verbose: command.verbose,
|
|
env: env,
|
|
settings: settings,
|
|
fetcher: tokenContext.fetcher(base: command.fetcher, provider: provider, env: env),
|
|
claudeFetcher: command.claudeFetcher,
|
|
browserDetection: command.browserDetection,
|
|
selectedTokenAccountID: account?.id,
|
|
tokenAccountTokenUpdater: tokenContext.tokenUpdater(for: account),
|
|
providerManualTokenUpdater: tokenContext.manualTokenUpdater(),
|
|
persistsCLISessions: Self.persistsCLISessions(provider: provider, command: command),
|
|
persistentCLISessionIdleWindow: command.persistentCLISessionIdleWindow)
|
|
let outcome = await Self.fetchProviderUsage(provider: provider, context: fetchContext)
|
|
if command.verbose, !command.jsonOnly {
|
|
Self.printFetchAttempts(provider: provider, attempts: outcome.attempts)
|
|
}
|
|
|
|
switch outcome.result {
|
|
case let .success(result):
|
|
let antigravityPlanInfo = await Self.fetchAntigravityPlanInfoIfNeeded(
|
|
provider: provider,
|
|
command: command)
|
|
await Self.emitAugmentDebugIfNeeded(provider: provider, command: command)
|
|
|
|
var usage = result.usage.scoped(to: provider)
|
|
if let account {
|
|
usage = tokenContext.applyAccountLabel(usage, provider: provider, account: account)
|
|
} else if let codexVisibleAccount {
|
|
usage = tokenContext.applyCodexVisibleAccountLabel(usage, account: codexVisibleAccount)
|
|
}
|
|
|
|
var dashboard = result.dashboard
|
|
if dashboard == nil, command.format == .json, provider == .codex {
|
|
dashboard = Self.loadOpenAIDashboardIfAvailable(
|
|
usage: usage,
|
|
sourceLabel: result.sourceLabel,
|
|
context: fetchContext)
|
|
}
|
|
|
|
let shouldDetectVersion = Self.shouldDetectVersion(provider: provider, result: result)
|
|
let version = Self.normalizeVersion(
|
|
raw: shouldDetectVersion
|
|
? Self.detectVersion(for: provider, browserDetection: command.browserDetection)
|
|
: nil)
|
|
let source = result.sourceLabel
|
|
let notes = Self.usageTextNotes(
|
|
provider: provider,
|
|
sourceMode: effectiveSourceMode,
|
|
resolvedSourceLabel: source)
|
|
|
|
Self.appendSuccessRenderOutput(
|
|
UsageSuccessRenderInput(
|
|
provider: provider,
|
|
accountLabel: account?.label ?? codexVisibleAccount?.menuDisplayName,
|
|
cacheAccountKey: cacheAccountKey,
|
|
version: version,
|
|
source: source,
|
|
status: status,
|
|
usage: usage,
|
|
credits: result.credits,
|
|
antigravityPlanInfo: antigravityPlanInfo,
|
|
dashboard: dashboard,
|
|
effectiveSourceMode: effectiveSourceMode,
|
|
command: command,
|
|
notes: notes),
|
|
output: &output)
|
|
case let .failure(error):
|
|
output.exitCode = Self.mapError(error)
|
|
if command.format == .json {
|
|
output.payload.append(Self.makeProviderErrorPayload(
|
|
provider: provider,
|
|
account: account?.label ?? codexVisibleAccount?.menuDisplayName,
|
|
cacheAccountKey: cacheAccountKey,
|
|
source: effectiveSourceMode.rawValue,
|
|
status: status,
|
|
error: error,
|
|
kind: .provider))
|
|
} else if command.cardsLayout {
|
|
output.cardFailures.append(CLICardFailure(
|
|
provider: provider,
|
|
accountLabel: account?.label ?? codexVisibleAccount?.menuDisplayName,
|
|
message: error.localizedDescription))
|
|
} else if !command.jsonOnly {
|
|
if let accountLabel = account?.label ?? codexVisibleAccount?.menuDisplayName {
|
|
Self.writeStderr(
|
|
"Error (\(provider.rawValue) - \(accountLabel)): \(error.localizedDescription)\n")
|
|
} else {
|
|
Self.writeStderr("Error: \(error.localizedDescription)\n")
|
|
}
|
|
if let summary = Self.kiloAutoFallbackSummary(
|
|
provider: provider,
|
|
sourceMode: effectiveSourceMode,
|
|
attempts: outcome.attempts)
|
|
{
|
|
Self.writeStderr("\(summary)\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
return await Self.finishUsageOutput(output, provider: provider, command: command)
|
|
}
|
|
|
|
static func shouldDetectVersion(provider: UsageProvider, result: ProviderFetchResult) -> Bool {
|
|
let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
|
|
guard descriptor.cli.versionDetector != nil else { return false }
|
|
guard result.strategyKind != .webDashboard else { return false }
|
|
return !(provider == .claude && result.strategyKind == .oauth)
|
|
}
|
|
|
|
private static func holdsAntigravitySession(
|
|
provider: UsageProvider,
|
|
command: UsageCommandContext) -> Bool
|
|
{
|
|
self.holdsAntigravityCLISessionForPlanDebug(
|
|
provider: provider,
|
|
planDebugEnabled: command.antigravityPlanDebug,
|
|
jsonOnly: command.jsonOnly,
|
|
persistsCLISessions: command.persistCLISessions)
|
|
}
|
|
|
|
private static func persistsCLISessions(
|
|
provider: UsageProvider,
|
|
command: UsageCommandContext) -> Bool
|
|
{
|
|
command.persistCLISessions || self.holdsAntigravitySession(provider: provider, command: command)
|
|
}
|
|
|
|
static func holdsAntigravityCLISessionForPlanDebug(
|
|
provider: UsageProvider,
|
|
planDebugEnabled: Bool,
|
|
jsonOnly: Bool,
|
|
persistsCLISessions: Bool) -> Bool
|
|
{
|
|
provider == .antigravity
|
|
&& planDebugEnabled
|
|
&& !jsonOnly
|
|
&& !persistsCLISessions
|
|
}
|
|
|
|
private static func finishUsageOutput(
|
|
_ output: UsageCommandOutput,
|
|
provider: UsageProvider,
|
|
command: UsageCommandContext) async -> UsageCommandOutput
|
|
{
|
|
if self.holdsAntigravitySession(provider: provider, command: command) {
|
|
await ProviderCLISessionLifecycle.shutdownPersistentSessions()
|
|
}
|
|
return output
|
|
}
|
|
|
|
private static func fetchAntigravityPlanInfoIfNeeded(
|
|
provider: UsageProvider,
|
|
command: UsageCommandContext) async -> AntigravityPlanInfoSummary?
|
|
{
|
|
guard command.antigravityPlanDebug,
|
|
provider == .antigravity,
|
|
!command.jsonOnly
|
|
else {
|
|
return nil
|
|
}
|
|
let info = try? await AntigravityStatusProbe().fetchPlanInfoSummary()
|
|
if command.format == .text, let info {
|
|
Self.printAntigravityPlanInfo(info)
|
|
}
|
|
return info
|
|
}
|
|
|
|
private static func emitAugmentDebugIfNeeded(
|
|
provider: UsageProvider,
|
|
command: UsageCommandContext) async
|
|
{
|
|
guard command.augmentDebug, provider == .augment else { return }
|
|
#if os(macOS)
|
|
let dump = await AugmentStatusProbe.latestDumps()
|
|
if command.format == .text, !dump.isEmpty, !command.jsonOnly {
|
|
Self.writeStderr("Augment API responses:\n\(dump)\n")
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private static func webSourceUnsupportedOutput(
|
|
provider: UsageProvider,
|
|
account: (label: String?, cacheKey: String?),
|
|
source: String,
|
|
status: ProviderStatusPayload?,
|
|
command: UsageCommandContext) -> UsageCommandOutput
|
|
{
|
|
var output = UsageCommandOutput()
|
|
let error = NSError(
|
|
domain: "CodexBarCLI",
|
|
code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey:
|
|
"Error: selected source requires web support and is only supported on macOS."])
|
|
output.exitCode = .failure
|
|
if command.format == .json {
|
|
output.payload.append(Self.makeProviderErrorPayload(
|
|
provider: provider,
|
|
account: account.label,
|
|
cacheAccountKey: account.cacheKey,
|
|
source: source,
|
|
status: status,
|
|
error: error,
|
|
kind: .runtime))
|
|
} else if command.cardsLayout {
|
|
output.cardFailures.append(CLICardFailure(
|
|
provider: provider,
|
|
accountLabel: account.label,
|
|
message: error.localizedDescription))
|
|
} else if !command.jsonOnly {
|
|
Self.writeStderr("Error: \(error.localizedDescription)\n")
|
|
}
|
|
return output
|
|
}
|
|
|
|
static func usageCacheAccountKey(
|
|
provider _: UsageProvider,
|
|
account: ProviderTokenAccount?,
|
|
codexVisibleAccount: CodexVisibleAccount?) -> String?
|
|
{
|
|
if let account {
|
|
return "token:\(account.id.uuidString.lowercased())"
|
|
}
|
|
if let codexVisibleAccount {
|
|
if let workspaceAccountID = codexVisibleAccount.workspaceAccountID?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!workspaceAccountID.isEmpty,
|
|
!codexVisibleAccount.email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
{
|
|
let email = codexVisibleAccount.email
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.lowercased()
|
|
return "codex:workspace:\(workspaceAccountID):email:\(email)"
|
|
}
|
|
if let storedAccountID = codexVisibleAccount.storedAccountID {
|
|
return "codex:stored:\(storedAccountID.uuidString.lowercased())"
|
|
}
|
|
if let authFingerprint = codexVisibleAccount.authFingerprint {
|
|
return "codex:auth:\(authFingerprint)"
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
static func sourceModeRequiresWebSupport(
|
|
_ sourceMode: ProviderSourceMode,
|
|
provider: UsageProvider,
|
|
environment: [String: String]? = nil,
|
|
settings: ProviderSettingsSnapshot? = nil) -> Bool
|
|
{
|
|
guard provider != .grok, provider != .amp else {
|
|
return false
|
|
}
|
|
if provider == .codex, sourceMode == .auto {
|
|
return false
|
|
}
|
|
if provider == .claude, sourceMode == .auto {
|
|
// Claude's cross-platform planner skips its unavailable web step and falls back to the CLI.
|
|
return false
|
|
}
|
|
if provider == .opencodego {
|
|
if sourceMode == .auto || settings?.opencodego?.cookieSource == .manual {
|
|
return false
|
|
}
|
|
}
|
|
if provider == .commandcode,
|
|
settings?.commandcode?.cookieSource == .manual
|
|
{
|
|
return false
|
|
}
|
|
#if os(Linux)
|
|
if provider == .cursor,
|
|
settings?.cursor?.cookieSource != .off
|
|
{
|
|
// Linux uses Cursor app auth and manual cookies; browser import remains macOS-only.
|
|
return false
|
|
}
|
|
#endif
|
|
if provider == .sakana,
|
|
sourceMode == .auto || sourceMode == .web,
|
|
environment.map({ SakanaSettingsReader.cookieHeader(environment: $0) != nil }) == true
|
|
{
|
|
return false
|
|
}
|
|
if provider == .qoder,
|
|
settings?.qoder?.cookieSource == .manual
|
|
{
|
|
return false
|
|
}
|
|
if provider == .ollama,
|
|
sourceMode == .auto
|
|
{
|
|
let hasEnvironmentToken = environment.map {
|
|
ProviderTokenResolver.ollamaToken(environment: $0) != nil
|
|
} == true
|
|
if settings?.ollama?.cookieSource == .off || hasEnvironmentToken {
|
|
return false
|
|
}
|
|
}
|
|
if provider == .kimi,
|
|
sourceMode == .auto,
|
|
environment.map({ environment in
|
|
ProviderTokenResolver.kimiAPIToken(environment: environment) != nil ||
|
|
KimiSettingsReader.hasKimiCodeCredential(environment: environment)
|
|
}) == true
|
|
{
|
|
return false
|
|
}
|
|
if provider == .factory,
|
|
sourceMode == .auto || sourceMode == .cli,
|
|
environment.map({ FactorySettingsReader.apiKey(environment: $0) != nil }) == true
|
|
{
|
|
// Linux Auto/legacy-cli can use FACTORY_API_KEY without browser cookies.
|
|
return false
|
|
}
|
|
if provider == .mimo,
|
|
sourceMode == .auto,
|
|
let environment,
|
|
MiMoLocalUsageFallback.cacheExists(environment: environment)
|
|
{
|
|
return false
|
|
}
|
|
return switch sourceMode {
|
|
case .web:
|
|
true
|
|
case .auto:
|
|
ProviderDescriptorRegistry.descriptor(for: provider).fetchPlan.sourceModes.contains(.web)
|
|
case .cli, .oauth, .api:
|
|
false
|
|
}
|
|
}
|
|
}
|