rootle-manager 0.10.0

Install, update, pin, and sweep rootle's stdio provider binaries (krew-model atomicity)
Documentation
//! Provider manager (plans/0010): install, list, update, upgrade,
//! pin, remove, use — for stdio provider binaries distributed as
//! GitHub release tarballs (the 4-target matrix rootle and its
//! providers ship: linux+macOS × x86_64+aarch64).
//!
//! Layout (XDG tiers, one purpose each — see the plan's survey):
//!
//! ```text
//! ~/.local/share/rootle/providers/<name>/<version>/rootle-<name>  binary
//! ~/.local/share/rootle/providers/<name>/current -> <version>/    pointer
//! ~/.local/state/rootle/providers/<name>.toml                    receipt
//! ```
//!
//! Atomicity (krew model): download to temp staging → verify sha256
//! (mandatory) → extract into the versioned dir → write the receipt
//! LAST → atomically re-point `current`. A failed step leaves the
//! previous version intact. The running TUI is never affected —
//! provider children die with the app, the next spawn picks up the
//! new `current`.
//!
//! Config declares, the store installs (mise pattern): `use` writes
//! the existing `[provider]` block; the runtime `build()` is
//! untouched and hand-edited configs keep working.
//!
//! Submodules by concern: `refs` (the install-reference grammar),
//! `release` (GitHub API, tarball, checksum), `install` (the
//! install/update/upgrade flows), `store` (local state on disk),
//! `bookkeeping` (pin/remove/use/list).

mod install;
pub mod progress;
mod reference;
mod release;
mod store;

pub use reference::ProviderReference;

/// The verified-download machinery, shared with `rootle update`
/// (0017 M2) — same integrity model as provider installs.
pub use release::latest_release_at;
pub use release::{
    checksum_sidecar, download_bytes, extract_binary, latest_release, pick_asset, platform_target,
    verify_checksum,
};

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// The receipt: provenance recorded beside the binary so list/upgrade
/// read local state, never the network (gh manifest.yml pattern).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Receipt {
    pub name: String,
    /// "owner/repo"
    pub source: String,
    pub tag: String,
    pub sha256: String,
    pub pinned: bool,
    #[serde(default)]
    pub installed_at: Option<String>,
    /// Latest tag known from `update` (krew's non-mutating refresh).
    #[serde(default)]
    pub latest_tag: Option<String>,
}

pub type Result<T, E = ManagerError> = std::result::Result<T, E>;

#[derive(Debug, thiserror::Error)]
pub enum ManagerError {
    #[error("{0}")]
    User(String),
    #[error("network: {0}")]
    Network(String),
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
}

pub struct Manager {
    store: PathBuf,
    state: PathBuf,
    /// Releases-API base — github.com in every real path; tests point
    /// it at a loopback host (the `latest_release_at` seam).
    api: String,
}

impl Manager {
    pub fn new() -> Result<Manager> {
        Ok(Manager {
            store: store::store_root().ok_or_else(|| ManagerError::User("no data dir".into()))?,
            state: store::state_root().ok_or_else(|| ManagerError::User("no state dir".into()))?,
            api: "https://api.github.com".into(),
        })
    }

    /// A manager rooted at throwaway dirs — tests never touch the
    /// user's real XDG store.
    #[cfg(test)]
    pub(crate) fn rooted_at(store: PathBuf, state: PathBuf) -> Manager {
        Manager {
            store,
            state,
            api: "https://api.github.com".into(),
        }
    }

    /// Point the releases API at a loopback host (tests).
    #[cfg(test)]
    pub(crate) fn with_api(mut self, api: &str) -> Manager {
        self.api = api.into();
        self
    }
}

/// An installed provider, as `list` reports.
#[derive(Debug, Clone)]
pub struct Installed {
    pub receipt: Receipt,
    pub active: bool,
    pub current: Option<String>,
}

/// One provider's result in `rootle update`'s sweep (0019 M1) —
/// rendered by the caller, never printed here.
#[derive(Debug, Clone)]
pub enum SweepOutcome {
    Upgraded {
        name: String,
        from: String,
        to: String,
    },
    /// Would upgrade (`--check`): nothing was swapped.
    Stale {
        name: String,
        from: String,
        to: String,
    },
    Current {
        name: String,
        tag: String,
    },
    Pinned {
        name: String,
        tag: String,
    },
    /// plain-HTTP / `--path` install: install-and-pin, untouched
    /// (plans/0014 #1b) — reported, not silently skipped.
    Untracked {
        name: String,
        source: String,
    },
    Failed {
        name: String,
        error: String,
    },
}