repoke 0.4.0

Finds the package managers on your system and keeps them up to date, interactively or unattended
use std::path::Path;
use std::slice;

use super::npm::Globals;
use crate::manager::{Installed, Manager, Package, Step, capture};

pub struct Pnpm;

impl Manager for Pnpm {
    fn id(&self) -> &'static str {
        "pnpm"
    }

    fn name(&self) -> &'static str {
        "pnpm (global)"
    }

    fn binary(&self) -> &'static str {
        "pnpm"
    }

    fn outdated(&self, bin: &Path) -> Result<Vec<Package>, String> {
        let json = capture(bin, ["outdated", "--global", "--format", "json"])?.expect(&[0, 1])?;
        super::npm::parse_outdated(&json)
    }

    /// pnpm refuses `add --global pnpm` (ERR_PNPM_GLOBAL_PNPM_INSTALL) and wants `self-update`.
    fn upgrade_steps(&self, _bin: &Path, packages: &[Package]) -> Vec<Step> {
        let (own, others): (Vec<Package>, Vec<Package>) =
            packages.iter().cloned().partition(|p| p.name == "pnpm");
        let mut steps = Vec::new();
        if !others.is_empty() {
            steps.push(super::npm::install("add", &others));
        }
        for p in own {
            steps.push(Step::new(["self-update", &p.latest], slice::from_ref(&p)));
        }
        steps
    }

    fn incompatible(&self, bin: &Path, packages: &[Package]) -> Vec<(Package, String)> {
        super::engines::incompatible(bin, bin, packages)
    }

    fn installed(&self, bin: &Path) -> Result<Vec<Installed>, String> {
        let json = capture(bin, ["ls", "--global", "--depth=0", "--json"])?.expect(&[0])?;
        let roots: Vec<Globals> =
            serde_json::from_str(&json).map_err(|e| format!("unexpected pnpm output: {e}"))?;
        Ok(roots.into_iter().flat_map(Globals::packages).collect())
    }

    fn install_steps(&self, _bin: &Path, packages: &[Installed]) -> Vec<Step> {
        Step::install_all(&["add", "--global"], packages)
    }

    /// The global rc lives in the platform's config folder.
    fn config_files(&self) -> &'static [&'static str] {
        &[".config/pnpm/rc", "Library/Preferences/pnpm/rc"]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn updates_itself_with_self_update() {
        let package = |name: &str, latest: &str| Package {
            name: name.into(),
            current: "1.0.0".into(),
            latest: latest.into(),
        };
        let steps = Pnpm.upgrade_steps(
            Path::new("pnpm"),
            &[package("pnpm", "12.15.0"), package("eslint", "10.13.0")],
        );
        let args: Vec<_> = steps.iter().map(|s| s.commands[0].join(" ")).collect();
        assert_eq!(args, ["add --global eslint@10.13.0", "self-update 12.15.0"]);
    }
}