Skip to main content

rootle_manager/
lib.rs

1//! Provider manager (plans/0010): install, list, update, upgrade,
2//! pin, remove, use — for stdio provider binaries distributed as
3//! GitHub release tarballs for Linux x86_64 and aarch64.
4//!
5//! Layout (XDG tiers, one purpose each — see the plan's survey):
6//!
7//! ```text
8//! ~/.local/share/rootle/providers/<name>/<version>/rootle-<name>  binary
9//! ~/.local/share/rootle/providers/<name>/current -> <version>/    pointer
10//! ~/.local/state/rootle/providers/<name>.toml                    receipt
11//! ```
12//!
13//! Atomicity (krew model): download to temp staging → verify sha256
14//! (mandatory) → extract into the versioned dir → write the receipt
15//! LAST → atomically re-point `current`. A failed step leaves the
16//! previous version intact. The running TUI is never affected —
17//! provider children die with the app, the next spawn picks up the
18//! new `current`.
19//!
20//! Config declares, the store installs (mise pattern): `use` writes
21//! the existing `[provider]` block; the runtime `build()` is
22//! untouched and hand-edited configs keep working.
23//!
24//! Submodules by concern: `refs` (the install-reference grammar),
25//! `release` (GitHub API, tarball, checksum), `install` (the
26//! install/update/upgrade flows), `store` (local state on disk),
27//! `bookkeeping` (pin/remove/use/list).
28
29mod install;
30pub mod progress;
31mod reference;
32mod release;
33mod store;
34
35pub use reference::ProviderReference;
36
37/// The verified-download machinery, shared with `rootle update`
38/// (0017 M2) — same integrity model as provider installs.
39pub use release::latest_release_at;
40pub use release::{
41    checksum_sidecar, download_bytes, extract_binary, latest_release, pick_asset, platform_target,
42    verify_checksum,
43};
44
45use serde::{Deserialize, Serialize};
46use std::path::PathBuf;
47
48/// The receipt: provenance recorded beside the binary so list/upgrade
49/// read local state, never the network (gh manifest.yml pattern).
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Receipt {
52    pub name: String,
53    /// "owner/repo"
54    pub source: String,
55    pub tag: String,
56    pub sha256: String,
57    pub pinned: bool,
58    #[serde(default)]
59    pub installed_at: Option<String>,
60    /// Latest tag known from `update` (krew's non-mutating refresh).
61    #[serde(default)]
62    pub latest_tag: Option<String>,
63}
64
65pub type Result<T, E = ManagerError> = std::result::Result<T, E>;
66
67#[derive(Debug, thiserror::Error)]
68pub enum ManagerError {
69    #[error("{0}")]
70    User(String),
71    #[error("network: {0}")]
72    Network(String),
73    #[error("io: {0}")]
74    Io(#[from] std::io::Error),
75}
76
77pub struct Manager {
78    store: PathBuf,
79    state: PathBuf,
80    /// Releases-API base — github.com in every real path; tests point
81    /// it at a loopback host (the `latest_release_at` seam).
82    api: String,
83}
84
85impl Manager {
86    pub fn new() -> Result<Manager> {
87        Ok(Manager {
88            store: store::store_root().ok_or_else(|| ManagerError::User("no data dir".into()))?,
89            state: store::state_root().ok_or_else(|| ManagerError::User("no state dir".into()))?,
90            api: "https://api.github.com".into(),
91        })
92    }
93
94    /// A manager rooted at throwaway dirs — tests never touch the
95    /// user's real XDG store.
96    #[cfg(test)]
97    pub(crate) fn rooted_at(store: PathBuf, state: PathBuf) -> Manager {
98        Manager {
99            store,
100            state,
101            api: "https://api.github.com".into(),
102        }
103    }
104
105    /// Point the releases API at a loopback host (tests).
106    #[cfg(test)]
107    pub(crate) fn with_api(mut self, api: &str) -> Manager {
108        self.api = api.into();
109        self
110    }
111}
112
113/// An installed provider, as `list` reports.
114#[derive(Debug, Clone)]
115pub struct Installed {
116    pub receipt: Receipt,
117    pub active: bool,
118    pub current: Option<String>,
119}
120
121/// One provider's result in `rootle update`'s sweep (0019 M1) —
122/// rendered by the caller, never printed here.
123#[derive(Debug, Clone)]
124pub enum SweepOutcome {
125    Upgraded {
126        name: String,
127        from: String,
128        to: String,
129    },
130    /// Would upgrade (`--check`): nothing was swapped.
131    Stale {
132        name: String,
133        from: String,
134        to: String,
135    },
136    Current {
137        name: String,
138        tag: String,
139    },
140    Pinned {
141        name: String,
142        tag: String,
143    },
144    /// plain-HTTP / `--path` install: install-and-pin, untouched
145    /// (plans/0014 #1b) — reported, not silently skipped.
146    Untracked {
147        name: String,
148        source: String,
149    },
150    Failed {
151        name: String,
152        error: String,
153    },
154}