browser_automation_cli/error.rs
1//! Typed CLI errors with sysexits-style exit codes.
2//!
3//! # Error kinds
4//!
5//! `ErrorKind` maps to process exit codes used by the binary and JSON envelopes.
6//!
7//! # Examples
8//!
9//! ```
10//! use browser_automation_cli::error::{CliError, ErrorKind};
11//!
12//! let err = CliError::with_suggestion(
13//! ErrorKind::Unavailable,
14//! "chrome not found",
15//! "install Chromium or Google Chrome",
16//! );
17//! assert_eq!(err.exit_code(), 69);
18//! assert_eq!(err.kind().as_str(), "unavailable");
19//! ```
20
21use thiserror::Error;
22
23/// High-level failure category mapped to a process exit code.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ErrorKind {
26 /// Invalid usage or clap parse failure (`2`).
27 Usage,
28 /// Invalid data or payload (`65`).
29 Data,
30 /// Required input missing (`66`).
31 NoInput,
32 /// Browser or dependency unavailable (`69`).
33 Unavailable,
34 /// Internal software failure (`70`).
35 Software,
36 /// Browser runtime failure (`70`).
37 Browser,
38 /// CDP/protocol failure (`70`).
39 Protocol,
40 /// Wall-clock timeout (`124`).
41 Timeout,
42 /// Cancelled by signal (`130`).
43 Cancelled,
44 /// Broken pipe on stdout (`141`).
45 BrokenPipe,
46 /// Configuration error (`78`).
47 Config,
48 /// I/O failure (`74`).
49 Io,
50}
51
52impl ErrorKind {
53 /// Process exit code for this kind.
54 pub fn exit_code(self) -> u8 {
55 match self {
56 ErrorKind::Usage => 2,
57 ErrorKind::Data => 65,
58 ErrorKind::NoInput => 66,
59 ErrorKind::Unavailable => 69,
60 ErrorKind::Software | ErrorKind::Browser | ErrorKind::Protocol => 70,
61 ErrorKind::Config => 78,
62 ErrorKind::Io => 74,
63 ErrorKind::Timeout => 124,
64 ErrorKind::Cancelled => 130,
65 ErrorKind::BrokenPipe => 141,
66 }
67 }
68
69 /// Stable machine-readable kind string for JSON envelopes.
70 pub fn as_str(self) -> &'static str {
71 match self {
72 ErrorKind::Usage => "usage",
73 ErrorKind::Data => "data",
74 ErrorKind::NoInput => "no-input",
75 ErrorKind::Unavailable => "unavailable",
76 ErrorKind::Software => "software",
77 ErrorKind::Browser => "browser",
78 ErrorKind::Protocol => "protocol",
79 ErrorKind::Timeout => "timeout",
80 ErrorKind::Cancelled => "cancelled",
81 ErrorKind::BrokenPipe => "broken-pipe",
82 ErrorKind::Config => "config",
83 ErrorKind::Io => "io",
84 }
85 }
86}
87
88/// Typed CLI error with optional remediation hint.
89#[derive(Debug, Clone, Error)]
90#[error("{message}")]
91pub struct CliError {
92 kind: ErrorKind,
93 message: String,
94 suggestion: Option<String>,
95 /// Optional partial JSON payload (e.g. fail-fast `run` steps).
96 data: Option<serde_json::Value>,
97}
98
99impl CliError {
100 /// Create an error without a suggestion.
101 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
102 Self {
103 kind,
104 message: message.into(),
105 suggestion: None,
106 data: None,
107 }
108 }
109
110 /// Create an error with a short remediation suggestion for agents.
111 pub fn with_suggestion(
112 kind: ErrorKind,
113 message: impl Into<String>,
114 suggestion: impl Into<String>,
115 ) -> Self {
116 Self {
117 kind,
118 message: message.into(),
119 suggestion: Some(suggestion.into()),
120 data: None,
121 }
122 }
123
124 /// Attach partial JSON `data` (kept on error envelopes).
125 pub fn with_data(mut self, data: serde_json::Value) -> Self {
126 self.data = Some(data);
127 self
128 }
129
130 /// Error category.
131 pub fn kind(&self) -> ErrorKind {
132 self.kind
133 }
134
135 /// Human-readable message (also used in JSON `error.message`).
136 pub fn message(&self) -> &str {
137 &self.message
138 }
139
140 /// Optional remediation hint.
141 pub fn suggestion(&self) -> Option<&str> {
142 self.suggestion.as_deref()
143 }
144
145 /// Optional partial data payload.
146 pub fn data(&self) -> Option<&serde_json::Value> {
147 self.data.as_ref()
148 }
149
150 /// Process exit code for this error.
151 pub fn exit_code(&self) -> u8 {
152 self.kind.exit_code()
153 }
154}
155