microclaw_engine/internal/clawhub/
gate.rs1use crate::internal::clawhub::types::Requires;
2use std::env;
3
4pub struct GateCheckResult {
5 pub missing_bins: Vec<String>,
6 pub missing_envs: Vec<String>,
7 pub wrong_os: bool,
8}
9
10pub fn check_requirements(requires: &Option<Requires>, os: &[String]) -> GateCheckResult {
12 let mut result = GateCheckResult {
13 missing_bins: vec![],
14 missing_envs: vec![],
15 wrong_os: false,
16 };
17
18 if let Some(req) = requires {
19 for bin in &req.bins {
21 if !command_exists(bin) {
22 result.missing_bins.push(bin.clone());
23 }
24 }
25
26 if !req.any_bins.is_empty() {
28 let any_exists = req.any_bins.iter().any(|b| command_exists(b));
29 if !any_exists && !req.bins.is_empty() {
30 }
32 }
33
34 for env_var in &req.env {
36 if env::var(env_var).is_err() {
37 result.missing_envs.push(env_var.clone());
38 }
39 }
40 }
41
42 if !os.is_empty() {
44 let current_os = current_platform();
45 let os_matches = os.iter().any(|o| {
46 let o = o.to_lowercase();
47 o == "all" || o == "*" || normalize_platform(&o) == current_os
48 });
49 result.wrong_os = !os_matches;
50 }
51
52 result
53}
54
55fn current_platform() -> String {
56 #[cfg(target_os = "macos")]
57 return "darwin".to_string();
58 #[cfg(target_os = "linux")]
59 return "linux".to_string();
60 #[cfg(target_os = "windows")]
61 return "windows".to_string();
62 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
63 return "unknown".to_string();
64}
65
66fn normalize_platform(s: &str) -> String {
67 let s = s.trim().to_lowercase();
68 match s.as_str() {
69 "macos" | "osx" => "darwin".to_string(),
70 _ => s,
71 }
72}
73
74fn command_exists(cmd: &str) -> bool {
75 if cmd.trim().is_empty() {
76 return true;
77 }
78 let path_var = env::var_os("PATH").unwrap_or_default();
79 let paths = env::split_paths(&path_var);
80
81 #[cfg(target_os = "windows")]
82 let candidates: Vec<String> = {
83 let exts = env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into());
84 let ext_list: Vec<String> = exts
85 .split(';')
86 .map(|s| s.trim().to_ascii_lowercase())
87 .filter(|s| !s.is_empty())
88 .collect();
89 let lower = cmd.to_ascii_lowercase();
90 if ext_list.iter().any(|ext| lower.ends_with(ext)) {
91 vec![cmd.to_string()]
92 } else {
93 let mut c = vec![cmd.to_string()];
94 for ext in ext_list {
95 c.push(format!("{}{}", cmd, ext));
96 }
97 c
98 }
99 };
100
101 #[cfg(not(target_os = "windows"))]
102 let candidates: Vec<String> = vec![cmd.to_string()];
103
104 for base in paths {
105 for candidate in &candidates {
106 let full = base.join(candidate);
107 if full.is_file() {
108 return true;
109 }
110 }
111 }
112 false
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_current_platform() {
121 let platform = current_platform();
122 assert!(!platform.is_empty());
123 }
124
125 #[test]
126 fn test_normalize_platform() {
127 assert_eq!(normalize_platform("darwin"), "darwin");
128 assert_eq!(normalize_platform("macOS"), "darwin");
129 assert_eq!(normalize_platform("osx"), "darwin");
130 }
131
132 #[test]
133 fn test_command_exists() {
134 #[cfg(target_os = "windows")]
135 assert!(command_exists("cmd"));
136
137 #[cfg(not(target_os = "windows"))]
138 assert!(command_exists("sh"));
139 }
140
141 #[test]
142 fn test_check_requirements_empty() {
143 let result = check_requirements(&None, &[]);
144 assert!(result.missing_bins.is_empty());
145 assert!(result.missing_envs.is_empty());
146 assert!(!result.wrong_os);
147 }
148}