use std::io::IsTerminal;
#[derive(Debug)]
pub struct ConfirmationRequired {
pub resource: String,
}
impl std::fmt::Display for ConfirmationRequired {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Deleting {} requires confirmation", self.resource)
}
}
impl std::error::Error for ConfirmationRequired {}
pub fn check_confirmation(yes: bool, resource: &str) -> anyhow::Result<()> {
if yes || std::io::stdin().is_terminal() {
return Ok(());
}
Err(ConfirmationRequired {
resource: resource.to_string(),
}
.into())
}
#[derive(Debug, Clone)]
pub struct CliError {
pub kind: &'static str,
pub message: String,
pub hint: Option<String>,
pub exit_code: i32,
}
pub fn classify_error(err: &anyhow::Error) -> CliError {
if let Some(cr) = err.downcast_ref::<ConfirmationRequired>() {
return CliError {
kind: "confirmation_required",
message: format!("Running {} requires confirmation", cr.resource),
hint: Some("Re-run with --yes to confirm.".to_string()),
exit_code: 2,
};
}
let message = format!("{err:#}");
let lower = message.to_lowercase();
let (kind, exit_code, hint) = if lower.starts_with("clap_error:") {
let cleaned = message.trim_start_matches("clap_error:").trim().to_string();
return CliError {
kind: "invalid_input",
message: cleaned,
hint: Some("Run with --help to see usage.".to_string()),
exit_code: 2,
};
} else if lower.contains("not found in cache") || lower.contains("did you mean") {
("device_not_found", 1, None)
} else if lower.contains("no cached devices") || lower.contains("no devices discovered") {
(
"no_cached_devices",
1,
Some("Run 'shelly discover --subnet YOUR_SUBNET/24' first.".to_string()),
)
} else if lower.contains("group") && lower.contains("not found") {
("group_not_found", 1, None)
} else if lower.contains("auth") || lower.contains("unauthorized") || lower.contains("401") {
(
"auth_required",
3,
Some("Use --password or set [auth] password in config.toml.".to_string()),
)
} else if lower.contains("timed out") || lower.contains("connect") {
("device_unreachable", 2, None)
} else if lower.contains("partial") || lower.contains("some devices") {
("partial_failure", 4, None)
} else if lower.contains("already exists") || lower.contains("conflict") {
("conflict", 6, None)
} else if lower.contains("invalid") || lower.contains("parse") || lower.contains("specify") {
("invalid_input", 1, None)
} else {
("network_error", 2, None)
};
CliError {
kind,
message,
hint,
exit_code,
}
}