Skip to main content

zag_agent/
preflight.rs

1//! CLI binary pre-flight validation.
2//!
3//! Checks that agent CLI binaries exist in PATH before attempting to spawn
4//! them, providing actionable error messages with install hints.
5
6use anyhow::{Result, bail};
7use log::debug;
8use std::path::{Path, PathBuf};
9
10/// Map an agent name to the binary it requires.
11pub fn binary_for_agent(agent: &str) -> &str {
12    match agent {
13        "claude" => "claude",
14        "codex" => "codex",
15        "gemini" => "gemini",
16        "copilot" => "copilot",
17        "ollama" => "ollama",
18        other => other,
19    }
20}
21
22/// Return a human-readable install hint for an agent.
23fn install_hint(agent: &str) -> &'static str {
24    match agent {
25        "claude" => "Install: npm install -g @anthropic-ai/claude-code",
26        "codex" => "Install: npm install -g @openai/codex",
27        "gemini" => "Install: npm install -g @anthropic-ai/gemini-cli",
28        "copilot" => {
29            "Install: npm install -g @github/copilot (see https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)"
30        }
31        "ollama" => "Install: https://ollama.ai/download",
32        _ => "Check that the CLI is installed and available in PATH",
33    }
34}
35
36/// Search for `binary_name` in the directories listed in the `PATH`
37/// environment variable. Returns the first match.
38fn find_in_path(binary_name: &str) -> Option<PathBuf> {
39    let path_var = std::env::var_os("PATH")?;
40    for dir in std::env::split_paths(&path_var) {
41        let candidate = dir.join(binary_name);
42        if is_executable(&candidate) {
43            return Some(candidate);
44        }
45    }
46    None
47}
48
49/// Check if a path points to an executable file.
50fn is_executable(path: &Path) -> bool {
51    #[cfg(unix)]
52    {
53        use std::os::unix::fs::PermissionsExt;
54        path.is_file()
55            && path
56                .metadata()
57                .map(|m| m.permissions().mode() & 0o111 != 0)
58                .unwrap_or(false)
59    }
60    #[cfg(not(unix))]
61    {
62        path.is_file()
63    }
64}
65
66/// Verify that the CLI binary for `agent_name` is available in PATH.
67///
68/// Returns the resolved path on success, or an actionable error with
69/// install instructions on failure.
70pub fn check_binary(agent_name: &str) -> Result<PathBuf> {
71    let binary = binary_for_agent(agent_name);
72    debug!("Preflight check: looking for '{}' in PATH", binary);
73
74    match find_in_path(binary) {
75        Some(path) => {
76            debug!("Found '{}' at {}", binary, path.display());
77            Ok(path)
78        }
79        None => {
80            bail!(
81                "'{}' CLI not found in PATH. {}\n\nEnsure '{}' is installed and available in your shell's PATH.",
82                binary,
83                install_hint(agent_name),
84                binary,
85            );
86        }
87    }
88}
89
90#[cfg(test)]
91#[path = "preflight_tests.rs"]
92mod tests;