nils-macos-agent 0.5.5

CLI crate for nils-macos-agent in the nils-cli workspace.
Documentation
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
    Usage,
    Runtime,
}

impl ErrorCategory {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Usage => "usage",
            Self::Runtime => "runtime",
        }
    }
}

#[derive(Debug, Clone)]
pub struct CliError {
    message: String,
    exit_code: u8,
    category: ErrorCategory,
    operation: Option<String>,
    hints: Vec<String>,
}

impl CliError {
    pub fn usage(message: impl Into<String>) -> Self {
        Self::new(message, 2, ErrorCategory::Usage)
    }

    pub fn runtime(message: impl Into<String>) -> Self {
        Self::new(message, 1, ErrorCategory::Runtime)
    }

    pub fn unsupported_platform() -> Self {
        Self::usage("macos-agent is only supported on macOS")
    }

    pub fn timeout(operation: &str, timeout_ms: u64) -> Self {
        let mut err = Self::runtime(format!("{operation} timed out after {timeout_ms}ms"))
            .with_operation(operation)
            .with_hint(
                "Increase --timeout-ms for slower apps or enable --retries for transient failures.",
            );
        if operation.starts_with("ax.") {
            err = err.with_hint(
                "For large UI trees, reduce --max-depth/--limit before retrying to keep AX queries bounded.",
            );
        }
        err
    }

    pub fn ax_payload_encode(operation: &str, detail: impl Into<String>) -> Self {
        Self::runtime(format!(
            "{operation} failed: unable to encode AX request payload ({})",
            detail.into().trim()
        ))
        .with_operation(operation)
        .with_hint("Simplify selector/text input and retry.")
    }

    pub fn ax_parse_failure(operation: &str, detail: impl Into<String>) -> Self {
        Self::runtime(format!(
            "{operation} failed: invalid AX backend JSON response ({})",
            detail.into().trim()
        ))
        .with_operation(operation)
        .with_hint("Run `macos-agent preflight --include-probes --strict` to verify Accessibility/Automation access.")
        .with_hint("Review preflight `ax_backend_capabilities` to confirm backend support and fallback behavior.")
        .with_hint("Use --trace to capture raw backend output for diagnosis.")
    }

    pub fn ax_contract_failure(operation: &str, detail: impl Into<String>) -> Self {
        Self::runtime(format!(
            "{operation} failed: AX backend contract violation ({})",
            detail.into().trim()
        ))
        .with_operation(operation)
        .with_hint("Adjust AX selector filters so exactly one element is targeted.")
        .with_hint("For attr/action/session/watch flows, ensure Hammerspoon backend is available.")
    }

    pub fn exit_code(&self) -> u8 {
        self.exit_code
    }

    pub fn category(&self) -> ErrorCategory {
        self.category
    }

    pub fn operation(&self) -> Option<&str> {
        self.operation.as_deref()
    }

    pub fn hints(&self) -> &[String] {
        &self.hints
    }

    pub fn message(&self) -> &str {
        &self.message
    }

    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
        let hint = hint.into();
        if !hint.trim().is_empty() {
            self.hints.push(hint);
        }
        self
    }

    pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
        let operation = operation.into();
        if !operation.trim().is_empty() {
            self.operation = Some(operation);
        }
        self
    }
}

impl CliError {
    fn new(message: impl Into<String>, exit_code: u8, category: ErrorCategory) -> Self {
        let message = message
            .into()
            .trim()
            .trim_start_matches("error:")
            .trim()
            .to_string();
        Self {
            message,
            exit_code,
            category,
            operation: None,
            hints: Vec::new(),
        }
    }
}

impl fmt::Display for CliError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "error: {}", self.message)
    }
}

impl std::error::Error for CliError {}