use bamboo_infrastructure::{
hide_window_for_std_command, hide_window_for_tokio_command, trace_windows_command,
};
use tracing::{debug, info};
#[cfg(windows)]
fn resolve_windows_program(program: &str) -> (String, Vec<String>) {
use std::path::Path;
let p = Path::new(program);
let mut resolved = program.to_string();
let ext = p
.extension()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_ascii_lowercase();
if ext.is_empty() {
if p.exists() && p.is_file() {
let exe = p.with_extension("exe");
let cmd = p.with_extension("cmd");
let bat = p.with_extension("bat");
if exe.exists() && exe.is_file() {
resolved = exe.to_string_lossy().to_string();
} else if cmd.exists() && cmd.is_file() {
resolved = cmd.to_string_lossy().to_string();
} else if bat.exists() && bat.is_file() {
resolved = bat.to_string_lossy().to_string();
}
} else if program.eq_ignore_ascii_case("claude") {
resolved = "claude.cmd".to_string();
}
}
let resolved_ext = Path::new(&resolved)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_ascii_lowercase();
if resolved_ext == "cmd" || resolved_ext == "bat" {
("cmd".to_string(), vec!["/C".to_string(), resolved])
} else {
(resolved, Vec::new())
}
}
#[cfg(not(windows))]
fn resolve_windows_program(program: &str) -> (String, Vec<String>) {
(program.to_string(), Vec::new())
}
fn collect_inherited_env(program: &str) -> Vec<(String, String)> {
let mut envs: Vec<(String, String)> = Vec::new();
for (key, value) in std::env::vars() {
if key == "PATH"
|| key == "HOME"
|| key == "USER"
|| key == "SHELL"
|| key == "LANG"
|| key == "LC_ALL"
|| key.starts_with("LC_")
|| key == "NODE_PATH"
|| key == "NVM_DIR"
|| key == "NVM_BIN"
|| key == "HOMEBREW_PREFIX"
|| key == "HOMEBREW_CELLAR"
|| key == "HTTP_PROXY"
|| key == "HTTPS_PROXY"
|| key == "NO_PROXY"
|| key == "ALL_PROXY"
{
debug!("Inheriting env var: {}={}", key, value);
envs.push((key, value));
}
}
let mut path_value = envs
.iter()
.find(|(k, _)| k == "PATH")
.map(|(_, v)| v.clone())
.unwrap_or_default();
if program.contains("/.nvm/versions/node/") {
if let Some(node_bin_dir) = std::path::Path::new(program).parent() {
let node_bin_str = node_bin_dir.to_string_lossy();
if !path_value.contains(node_bin_str.as_ref()) {
let joined = std::env::join_paths(
std::iter::once(node_bin_dir.to_path_buf())
.chain(std::env::split_paths(&path_value)),
)
.map(|os| os.to_string_lossy().to_string())
.unwrap_or_else(|_| format!("{}:{}", node_bin_str, path_value));
debug!("Adding NVM bin directory to PATH: {}", node_bin_str);
path_value = joined;
}
}
}
if program.contains("/homebrew/") || program.contains("/opt/homebrew/") {
if let Some(program_dir) = std::path::Path::new(program).parent() {
let homebrew_bin_str = program_dir.to_string_lossy();
if !path_value.contains(homebrew_bin_str.as_ref()) {
let joined = std::env::join_paths(
std::iter::once(program_dir.to_path_buf())
.chain(std::env::split_paths(&path_value)),
)
.map(|os| os.to_string_lossy().to_string())
.unwrap_or_else(|_| format!("{}:{}", homebrew_bin_str, path_value));
debug!(
"Adding Homebrew bin directory to PATH: {}",
homebrew_bin_str
);
path_value = joined;
}
}
}
if let Some((_, v)) = envs.iter_mut().find(|(k, _)| k == "PATH") {
*v = path_value;
} else if !path_value.is_empty() {
envs.push(("PATH".to_string(), path_value));
}
envs
}
fn log_proxy_settings() {
info!("Command will use proxy settings:");
if let Ok(http_proxy) = std::env::var("HTTP_PROXY") {
info!(" HTTP_PROXY={}", http_proxy);
}
if let Ok(https_proxy) = std::env::var("HTTPS_PROXY") {
info!(" HTTPS_PROXY={}", https_proxy);
}
}
pub fn create_command_with_env(program: &str) -> std::process::Command {
let (exe, prefix_args) = resolve_windows_program(program);
let mut cmd = std::process::Command::new(&exe);
hide_window_for_std_command(&mut cmd);
trace_windows_command(
"claude.create_command_with_env",
&exe,
prefix_args.iter().map(String::as_str),
);
cmd.args(prefix_args);
info!("Creating command for: {} (exec: {})", program, exe);
for (key, value) in collect_inherited_env(program) {
cmd.env(&key, &value);
}
log_proxy_settings();
cmd
}
pub fn create_tokio_command_with_env(program: &str) -> tokio::process::Command {
let (exe, prefix_args) = resolve_windows_program(program);
let mut cmd = tokio::process::Command::new(&exe);
hide_window_for_tokio_command(&mut cmd);
trace_windows_command(
"claude.create_tokio_command_with_env",
&exe,
prefix_args.iter().map(String::as_str),
);
cmd.args(prefix_args);
info!("Creating tokio command for: {} (exec: {})", program, exe);
for (key, value) in collect_inherited_env(program) {
cmd.env(&key, &value);
}
log_proxy_settings();
cmd
}
#[cfg(test)]
mod tests {
#[test]
fn test_join_paths_with_env_functions() {
let paths = vec![
std::path::PathBuf::from("/usr/local/bin"),
std::path::PathBuf::from("/usr/bin"),
std::path::PathBuf::from("/bin"),
];
let joined = std::env::join_paths(&paths).expect("Failed to join paths");
let joined_str = joined.to_str().expect("Invalid UTF-8");
let split: Vec<_> = std::env::split_paths(joined_str).collect();
assert_eq!(split.len(), 3);
assert!(split.contains(&std::path::PathBuf::from("/usr/local/bin")));
assert!(split.contains(&std::path::PathBuf::from("/usr/bin")));
assert!(split.contains(&std::path::PathBuf::from("/bin")));
}
#[test]
fn test_join_paths_prepends_to_existing_path() {
let new_dir = std::path::PathBuf::from("/new/bin");
let existing_path = "/usr/local/bin:/usr/bin:/bin";
let new_path = std::env::join_paths(
std::iter::once(new_dir.clone()).chain(std::env::split_paths(existing_path)),
);
assert!(new_path.is_ok());
let new_path_str = new_path.unwrap().to_str().unwrap().to_string();
assert!(new_path_str.starts_with("/new/bin"));
assert!(new_path_str.contains("/usr/local/bin"));
assert!(new_path_str.contains("/usr/bin"));
assert!(new_path_str.contains("/bin"));
}
#[test]
fn test_path_separator_is_platform_specific() {
let paths = vec![
std::path::PathBuf::from("/first"),
std::path::PathBuf::from("/second"),
];
let joined = std::env::join_paths(&paths).unwrap();
let joined_str = joined.to_str().unwrap();
#[cfg(unix)]
assert!(joined_str.contains(':'), "Unix uses ':' as PATH separator");
#[cfg(windows)]
assert!(
joined_str.contains(';'),
"Windows uses ';' as PATH separator"
);
}
}