1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! CLI error type and exit-code mapping.
//!
//! New variants are added by their first concrete user. Exit codes
//! follow loose `sysexits.h` precedent where reasonable: 0 success,
//! 1 general failure, 2 misuse / clap rejection, 64 not-yet-implemented.
use std::fmt;
use std::path::PathBuf;
pub type CliResult<T> = Result<T, CliError>;
/// Top-level CLI failure mode.
#[derive(Debug)]
pub enum CliError {
/// A defined subcommand whose body has not yet landed. The `hint`
/// field is appended to the message so the user knows where to
/// look for the planned design / when to expect it.
NotImplemented {
what: &'static str,
hint: &'static str,
},
/// The current working directory is not inside an Aristo workspace
/// (no `aristo.toml` found by walking upward).
NotInWorkspace { searched_from: PathBuf },
/// I/O failure during file read/write.
Io(std::io::Error),
/// Catch-all for command-specific errors that carry their own message
/// and exit code. Use when no structured variant fits and the message
/// is the user-facing diagnostic (e.g. `aristo lang`'s
/// "no supported language detected").
Other { message: String, exit_code: u8 },
/// Non-success exit where the command has already printed its own
/// human-readable output (e.g. `aristo lint --check` listing
/// findings). Lib's top-level wrapper skips the generic
/// `error: ...` line for this variant.
Silent { exit_code: u8 },
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::NotImplemented { what, hint } => {
write!(f, "{what} is not implemented yet — {hint}")
}
CliError::NotInWorkspace { searched_from } => {
write!(
f,
"not inside an Aristo workspace (no aristo.toml found at or above {})\n\
hint: run `aristo init` to bootstrap a new workspace here",
searched_from.display()
)
}
CliError::Io(e) => write!(f, "io: {e}"),
CliError::Other { message, .. } => write!(f, "{message}"),
CliError::Silent { .. } => Ok(()),
}
}
}
impl CliError {
/// True iff the error wants the top-level wrapper to skip its
/// generic `error: ...` print (because the command already produced
/// its own structured output).
pub fn is_silent(&self) -> bool {
matches!(self, CliError::Silent { .. })
}
}
impl std::error::Error for CliError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CliError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for CliError {
fn from(e: std::io::Error) -> Self {
CliError::Io(e)
}
}
impl CliError {
/// Process exit code for this error class.
pub fn exit_code(&self) -> u8 {
match self {
CliError::NotImplemented { .. } => 64,
CliError::NotInWorkspace { .. } => 2,
CliError::Io(_) => 1,
CliError::Other { exit_code, .. } => *exit_code,
CliError::Silent { exit_code } => *exit_code,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_implemented_message_includes_what_and_hint() {
let e = CliError::NotImplemented {
what: "aristo init",
hint: "see docs/deferred/...",
};
let msg = e.to_string();
assert!(msg.contains("aristo init"), "msg: {msg}");
assert!(msg.contains("see docs/deferred/..."), "msg: {msg}");
assert!(msg.contains("not implemented yet"), "msg: {msg}");
}
#[test]
fn not_in_workspace_includes_search_origin_and_hint() {
let e = CliError::NotInWorkspace {
searched_from: PathBuf::from("/tmp/elsewhere"),
};
let msg = e.to_string();
assert!(msg.contains("/tmp/elsewhere"));
assert!(msg.contains("aristo init"), "should hint at init: {msg}");
}
#[test]
fn exit_codes_are_distinct_per_class() {
let ni = CliError::NotImplemented {
what: "x",
hint: "y",
}
.exit_code();
let nw = CliError::NotInWorkspace {
searched_from: PathBuf::new(),
}
.exit_code();
let io = CliError::Io(std::io::Error::other("boom")).exit_code();
assert_ne!(ni, nw);
assert_ne!(nw, io);
assert_ne!(ni, io);
}
}