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_node_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_node(program: &Path) -> String {
prepend_program_dir(program, &augmented_node_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_node_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_node_path() -> String {
static CACHED: OnceLock<String> = OnceLock::new();
CACHED.get_or_init(compute_augmented_node_path).clone()
}
fn compute_augmented_node_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 parts: Vec<String> = Vec::new();
if let Some(existing) = process_path.filter(|path| !path.is_empty()) {
parts.push(existing);
}
parts.push(discovered);
keep_absolute_entries(&parts.join(":"))
}
fn keep_absolute_entries(path: &str) -> String {
path.split(':')
.filter(|entry| entry.starts_with('/'))
.collect::<Vec<_>>()
.join(":")
}
#[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 {
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_node(&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<&str> {
path.split(':').collect()
}
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!(entry.starts_with('/'), "{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(|entry| entry.starts_with('/')) {
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!(entry.starts_with('/'), "{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");
}
#[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() {
assert_eq!(
keep_absolute_entries("/opt/homebrew/bin:node_modules/.bin:/usr/bin:.::/bin"),
"/opt/homebrew/bin:/usr/bin:/bin"
);
assert_eq!(keep_absolute_entries("/usr/bin"), "/usr/bin");
assert_eq!(keep_absolute_entries(".:rel:"), "");
}
#[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 combined = prepend_program_dir(
Path::new("/Users/x/.nvm/versions/node/v22/bin/bob"),
"/opt/homebrew/bin:/usr/bin",
);
assert!(combined.starts_with("/Users/x/.nvm/versions/node/v22/bin:"));
assert!(combined.contains("/opt/homebrew/bin"));
assert_eq!(
prepend_program_dir(Path::new("bob"), "/usr/bin"),
"/usr/bin"
);
}
#[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 resolved = resolve_program(PathBuf::from("sh"));
assert!(resolved.is_absolute(), "a name on PATH resolves to its real location: {resolved:?}");
assert!(resolved.ends_with("sh"), "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_node_path();
let first = existing.split(':').find(|e| e.starts_with('/')).expect("an absolute entry");
assert!(augmented.contains(first), "{first} must survive into {augmented}");
}
#[test]
fn the_process_path_leads_and_an_absent_one_contributes_nothing() {
assert_eq!(
compose_augmented_path(Some("/host/bin".to_owned()), "/found/bin".to_owned()),
"/host/bin:/found/bin",
);
assert_eq!(compose_augmented_path(None, "/found/bin".to_owned()), "/found/bin");
assert_eq!(
compose_augmented_path(Some(String::new()), "/found/bin".to_owned()),
"/found/bin",
);
}
#[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_node_path_is_nonempty_and_resolves_system_bin() {
let path = augmented_node_path();
assert!(!path.is_empty());
assert!(path.contains("/usr/bin"), "system bin must always resolve");
}
#[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());
}
}