445 lines
16 KiB
Swift
445 lines
16 KiB
Swift
import CodexBarCore
|
|
import Commander
|
|
import Foundation
|
|
|
|
extension CodexBarCLI {
|
|
static func runConfigValidate(_ values: ParsedValues) {
|
|
let output = CLIOutputPreferences.from(values: values)
|
|
let config = Self.loadConfig(output: output)
|
|
let issues = CodexBarConfigValidator.validate(config)
|
|
let hasErrors = issues.contains(where: { $0.severity == .error })
|
|
|
|
switch output.format {
|
|
case .text:
|
|
if issues.isEmpty {
|
|
print("Config: OK")
|
|
} else {
|
|
for issue in issues {
|
|
let provider = issue.provider?.rawValue ?? "config"
|
|
let field = issue.field ?? ""
|
|
let prefix = "[\(issue.severity.rawValue.uppercased())]"
|
|
let suffix = field.isEmpty ? "" : " (\(field))"
|
|
print("\(prefix) \(provider)\(suffix): \(issue.message)")
|
|
}
|
|
}
|
|
case .json:
|
|
Self.printJSON(issues, pretty: output.pretty)
|
|
}
|
|
|
|
Self.exit(code: hasErrors ? .failure : .success, output: output, kind: .config)
|
|
}
|
|
|
|
static func runConfigDump(_ values: ParsedValues) {
|
|
let output = CLIOutputPreferences.from(values: values)
|
|
let config = Self.loadConfig(output: output)
|
|
Self.printJSON(config, pretty: output.pretty)
|
|
Self.exit(code: .success, output: output, kind: .config)
|
|
}
|
|
|
|
static func runConfigProviders(_ values: ParsedValues) {
|
|
let output = CLIOutputPreferences.from(values: values)
|
|
let config = Self.loadConfig(output: output)
|
|
let results = Self.configProviderStatuses(config)
|
|
|
|
switch output.format {
|
|
case .text:
|
|
for result in results {
|
|
let state = result.enabled ? "enabled" : "disabled"
|
|
let marker = result.defaultEnabled ? " default" : ""
|
|
print("\(result.provider): \(state)\(marker) (\(result.displayName))")
|
|
}
|
|
case .json:
|
|
Self.printJSON(results, pretty: output.pretty)
|
|
}
|
|
|
|
Self.exit(code: .success, output: output, kind: .config)
|
|
}
|
|
|
|
static func runConfigSetProviderEnabled(_ values: ParsedValues, enabled: Bool) {
|
|
let output = CLIOutputPreferences.from(values: values)
|
|
guard let rawProvider = values.options["provider"]?.last,
|
|
let provider = ProviderDescriptorRegistry.cliNameMap[rawProvider.lowercased()]
|
|
else {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Unknown or missing provider. Use --provider <name>.",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
|
|
let store = CodexBarConfigStore()
|
|
var config = Self.loadConfig(output: output)
|
|
config = Self.configSettingProviderEnabled(config, provider: provider, enabled: enabled)
|
|
|
|
do {
|
|
try store.save(config)
|
|
} catch {
|
|
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .config)
|
|
}
|
|
|
|
let metadata = ProviderDescriptorRegistry.descriptor(for: provider).metadata
|
|
let result = ConfigProviderToggleResult(
|
|
provider: provider.rawValue,
|
|
displayName: metadata.displayName,
|
|
enabled: enabled,
|
|
configPath: store.fileURL.path)
|
|
|
|
switch output.format {
|
|
case .text:
|
|
let state = enabled ? "enabled" : "disabled"
|
|
print("Config: \(state) \(metadata.displayName)")
|
|
case .json:
|
|
Self.printJSON(result, pretty: output.pretty)
|
|
}
|
|
|
|
Self.exit(code: .success, output: output, kind: .config)
|
|
}
|
|
|
|
static func runConfigSetAPIKey(_ values: ParsedValues) {
|
|
let output = CLIOutputPreferences.from(values: values)
|
|
|
|
guard let rawProvider = values.options["provider"]?.last,
|
|
let provider = ProviderDescriptorRegistry.cliNameMap[rawProvider.lowercased()]
|
|
else {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Unknown or missing provider. Use --provider <name>.",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
guard ProviderConfigEnvironment.supportsAPIKeyOverride(for: provider) else {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "\(rawProvider) does not support config API keys.",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
|
|
let apiKey: String
|
|
do {
|
|
apiKey = try Self.resolveConfigAPIKeyInput(
|
|
apiKey: values.options["apiKey"]?.last,
|
|
readFromStdin: values.flags.contains("stdin"))
|
|
} catch {
|
|
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .args)
|
|
}
|
|
|
|
let enableProvider = !values.flags.contains("noEnable")
|
|
let store = CodexBarConfigStore()
|
|
var config = Self.loadConfig(output: output)
|
|
let accountOptions: ConfigAPIKeyAccountOptions?
|
|
do {
|
|
accountOptions = try Self.resolveConfigAPIKeyAccountOptions(
|
|
provider: provider,
|
|
label: values.options["label"]?.last,
|
|
usageScope: values.options["usageScope"]?.last,
|
|
organizationID: values.options["organizationId"]?.last,
|
|
workspaceID: values.options["workspaceId"]?.last)
|
|
} catch {
|
|
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .args)
|
|
}
|
|
config = Self.configSettingAPIKey(
|
|
config,
|
|
provider: provider,
|
|
apiKey: apiKey,
|
|
enableProvider: enableProvider,
|
|
accountOptions: accountOptions)
|
|
|
|
do {
|
|
try store.save(config)
|
|
} catch {
|
|
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .config)
|
|
}
|
|
|
|
let result = ConfigSetAPIKeyResult(
|
|
provider: provider.rawValue,
|
|
enabled: config.providerConfig(for: provider)?.enabled ?? false,
|
|
configPath: store.fileURL.path)
|
|
|
|
switch output.format {
|
|
case .text:
|
|
let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
|
|
let suffix = result.enabled ? " and enabled" : ""
|
|
let action = accountOptions == nil ? "stored API key" : "stored team token account"
|
|
print("Config: \(action) for \(name)\(suffix)")
|
|
case .json:
|
|
Self.printJSON(result, pretty: output.pretty)
|
|
}
|
|
|
|
Self.exit(code: .success, output: output, kind: .config)
|
|
}
|
|
|
|
static func resolveConfigAPIKeyInput(apiKey: String?, readFromStdin: Bool) throws -> String {
|
|
if apiKey != nil, readFromStdin {
|
|
throw CLIArgumentError("Use either --api-key or --stdin, not both.")
|
|
}
|
|
|
|
let raw: String? = if readFromStdin {
|
|
String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8)
|
|
} else {
|
|
apiKey
|
|
}
|
|
|
|
guard let value = Self.cleanConfigSecret(raw) else {
|
|
throw CLIArgumentError("Missing API key. Pass --api-key <key> or pipe it with --stdin.")
|
|
}
|
|
return value
|
|
}
|
|
|
|
static func configSettingAPIKey(
|
|
_ config: CodexBarConfig,
|
|
provider: UsageProvider,
|
|
apiKey: String,
|
|
enableProvider: Bool,
|
|
accountOptions: ConfigAPIKeyAccountOptions? = nil) -> CodexBarConfig
|
|
{
|
|
var updated = config.normalized()
|
|
var providerConfig = updated.providerConfig(for: provider) ?? ProviderConfig(id: provider)
|
|
if let accountOptions {
|
|
let existing = providerConfig.tokenAccounts
|
|
let accounts = existing?.accounts ?? []
|
|
let account = ProviderTokenAccount(
|
|
id: UUID(),
|
|
label: accountOptions.label,
|
|
token: apiKey,
|
|
addedAt: Date().timeIntervalSince1970,
|
|
lastUsed: nil,
|
|
usageScope: accountOptions.usageScope.rawValue,
|
|
organizationID: accountOptions.organizationID,
|
|
workspaceID: accountOptions.workspaceID)
|
|
providerConfig.tokenAccounts = ProviderTokenAccountData(
|
|
version: existing?.version ?? 1,
|
|
accounts: accounts + [account],
|
|
activeIndex: accounts.count)
|
|
providerConfig.apiKey = nil
|
|
if enableProvider {
|
|
providerConfig.enabled = true
|
|
}
|
|
updated.setProviderConfig(providerConfig)
|
|
return updated
|
|
}
|
|
providerConfig.apiKey = apiKey
|
|
if enableProvider {
|
|
providerConfig.enabled = true
|
|
}
|
|
updated.setProviderConfig(providerConfig)
|
|
return updated
|
|
}
|
|
|
|
static func resolveConfigAPIKeyAccountOptions(
|
|
provider: UsageProvider,
|
|
label: String?,
|
|
usageScope: String?,
|
|
organizationID: String?,
|
|
workspaceID: String?) throws -> ConfigAPIKeyAccountOptions?
|
|
{
|
|
let cleanedLabel = Self.cleanConfigValue(label)
|
|
let cleanedScope = Self.cleanConfigValue(usageScope)
|
|
let cleanedOrganizationID = try Self.cleanSingleLineConfigValue(
|
|
organizationID,
|
|
fieldName: "organization-id")
|
|
let cleanedWorkspaceID = try Self.cleanSingleLineConfigValue(
|
|
workspaceID,
|
|
fieldName: "workspace-id")
|
|
let hasAccountOptions = cleanedLabel != nil ||
|
|
cleanedScope != nil ||
|
|
cleanedOrganizationID != nil ||
|
|
cleanedWorkspaceID != nil
|
|
guard hasAccountOptions else { return nil }
|
|
|
|
guard provider == .zai else {
|
|
throw CLIArgumentError("Token-account options are only supported for --provider zai.")
|
|
}
|
|
|
|
guard cleanedScope?.lowercased() == ZaiUsageScope.team.rawValue else {
|
|
throw CLIArgumentError("Use --usage-scope team for z.ai team accounts, or omit account options.")
|
|
}
|
|
guard let organizationID = cleanedOrganizationID else {
|
|
throw CLIArgumentError("Missing --organization-id for z.ai team usage.")
|
|
}
|
|
guard let workspaceID = cleanedWorkspaceID else {
|
|
throw CLIArgumentError("Missing --workspace-id for z.ai team usage.")
|
|
}
|
|
|
|
return ConfigAPIKeyAccountOptions(
|
|
label: cleanedLabel ?? "Team",
|
|
usageScope: .team,
|
|
organizationID: organizationID,
|
|
workspaceID: workspaceID)
|
|
}
|
|
|
|
static func configSettingProviderEnabled(
|
|
_ config: CodexBarConfig,
|
|
provider: UsageProvider,
|
|
enabled: Bool) -> CodexBarConfig
|
|
{
|
|
var updated = config.normalized()
|
|
var providerConfig = updated.providerConfig(for: provider) ?? ProviderConfig(id: provider)
|
|
providerConfig.enabled = enabled
|
|
updated.setProviderConfig(providerConfig)
|
|
return updated
|
|
}
|
|
|
|
static func configProviderStatuses(_ config: CodexBarConfig) -> [ConfigProviderStatusResult] {
|
|
let metadata = ProviderDescriptorRegistry.metadata
|
|
return config.normalized().providers.map { providerConfig in
|
|
let meta = metadata[providerConfig.id]
|
|
let defaultEnabled = meta?.defaultEnabled ?? false
|
|
return ConfigProviderStatusResult(
|
|
provider: providerConfig.id.rawValue,
|
|
displayName: meta?.displayName ?? providerConfig.id.rawValue,
|
|
enabled: providerConfig.enabled ?? defaultEnabled,
|
|
defaultEnabled: defaultEnabled)
|
|
}
|
|
}
|
|
|
|
private static func cleanConfigSecret(_ raw: String?) -> String? {
|
|
guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
|
|
return nil
|
|
}
|
|
if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
|
|
(value.hasPrefix("'") && value.hasSuffix("'"))
|
|
{
|
|
value = String(value.dropFirst().dropLast())
|
|
}
|
|
value = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return value.isEmpty ? nil : value
|
|
}
|
|
|
|
private static func cleanConfigValue(_ raw: String?) -> String? {
|
|
guard let value = self.cleanConfigSecret(raw) else { return nil }
|
|
return value
|
|
}
|
|
|
|
private static func cleanSingleLineConfigValue(_ raw: String?, fieldName: String) throws -> String? {
|
|
guard let value = self.cleanConfigValue(raw) else { return nil }
|
|
guard !value.contains(where: \.isNewline) else {
|
|
throw CLIArgumentError("--\(fieldName) must be a single line.")
|
|
}
|
|
return value
|
|
}
|
|
}
|
|
|
|
struct ConfigAPIKeyAccountOptions: Equatable {
|
|
let label: String
|
|
let usageScope: ZaiUsageScope
|
|
let organizationID: String
|
|
let workspaceID: String
|
|
}
|
|
|
|
struct ConfigOptions: 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
|
|
}
|
|
|
|
struct ConfigSetAPIKeyOptions: 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
|
|
|
|
@Option(name: .long("provider"), help: ProviderHelp.optionHelp)
|
|
var provider: String?
|
|
|
|
@Option(name: .long("api-key"), help: "API key to store")
|
|
var apiKey: String?
|
|
|
|
@Flag(name: .long("stdin"), help: "Read API key from stdin")
|
|
var stdin: Bool = false
|
|
|
|
@Flag(name: .long("no-enable"), help: "Store the key without enabling the provider")
|
|
var noEnable: Bool = false
|
|
|
|
@Option(name: .long("label"), help: "Token-account label (z.ai team mode)")
|
|
var label: String?
|
|
|
|
@Option(name: .long("usage-scope"), help: "Token-account usage scope (z.ai: team)")
|
|
var usageScope: String?
|
|
|
|
@Option(name: .long("organization-id"), help: "z.ai BigModel organization ID for team usage")
|
|
var organizationId: String?
|
|
|
|
@Option(name: .long("workspace-id"), help: "z.ai BigModel project ID for team usage")
|
|
var workspaceId: String?
|
|
}
|
|
|
|
struct ConfigProviderToggleOptions: 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
|
|
|
|
@Option(name: .long("provider"), help: ProviderHelp.optionHelp)
|
|
var provider: String?
|
|
}
|
|
|
|
private struct ConfigSetAPIKeyResult: Encodable {
|
|
let provider: String
|
|
let enabled: Bool
|
|
let configPath: String
|
|
}
|
|
|
|
struct ConfigProviderStatusResult: Encodable, Equatable {
|
|
let provider: String
|
|
let displayName: String
|
|
let enabled: Bool
|
|
let defaultEnabled: Bool
|
|
}
|
|
|
|
private struct ConfigProviderToggleResult: Encodable {
|
|
let provider: String
|
|
let displayName: String
|
|
let enabled: Bool
|
|
let configPath: String
|
|
}
|