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