213 lines
8.4 KiB
Swift
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)
|
|
}
|
|
}
|