Skip to main content

torvyn_cli/errors/
mod.rs

1//! CLI error types and exit code mapping.
2//!
3//! The [`CliError`] type wraps all possible error conditions from every
4//! subcommand and maps them to user-facing error output and exit codes.
5
6pub mod diagnostic;
7
8use crate::output::OutputContext;
9
10/// Top-level CLI error type.
11///
12/// ## Invariants
13/// - Every variant maps to a specific exit code.
14/// - Every variant carries enough context for a four-part error message.
15///
16/// ## Error codes (per Doc 10 C07-2, Doc 09 G-08)
17/// - E0001–E0099: General CLI errors
18/// - E0100–E0199: Contract errors
19/// - E0200–E0299: Linking errors
20/// - E0300–E0399: Resource errors
21/// - E0400–E0499: Reactor errors
22/// - E0500–E0599: Security errors
23/// - E0600–E0699: Packaging errors
24/// - E0700–E0799: Configuration errors
25#[derive(Debug)]
26pub enum CliError {
27    /// Configuration file not found or invalid.
28    Config {
29        /// What went wrong.
30        detail: String,
31        /// File path (if applicable).
32        file: Option<String>,
33        /// How to fix it.
34        suggestion: String,
35    },
36    /// WIT contract validation failure.
37    Contract {
38        /// What went wrong.
39        detail: String,
40        /// Detailed diagnostic messages.
41        diagnostics: Vec<String>,
42    },
43    /// Pipeline linking/composition failure.
44    Link {
45        /// What went wrong.
46        detail: String,
47        /// Detailed diagnostic messages.
48        diagnostics: Vec<String>,
49    },
50    /// Runtime execution failure.
51    Runtime {
52        /// What went wrong.
53        detail: String,
54        /// Additional context.
55        context: Option<String>,
56    },
57    /// Packaging or publish failure.
58    Packaging {
59        /// What went wrong.
60        detail: String,
61        /// How to fix it.
62        suggestion: String,
63    },
64    /// Security/capability failure.
65    Security {
66        /// What went wrong.
67        detail: String,
68        /// How to fix it.
69        suggestion: String,
70    },
71    /// Environment issue (missing tool, wrong version).
72    #[allow(dead_code)]
73    Environment {
74        /// What went wrong.
75        detail: String,
76        /// How to fix it.
77        fix: String,
78    },
79    /// Filesystem error (can't create directory, can't write file).
80    Io {
81        /// What went wrong.
82        detail: String,
83        /// File path (if applicable).
84        path: Option<String>,
85    },
86    /// Generic internal error (should not normally reach users).
87    #[allow(dead_code)]
88    Internal {
89        /// What went wrong.
90        detail: String,
91    },
92    /// Command not yet implemented.
93    #[allow(dead_code)]
94    NotImplemented {
95        /// Command name.
96        command: String,
97    },
98}
99
100impl CliError {
101    /// Map this error to a process exit code.
102    ///
103    /// - Returns 1 for command failures (validation, runtime, packaging).
104    /// - Returns 2 for usage errors (bad config, missing files).
105    /// - Returns 3 for environment errors (missing tools).
106    pub fn exit_code(&self) -> i32 {
107        match self {
108            Self::Config { .. } => 2,
109            Self::Contract { .. } => 1,
110            Self::Link { .. } => 1,
111            Self::Runtime { .. } => 1,
112            Self::Packaging { .. } => 1,
113            Self::Security { .. } => 1,
114            Self::Environment { .. } => 3,
115            Self::Io { .. } => 2,
116            Self::Internal { .. } => 1,
117            Self::NotImplemented { .. } => 1,
118        }
119    }
120
121    /// Render this error to the terminal.
122    ///
123    /// COLD PATH — called at most once per invocation.
124    pub fn render(&self, ctx: &OutputContext) {
125        use crate::cli::OutputFormat;
126        match ctx.format {
127            OutputFormat::Json => {
128                let err_obj = self.to_json_value();
129                crate::output::json::print_json(&err_obj);
130            }
131            OutputFormat::Human => {
132                diagnostic::render_cli_error(ctx, self);
133            }
134        }
135    }
136
137    /// Convert to a JSON-serializable value.
138    fn to_json_value(&self) -> serde_json::Value {
139        match self {
140            Self::Config {
141                detail,
142                file,
143                suggestion,
144            } => serde_json::json!({
145                "error": true,
146                "category": "config",
147                "detail": detail,
148                "file": file,
149                "suggestion": suggestion,
150            }),
151            Self::Contract {
152                detail,
153                diagnostics,
154            } => serde_json::json!({
155                "error": true,
156                "category": "contract",
157                "detail": detail,
158                "diagnostics": diagnostics,
159            }),
160            Self::Link {
161                detail,
162                diagnostics,
163            } => serde_json::json!({
164                "error": true,
165                "category": "link",
166                "detail": detail,
167                "diagnostics": diagnostics,
168            }),
169            Self::Runtime { detail, context } => serde_json::json!({
170                "error": true,
171                "category": "runtime",
172                "detail": detail,
173                "context": context,
174            }),
175            Self::Packaging { detail, suggestion } => serde_json::json!({
176                "error": true,
177                "category": "packaging",
178                "detail": detail,
179                "suggestion": suggestion,
180            }),
181            Self::Security { detail, suggestion } => serde_json::json!({
182                "error": true,
183                "category": "security",
184                "detail": detail,
185                "suggestion": suggestion,
186            }),
187            Self::Environment { detail, fix } => serde_json::json!({
188                "error": true,
189                "category": "environment",
190                "detail": detail,
191                "fix": fix,
192            }),
193            Self::Io { detail, path } => serde_json::json!({
194                "error": true,
195                "category": "io",
196                "detail": detail,
197                "path": path,
198            }),
199            Self::Internal { detail } => serde_json::json!({
200                "error": true,
201                "category": "internal",
202                "detail": detail,
203            }),
204            Self::NotImplemented { command } => serde_json::json!({
205                "error": true,
206                "category": "not_implemented",
207                "detail": format!("Command '{command}' is not yet implemented (Part B)"),
208            }),
209        }
210    }
211}
212
213impl From<torvyn_types::TorvynError> for CliError {
214    fn from(err: torvyn_types::TorvynError) -> Self {
215        match err {
216            torvyn_types::TorvynError::Config(e) => CliError::Config {
217                detail: e.to_string(),
218                file: None,
219                suggestion: "Check your Torvyn.toml for errors.".into(),
220            },
221            torvyn_types::TorvynError::Contract(e) => CliError::Contract {
222                detail: e.to_string(),
223                diagnostics: vec![],
224            },
225            torvyn_types::TorvynError::Link(e) => CliError::Link {
226                detail: e.to_string(),
227                diagnostics: vec![],
228            },
229            torvyn_types::TorvynError::Resource(e) => CliError::Runtime {
230                detail: e.to_string(),
231                context: Some("resource error".into()),
232            },
233            torvyn_types::TorvynError::Reactor(e) => CliError::Runtime {
234                detail: e.to_string(),
235                context: Some("reactor error".into()),
236            },
237            torvyn_types::TorvynError::Security(e) => CliError::Security {
238                detail: e.to_string(),
239                suggestion: "Check capability grants in your security configuration.".into(),
240            },
241            torvyn_types::TorvynError::Packaging(e) => CliError::Packaging {
242                detail: e.to_string(),
243                suggestion: "Check your packaging configuration.".into(),
244            },
245            torvyn_types::TorvynError::Process(e) => CliError::Runtime {
246                detail: e.to_string(),
247                context: Some("component process error".into()),
248            },
249            torvyn_types::TorvynError::Engine(e) => CliError::Runtime {
250                detail: e.to_string(),
251                context: Some("engine error".into()),
252            },
253            torvyn_types::TorvynError::Io(e) => CliError::Io {
254                detail: e.to_string(),
255                path: None,
256            },
257        }
258    }
259}
260
261impl From<std::io::Error> for CliError {
262    fn from(err: std::io::Error) -> Self {
263        CliError::Io {
264            detail: err.to_string(),
265            path: None,
266        }
267    }
268}