cmdhub-cli 0.1.0

cmdh — the CmdHub CLI client for offline command search and execution
Documentation
use crate::config::Config;
use crate::os_detector::detect_os;
use cmdhub_shared::AciCommandContract;
use serde::Serialize;

#[derive(Serialize)]
pub struct UsageDto {
    pub cmd_path: String,
    pub example_template: Option<String>,
}

#[derive(Serialize)]
pub struct MinimalDto {
    pub cmd_path: String,
}

#[derive(Serialize)]
pub struct FullDto {
    pub app_id: String,
    pub name: String,
    pub cmd_path: String,
    pub node_type: String,
    pub description: String,
    pub risk_level: String,
    pub example_template: Option<String>,
    pub install_command: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub docker_image: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub script_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_url: Option<String>,
}

pub fn resolve_install_command(contract: &AciCommandContract, config: &Config) -> Option<String> {
    let instructions = contract.install_instructions.as_ref()?;

    let os = config.install.os.clone().or_else(detect_os);
    let sys_installer = os.as_ref().map(|o| map_os_to_package_manager(o));

    let mut resolved = None;
    if let Some(installer) = sys_installer {
        if let Some(cmd) = instructions.get_command(installer) {
            resolved = Some((cmd.clone(), is_system_installer(installer)));
        }
    }

    if resolved.is_none() {
        for pm in &config.install.package_managers {
            if let Some(cmd) = instructions.get_command(pm) {
                resolved = Some((cmd.clone(), is_system_installer(pm)));
                break;
            }
        }
    }

    if let Some((cmd, is_sys)) = resolved {
        if is_sys && !is_root_user() {
            Some(format!("sudo {}", cmd))
        } else {
            Some(cmd)
        }
    } else {
        None
    }
}

#[cfg(unix)]
fn is_root_user() -> bool {
    unsafe { libc::getuid() == 0 }
}

#[cfg(not(unix))]
fn is_root_user() -> bool {
    false
}

fn is_system_installer(installer: &str) -> bool {
    matches!(
        installer,
        "apt" | "pacman" | "dnf" | "apk" | "emerge" | "zypper" | "yum"
    )
}

fn map_os_to_package_manager(os: &str) -> &str {
    match os {
        "macos" => "brew",
        "windows" => "scoop",
        "arch" => "pacman",
        "ubuntu" | "debian" => "apt",
        "fedora" => "dnf",
        "centos" | "rhel" => "yum",
        "gentoo" => "emerge",
        "alpine" => "apk",
        "opensuse" => "zypper",
        "nixos" => "nix-env",
        other => other,
    }
}

pub fn format_results(
    contracts: Vec<AciCommandContract>,
    mode: &str,
    config: &Config,
) -> serde_json::Value {
    match mode {
        "usage" => {
            let dtos: Vec<UsageDto> = contracts
                .into_iter()
                .map(|c| UsageDto {
                    cmd_path: c.cmd_path,
                    example_template: c.example_template,
                })
                .collect();
            serde_json::to_value(&dtos).unwrap()
        }
        "minimal" => {
            let dtos: Vec<MinimalDto> = contracts
                .into_iter()
                .map(|c| MinimalDto {
                    cmd_path: c.cmd_path,
                })
                .collect();
            serde_json::to_value(&dtos).unwrap()
        }
        _ => {
            let dtos: Vec<FullDto> = contracts
                .into_iter()
                .map(|c| {
                    let install_command = resolve_install_command(&c, config);
                    FullDto {
                        app_id: c.app_id,
                        name: c.name,
                        cmd_path: c.cmd_path,
                        node_type: format!("{:?}", c.node_type).to_lowercase(),
                        description: c.description,
                        risk_level: format!("{:?}", c.risk_level).to_lowercase(),
                        example_template: c.example_template,
                        install_command,
                        docker_image: c.docker_image,
                        script_url: c.script_url,
                        source_url: c.source_url,
                    }
                })
                .collect();
            serde_json::to_value(&dtos).unwrap()
        }
    }
}

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

    #[test]
    fn test_resolve_install_command_sudo() {
        let contract = AciCommandContract {
            app_id: "test".to_string(),
            name: "test".to_string(),
            cmd_path: "test".to_string(),
            node_type: cmdhub_shared::NodeType::Root,
            description: "test".to_string(),
            risk_level: cmdhub_shared::RiskLevel::Safe,
            example_template: None,
            install_instructions: Some(InstallInstructions {
                brew: Some("brew install test".to_string()),
                apt: Some("apt-get install test".to_string()),
                pacman: None,
                cargo: None,
                scoop: Some("scoop install test".to_string()),
                others: std::collections::HashMap::new(),
            }),
            docker_image: None,
            script_url: None,
            source_url: None,
        };

        let mut config = Config::default();
        config.install.os = Some("debian".to_string());

        let cmd = resolve_install_command(&contract, &config).unwrap();
        if is_root_user() {
            assert_eq!(cmd, "apt-get install test");
        } else {
            assert_eq!(cmd, "sudo apt-get install test");
        }

        config.install.os = Some("macos".to_string());
        let cmd_brew = resolve_install_command(&contract, &config).unwrap();
        assert_eq!(cmd_brew, "brew install test");
    }
}