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}