#[cfg(not(target_arch = "wasm32"))] use command::r#async::Command; /// A wrapper around `path_env_var` that produces correctly-configured commands. /// /// This follows the same wrapping pattern as `command::r#async::Command`: /// callers construct commands through the executor, which transparently sets /// the PATH environment variable. On wasm, a dummy implementation is provided /// so that consumer code doesn't need cfg gating. #[derive(Clone)] pub struct CommandBuilder { path_env_var: Option, } impl CommandBuilder { /// Creates a new CommandBuilder with the given PATH environment variable. pub fn new(path_env_var: Option) -> Self { Self { path_env_var } } /// Returns the PATH environment variable, if set. pub fn path_env_var(&self) -> Option<&str> { self.path_env_var.as_deref() } /// Creates a new Command with PATH already set. /// /// Use this when you need to run a command. The returned Command has the /// same API as `command::r#async::Command`, so callers don't need to change /// how they construct commands. /// /// On Windows, the command is wrapped in `cmd.exe /c` so that `.cmd` and /// `.bat` scripts on PATH are resolved correctly (e.g. `npm.cmd`, /// `typescript-language-server.cmd`). Rust's `Command::new` uses /// `CreateProcessW` which only resolves `.exe` extensions. #[cfg(not(target_arch = "wasm32"))] pub fn command(&self, program: impl AsRef) -> Command { #[cfg(windows)] let mut cmd = { let mut cmd = Command::new("cmd.exe"); cmd.arg("/c").arg(program); cmd }; #[cfg(not(windows))] let mut cmd = Command::new(program); if let Some(path) = &self.path_env_var { cmd.env("PATH", path); } cmd } /// Attempts to capture PATH from the user's login shell. /// This is a fallback for when the PATH passed in from the terminal session /// doesn't include paths set by tools like nvm, pyenv, rbenv, etc. that /// modify PATH in shell rc files. #[cfg(not(target_arch = "wasm32"))] pub async fn capture_interactive_shell_path() -> Option { let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); let output = Command::new(&shell) .args(["-i", "-l", "-c", "echo $PATH"]) .output() .await .ok()?; if !output.status.success() { log::warn!( "Failed to capture PATH from interactive shell ({}): {}", shell, String::from_utf8_lossy(&output.stderr) ); return None; } let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if path.is_empty() { log::warn!("Interactive shell returned empty PATH"); return None; } log::info!("Captured PATH from interactive shell ({shell})"); Some(path) } }