1use std::net::SocketAddr;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
6pub enum CliError {
7 #[error("Failed to connect to Theater server at {address}")]
9 ConnectionFailed {
10 address: SocketAddr,
11 #[source]
12 source: anyhow::Error,
13 },
14
15 #[error("Connection lost to Theater server")]
16 ConnectionLost,
17
18 #[error("Connection timeout after {timeout}s")]
19 ConnectionTimeout { timeout: u64 },
20
21 #[error("Actor '{actor_id}' not found")]
23 ActorNotFound { actor_id: String },
24
25 #[error("Actor '{actor_id}' failed to start: {reason}")]
26 ActorStartFailed { actor_id: String, reason: String },
27
28 #[error("Actor '{actor_id}' is not running")]
29 ActorNotRunning { actor_id: String },
30
31 #[error("Invalid project directory: {path}")]
33 InvalidProjectDirectory { path: String },
34
35 #[error("Build failed: {output}")]
36 BuildFailed { output: String },
37
38 #[error("Missing required tool: {tool}. Please install it with: {install_command}")]
39 MissingTool {
40 tool: String,
41 install_command: String,
42 },
43
44 #[error("Invalid manifest file: {reason}")]
46 InvalidManifest { reason: String },
47
48 #[error("Configuration error: {reason}")]
49 ConfigError { reason: String },
50
51 #[error("Template '{template}' not found. Available templates: {available}")]
53 TemplateNotFound { template: String, available: String },
54
55 #[error("Template error: {reason}")]
56 TemplateError { reason: String },
57
58 #[error("File operation failed: {operation} on {path}")]
60 FileOperationFailed {
61 operation: String,
62 path: String,
63 #[source]
64 source: std::io::Error,
65 },
66
67 #[error("Permission denied: {operation} on {path}")]
68 PermissionDenied { operation: String, path: String },
69
70 #[error("Invalid input: {field} = '{value}'. {suggestion}")]
72 InvalidInput {
73 field: String,
74 value: String,
75 suggestion: String,
76 },
77
78 #[error("Validation failed: {reason}")]
79 ValidationError { reason: String },
80
81 #[error("Server error: {message}")]
83 ServerError { message: String },
84
85 #[error("Protocol error: {reason}")]
86 ProtocolError { reason: String },
87
88 #[error("Unexpected response from server: {response}")]
89 UnexpectedResponse { response: String },
90
91 #[error("Event stream error: {reason}")]
93 EventStreamError { reason: String },
94
95 #[error("Event filter error: {filter} is invalid")]
96 EventFilterError { filter: String },
97
98 #[error("Internal error: {0}")]
100 Internal(#[from] anyhow::Error),
101
102 #[error("Serialization error: {0}")]
103 Serialization(#[from] serde_json::Error),
104
105 #[error("I/O error: {0}")]
106 Io(#[from] std::io::Error),
107
108 #[error("Network error during {operation}: {source}")]
110 NetworkError {
111 operation: String,
112 source: Box<dyn std::error::Error + Send + Sync>,
113 },
114
115 #[error("Invalid response: {message}")]
116 InvalidResponse {
117 message: String,
118 source: Option<Box<dyn std::error::Error + Send + Sync>>,
119 },
120
121 #[error("I/O operation failed: {operation}")]
122 IoError {
123 operation: String,
124 #[source]
125 source: std::io::Error,
126 },
127
128 #[error("Parse error: {message}")]
129 ParseError { message: String },
130
131 #[error("Not implemented: {feature}")]
132 NotImplemented { feature: String, message: String },
133}
134
135impl CliError {
136 pub fn connection_failed(address: SocketAddr, source: impl Into<anyhow::Error>) -> Self {
138 Self::ConnectionFailed {
139 address,
140 source: source.into(),
141 }
142 }
143
144 pub fn actor_not_found(actor_id: impl Into<String>) -> Self {
146 Self::ActorNotFound {
147 actor_id: actor_id.into(),
148 }
149 }
150
151 pub fn build_failed(output: impl Into<String>) -> Self {
153 Self::BuildFailed {
154 output: output.into(),
155 }
156 }
157
158 pub fn invalid_manifest(reason: impl Into<String>) -> Self {
160 Self::InvalidManifest {
161 reason: reason.into(),
162 }
163 }
164
165 pub fn template_not_found(template: impl Into<String>, available: Vec<String>) -> Self {
167 Self::TemplateNotFound {
168 template: template.into(),
169 available: available.join(", "),
170 }
171 }
172
173 pub fn file_operation_failed(
175 operation: impl Into<String>,
176 path: impl Into<String>,
177 source: std::io::Error,
178 ) -> Self {
179 Self::FileOperationFailed {
180 operation: operation.into(),
181 path: path.into(),
182 source,
183 }
184 }
185
186 pub fn invalid_input(
188 field: impl Into<String>,
189 value: impl Into<String>,
190 suggestion: impl Into<String>,
191 ) -> Self {
192 Self::InvalidInput {
193 field: field.into(),
194 value: value.into(),
195 suggestion: suggestion.into(),
196 }
197 }
198
199 pub fn invalid_actor_id(actor_id: impl Into<String>) -> Self {
201 let actor_id = actor_id.into();
202 Self::InvalidInput {
203 field: "actor_id".to_string(),
204 value: actor_id,
205 suggestion:
206 "Actor ID must be a valid UUID (e.g., 123e4567-e89b-12d3-a456-426614174000)"
207 .to_string(),
208 }
209 }
210
211 pub fn operation_timeout(_operation: impl Into<String>, timeout: u64) -> Self {
212 Self::ConnectionTimeout { timeout }
213 }
214
215 pub fn not_implemented(feature: impl Into<String>, message: impl Into<String>) -> Self {
217 Self::NotImplemented {
218 feature: feature.into(),
219 message: message.into(),
220 }
221 }
222
223 pub fn user_message(&self) -> String {
225 match self {
226 Self::ConnectionFailed { address, .. } => {
227 format!(
228 "Could not connect to Theater server at {}.\n\n\
229 Possible solutions:\n\
230 • Start a Theater server with: theater server\n\
231 • Check if the server address is correct\n\
232 • Verify the server is running and accessible",
233 address
234 )
235 }
236 Self::ActorNotFound { actor_id } => {
237 format!(
238 "Actor '{}' was not found.\n\n\
239 Possible solutions:\n\
240 • Check the actor ID is correct\n\
241 • List running actors with: theater list\n\
242 • Start the actor with: theater start <manifest>",
243 actor_id
244 )
245 }
246 Self::BuildFailed { output } => {
247 format!(
248 "Build failed.\n\n\
249 Build output:\n{}\n\n\
250 Possible solutions:\n\
251 • Check for compilation errors in your Rust code\n\
252 • Ensure all dependencies are available\n\
253 • Try a clean build with: theater build --clean",
254 output
255 )
256 }
257 Self::MissingTool {
258 tool,
259 install_command,
260 } => {
261 format!(
262 "Required tool '{}' is not installed.\n\n\
263 Install it with:\n {}",
264 tool, install_command
265 )
266 }
267 Self::TemplateNotFound {
268 template,
269 available,
270 } => {
271 format!(
272 "Template '{}' was not found.\n\n\
273 Available templates: {}\n\n\
274 Use one of the available templates or check your template configuration.",
275 template, available
276 )
277 }
278 Self::InvalidInput {
279 field,
280 value,
281 suggestion,
282 } => {
283 format!("Invalid {}: '{}'\n\n{}", field, value, suggestion)
284 }
285 Self::NotImplemented { feature, message } => {
286 format!(
287 "Feature '{}' is not implemented.\n\n\
288 {}\n\n\
289 ",
290 feature, message
291 )
292 }
293 _ => self.to_string(),
294 }
295 }
296
297 pub fn is_retryable(&self) -> bool {
299 matches!(
300 self,
301 Self::ConnectionFailed { .. }
302 | Self::ConnectionLost
303 | Self::ConnectionTimeout { .. }
304 | Self::ServerError { .. }
305 )
306 }
307
308 pub fn category(&self) -> &'static str {
310 match self {
311 Self::ConnectionFailed { .. }
312 | Self::ConnectionLost
313 | Self::ConnectionTimeout { .. } => "connection",
314 Self::ActorNotFound { .. }
315 | Self::ActorStartFailed { .. }
316 | Self::ActorNotRunning { .. } => "actor",
317 Self::InvalidProjectDirectory { .. }
318 | Self::BuildFailed { .. }
319 | Self::MissingTool { .. } => "build",
320 Self::InvalidManifest { .. } | Self::ConfigError { .. } => "config",
321 Self::TemplateNotFound { .. } | Self::TemplateError { .. } => "template",
322 Self::FileOperationFailed { .. } | Self::PermissionDenied { .. } => "filesystem",
323 Self::InvalidInput { .. } | Self::ValidationError { .. } => "validation",
324 Self::ServerError { .. }
325 | Self::ProtocolError { .. }
326 | Self::UnexpectedResponse { .. } => "server",
327 Self::EventStreamError { .. } | Self::EventFilterError { .. } => "events",
328 Self::Internal(_) | Self::Serialization(_) | Self::Io(_) | Self::ParseError { .. } => {
329 "internal"
330 }
331 Self::NetworkError { .. } => "network",
332 Self::InvalidResponse { .. } => "response",
333 Self::IoError { .. } => "io",
334 Self::NotImplemented { .. } => "not_implemented",
335 }
336 }
337}
338
339pub type CliResult<T> = Result<T, CliError>;
341
342pub trait IntoCliError<T> {
344 fn into_cli_error(self) -> CliResult<T>;
345 fn with_cli_context(self, context: impl FnOnce() -> CliError) -> CliResult<T>;
346}
347
348impl<T, E> IntoCliError<T> for Result<T, E>
349where
350 E: Into<anyhow::Error>,
351{
352 fn into_cli_error(self) -> CliResult<T> {
353 self.map_err(|e| CliError::Internal(e.into()))
354 }
355
356 fn with_cli_context(self, context: impl FnOnce() -> CliError) -> CliResult<T> {
357 self.map_err(|_| context())
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn test_error_categories() {
367 assert_eq!(CliError::actor_not_found("test").category(), "actor");
368 assert_eq!(CliError::build_failed("failed").category(), "build");
369 assert_eq!(
370 CliError::template_not_found("basic", vec!["http".to_string()]).category(),
371 "template"
372 );
373 }
374
375 #[test]
376 fn test_retryable_errors() {
377 assert!(CliError::ConnectionLost.is_retryable());
378 assert!(!CliError::actor_not_found("test").is_retryable());
379 }
380
381 #[test]
382 fn test_user_messages() {
383 let error = CliError::actor_not_found("test-actor");
384 let message = error.user_message();
385 assert!(message.contains("test-actor"));
386 assert!(message.contains("theater list"));
387 }
388}