import CodexBarCoreimport Commanderimport 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 <name>.", 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}
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 <name>.",
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
}