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