import CodexBarCore import Commander import Foundation extension CodexBarCLI { static func runHooks(path: [String], values: ParsedValues) async { switch path { case ["hooks", "list"]: self.runHooksList(values) case ["hooks", "enable"]: self.runHooksSetEnabled(values, enabled: true) case ["hooks", "disable"]: self.runHooksSetEnabled(values, enabled: false) case ["hooks", "test"]: await self.runHooksTest(values) default: self.exit( code: .failure, message: "Unknown command", output: CLIOutputPreferences.from(values: values), kind: .args) } } static func runHooksList(_ values: ParsedValues) { let output = CLIOutputPreferences.from(values: values) let hooks = Self.loadConfig(output: output).hooks ?? HooksConfig() switch output.format { case .text: print("Hooks: \(hooks.enabled ? "enabled" : "disabled")") if hooks.events.isEmpty { print("No rules configured.") } else { for rule in hooks.events { let provider = rule.provider ?? "any" let state = rule.enabled ? "on" : "off" let command = ([rule.executable] + rule.arguments).joined(separator: " ") print("[\(state)] \(rule.event.rawValue) provider=\(provider): \(command)") } } case .json: Self.printJSON(hooks, pretty: output.pretty) } Self.exit(code: .success, output: output, kind: .config) } static func runHooksSetEnabled(_ values: ParsedValues, enabled: Bool) { let output = CLIOutputPreferences.from(values: values) let store = CodexBarConfigStore() var config = Self.loadConfig(output: output) var hooks = config.hooks ?? HooksConfig() hooks.enabled = enabled config.hooks = hooks do { try store.save(config) } catch { Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .config) } switch output.format { case .text: print("Hooks: \(enabled ? "enabled" : "disabled")") case .json: Self.printJSON(hooks, pretty: output.pretty) } Self.exit(code: .success, output: output, kind: .config) } static func runHooksTest(_ values: ParsedValues) async { let output = CLIOutputPreferences.from(values: values) guard let rawEvent = values.positional.first, let eventType = HookEventType(rawValue: rawEvent) else { let names = HookEventType.allCases.map(\.rawValue).joined(separator: ", ") Self.exit( code: .failure, message: "Unknown or missing event. Use one of: \(names).", output: output, kind: .args) } 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 .", output: output, kind: .args) } let event = Self.sampleHookEvent(type: eventType, provider: provider.rawValue) let hooks = Self.loadConfig(output: output).hooks ?? HooksConfig() let rules = hooks.matchingRules(for: event) guard !rules.isEmpty else { Self.exit( code: .failure, message: hooks.enabled ? "No hook rule matches \(eventType.rawValue) for \(provider.rawValue)." : "Hooks are disabled.", output: output, kind: .config) } var results: [HookTestResult] = [] for rule in rules { do { let result = try await HookRunner.run(rule: rule, event: event) let stdout = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines) results.append(HookTestResult( ruleID: rule.id, executable: rule.executable, event: eventType.rawValue, provider: provider.rawValue, success: true, stdout: stdout.isEmpty ? nil : stdout, error: nil)) if !output.usesJSONOutput { print("ran \(rule.executable): OK\(stdout.isEmpty ? "" : " — \(stdout)")") } } catch { let summary = HookRunner.failureSummary(error) results.append(HookTestResult( ruleID: rule.id, executable: rule.executable, event: eventType.rawValue, provider: provider.rawValue, success: false, stdout: nil, error: summary)) if !output.usesJSONOutput { Self.writeStderr("ran \(rule.executable): \(summary)\n") } } } if output.usesJSONOutput { Self.printJSON(results, pretty: output.pretty) } let succeeded = results.allSatisfy(\.success) Self.exit(code: succeeded ? .success : .failure, output: output, kind: .runtime) } /// A representative event for `hooks test`: quota events report high usage so a /// thresholded `quota_low` rule fires; reset reports empty usage. static func sampleHookEvent(type: HookEventType, provider: String) -> HookEvent { let usagePercent: Double? let status: String? switch type { case .quotaLow, .quotaReached: usagePercent = 1 status = nil case .quotaReset: usagePercent = 0 status = nil case .providerUnavailable: usagePercent = nil status = "major" case .providerRecovered: usagePercent = nil status = "none" case .refreshFailed: usagePercent = nil status = "error" } return HookEvent( event: type, provider: provider, window: type == .quotaReached || type == .quotaLow || type == .quotaReset ? "session" : nil, usagePercent: usagePercent, status: status, timestamp: Date()) } } struct HookTestResult: Codable, Sendable, Equatable { let ruleID: String let executable: String let event: String let provider: String let success: Bool let stdout: String? let error: String? } struct HooksOptions: CommanderParsable { @Option(name: .long("format"), help: "Output format: text | json") var format: OutputFormat? @Flag(name: .long("json"), help: "Emit JSON") 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 HooksTestOptions: CommanderParsable { @Argument(help: "Event name (e.g. quota_reached)") var event: String = "" @Option(name: .long("provider"), help: ProviderHelp.optionHelp) var provider: String? @Option(name: .long("format"), help: "Output format: text | json") var format: OutputFormat? @Flag(name: .long("json"), help: "Emit JSON") 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 }