use sysknife_brain::planner::PlanRiskLevel;
use crate::approval::MaxRisk;
#[derive(Debug, thiserror::Error)]
pub enum CliError {
#[error("plan rejected by user")]
Rejected,
#[error("execution failed: {0}")]
ExecutionFailed(String),
#[error("planning failed: {0}")]
PlanningFailed(String),
#[error("config/daemon error: {0}")]
ConfigOrDaemon(String),
#[error("plan contains a {} step, but --max-risk ceiling is {}", .highest.as_str(), .ceiling.as_str())]
RiskCeilingExceeded {
highest: PlanRiskLevel,
ceiling: MaxRisk,
},
#[error("plan requires interactive approval but --non-interactive was set")]
NonInteractive,
#[error(
"sysknife approve needs a terminal to confirm on, and stdin is not one \
(it is a pipe, a redirect, or a non-interactive ssh command). \
Run it directly in a shell."
)]
ApprovalNeedsTerminal,
#[error("subcommand exit code {0}")]
Exit(i32),
}
impl CliError {
pub fn exit_code(&self) -> i32 {
match self {
Self::Rejected
| Self::RiskCeilingExceeded { .. }
| Self::NonInteractive
| Self::ApprovalNeedsTerminal => 1,
Self::ExecutionFailed(_) => 2,
Self::PlanningFailed(_) => 3,
Self::ConfigOrDaemon(_) => 4,
Self::Exit(code) => *code,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exit_code_rejected_is_1() {
assert_eq!(CliError::Rejected.exit_code(), 1);
}
#[test]
fn exit_code_risk_ceiling_exceeded_is_1() {
assert_eq!(
CliError::RiskCeilingExceeded {
highest: PlanRiskLevel::High,
ceiling: MaxRisk::Medium,
}
.exit_code(),
1
);
}
#[test]
fn exit_code_non_interactive_is_1() {
assert_eq!(CliError::NonInteractive.exit_code(), 1);
}
#[test]
fn exit_code_approval_needs_terminal_is_1() {
assert_eq!(CliError::ApprovalNeedsTerminal.exit_code(), 1);
}
#[test]
fn a_missing_terminal_is_not_reported_as_a_flag_the_user_never_passed() {
let msg = CliError::ApprovalNeedsTerminal.to_string();
assert!(
!msg.contains("--non-interactive"),
"must not blame a flag that does not exist for this subcommand: {msg}"
);
assert!(msg.contains("terminal"), "names the real cause: {msg}");
assert!(
msg.contains("Run it directly in a shell"),
"tells the user what to do instead: {msg}"
);
assert!(CliError::NonInteractive
.to_string()
.contains("--non-interactive"));
}
#[test]
fn exit_code_execution_failed_is_2() {
assert_eq!(CliError::ExecutionFailed("boom".into()).exit_code(), 2);
}
#[test]
fn exit_code_planning_failed_is_3() {
assert_eq!(CliError::PlanningFailed("bad".into()).exit_code(), 3);
}
#[test]
fn exit_code_config_or_daemon_is_4() {
assert_eq!(CliError::ConfigOrDaemon("nope".into()).exit_code(), 4);
}
}