Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ extension CodexBarCLI {
let providers = providerSelection.asList
let pretty = values.flags.contains("pretty")
let verbose = values.flags.contains("verbose")
let outputPath = values.options["output"]?.last
let browserDetection = BrowserDetection()
let baseFetcher = UsageFetcher()

Expand Down Expand Up @@ -72,7 +73,11 @@ extension CodexBarCLI {
}
var jsonString = String(data: data, encoding: .utf8) ?? "{}"
jsonString = LogRedactor.redact(jsonString)
print(jsonString)
if let outputPath, !outputPath.isEmpty {
try Self.writeDiagnosticExport(jsonString, to: outputPath)
} else {
print(jsonString)
}
} catch {
Self.exit(
code: .failure,
Expand All @@ -83,6 +88,17 @@ extension CodexBarCLI {

Self.exit(code: .success, output: output, kind: .runtime)
}

static func writeDiagnosticExport(_ jsonString: String, to path: String) throws {
let url = URL(fileURLWithPath: path)
let parent = url.deletingLastPathComponent()
if !parent.path.isEmpty {
try FileManager.default.createDirectory(
at: parent,
withIntermediateDirectories: true)
}
try jsonString.write(to: url, atomically: true, encoding: .utf8)
}
}

extension CodexBarCLI {
Expand Down Expand Up @@ -138,7 +154,8 @@ extension CodexBarCLI {
account: account,
config: tokenContext.config.providerConfig(for: provider),
environment: env,
settings: settings)))
settings: settings),
appVersion: Self.currentVersion()))
}

