oxios_kernel/
host_tools.rs1use std::collections::HashMap;
7use std::process::Command;
8
9pub struct HostToolValidator {
15 required: Vec<String>,
17 optional: Vec<String>,
19}
20
21impl Clone for HostToolValidator {
22 fn clone(&self) -> Self {
23 Self {
24 required: self.required.clone(),
25 optional: self.optional.clone(),
26 }
27 }
28}
29
30impl HostToolValidator {
31 pub fn new(required: Vec<String>, optional: Vec<String>) -> Self {
33 Self { required, optional }
34 }
35
36 pub fn validate_required(&self) -> Vec<String> {
40 self.required
41 .iter()
42 .filter(|tool| !Self::is_tool_available(tool))
43 .cloned()
44 .collect()
45 }
46
47 pub fn check_optional(&self) -> HashMap<String, bool> {
51 self.optional
52 .iter()
53 .map(|tool| (tool.clone(), Self::is_tool_available(tool)))
54 .collect()
55 }
56
57 pub fn full_check(&self) -> HostToolStatus {
61 let missing_required = self.validate_required();
62 let optional_available = self.check_optional();
63
64 HostToolStatus {
65 all_required_present: missing_required.is_empty(),
66 missing_required,
67 optional_available,
68 }
69 }
70
71 pub fn is_tool_available(tool: &str) -> bool {
73 Self::check_command(tool, &["--version"])
74 || Self::check_command(tool, &["-v"])
75 || Self::check_command(tool, &["version"])
76 }
77
78 fn check_command(cmd: &str, args: &[&str]) -> bool {
80 Command::new(cmd)
81 .args(args)
82 .output()
83 .map(|output| output.status.success())
84 .unwrap_or(false)
85 }
86
87 pub fn required_tools(&self) -> &[String] {
89 &self.required
90 }
91
92 pub fn optional_tools(&self) -> &[String] {
94 &self.optional
95 }
96}
97
98#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
100pub struct HostToolStatus {
101 pub all_required_present: bool,
103 pub missing_required: Vec<String>,
105 pub optional_available: HashMap<String, bool>,
107}
108
109pub mod common {
111 pub const REQUIRED: &[&str] = &["git"];
113
114 pub const OPTIONAL: &[&str] = &[
116 "gh", "remindctl", "shortcuts", "osascript", "open", "jq", "curl", "ripgrep", "sqlite3", ];
126
127 pub const CONTAINER_MINIMAL: &[&str] =
129 &["bash", "python3", "git", "curl", "jq", "ripgrep", "sqlite3"];
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
139 fn test_validate_required_all_present() {
140 let validator = HostToolValidator::new(vec!["echo".to_string()], Vec::new());
142
143 let missing = validator.validate_required();
144 assert!(missing.is_empty());
145 }
146
147 #[test]
148 fn test_validate_required_missing() {
149 let validator = HostToolValidator::new(
150 vec!["definitely-not-a-real-tool-12345".to_string()],
151 Vec::new(),
152 );
153
154 let missing = validator.validate_required();
155 assert_eq!(missing.len(), 1);
156 assert_eq!(missing[0], "definitely-not-a-real-tool-12345");
157 }
158
159 #[test]
160 fn test_validate_required_multiple_missing() {
161 let validator = HostToolValidator::new(
162 vec!["not-real-1".to_string(), "not-real-2".to_string()],
163 Vec::new(),
164 );
165
166 let missing = validator.validate_required();
167 assert_eq!(missing.len(), 2);
168 }
169
170 #[test]
171 fn test_check_optional() {
172 let validator = HostToolValidator::new(
173 Vec::new(),
174 vec!["echo".to_string(), "definitely-not-real".to_string()],
175 );
176
177 let results = validator.check_optional();
178 assert_eq!(results.len(), 2);
179 assert!(results["echo"]);
180 assert!(!results["definitely-not-real"]);
181 }
182
183 #[test]
184 fn test_is_tool_available() {
185 assert!(HostToolValidator::is_tool_available("echo"));
187 assert!(HostToolValidator::is_tool_available("ls"));
188 assert!(HostToolValidator::is_tool_available("cat"));
189 }
190
191 #[test]
192 fn test_is_tool_available_not_found() {
193 assert!(!HostToolValidator::is_tool_available(
194 "this-tool-definitely-does-not-exist-abc123"
195 ));
196 }
197
198 #[test]
199 fn test_full_check() {
200 let validator = HostToolValidator::new(vec!["echo".to_string()], vec!["cat".to_string()]);
201
202 let status = validator.full_check();
203 assert!(status.all_required_present);
204 assert!(status.missing_required.is_empty());
205 assert!(status.optional_available["cat"]);
206 }
207
208 #[test]
209 fn test_full_check_missing_required() {
210 let validator = HostToolValidator::new(
211 vec!["echo".to_string(), "not-real-xyz".to_string()],
212 Vec::new(),
213 );
214
215 let status = validator.full_check();
216 assert!(!status.all_required_present);
217 assert_eq!(status.missing_required.len(), 1);
218 }
219
220 #[test]
221 fn test_required_tools_accessors() {
222 let validator = HostToolValidator::new(
223 vec!["git".to_string(), "gh".to_string()],
224 vec!["jq".to_string()],
225 );
226
227 assert_eq!(validator.required_tools(), &["git", "gh"]);
228 assert_eq!(validator.optional_tools(), &["jq"]);
229 }
230
231 #[test]
234 fn test_common_tools_constants() {
235 assert!(!common::REQUIRED.is_empty());
236 assert!(common::REQUIRED.contains(&"git"));
237
238 assert!(!common::OPTIONAL.is_empty());
239 assert!(common::OPTIONAL.contains(&"gh"));
240 assert!(common::OPTIONAL.contains(&"jq"));
241 assert!(common::OPTIONAL.contains(&"curl"));
242
243 assert!(!common::CONTAINER_MINIMAL.is_empty());
244 assert!(common::CONTAINER_MINIMAL.contains(&"bash"));
245 assert!(common::CONTAINER_MINIMAL.contains(&"git"));
246 }
247
248 #[test]
251 fn test_host_tool_status_serialization() {
252 let status = HostToolStatus {
253 all_required_present: true,
254 missing_required: vec!["git".to_string()],
255 optional_available: HashMap::from([
256 ("jq".to_string(), true),
257 ("curl".to_string(), false),
258 ]),
259 };
260
261 let json = serde_json::to_string(&status).unwrap();
262 assert!(json.contains("all_required_present"));
263 assert!(json.contains("missing_required"));
264 assert!(json.contains("optional_available"));
265
266 let loaded: HostToolStatus = serde_json::from_str(&json).unwrap();
268 assert!(loaded.all_required_present);
269 assert_eq!(loaded.missing_required.len(), 1);
270 }
271}