Skip to main content

cmdhub_cli/
dto.rs

1use crate::config::Config;
2use crate::os_detector::detect_os;
3use cmdhub_shared::AciCommandContract;
4use serde::Serialize;
5
6#[derive(Serialize)]
7pub struct UsageDto {
8    pub cmd_path: String,
9    pub example_template: Option<String>,
10}
11
12#[derive(Serialize)]
13pub struct MinimalDto {
14    pub cmd_path: String,
15}
16
17#[derive(Serialize)]
18pub struct FullDto {
19    pub app_id: String,
20    pub name: String,
21    pub cmd_path: String,
22    pub node_type: String,
23    pub description: String,
24    pub risk_level: String,
25    pub example_template: Option<String>,
26    pub install_command: Option<String>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub docker_image: Option<String>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub script_url: Option<String>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub source_url: Option<String>,
33}
34
35pub fn resolve_install_command(contract: &AciCommandContract, config: &Config) -> Option<String> {
36    let instructions = contract.install_instructions.as_ref()?;
37
38    let os = config.install.os.clone().or_else(detect_os);
39    let sys_installer = os.as_ref().map(|o| map_os_to_package_manager(o));
40
41    let mut resolved = None;
42    if let Some(installer) = sys_installer {
43        if let Some(cmd) = instructions.get_command(installer) {
44            resolved = Some((cmd.clone(), is_system_installer(installer)));
45        }
46    }
47
48    if resolved.is_none() {
49        for pm in &config.install.package_managers {
50            if let Some(cmd) = instructions.get_command(pm) {
51                resolved = Some((cmd.clone(), is_system_installer(pm)));
52                break;
53            }
54        }
55    }
56
57    if let Some((cmd, is_sys)) = resolved {
58        if is_sys && !is_root_user() {
59            Some(format!("sudo {}", cmd))
60        } else {
61            Some(cmd)
62        }
63    } else {
64        None
65    }
66}
67
68#[cfg(unix)]
69fn is_root_user() -> bool {
70    unsafe { libc::getuid() == 0 }
71}
72
73#[cfg(not(unix))]
74fn is_root_user() -> bool {
75    false
76}
77
78fn is_system_installer(installer: &str) -> bool {
79    matches!(
80        installer,
81        "apt" | "pacman" | "dnf" | "apk" | "emerge" | "zypper" | "yum"
82    )
83}
84
85fn map_os_to_package_manager(os: &str) -> &str {
86    match os {
87        "macos" => "brew",
88        "windows" => "scoop",
89        "arch" => "pacman",
90        "ubuntu" | "debian" => "apt",
91        "fedora" => "dnf",
92        "centos" | "rhel" => "yum",
93        "gentoo" => "emerge",
94        "alpine" => "apk",
95        "opensuse" => "zypper",
96        "nixos" => "nix-env",
97        other => other,
98    }
99}
100
101pub fn format_results(
102    contracts: Vec<AciCommandContract>,
103    mode: &str,
104    config: &Config,
105) -> serde_json::Value {
106    match mode {
107        "usage" => {
108            let dtos: Vec<UsageDto> = contracts
109                .into_iter()
110                .map(|c| UsageDto {
111                    cmd_path: c.cmd_path,
112                    example_template: c.example_template,
113                })
114                .collect();
115            serde_json::to_value(&dtos).unwrap()
116        }
117        "minimal" => {
118            let dtos: Vec<MinimalDto> = contracts
119                .into_iter()
120                .map(|c| MinimalDto {
121                    cmd_path: c.cmd_path,
122                })
123                .collect();
124            serde_json::to_value(&dtos).unwrap()
125        }
126        _ => {
127            let dtos: Vec<FullDto> = contracts
128                .into_iter()
129                .map(|c| {
130                    let install_command = resolve_install_command(&c, config);
131                    FullDto {
132                        app_id: c.app_id,
133                        name: c.name,
134                        cmd_path: c.cmd_path,
135                        node_type: format!("{:?}", c.node_type).to_lowercase(),
136                        description: c.description,
137                        risk_level: format!("{:?}", c.risk_level).to_lowercase(),
138                        example_template: c.example_template,
139                        install_command,
140                        docker_image: c.docker_image,
141                        script_url: c.script_url,
142                        source_url: c.source_url,
143                    }
144                })
145                .collect();
146            serde_json::to_value(&dtos).unwrap()
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use cmdhub_shared::InstallInstructions;
155
156    #[test]
157    fn test_resolve_install_command_sudo() {
158        let contract = AciCommandContract {
159            app_id: "test".to_string(),
160            name: "test".to_string(),
161            cmd_path: "test".to_string(),
162            node_type: cmdhub_shared::NodeType::Root,
163            description: "test".to_string(),
164            risk_level: cmdhub_shared::RiskLevel::Safe,
165            example_template: None,
166            install_instructions: Some(InstallInstructions {
167                brew: Some("brew install test".to_string()),
168                apt: Some("apt-get install test".to_string()),
169                pacman: None,
170                cargo: None,
171                scoop: Some("scoop install test".to_string()),
172                others: std::collections::HashMap::new(),
173            }),
174            docker_image: None,
175            script_url: None,
176            source_url: None,
177        };
178
179        let mut config = Config::default();
180        config.install.os = Some("debian".to_string());
181
182        let cmd = resolve_install_command(&contract, &config).unwrap();
183        if is_root_user() {
184            assert_eq!(cmd, "apt-get install test");
185        } else {
186            assert_eq!(cmd, "sudo apt-get install test");
187        }
188
189        config.install.os = Some("macos".to_string());
190        let cmd_brew = resolve_install_command(&contract, &config).unwrap();
191        assert_eq!(cmd_brew, "brew install test");
192    }
193}