forte-cli 0.3.23

CLI for the Forte fullstack web framework
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;

fn tools_dir() -> Result<PathBuf> {
    let home = std::env::var("HOME").context("HOME not set")?;
    Ok(PathBuf::from(home).join(".forte").join("bin"))
}

fn target_triple() -> Result<&'static str> {
    match (std::env::consts::OS, std::env::consts::ARCH) {
        ("macos", "aarch64") => Ok("aarch64-apple-darwin"),
        ("macos", "x86_64") => Ok("x86_64-apple-darwin"),
        ("linux", "x86_64") => Ok("x86_64-unknown-linux-gnu"),
        ("linux", "aarch64") => Ok("aarch64-unknown-linux-gnu"),
        (os, arch) => anyhow::bail!("Unsupported platform: {os}-{arch}"),
    }
}

pub async fn ensure_github_tool(
    name: &str,
    version: &str,
    download_url: &str,
    binary_name: &str,
) -> Result<PathBuf> {
    let dir = tools_dir()?;
    let path = dir.join(format!("{}-{}", name, version));

    if path.exists() {
        return Ok(path);
    }

    std::fs::create_dir_all(&dir)?;

    println!("Downloading {} {}...", name, version);

    let response = reqwest::get(download_url).await?.error_for_status()?;
    let bytes = response.bytes().await?;

    let temp_archive = dir.join(format!("{}-temp.tar.xz", name));
    std::fs::write(&temp_archive, &bytes)?;

    let temp_extract = dir.join(format!("{}-temp_extract", name));
    std::fs::create_dir_all(&temp_extract)?;

    let status = Command::new("tar")
        .arg("xf")
        .arg(&temp_archive)
        .arg("-C")
        .arg(&temp_extract)
        .status()
        .context("Failed to run tar")?;

    std::fs::remove_file(&temp_archive)?;

    if !status.success() {
        let _ = std::fs::remove_dir_all(&temp_extract);
        anyhow::bail!("Failed to extract {} archive", name);
    }

    let extracted = find_binary(&temp_extract, binary_name)?;
    std::fs::rename(&extracted, &path)?;
    let _ = std::fs::remove_dir_all(&temp_extract);

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))?;
    }

    println!("{} {} ready.", name, version);
    Ok(path)
}

pub async fn ensure_github_tool_with_libs(
    name: &str,
    version: &str,
    download_url: &str,
    binary_name: &str,
) -> Result<PathBuf> {
    let dir = tools_dir()?;
    let install_dir = dir.join(format!("{}-{}", name, version));
    let binary_path = install_dir.join(binary_name);

    if binary_path.exists() {
        return Ok(binary_path);
    }

    std::fs::create_dir_all(&dir)?;

    println!("Downloading {} {}...", name, version);

    let response = reqwest::get(download_url).await?.error_for_status()?;
    let bytes = response.bytes().await?;

    let temp_archive = dir.join(format!("{}-temp.tar.xz", name));
    std::fs::write(&temp_archive, &bytes)?;

    let temp_extract = dir.join(format!("{}-temp_extract", name));
    std::fs::create_dir_all(&temp_extract)?;

    let status = Command::new("tar")
        .arg("xf")
        .arg(&temp_archive)
        .arg("-C")
        .arg(&temp_extract)
        .status()
        .context("Failed to run tar")?;

    std::fs::remove_file(&temp_archive)?;

    if !status.success() {
        let _ = std::fs::remove_dir_all(&temp_extract);
        anyhow::bail!("Failed to extract {} archive", name);
    }

    let extracted_binary = find_binary(&temp_extract, binary_name)?;
    let extracted_dir = extracted_binary.parent().unwrap();

    if install_dir.exists() {
        std::fs::remove_dir_all(&install_dir)?;
    }
    rename_dir(extracted_dir, &install_dir)?;
    let _ = std::fs::remove_dir_all(&temp_extract);

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&binary_path, std::fs::Permissions::from_mode(0o755))?;
    }

    println!("{} {} ready.", name, version);
    Ok(binary_path)
}

fn rename_dir(from: &Path, to: &Path) -> Result<()> {
    if std::fs::rename(from, to).is_ok() {
        return Ok(());
    }
    copy_dir_recursive(from, to)?;
    std::fs::remove_dir_all(from)?;
    Ok(())
}

fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
    std::fs::create_dir_all(dst)?;
    for entry in std::fs::read_dir(src)? {
        let entry = entry?;
        let src_path = entry.path();
        let dst_path = dst.join(entry.file_name());
        if src_path.is_dir() {
            copy_dir_recursive(&src_path, &dst_path)?;
        } else {
            std::fs::copy(&src_path, &dst_path)?;
        }
    }
    Ok(())
}

fn find_binary(dir: &Path, binary_name: &str) -> Result<PathBuf> {
    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            if let Ok(found) = find_binary(&path, binary_name) {
                return Ok(found);
            }
        } else if path.file_name().is_some_and(|n| n == binary_name) {
            return Ok(path);
        }
    }
    anyhow::bail!("{} binary not found in archive", binary_name)
}

pub fn fn0_release_url(package: &str, version: &str) -> Result<String> {
    let triple = target_triple()?;
    Ok(format!(
        "https://github.com/NamseEnt/fn0/releases/download/{package}-v{version}/{package}-{triple}.tar.xz"
    ))
}