use std::fmt;
use serde::Serialize;
pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, Error>;
pub const SCHEMA_VERSION: &str = "1";
#[derive(Debug, Clone, Serialize)]
pub struct CliError {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub likely_causes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub suggested_commands: Vec<String>,
}
impl CliError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
hint: None,
likely_causes: Vec::new(),
suggested_commands: Vec::new(),
}
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
let trimmed = hint.into().trim().to_string();
self.hint = if trimmed.is_empty() {
None
} else {
Some(trimmed)
};
self
}
pub fn enriched(mut self) -> Self {
let (causes, suggestions) = lookup_metadata(&self.code);
if self.likely_causes.is_empty() {
self.likely_causes = causes;
}
if self.suggested_commands.is_empty() {
self.suggested_commands = suggestions;
}
self
}
fn is_http(&self) -> bool {
self.code
.parse::<u16>()
.map(|n| (100..1000).contains(&n))
.unwrap_or(false)
}
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_http() {
write!(f, "API error {} — {}", self.code, self.message)?;
} else {
write!(f, "{}", self.message)?;
}
if let Some(h) = &self.hint {
write!(f, "\n {}", h)?;
}
Ok(())
}
}
impl std::error::Error for CliError {}
pub fn extract_cli_error(err: &(dyn std::error::Error + 'static)) -> CliError {
if let Some(c) = err.downcast_ref::<CliError>() {
return c.clone();
}
CliError::new("error", err.to_string()).enriched()
}
fn lookup_metadata(code: &str) -> (Vec<String>, Vec<String>) {
match code {
"400" => (
vec!["Configuration values are out of range or wrong type".into()],
vec!["partiri validate".into()],
),
"401" => (
vec![
"API key expired or revoked".into(),
"Wrong PARTIRI_API_URL".into(),
],
vec!["partiri auth --key <K>".into(), "partiri llm doctor".into()],
),
"402" => (
vec!["Workspace balance is empty".into()],
vec!["partiri llm whoami".into()],
),
"403" => (
vec![
"Account lacks permission".into(),
"Workspace limit reached".into(),
],
vec!["partiri llm whoami".into()],
),
"404" => (
vec![
"Resource was deleted".into(),
"Wrong UUID for workspace/project/region/pod".into(),
],
vec!["partiri llm context".into()],
),
"409" => (
vec!["Conflicting operation in progress".into()],
vec!["partiri service jobs".into()],
),
"422" => (
vec![
"Invalid request data".into(),
"Schema mismatch with the API".into(),
],
vec!["partiri validate --remote".into()],
),
"429" => (vec!["Rate limit exceeded".into()], vec![]),
"auth" => (
vec!["No API key configured".into()],
vec!["partiri auth --key <K>".into()],
),
"validation" => (vec![], vec!["partiri llm next".into()]),
"network" => (
vec![
"API host unreachable".into(),
"Wrong PARTIRI_API_URL".into(),
],
vec!["partiri llm doctor".into()],
),
"config" => (
vec!["Missing or unparseable .partiri.jsonc".into()],
vec!["partiri init --template".into()],
),
"missing_dependency" => (vec![], vec!["partiri llm next".into()]),
_ => (vec![], vec![]),
}
}