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