hyper-jump 0.11.0

A generalist all-in-one command-line version manager toolset for a more efficient development workflow.
use std::path::Path;

use anyhow::anyhow;
use anyhow::Result;
use tracing::info;

use crate::ports::Env;
use crate::ports::Fs;
use crate::ports::Paths;
use crate::ports::Process;
use crate::ports::ProxyInstaller;

#[derive(Clone)]
pub struct ProxyFsCopier<P, E, F, Proc> {
    paths: P,
    env: E,
    fs: F,
    process: Proc,
}

impl<P, E, F, Proc> ProxyFsCopier<P, E, F, Proc> {
    pub fn new(paths: P, env: E, fs: F, process: Proc) -> Self {
        Self {
            paths,
            env,
            fs,
            process,
        }
    }
}

impl<P, E, F, Proc> ProxyInstaller for ProxyFsCopier<P, E, F, Proc>
where
    P: Paths + Clone + Send + Sync,
    E: Env + Clone + Send + Sync,
    F: Fs + Clone + Send + Sync,
    Proc: Process + Clone + Send + Sync,
{
    async fn ensure_proxy(&self, alias: &str) -> anyhow::Result<()> {
        let paths = self.paths.clone();
        let env = self.env.clone();
        let fs = self.fs.clone();
        let process = self.process.clone();
        copy_package_proxy(&paths, &env, &fs, &process, alias).await
    }
}

async fn copy_package_proxy(
    paths: &impl Paths,
    env: &impl Env,
    fs: &impl Fs,
    process: &impl Process,
    alias: &str,
) -> Result<()> {
    let exe_path = env.current_exe()?;
    let mut installation_dir = paths.installation_dir().await?;

    if !fs.exists(&installation_dir).await? {
        fs.ensure_dir(&installation_dir).await?;
    }

    add_to_path(env, &installation_dir)?;

    installation_dir.push(alias);
    let proxy_version = read_proxy_version(process, alias).await?;
    if matches!(proxy_version, Some(version) if version == env!("CARGO_PKG_VERSION")) {
        return Ok(());
    }

    fs.copy(&exe_path, &installation_dir)
        .await
        .map_err(|_| anyhow!("Could not copy the proxy"))?;

    Ok(())
}

fn add_to_path(env: &impl Env, installation_dir: &Path) -> Result<()> {
    let installation_dir = installation_dir.to_str().unwrap();

    let path_var = env.path_var().unwrap_or_default();
    if !path_var.contains("bin") {
        info!("Make sure to have {installation_dir} in PATH");
    }

    Ok(())
}

async fn read_proxy_version(process: &impl Process, alias: &str) -> Result<Option<String>> {
    let version_arg = format!("--{}", env!("CARGO_BIN_NAME"));
    let output = match process.output(Path::new(alias), &[version_arg]).await {
        Ok(output) => output,
        Err(_) => return Ok(None),
    };

    let version = String::from_utf8(output)?.trim().to_string();
    if version.is_empty() {
        Ok(None)
    } else {
        Ok(Some(version))
    }
}