1use anyhow::{Result, bail};
7use log::debug;
8use std::path::{Path, PathBuf};
9
10pub 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
22fn 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
36fn 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
49fn 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
66pub 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;