467 lines · 17.2 KB · Swift
RawDownload
import CodexBarCoreimport Commanderimport Foundation extension CodexBarCLI {    static func runConfig(path: [String], values: ParsedValues) {        switch path {        case ["config", "validate"]:            self.runConfigValidate(values)        case ["config", "dump"]:            self.runConfigDump(values)        case ["config", "providers"]:            self.runConfigProviders(values)        case ["config", "enable"]:            self.runConfigSetProviderEnabled(values, enabled: true)        case ["config", "disable"]:            self.runConfigSetProviderEnabled(values, enabled: false)        case ["config", "set-api-key"]:            self.runConfigSetAPIKey(values)        default:            self.exit(                code: .failure,                message: "Unknown command",                output: CLIOutputPreferences.from(values: values),                kind: .args)        }    }     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}