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

153 lines
5.7 KiB
Swift

import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
static func runCacheClear(_ values: ParsedValues) {
let output = CLIOutputPreferences.from(values: values)
let cookies = values.flags.contains("cookies")
let cost = values.flags.contains("cost")
let all = values.flags.contains("all")
let rawProvider = values.options["provider"]?.last
let clearCookies = cookies || all
let clearCost = cost || all
if !clearCookies, !clearCost {
Self.exit(
code: .failure,
message: "Specify --cookies, --cost, or --all.",
output: output,
kind: .args)
}
if let error = Self.cacheClearProviderScopeError(rawProvider: rawProvider, clearCost: clearCost) {
Self.exit(code: .failure, message: error, output: output, kind: .args)
}
var results: [CacheClearResult] = []
if clearCookies {
if let rawProvider {
if let provider = ProviderDescriptorRegistry.cliNameMap[rawProvider.lowercased()] {
let summary = CookieHeaderCache.clearAllScopesDetailed(provider: provider)
results.append(CacheClearResult(
cache: "cookies",
provider: provider.rawValue,
cleared: summary.clearedCount,
error: Self.cookieClearError(failedCount: summary.failedCount)))
} else {
Self.exit(
code: .failure,
message: "Unknown provider: \(rawProvider)",
output: output,
kind: .args)
}
} else {
let summary = CookieHeaderCache.clearAllDetailed()
results.append(CacheClearResult(
cache: "cookies",
provider: nil,
cleared: summary.clearedCount,
error: Self.cookieClearError(failedCount: summary.failedCount)))
}
}
if clearCost {
let fm = FileManager.default
let cacheDir = Self.costUsageCacheDirectory(fileManager: fm)
var cleared = 0
var costError: String?
if fm.fileExists(atPath: cacheDir.path) {
do {
try fm.removeItem(at: cacheDir)
cleared = 1
} catch {
costError = error.localizedDescription
}
}
results.append(CacheClearResult(cache: "cost", provider: nil, cleared: cleared, error: costError))
}
switch output.format {
case .text:
for result in results {
let scope = result.provider ?? "all providers"
if let error = result.error {
print("\(result.cache): failed to clear (\(scope)) - \(error)")
} else if result.cleared > 0 {
print("\(result.cache): cleared (\(scope))")
} else {
print("\(result.cache): nothing to clear (\(scope))")
}
}
case .json:
Self.printJSON(results, pretty: output.pretty)
}
let hasErrors = results.contains(where: { $0.error != nil })
Self.exit(code: hasErrors ? .failure : .success, output: output, kind: .runtime)
}
static func cacheClearProviderScopeError(rawProvider: String?, clearCost: Bool) -> String? {
guard rawProvider != nil, clearCost else { return nil }
return "--provider only scopes cookie caches. Use --cookies --provider <name>, or omit --provider."
}
private static func cookieClearError(failedCount: Int) -> String? {
guard failedCount > 0 else { return nil }
return "Cookie cache cleanup failed for \(failedCount) operation\(failedCount == 1 ? "" : "s")"
}
}
struct CacheOptions: CommanderParsable {
@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("format"), help: "Output format: text | json")
var format: OutputFormat?
@Flag(name: .long("json"), help: "")
var jsonShortcut: Bool = false
@Flag(name: .long("json-only"), help: "Emit JSON only (suppress non-JSON output)")
var jsonOnly: Bool = false
@Flag(name: .long("pretty"), help: "Pretty-print JSON output")
var pretty: Bool = false
@Flag(name: .long("cookies"), help: "Clear browser cookie caches")
var cookies: Bool = false
@Flag(name: .long("cost"), help: "Clear cost usage caches")
var cost: Bool = false
@Flag(name: .long("all"), help: "Clear all caches")
var all: Bool = false
@Option(name: .long("provider"), help: "Clear cache for a specific provider only")
var provider: String?
}
private struct CacheClearResult: Encodable {
let cache: String
let provider: String?
let cleared: Int
var error: String?
}
extension CodexBarCLI {
/// Mirrors the cost usage cache directory used by the app (UsageStore.costUsageCacheDirectory).
static func costUsageCacheDirectory(fileManager: FileManager = .default) -> URL {
let root = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
return root
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("cost-usage", isDirectory: true)
}
}