use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use cli_stream::hidden_command;
pub fn probe_version(program: &str) -> Option<String> {
let output = hidden_command(program).arg("--version").env("PATH", augmented_path()).output().ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout).trim().to_owned();
(!text.is_empty()).then_some(text)
}
fn augment_path_for_program(program: &Path) -> String {
prepend_program_dir(program, &augmented_path())
}
pub fn resolve_program(program: PathBuf) -> PathBuf {
if program.parent().is_some_and(|p| !p.as_os_str().is_empty()) {
return program; }
resolve_on_path(&program, &augmented_path()).unwrap_or(program)
}
fn resolve_on_path(name: &Path, path_env: &str) -> Option<PathBuf> {
let extensions = split_extensions(&pathext());
std::env::split_paths(path_env)
.filter(|dir| !dir.as_os_str().is_empty())
.flat_map(|dir| {
let base = dir.join(name);
let mut candidates = vec![base.clone()];
for extension in &extensions {
let mut with_extension = base.clone().into_os_string();
with_extension.push(extension);
candidates.push(PathBuf::from(with_extension));
}
candidates
})
.find(|candidate| is_executable_file(candidate))
}
#[cfg(unix)]
fn pathext() -> String {
String::new()
}
#[cfg(not(unix))]
fn pathext() -> String {
std::env::var("PATHEXT").unwrap_or_else(|_| ".EXE;.CMD;.BAT;.COM".to_owned())
}
fn split_extensions(pathext: &str) -> Vec<String> {
pathext
.split(';')
.filter(|extension| !extension.is_empty())
.map(str::to_owned)
.collect()
}
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
fn prepend_program_dir(program: &Path, base_path: &str) -> String {
let Some(dir) = program.parent().filter(|dir| dir.is_absolute()) else {
return base_path.to_owned();
};
let entries = std::iter::once(dir.to_path_buf()).chain(std::env::split_paths(base_path));
std::env::join_paths(entries)
.map_or_else(|_| base_path.to_owned(), |joined| joined.to_string_lossy().into_owned())
}
pub fn augmented_path() -> String {
static CACHED: OnceLock<String> = OnceLock::new();
CACHED.get_or_init(compute_augmented_path).clone()
}
fn compute_augmented_path() -> String {
let discovered = login_shell_path().unwrap_or_else(hardcoded_node_dirs);
compose_augmented_path(std::env::var("PATH").ok(), discovered)
}
fn compose_augmented_path(process_path: Option<String>, discovered: String) -> String {
let mut entries: Vec<PathBuf> = Vec::new();
if let Some(existing) = process_path.filter(|path| !path.is_empty()) {
entries.extend(std::env::split_paths(&existing));
}
entries.extend(std::env::split_paths(&discovered));
let joined = std::env::join_paths(entries)
.map(|value| value.to_string_lossy().into_owned())
.unwrap_or_default();
keep_absolute_entries(&joined)
}
fn keep_absolute_entries(path: &str) -> String {
let absolute: Vec<PathBuf> =
std::env::split_paths(path).filter(|entry| entry.is_absolute()).collect();
std::env::join_paths(absolute)
.map(|joined| joined.to_string_lossy().into_owned())
.unwrap_or_default()
}
#[cfg(unix)]
mod login_shell {
use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
const PATH_SENTINEL: &str = "__CLI_STREAM_PATH__";
pub(super) fn query() -> Option<String> {
let shell = std::env::var("SHELL").ok().filter(|s| !s.is_empty())?;
let script = format!("printf '\\n{PATH_SENTINEL}\\n'; env");
let mut child = Command::new(&shell)
.arg("-lic") .arg(&script)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let mut stdout = child.stdout.take()?;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let mut buf = Vec::new();
let _ = stdout.read_to_end(&mut buf);
let _ = tx.send(String::from_utf8_lossy(&buf).into_owned());
});
let output = match rx.recv_timeout(Duration::from_secs(4)) {
Ok(buf) => buf,
Err(_) => {
let _ = child.kill();
let _ = child.wait();
return None;
}
};
let _ = child.wait();
parse_path_from_shell_output(&output)
}
pub(super) fn parse_path_from_shell_output(output: &str) -> Option<String> {
output
.rsplit_once(PATH_SENTINEL)?
.1
.lines()
.find_map(|line| line.strip_prefix("PATH="))
.map(str::trim)
.filter(|p| !p.is_empty())
.map(str::to_owned)
}
}
fn login_shell_path() -> Option<String> {
#[cfg(unix)]
{
login_shell::query()
}
#[cfg(not(unix))]
{
None
}
}
fn hardcoded_node_dirs() -> String {
if cfg!(windows) {
return String::new();
}
let mut parts: Vec<String> =
vec!["/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin".to_owned()];
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
let home_path = Path::new(&home);
parts.push(home_path.join(".local/bin").display().to_string());
if let Ok(entries) = std::fs::read_dir(home_path.join(".nvm/versions/node")) {
for entry in entries.flatten() {
let bin = entry.path().join("bin");
if bin.is_dir() {
parts.push(bin.display().to_string());
}
}
}
}
}
parts.join(":")
}
pub trait ResolveCli {
#[must_use]
fn resolve_cli(self) -> Self;
}
impl ResolveCli for cli_stream::Command {
fn resolve_cli(self) -> Self {
let program = resolve_program(self.program);
let mut env = vec![("PATH".to_owned(), augment_path_for_program(&program))];
env.extend(self.env);
cli_stream::Command { program, env, ..self }
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn path_entry() -> impl Strategy<Value = String> {
prop_oneof![
4 => "/(usr|opt|home)(/[a-z]{1,6}){0,3}",
1 => "[a-z]{1,6}(/[a-z]{1,6}){0,2}",
1 => Just(".".to_owned()),
1 => Just(String::new()),
]
}
fn path_string() -> impl Strategy<Value = String> {
prop::collection::vec(path_entry(), 0..8).prop_map(|entries| entries.join(":"))
}
fn entries(path: &str) -> Vec<String> {
std::env::split_paths(path).map(|e| e.to_string_lossy().into_owned()).collect()
}
#[test]
fn a_real_path_survives_composition() {
let (process_dir, discovered_dir) = if cfg!(windows) {
(r"C:\Windows\System32", r"C:\tools\bin")
} else {
("/usr/bin", "/opt/tools/bin")
};
let join = |dir: &str| {
std::env::join_paths([dir]).expect("joinable").into_string().expect("utf-8")
};
let composed = compose_augmented_path(Some(join(process_dir)), join(discovered_dir));
let kept: Vec<PathBuf> = std::env::split_paths(&composed).collect();
assert_eq!(
kept.first(),
Some(&PathBuf::from(process_dir)),
"the process PATH must lead: {composed}"
);
assert!(
kept.contains(&PathBuf::from(discovered_dir)),
"the discovered PATH must survive: {composed}"
);
}
proptest! {
#[test]
fn no_entry_that_resolves_against_the_cwd_survives(path in path_string()) {
let kept = keep_absolute_entries(&path);
if kept.is_empty() {
return Ok(());
}
for entry in entries(&kept) {
prop_assert!(Path::new(&entry).is_absolute(), "{entry:?} is not absolute");
}
}
#[test]
fn every_absolute_directory_survives(path in path_string()) {
let kept = keep_absolute_entries(&path);
let survivors = entries(&kept);
for entry in entries(&path).into_iter().filter(|e| Path::new(e).is_absolute()) {
prop_assert!(survivors.contains(&entry), "dropped {entry:?}");
}
}
#[test]
fn filtering_never_invents_an_entry(path in path_string()) {
let kept = keep_absolute_entries(&path);
if kept.is_empty() {
return Ok(());
}
let original = entries(&path);
for entry in entries(&kept) {
prop_assert!(original.contains(&entry), "{entry:?} was not in the input");
}
}
#[test]
fn prepending_cannot_reintroduce_a_cwd_relative_entry(
program in "([a-z]{1,6}/){0,3}[a-z]{1,6}",
base in path_string(),
) {
let base = keep_absolute_entries(&base);
let combined = prepend_program_dir(Path::new(&program), &base);
if combined.is_empty() {
return Ok(());
}
for entry in entries(&combined) {
prop_assert!(Path::new(&entry).is_absolute(), "{entry:?} is not absolute");
}
}
#[test]
fn prepending_keeps_every_directory_it_was_given(base in path_string()) {
let base = keep_absolute_entries(&base);
let combined = prepend_program_dir(Path::new("/opt/tool/bin/claude"), &base);
for entry in entries(&base).into_iter().filter(|entry| !entry.is_empty()) {
prop_assert!(entries(&combined).contains(&entry), "lost {entry:?}");
}
}
}
#[cfg(unix)]
fn fake_cli(tag: &str, script: &str) -> std::path::PathBuf {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("cs-probe-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("cli");
std::fs::write(&path, format!("#!/bin/sh\n{script}\n")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[cfg(unix)]
#[test]
fn a_version_is_only_reported_when_the_cli_actually_gave_one() {
let ok = fake_cli("version", "echo '1.2.3 (Some CLI)'");
assert_eq!(probe_version(ok.to_str().unwrap()).as_deref(), Some("1.2.3 (Some CLI)"));
let blank = fake_cli("blank", "exit 0");
assert_eq!(probe_version(blank.to_str().unwrap()), None, "no version is not a version");
let broken = fake_cli("broken", "echo 9.9.9; exit 3");
assert_eq!(probe_version(broken.to_str().unwrap()), None, "a failed probe is not installed");
assert_eq!(probe_version("definitely-not-a-real-binary-xyz"), None, "and neither is an absent one");
}
#[cfg(unix)]
#[test]
fn hardcoded_fallback_includes_macos_defaults() {
let path = hardcoded_node_dirs();
assert!(
path.contains("/opt/homebrew/bin"),
"missing Apple-Silicon Homebrew bin"
);
assert!(
path.contains("/usr/local/bin"),
"missing Intel Homebrew / system bin"
);
assert!(path.contains("/usr/bin"), "missing system bin");
}
#[cfg(unix)]
#[test]
fn parse_path_from_shell_output_skips_chatter_before_the_sentinel() {
use super::login_shell::parse_path_from_shell_output;
let output = "\u{1b}]1337;RemoteHost=x\u{7}welcome banner\nPATH=/decoy\n__CLI_STREAM_PATH__\nHOME=/Users/x\nPATH=/opt/homebrew/bin:/usr/bin\nLANG=en_US";
assert_eq!(
parse_path_from_shell_output(output).as_deref(),
Some("/opt/homebrew/bin:/usr/bin")
);
assert_eq!(parse_path_from_shell_output("PATH=/usr/bin"), None);
assert_eq!(
parse_path_from_shell_output("__CLI_STREAM_PATH__\nFOO=bar"),
None
);
assert_eq!(
parse_path_from_shell_output("__CLI_STREAM_PATH__\nPATH=\nFOO=bar"),
None
);
}
#[test]
fn keep_absolute_entries_drops_relative_and_empty() {
let (a, b, c) = if cfg!(windows) {
(r"C:\tools\bin", r"C:\Windows\System32", r"C:\Windows")
} else {
("/opt/homebrew/bin", "/usr/bin", "/bin")
};
let mixed = [a, "node_modules/.bin", b, ".", "", c].join(&sep().to_string());
let expected = [a, b, c].join(&sep().to_string());
assert_eq!(keep_absolute_entries(&mixed), expected);
assert_eq!(keep_absolute_entries(b), b);
let relative_only = [".", "rel", ""].join(&sep().to_string());
assert_eq!(keep_absolute_entries(&relative_only), "");
}
#[test]
fn pathext_becomes_suffixes_with_the_empty_ones_dropped() {
assert_eq!(
split_extensions(".EXE;.CMD;;.BAT;"),
[".EXE", ".CMD", ".BAT"],
);
assert!(split_extensions("").is_empty());
}
#[test]
fn prepend_program_dir_puts_the_binary_dir_first() {
let bin = if cfg!(windows) { r"C:\tools\bin" } else { "/opt/tools/bin" };
let other = if cfg!(windows) { r"C:\Windows\System32" } else { "/usr/bin" };
let base = std::env::join_paths([other]).expect("joinable").into_string().expect("utf-8");
let combined = prepend_program_dir(&Path::new(bin).join("bob"), &base);
let entries: Vec<PathBuf> = std::env::split_paths(&combined).collect();
assert_eq!(entries.first(), Some(&PathBuf::from(bin)), "the binary's dir leads: {combined}");
assert!(entries.contains(&PathBuf::from(other)), "and the base survives: {combined}");
assert_eq!(prepend_program_dir(Path::new("bob"), &base), base);
}
#[cfg(unix)]
#[test]
fn only_a_runnable_file_counts_as_the_program() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("hl-exec-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let runnable = dir.join("runnable");
std::fs::write(&runnable, "#!/bin/sh\n").unwrap();
std::fs::set_permissions(&runnable, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(is_executable_file(&runnable));
let plain = dir.join("plain.txt");
std::fs::write(&plain, "not a program").unwrap();
assert!(!is_executable_file(&plain), "a readable file is not a runnable one");
assert!(!is_executable_file(&dir), "a directory is not a program");
assert!(!is_executable_file(&dir.join("absent")), "and neither is nothing");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_bare_name_is_resolved_to_the_binary_it_will_actually_run() {
let name = if cfg!(windows) { "cmd" } else { "sh" };
let resolved = resolve_program(PathBuf::from(name));
assert!(resolved.is_absolute(), "a name on PATH resolves to its real location: {resolved:?}");
assert!(
resolved.file_stem().is_some_and(|stem| stem.eq_ignore_ascii_case(name)),
"and to the right binary: {resolved:?}"
);
let unknown = PathBuf::from("definitely-not-a-real-binary-xyz");
assert_eq!(
resolve_program(unknown.clone()),
unknown,
"an unresolvable name is left alone so the spawn reports the real error"
);
}
#[test]
fn the_augmented_path_extends_the_one_we_already_have() {
let existing = std::env::var("PATH").expect("a test process has a PATH");
let augmented = compute_augmented_path();
let first = std::env::split_paths(&existing)
.find(|entry| entry.is_absolute())
.expect("an absolute entry");
let kept = std::env::split_paths(&augmented).any(|entry| entry == first);
assert!(kept, "{first:?} must survive into {augmented}");
}
#[test]
fn the_process_path_leads_and_an_absent_one_contributes_nothing() {
let (host, found) = if cfg!(windows) {
(r"C:\host\bin", r"C:\found\bin")
} else {
("/host/bin", "/found/bin")
};
assert_eq!(
compose_augmented_path(Some(host.to_owned()), found.to_owned()),
[host, found].join(&sep().to_string()),
);
assert_eq!(compose_augmented_path(None, found.to_owned()), found);
assert_eq!(compose_augmented_path(Some(String::new()), found.to_owned()), found);
}
fn sep() -> char {
if cfg!(windows) {
';'
} else {
':'
}
}
#[cfg(unix)]
#[test]
fn the_fallback_looks_where_agent_clis_are_actually_installed() {
let dirs = hardcoded_node_dirs();
assert!(dirs.contains("/usr/local/bin") && dirs.contains("/opt/homebrew/bin"));
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
assert!(
dirs.contains(&format!("{home}/.local/bin")),
"the official-installer location is where several agent CLIs land: {dirs}"
);
}
}
}
#[test]
fn augmented_path_is_nonempty_and_usable() {
let path = augmented_path();
assert!(!path.is_empty(), "an empty PATH finds nothing at all");
let usable = std::env::split_paths(&path).any(|entry| entry.is_dir());
assert!(usable, "no entry of the augmented PATH is a real directory: {path}");
}
#[test]
fn resolve_program_returns_explicit_paths_untouched() {
let explicit = PathBuf::from("/opt/somewhere/bob");
assert_eq!(resolve_program(explicit.clone()), explicit);
let relative = PathBuf::from("./bin/bob");
assert_eq!(resolve_program(relative.clone()), relative);
}
#[cfg(unix)]
#[test]
fn resolve_on_path_finds_the_first_executable_match() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("tempdir");
let dir_a = root.path().join("a");
let dir_b = root.path().join("b");
std::fs::create_dir_all(&dir_a).unwrap();
std::fs::create_dir_all(&dir_b).unwrap();
std::fs::write(dir_a.join("bob"), "#!/bin/sh\n").unwrap();
let exec = dir_b.join("bob");
std::fs::write(&exec, "#!/bin/sh\n").unwrap();
std::fs::set_permissions(&exec, std::fs::Permissions::from_mode(0o755)).unwrap();
let path_env = format!("{}:{}", dir_a.display(), dir_b.display());
assert_eq!(resolve_on_path(Path::new("bob"), &path_env), Some(exec));
assert_eq!(
resolve_on_path(Path::new("definitely-missing"), &path_env),
None
);
}
fn install_runnable(dir: &Path, stem: &str) -> PathBuf {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let path = dir.join(stem);
std::fs::write(&path, "#!/bin/sh\n").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[cfg(not(unix))]
{
let path = dir.join(format!("{stem}.EXE"));
std::fs::write(&path, "").unwrap();
path
}
}
#[cfg(unix)]
#[test]
fn unix_does_not_invent_a_suffix_the_os_would_not_run() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("tempdir");
let decoy = root.path().join("tool.EXE");
std::fs::write(&decoy, "#!/bin/sh\n").unwrap();
std::fs::set_permissions(&decoy, std::fs::Permissions::from_mode(0o755)).unwrap();
let path_env = root.path().display().to_string();
assert_eq!(resolve_on_path(Path::new("tool"), &path_env), None);
}
#[test]
fn a_bare_name_resolves_however_the_platform_spells_the_file() {
let root = tempfile::tempdir().expect("tempdir");
let installed = install_runnable(root.path(), "tool");
let path_env = root.path().display().to_string();
assert_eq!(resolve_on_path(Path::new("tool"), &path_env), Some(installed));
assert_eq!(resolve_on_path(Path::new("tool-missing"), &path_env), None);
}
}
#[cfg(all(test, unix))]
mod spawned {
use super::*;
use std::sync::{Arc, Mutex};
fn path_echoing_cli(tag: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("hl-spawn-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cli = dir.join("fake-agent");
std::fs::write(&cli, "#!/bin/sh\nprintf '%s\\n' \"$PATH\"\n").unwrap();
std::fs::set_permissions(&cli, std::fs::Permissions::from_mode(0o755)).unwrap();
cli
}
fn fake_login_shell(tag: &str, path_line: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("hl-shell-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let shell = dir.join("fake-shell");
std::fs::write(
&shell,
format!(
"#!/bin/sh\nprintf 'rc chatter\\n'\nprintf '\\n__CLI_STREAM_PATH__\\n'\n\
printf 'HOME=/x\\n{path_line}\\nTERM=xterm\\n'\n"
),
)
.unwrap();
std::fs::set_permissions(&shell, std::fs::Permissions::from_mode(0o755)).unwrap();
shell
}
static SHELL_ENV: Mutex<()> = Mutex::new(());
#[test]
fn the_path_comes_from_the_shell_we_asked() {
let _guard = SHELL_ENV.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let restore = std::env::var("SHELL").ok();
let shell = fake_login_shell("ok", "PATH=/fake/node/bin:/usr/bin");
std::env::set_var("SHELL", &shell);
assert_eq!(
login_shell_path().as_deref(),
Some("/fake/node/bin:/usr/bin"),
"the answer must come from the shell, past its startup chatter",
);
std::env::set_var("SHELL", "");
assert_eq!(login_shell_path(), None);
match restore {
Some(value) => std::env::set_var("SHELL", value),
None => std::env::remove_var("SHELL"),
}
}
fn run(program: PathBuf, env: Vec<(String, String)>) -> String {
let lines: Arc<Mutex<Vec<String>>> = Arc::default();
let sink = Arc::clone(&lines);
let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
let flag = Arc::clone(&done);
let spawn = cli_stream::Command::new(program).cwd(std::env::temp_dir()).run_id("t").env(env);
let _handle = spawn.resolve_cli().stream(move |event| {
match event {
cli_stream::Event::Stdout { line, .. } => sink.lock().unwrap().push(line),
cli_stream::Event::Exited { .. } => flag.store(true, std::sync::atomic::Ordering::SeqCst),
_ => {}
}
})
.expect("the fixture should spawn");
let mut finished = false;
for _ in 0..200 {
if done.load(std::sync::atomic::Ordering::SeqCst) {
finished = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
assert!(finished, "the fixture never exited; its output would be whatever arrived in time");
let out = lines.lock().unwrap().join("\n");
out
}
#[test]
fn a_spawned_cli_gets_its_own_directory_at_the_front_of_path() {
let cli = path_echoing_cli("front");
let parent = cli.parent().unwrap().display().to_string();
let seen = run(cli.clone(), Vec::new());
assert!(
seen.starts_with(&parent),
"the program's own directory must lead PATH.\n wanted first: {parent}\n child saw: {seen}"
);
let _ = std::fs::remove_dir_all(cli.parent().unwrap());
}
#[test]
fn a_path_the_caller_supplies_still_wins() {
let cli = path_echoing_cli("override");
let seen = run(cli.clone(), vec![("PATH".to_owned(), "/only/this".to_owned())]);
assert_eq!(seen.trim(), "/only/this", "the caller's PATH is applied last");
let _ = std::fs::remove_dir_all(cli.parent().unwrap());
}
}