static func diagnosticAuthSummary(
Expand Down
5 changes: 4 additions & 1 deletion Sources/CodexBarCLI/CLIHelp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ extension CodexBarCLI {
codexbar diagnose --provider <name|all> --format json
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
[--redact] [--output <path>]
[--pretty]

Description:
Expand All @@ -185,6 +186,7 @@ extension CodexBarCLI {
account IDs, org IDs, raw responses, and billing-history records.

Examples:
codexbar diagnose --provider minimax --format json --redact --output diagnostic.json
codexbar diagnose --provider minimax --format json --pretty
codexbar diagnose --provider claude --format json --pretty
codexbar diagnose --provider all --format json
Expand Down Expand Up @@ -222,7 +224,7 @@ extension CodexBarCLI {
codexbar config disable --provider <name>
codexbar config set-api-key --provider <name> (--api-key <key>|--stdin)
codexbar cache clear <--cookies|--cost|--all> [--provider <name>]
codexbar diagnose --provider <name|all> --format json [--pretty]
codexbar diagnose --provider <name|all> --format json [--redact] [--output <path>] [--pretty]

Global flags:
-h, --help Show help
Expand All @@ -243,6 +245,7 @@ extension CodexBarCLI {
codexbar config enable --provider grok
codexbar config set-api-key --provider elevenlabs --stdin
codexbar cache clear --cookies
codexbar diagnose --provider minimax --format json --redact --output diagnostic.json
codexbar diagnose --provider minimax --format json --pretty
codexbar diagnose --provider all --format json
"""
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBarCLI/DiagnoseOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ struct DiagnoseOptions: CommanderParsable {
@Option(name: .long("format"), help: "Output format: json")
var format: String?

@Flag(name: .long("redact"), help: "Explicitly redact sensitive values (always enabled for diagnose)")
var redact: Bool = false

@Option(name: .long("output"), help: "Write redacted JSON diagnostic export to a file")
var output: String?

@Flag(name: .long("pretty"), help: "Pretty-print JSON output")
var pretty: Bool = false
}
24 changes: 23 additions & 1 deletion Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public struct ProviderDiagnosticBatchExport: Codable, Sendable {
public struct ProviderDiagnosticExport: Codable, Sendable {
public let schemaVersion: String
public let timestamp: Date
public let platform: String
public let appVersion: String?
public let provider: String
public let displayName: String
public let source: String
Expand All @@ -33,6 +35,8 @@ public struct ProviderDiagnosticExport: Codable, Sendable {
public init(
schemaVersion: String = "1.0",
timestamp: Date,
platform: String = ProviderDiagnosticPlatform.current,
appVersion: String? = nil,
provider: String,
displayName: String,
source: String,
Expand All @@ -46,6 +50,8 @@ public struct ProviderDiagnosticExport: Codable, Sendable {
{
self.schemaVersion = schemaVersion
self.timestamp = timestamp
self.platform = platform
self.appVersion = appVersion
self.provider = provider
self.displayName = displayName
self.source = source
Expand All @@ -59,6 +65,18 @@ public struct ProviderDiagnosticExport: Codable, Sendable {
}
}

public enum ProviderDiagnosticPlatform {
public static var current: String {
#if os(macOS)
"macOS"
#elseif os(Linux)
"Linux"
#else
"unknown"
#endif
}
}

public struct ProviderDiagnosticAuthSummary: Codable, Sendable {
public let configured: Bool
public let modes: [String]
Expand Down Expand Up @@ -445,21 +463,24 @@ public enum ProviderDiagnosticExportBuilder {
public let sourceMode: ProviderSourceMode
public let settings: ProviderSettingsSnapshot?
public let auth: ProviderDiagnosticAuthSummary
public let appVersion: String?

public init(
provider: UsageProvider,
descriptor: ProviderDescriptor,
outcome: ProviderFetchOutcome,
sourceMode: ProviderSourceMode,
settings: ProviderSettingsSnapshot?,
auth: ProviderDiagnosticAuthSummary)
auth: ProviderDiagnosticAuthSummary,
appVersion: String? = nil)
{
self.provider = provider
self.descriptor = descriptor
self.outcome = outcome
self.sourceMode = sourceMode
self.settings = settings
self.auth = auth
self.appVersion = appVersion
}
}

Expand All @@ -474,6 +495,7 @@ public enum ProviderDiagnosticExportBuilder {

return ProviderDiagnosticExport(
timestamp: Date(),
appVersion: input.appVersion,
provider: input.provider.rawValue,
displayName: input.descriptor.metadata.displayName,
source: input.outcome.sourceLabel,
Expand Down
15 changes: 15 additions & 0 deletions Tests/CodexBarTests/CLIArgumentParsingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,19 @@ struct CLIArgumentParsingTests {
Issue.record("diagnose should not emit provider logs beside the safe JSON export")
}
}

@Test
func `diagnose accepts explicit redact and output path`() throws {
let signature = CodexBarCLI._diagnoseSignatureForTesting()
let parser = CommandParser(signature: signature)
let parsed = try parser.parse(arguments: [
"--provider", "minimax",
"--format", "json",
"--redact",
"--output", "diagnostic.json",
])

#expect(parsed.flags.contains("redact"))
#expect(parsed.options["output"] == ["diagnostic.json"])
}
}
16 changes: 16 additions & 0 deletions Tests/CodexBarTests/CLIDiagnoseCommandTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CodexBarCore
import Foundation
import Testing
@testable import CodexBarCLI

Expand All @@ -9,10 +10,25 @@ struct CLIDiagnoseCommandTests {

#expect(help.contains("codexbar diagnose --provider <name|all> --format json"))
#expect(help.contains("codexbar diagnose --provider all --format json"))
#expect(help.contains("--redact"))
#expect(help.contains("--output <path>"))
#expect(help.contains("safe JSON export"))
#expect(help.contains("raw API tokens"))
}

@Test
func `diagnose output writer creates parent directories`() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("CodexBarDiagnoseTests-\(UUID().uuidString)")
defer { try? FileManager.default.removeItem(at: root) }

let output = root.appendingPathComponent("nested/diagnostic.json")
try CodexBarCLI.writeDiagnosticExport(#"{"provider":"minimax"}"#, to: output.path)

let contents = try String(contentsOf: output, encoding: .utf8)
#expect(contents == #"{"provider":"minimax"}"#)
}

private func makeSettingsWithMiniMaxCookie(_ manualCookieHeader: String) -> ProviderSettingsSnapshot {
ProviderSettingsSnapshot(
debugMenuEnabled: false,
Expand Down
6 changes: 5 additions & 1 deletion Tests/CodexBarTests/ProviderDiagnosticExportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct ProviderDiagnosticExportTests {

#expect(json.contains("\"provider\""))
#expect(json.contains("\"openai\""))
#expect(json.contains("\"platform\""))
#expect(json.contains("\"auth\""))
#expect(json.contains("\"dataConfidence\""))
#expect(json.contains("\"unknown\""))
Expand Down Expand Up @@ -478,9 +479,12 @@ struct ProviderDiagnosticExportTests {
outcome: outcome,
sourceMode: .auto,
settings: nil,
auth: ProviderDiagnosticAuthSummary(configured: true, modes: ["apiToken"])))
auth: ProviderDiagnosticAuthSummary(configured: true, modes: ["apiToken"]),
appVersion: "9.8.7"))

#expect(diag.provider == "minimax")
#expect(diag.platform == ProviderDiagnosticPlatform.current)
#expect(diag.appVersion == "9.8.7")
#expect(diag.source == "failed")
#expect(diag.auth.configured == true)
#expect(diag.usage == nil)
Expand Down