Skip to main content

zenith_cli/commands/plugin/
path_check.rs

1//! Non-invasive PATH reachability check for the `zenith` binary.
2//!
3//! The skill installed by `zenith plugin install` instructs AI agents to call
4//! `zenith` by name. If the binary is not resolvable on the user's `PATH`, the
5//! agent hits `command not found` with no guidance. These helpers detect that
6//! situation so the command can print a warning — they never modify `PATH` or
7//! any shell file.
8
9use std::ffi::OsStr;
10use std::path::PathBuf;
11
12/// Resolve `bin` on a `PATH`-style value, returning the first matching file.
13///
14/// `path_var` is the raw `PATH` value (e.g. from [`std::env::var_os`]); it is
15/// split with [`std::env::split_paths`], which honours the platform separator.
16/// For each directory we test `dir/<bin><EXE_SUFFIX>` and return the first entry
17/// that exists and is a file. A `None` / empty `PATH` resolves to `None`.
18pub fn resolve_on_path(path_var: Option<&OsStr>, bin: &str) -> Option<PathBuf> {
19    let path_var = path_var?;
20    let file_name = format!("{bin}{}", std::env::consts::EXE_SUFFIX);
21    for dir in std::env::split_paths(path_var) {
22        let candidate = dir.join(&file_name);
23        if candidate.is_file() {
24            return Some(candidate);
25        }
26    }
27    None
28}
29
30/// Resolve the `zenith` binary on the current process's `PATH`.
31pub fn zenith_on_path() -> Option<PathBuf> {
32    resolve_on_path(std::env::var_os("PATH").as_deref(), "zenith")
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use std::fs::File;
39
40    #[test]
41    fn resolves_binary_present_in_path_dir() {
42        let dir = tempfile::tempdir().unwrap();
43        let bin_name = format!("zenith{}", std::env::consts::EXE_SUFFIX);
44        let bin_path = dir.path().join(&bin_name);
45        File::create(&bin_path).unwrap();
46
47        let path_var = std::env::join_paths([dir.path()]).unwrap();
48        let found = resolve_on_path(Some(path_var.as_os_str()), "zenith");
49        assert_eq!(found.as_deref(), Some(bin_path.as_path()));
50    }
51
52    #[test]
53    fn none_when_path_missing() {
54        assert!(resolve_on_path(None, "zenith").is_none());
55    }
56
57    #[test]
58    fn none_when_path_empty() {
59        assert!(resolve_on_path(Some(OsStr::new("")), "zenith").is_none());
60    }
61
62    #[test]
63    fn none_when_dir_lacks_binary() {
64        let dir = tempfile::tempdir().unwrap();
65        let path_var = std::env::join_paths([dir.path()]).unwrap();
66        assert!(resolve_on_path(Some(path_var.as_os_str()), "zenith").is_none());
67    }
68}