Skip to main content

macos_agent/
error.rs

1use std::fmt;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ErrorCategory {
5    Usage,
6    Runtime,
7}
8
9impl ErrorCategory {
10    pub fn as_str(self) -> &'static str {
11        match self {
12            Self::Usage => "usage",
13            Self::Runtime => "runtime",
14        }
15    }
16}
17
18#[derive(Debug, Clone)]
19pub struct CliError {
20    message: String,
21    exit_code: u8,
22    category: ErrorCategory,
23    operation: Option<String>,
24    hints: Vec<String>,
25}
26
27impl CliError {
28    pub fn usage(message: impl Into<String>) -> Self {
29        Self::new(message, 2, ErrorCategory::Usage)
30    }
31
32    pub fn runtime(message: impl Into<String>) -> Self {
33        Self::new(message, 1, ErrorCategory::Runtime)
34    }
35
36    pub fn unsupported_platform() -> Self {
37        Self::usage("macos-agent is only supported on macOS")
38    }
39
40    pub fn timeout(operation: &str, timeout_ms: u64) -> Self {
41        let mut err = Self::runtime(format!("{operation} timed out after {timeout_ms}ms"))
42            .with_operation(operation)
43            .with_hint(
44                "Increase --timeout-ms for slower apps or enable --retries for transient failures.",
45            );
46        if operation.starts_with("ax.") {
47            err = err.with_hint(
48                "For large UI trees, reduce --max-depth/--limit before retrying to keep AX queries bounded.",
49            );
50        }
51        err
52    }
53
54    pub fn ax_payload_encode(operation: &str, detail: impl Into<String>) -> Self {
55        Self::runtime(format!(
56            "{operation} failed: unable to encode AX request payload ({})",
57            detail.into().trim()
58        ))
59        .with_operation(operation)
60        .with_hint("Simplify selector/text input and retry.")
61    }
62
63    pub fn ax_parse_failure(operation: &str, detail: impl Into<String>) -> Self {
64        Self::runtime(format!(
65            "{operation} failed: invalid AX backend JSON response ({})",
66            detail.into().trim()
67        ))
68        .with_operation(operation)
69        .with_hint("Run `macos-agent preflight --include-probes --strict` to verify Accessibility/Automation access.")
70        .with_hint("Review preflight `ax_backend_capabilities` to confirm backend support and fallback behavior.")
71        .with_hint("Use --trace to capture raw backend output for diagnosis.")
72    }
73
74    pub fn ax_contract_failure(operation: &str, detail: impl Into<String>) -> Self {
75        Self::runtime(format!(
76            "{operation} failed: AX backend contract violation ({})",
77            detail.into().trim()
78        ))
79        .with_operation(operation)
80        .with_hint("Adjust AX selector filters so exactly one element is targeted.")
81        .with_hint("For attr/action/session/watch flows, ensure Hammerspoon backend is available.")
82    }
83
84    pub fn exit_code(&self) -> u8 {
85        self.exit_code
86    }
87
88    pub fn category(&self) -> ErrorCategory {
89        self.category
90    }
91
92    pub fn operation(&self) -> Option<&str> {
93        self.operation.as_deref()
94    }
95
96    pub fn hints(&self) -> &[String] {
97        &self.hints
98    }
99
100    pub fn message(&self) -> &str {
101        &self.message
102    }
103
104    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
105        let hint = hint.into();
106        if !hint.trim().is_empty() {
107            self.hints.push(hint);
108        }
109        self
110    }
111
112    pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
113        let operation = operation.into();
114        if !operation.trim().is_empty() {
115            self.operation = Some(operation);
116        }
117        self
118    }
119}
120
121impl CliError {
122    fn new(message: impl Into<String>, exit_code: u8, category: ErrorCategory) -> Self {
123        let message = message
124            .into()
125            .trim()
126            .trim_start_matches("error:")
127            .trim()
128            .to_string();
129        Self {
130            message,
131            exit_code,
132            category,
133            operation: None,
134            hints: Vec::new(),
135        }
136    }
137}
138
139impl fmt::Display for CliError {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "error: {}", self.message)
142    }
143}
144
145impl std::error::Error for CliError {}