zoi-cli 1.24.2

Advanced Package Manager & Environment Orchestrator
Documentation
/// Implements "Direct Repository Installation" (`zoi install --repo`).
///
/// This allows Zoi to install a package directly from a Git repository
/// that contains a `zoi.yaml` or `zoi.lua` project file. It:
/// - Detects the Git provider (GitHub, GitLab, Codeberg).
/// - Fetches the project configuration over HTTP (RAW URL).
/// - Resolves and installs the specific package defined in the project.
use crate::pkg::types;
use anyhow::{Result, anyhow};
use colored::*;
use serde::Deserialize;
use std::env;
use std::fs;
use zoi_core::types::SourceType;

#[derive(Debug, Deserialize)]
struct RepoFile {
    package: String,
}

pub fn run(
    repo_spec: &str,
    force: bool,
    all_optional: bool,
    yes: bool,
    scope: Option<crate::cli::SetupScope>,
    plugin_manager: Option<&crate::pkg::plugin::PluginManager>,
) -> Result<()> {
    println!(
        "Installing from git repository: {}",
        repo_spec.cyan().bold()
    );

    let (provider, repo_path) = parse_repo_spec(repo_spec)?;

    crate::pkg::utils::confirm_untrusted_source(&SourceType::GitRepo(repo_spec.to_string()), yes)?;

    let repo_file_names = ["zoi.yaml"];
    let mut repo_file_content: Option<String> = None;
    let mut used_url = String::new();

    for file_name in &repo_file_names {
        if let Ok(url) = get_repo_file_url(&provider, &repo_path, file_name) {
            println!("Attempting to fetch repo config from: {}", url);
            let client = crate::pkg::utils::get_http_client().ok();
            if let Some(c) = client
                && let Ok(content_res) = c.get(&url).send()
                && content_res.status().is_success()
            {
                repo_file_content = Some(content_res.text()?);
                used_url = url;
                break;
            }
        }
    }

    let repo_file_content = repo_file_content.ok_or_else(|| {
        anyhow!("Could not find zoi.yaml in the repository on main/master branches.")
    })?;
    println!("Using repo config from: {}", used_url.cyan());

    let repo_file: RepoFile = serde_yaml::from_str(&repo_file_content)?;

    let package_source = &repo_file.package;

    let scope_override = scope.map(|s| match s {
        crate::cli::SetupScope::User => types::Scope::User,
        crate::cli::SetupScope::System => types::Scope::System,
    });

    println!("Starting installation of package from git repo...");

    let source_to_install = if package_source.starts_with("http") {
        println!("Package source is a URL: {}", package_source.cyan());
        let client = crate::pkg::utils::get_http_client()?;
        let pkg_content = client.get(package_source).send()?.text()?;
        let temp_path = env::temp_dir().join(format!(
            "zoi-repo-install-{}.pkg.lua",
            chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
        ));
        fs::write(&temp_path, pkg_content)?;
        temp_path
            .to_str()
            .ok_or_else(|| anyhow!("Temporary path contains invalid UTF-8"))?
            .to_string()
    } else if package_source.ends_with(".pkg.lua")
        || (package_source.contains('/') && !package_source.starts_with('@'))
    {
        println!(
            "Package source is a path in the repo: {}",
            package_source.cyan()
        );
        let pkg_url = get_repo_file_url(&provider, &repo_path, package_source)?;
        let client = crate::pkg::utils::get_http_client()?;
        let pkg_content = client.get(&pkg_url).send()?.text()?;
        let temp_path = env::temp_dir().join(format!(
            "zoi-repo-install-{}.pkg.lua",
            chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
        ));
        fs::write(&temp_path, pkg_content)?;
        temp_path
            .to_str()
            .ok_or_else(|| anyhow!("Temporary path contains invalid UTF-8"))?
            .to_string()
    } else {
        println!(
            "Package source is a package name: {}",
            package_source.cyan()
        );
        package_source.to_string()
    };

    crate::cmd::install::run(
        &[source_to_install],
        None,
        force,
        all_optional,
        yes,
        scope_override.map(|s| match s {
            types::Scope::User => crate::cli::InstallScope::User,
            types::Scope::System => crate::cli::InstallScope::System,
            types::Scope::Project => crate::cli::InstallScope::Project,
        }),
        false,
        false,
        false,
        None,
        false,
        plugin_manager,
        false,
        false,
        false,
        false,
        3,
        false,
        false,
        None,
    )?;

    Ok(())
}

fn parse_repo_spec(spec: &str) -> Result<(String, String)> {
    if let Some((provider_alias, path)) = spec.split_once(':') {
        let provider = match provider_alias {
            "gh" | "github" => "github",
            "gl" | "gitlab" => "gitlab",
            "cb" | "codeberg" => "codeberg",
            _ => return Err(anyhow!("Unknown provider alias: {}", provider_alias)),
        };
        Ok((provider.to_string(), path.to_string()))
    } else {
        Ok(("github".to_string(), spec.to_string()))
    }
}

fn get_repo_file_url(provider: &str, repo_path: &str, file_path: &str) -> Result<String> {
    let branches = ["main", "master"];
    let client = crate::pkg::utils::get_http_client()?;
    for branch in &branches {
        let url = match provider {
            "github" => format!(
                "https://raw.githubusercontent.com/{}/refs/heads/{}/{}",
                repo_path, branch, file_path
            ),
            "gitlab" => format!(
                "https://gitlab.com/{}/-/raw/{}/{}",
                repo_path, branch, file_path
            ),
            "codeberg" => format!(
                "https://codeberg.org/{}/raw/branch/{}/{}",
                repo_path, branch, file_path
            ),
            _ => return Err(anyhow!("Unknown provider")),
        };

        let res = client.get(&url).send();
        if let Ok(response) = res
            && response.status().is_success()
        {
            return Ok(url);
        }
    }
    Err(anyhow!(
        "Could not find '{}' in repo '{}' on branches main or master.",
        file_path,
        repo_path
    ))
}