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}