Skip to main content

microclaw_engine/internal/clawhub/
gate.rs

1use 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
10/// Check if skill requirements are met
11pub 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        // Check bins
20        for bin in &req.bins {
21            if !command_exists(bin) {
22                result.missing_bins.push(bin.clone());
23            }
24        }
25
26        // Check anyBins (at least one must exist)
27        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                // If anyBins is specified and no bins passed, check if we should warn
31            }
32        }
33
34        // Check env vars
35        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    // Check OS
43    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}