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

213 lines
8.4 KiB
Swift

import CodexBarCore
import Commander
import Foundation
struct CardsOptions: CommanderParsable {
private static let sourceHelp: String = {
#if os(macOS)
"Data source: auto | web | cli | oauth | api (auto behavior is provider-specific)"
#else
"Data source: auto | web | cli | oauth | api (web/auto are macOS only for web-capable providers)"
#endif
}()
@Flag(names: [.short("v"), .long("verbose")], help: "Enable verbose logging")
var verbose: Bool = false
@Flag(name: .long("json-output"), help: "Emit machine-readable logs")
var jsonOutput: Bool = false
@Option(name: .long("log-level"), help: "Set log level (trace|verbose|debug|info|warning|error|critical)")
var logLevel: String?
@Option(
name: .long("provider"),
help: ProviderHelp.optionHelp)
var provider: ProviderSelection?
@Option(name: .long("account"), help: "Token account label to use (from config.json)")
var account: String?
@Option(name: .long("account-index"), help: "Token account index (1-based)")
var accountIndex: Int?
@Flag(name: .long("all-accounts"), help: "Fetch all token accounts, or all visible Codex accounts")
var allAccounts: Bool = false
@Flag(name: .long("no-credits"), help: "Skip Codex credits line")
var noCredits: Bool = false
@Flag(name: .long("no-color"), help: "Disable ANSI colors in text output")
var noColor: Bool = false
@Flag(name: .long("status"), help: "Fetch and include provider status")
var status: Bool = false
@Flag(name: .long("web"), help: "Alias for --source web")
var web: Bool = false
@Option(name: .long("source"), help: Self.sourceHelp)
var source: String?
@Option(name: .long("web-timeout"), help: "Web fetch timeout (seconds; source=auto or web)")
var webTimeout: Double?
@Flag(name: .long("web-debug-dump-html"), help: "Dump HTML snapshots to /tmp when Codex dashboard data is missing")
var webDebugDumpHtml: Bool = false
@Flag(name: .long("antigravity-plan-debug"), help: "Emit Antigravity planInfo fields (debug)")
var antigravityPlanDebug: Bool = false
@Flag(name: .long("augment-debug"), help: "Emit Augment API responses (debug)")
var augmentDebug: Bool = false
@Flag(name: .long("brief"), help: "Compact table layout instead of the card grid")
var brief: Bool = false
}
extension CodexBarCLI {
static func runCards(_ values: ParsedValues) async {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
let provider = Self.decodeProvider(from: values, config: config)
let includeCredits = !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: .text)
let brief = values.flags.contains("brief")
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 cards: [CLICardModel] = []
var failures: [CLICardFailure] = []
var exitCode: ExitCode = .success
let command = UsageCommandContext(
format: .text,
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,
cardsLayout: true)
for provider in providerList {
let status = includeStatus ? await Self.fetchStatus(for: provider) : nil
let result = await ProviderInteractionContext.$current.withValue(.background) {
await Self.fetchUsageOutputs(
provider: provider,
status: status,
tokenContext: tokenContext,
command: command)
}
if result.exitCode != .success {
exitCode = result.exitCode
}
cards.append(contentsOf: result.cards)
failures.append(contentsOf: result.cardFailures)
}
let rendered: String
let enhanced = CLITerminalCapabilities.supportsEnhancedCards(useColor: useColor)
if brief {
let rows = CLICardsBriefRenderer.makeRows(cards: cards)
rendered = CLICardsBriefRenderer.render(
rows: rows,
failures: failures,
terminalWidth: CLICardsRenderer.terminalColumnCount(),
useColor: useColor,
enhanced: enhanced)
} else {
rendered = CLICardsRenderer.render(
cards: cards,
failures: failures,
terminalWidth: CLICardsRenderer.terminalColumnCount(),
useColor: useColor,
enhanced: enhanced)
}
if !rendered.isEmpty {
print(rendered)
}
Self.exit(code: exitCode, output: output, kind: exitCode == .success ? .runtime : .provider)
}
}