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