use systemprompt_models::domain_error;
domain_error! {
pub enum SyncError {
common: [io, http, json, yaml],
#[error("API error {status}: {message}")]
ApiError { status: u16, message: String },
#[error("Unauthorized - run 'systemprompt cloud login'")]
Unauthorized,
#[error("Tenant has no associated app")]
TenantNoApp,
#[error("Must run from project root (with infrastructure/ directory)")]
NotProjectRoot,
#[error("Command failed: {command}")]
CommandFailed { command: String },
#[error("Failed to spawn `{command}`: {source}")]
CommandSpawnFailed {
command: String,
#[source]
source: std::io::Error,
},
#[error("Failed to open file {path}: {source}")]
FileOpenFailed {
path: String,
#[source]
source: std::io::Error,
},
#[error("Docker login failed")]
DockerLoginFailed,
#[error("Git SHA unavailable")]
GitShaUnavailable,
#[error("Missing configuration: {0}")]
MissingConfig(String),
#[error("Partial import failure after {completed}/{total} items: {message}")]
PartialImport {
completed: usize,
total: usize,
message: String,
},
#[error("Unsafe tarball entry rejected: {0}")]
TarballUnsafe(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Path error: {0}")]
StripPrefix(#[from] std::path::StripPrefixError),
#[error("Zip error: {0}")]
Zip(#[from] zip::result::ZipError),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("internal: {0}")]
Internal(String),
}
}
impl SyncError {
pub fn internal(cause: impl std::fmt::Display) -> Self {
Self::Internal(cause.to_string())
}
pub fn invalid_input(cause: impl std::fmt::Display) -> Self {
Self::InvalidInput(cause.to_string())
}
pub const fn is_retryable(&self) -> bool {
matches!(self, Self::Http(_))
|| matches!(
self,
Self::ApiError { status, .. }
if *status == 502 || *status == 503 || *status == 504 || *status == 429
)
}
}
pub type SyncResult<T> = Result<T, SyncError>;