1use crate::exit_codes;
2use crate::output::CliError;
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum VirtuosoError {
7 #[error("connection failed: {0}")]
8 Connection(String),
9
10 #[error("execution failed: {0}")]
11 Execution(String),
12
13 #[error("ssh error: {0}")]
14 Ssh(String),
15
16 #[error("io error: {0}")]
17 Io(#[from] std::io::Error),
18
19 #[error("json error: {0}")]
20 Json(#[from] serde_json::Error),
21
22 #[error("timeout after {0}s")]
23 Timeout(u64),
24
25 #[error("config error: {0}")]
26 Config(String),
27
28 #[error("not found: {0}")]
29 NotFound(String),
30
31 #[error("conflict: {0}")]
32 Conflict(String),
33}
34
35impl VirtuosoError {
36 pub fn exit_code(&self) -> i32 {
37 match self {
38 Self::Config(_) => exit_codes::USAGE_ERROR,
39 Self::NotFound(_) => exit_codes::NOT_FOUND,
40 Self::Conflict(_) => exit_codes::CONFLICT,
41 Self::Connection(_) | Self::Ssh(_) | Self::Timeout(_) => exit_codes::GENERAL_ERROR,
42 Self::Execution(_) | Self::Io(_) | Self::Json(_) => exit_codes::GENERAL_ERROR,
43 }
44 }
45
46 pub fn error_type(&self) -> &'static str {
47 match self {
48 Self::Connection(_) => "connection_failed",
49 Self::Execution(_) => "execution_failed",
50 Self::Ssh(_) => "ssh_error",
51 Self::Io(_) => "io_error",
52 Self::Json(_) => "json_error",
53 Self::Timeout(_) => "timeout",
54 Self::Config(_) => "config_error",
55 Self::NotFound(_) => "not_found",
56 Self::Conflict(_) => "conflict",
57 }
58 }
59
60 pub fn retryable(&self) -> bool {
61 matches!(self, Self::Connection(_) | Self::Timeout(_))
62 }
63
64 pub fn suggestion(&self) -> Option<String> {
65 match self {
66 Self::Config(msg) if msg.contains("VB_REMOTE_HOST") => {
67 Some("Run: virtuoso init".into())
68 }
69 Self::Connection(_) => Some("Run: virtuoso tunnel start".into()),
70 Self::Timeout(secs) => Some(format!("Retry with --timeout {}", secs * 2)),
71 Self::Ssh(msg) if msg.contains("authentication") => {
72 Some("Check SSH keys: ssh-add -l".into())
73 }
74 Self::Execution(msg) if msg.ends_with(": nil") || msg.contains("unbound") => {
75 Some("SKILL returned nil — check if a cellview/session is open".into())
76 }
77 Self::NotFound(_) => Some("Use 'vcli session list' to see active sessions".into()),
78 _ => None,
79 }
80 }
81
82 pub fn to_cli_error(&self) -> CliError {
83 CliError {
84 error: self.error_type().to_string(),
85 message: self.to_string(),
86 suggestion: self.suggestion(),
87 retryable: self.retryable(),
88 }
89 }
90}
91
92pub type Result<T> = std::result::Result<T, VirtuosoError>;