1use std::fmt;
8use std::path::PathBuf;
9
10pub type CliResult<T> = Result<T, CliError>;
11
12#[derive(Debug)]
14pub enum CliError {
15 NotImplemented {
19 what: &'static str,
20 hint: &'static str,
21 },
22 NotInWorkspace { searched_from: PathBuf },
25 Io(std::io::Error),
27 Other { message: String, exit_code: u8 },
32 Silent { exit_code: u8 },
37}
38
39impl fmt::Display for CliError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 CliError::NotImplemented { what, hint } => {
43 write!(f, "{what} is not implemented yet — {hint}")
44 }
45 CliError::NotInWorkspace { searched_from } => {
46 write!(
47 f,
48 "not inside an Aristo workspace (no aristo.toml found at or above {})\n\
49 hint: run `aristo init` to bootstrap a new workspace here",
50 searched_from.display()
51 )
52 }
53 CliError::Io(e) => write!(f, "io: {e}"),
54 CliError::Other { message, .. } => write!(f, "{message}"),
55 CliError::Silent { .. } => Ok(()),
56 }
57 }
58}
59
60impl CliError {
61 pub fn is_silent(&self) -> bool {
65 matches!(self, CliError::Silent { .. })
66 }
67}
68
69impl std::error::Error for CliError {
70 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71 match self {
72 CliError::Io(e) => Some(e),
73 _ => None,
74 }
75 }
76}
77
78impl From<std::io::Error> for CliError {
79 fn from(e: std::io::Error) -> Self {
80 CliError::Io(e)
81 }
82}
83
84impl CliError {
85 pub fn exit_code(&self) -> u8 {
87 match self {
88 CliError::NotImplemented { .. } => 64,
89 CliError::NotInWorkspace { .. } => 2,
90 CliError::Io(_) => 1,
91 CliError::Other { exit_code, .. } => *exit_code,
92 CliError::Silent { exit_code } => *exit_code,
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn not_implemented_message_includes_what_and_hint() {
103 let e = CliError::NotImplemented {
104 what: "aristo init",
105 hint: "see docs/deferred/...",
106 };
107 let msg = e.to_string();
108 assert!(msg.contains("aristo init"), "msg: {msg}");
109 assert!(msg.contains("see docs/deferred/..."), "msg: {msg}");
110 assert!(msg.contains("not implemented yet"), "msg: {msg}");
111 }
112
113 #[test]
114 fn not_in_workspace_includes_search_origin_and_hint() {
115 let e = CliError::NotInWorkspace {
116 searched_from: PathBuf::from("/tmp/elsewhere"),
117 };
118 let msg = e.to_string();
119 assert!(msg.contains("/tmp/elsewhere"));
120 assert!(msg.contains("aristo init"), "should hint at init: {msg}");
121 }
122
123 #[test]
124 fn exit_codes_are_distinct_per_class() {
125 let ni = CliError::NotImplemented {
126 what: "x",
127 hint: "y",
128 }
129 .exit_code();
130 let nw = CliError::NotInWorkspace {
131 searched_from: PathBuf::new(),
132 }
133 .exit_code();
134 let io = CliError::Io(std::io::Error::other("boom")).exit_code();
135 assert_ne!(ni, nw);
136 assert_ne!(nw, io);
137 assert_ne!(ni, io);
138 }
139}