Skip to main content

horto_os_ui_shared/
error.rs

1//! Typed setup/ops errors for the Horto engine.
2//!
3//! # Layering (ITC habit)
4//!
5//! | Layer | Crate | Tool |
6//! | ----- | ----- | ---- |
7//! | Library / step API | `horto-os-ui-shared` | `thiserror` via [`HortoError`] |
8//! | Process binaries (CLI, TUI, API, KPI) | each binary | `anyhow` at `main`; `?` maps [`HortoError`] |
9//! | Diagnostics | shared emits; CLI/API/KPI/desktop call [`crate::init_tracing`] | `tracing` only (stderr via subscriber); TUI uses [`crate::HostContext::logs`] |
10//!
11//! Do **not** put `thiserror` on every surface crate. Surfaces that only talk HTTP
12//! (KPI, web) never see [`HortoError`]. Surfaces that call the engine convert with `?`.
13
14use thiserror::Error;
15
16/// Crate-wide result alias for the setup engine.
17pub type Result<T> = std::result::Result<T, HortoError>;
18
19/// Failures from steps, kits, resume, and host ops.
20#[derive(Debug, Error)]
21pub enum HortoError {
22    /// Free-form message when no tighter variant fits yet.
23    #[error("{0}")]
24    Message(String),
25
26    /// Apply mode needs root (sudo).
27    #[error("root required for apply mode; re-run with sudo")]
28    RootRequired,
29
30    /// Step id is not registered.
31    #[error("unknown step id: {0}")]
32    UnknownStep(String),
33
34    /// Step id exists but is not in the selected Full/Minimal pipeline.
35    #[error("step {step} is not in the {kind} pipeline")]
36    NotInPipeline {
37        /// Step id requested.
38        step: String,
39        /// Pipeline kind (`full` / `minimal`).
40        kind: String,
41    },
42
43    /// Required dependency steps are incomplete.
44    #[error("step {0} depends on incomplete step(s): {1}")]
45    MissingDependency(String, String),
46
47    /// Embedded asset path missing from the binary.
48    #[error("missing embedded asset: {0}")]
49    EmbedMissing(String),
50
51    /// External process failed (apt, systemctl, docker, iptables, …).
52    #[error("command failed: {program}: {detail}")]
53    CommandFailed {
54        /// Program name or short label.
55        program: String,
56        /// Status / stderr summary.
57        detail: String,
58    },
59
60    /// Filesystem IO.
61    #[error("IO error: {0}")]
62    Io(#[from] std::io::Error),
63
64    /// JSON encode/decode.
65    #[error("JSON error: {0}")]
66    Json(#[from] serde_json::Error),
67}
68
69impl HortoError {
70    /// Build a [`Message`](Self::Message) error.
71    pub fn msg(s: impl Into<String>) -> Self {
72        Self::Message(s.into())
73    }
74
75    /// External command failure helper.
76    pub fn command(program: impl Into<String>, detail: impl Into<String>) -> Self {
77        Self::CommandFailed {
78            program: program.into(),
79            detail: detail.into(),
80        }
81    }
82
83    /// Suggested process exit code for CLI surfaces.
84    #[must_use]
85    pub const fn exit_code(&self) -> i32 {
86        match self {
87            Self::RootRequired => 77,
88            Self::UnknownStep(_) | Self::NotInPipeline { .. } => 2,
89            _ => 1,
90        }
91    }
92}