From f72f5c31974fc390bc94b1bfb7b688166870a528 Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 01:02:51 +0800 Subject: [PATCH 01/10] refactor(shell): unify shell detection with VP_SHELL env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace VP_SHELL_NU and VP_SHELL_PWSH with a single VP_SHELL env var that accepts shell names (bash, fish, nu, pwsh, cmd). Detection now follows: VP_SHELL → process tree inference → platform default. - Add Shell::from_str for case-insensitive name parsing - Add process tree walking to infer shell from parent processes - Add sysinfo dep for Windows process enumeration - Update shell wrapper scripts to remove shell-specific env vars --- Cargo.lock | 51 +++- Cargo.toml | 1 + crates/vite_global_cli/Cargo.toml | 3 + .../vite_global_cli/src/commands/env/setup.rs | 8 +- .../vite_global_cli/src/commands/env/use.rs | 52 +--- crates/vite_global_cli/src/commands/shell.rs | 237 ++++++++++++++++-- crates/vite_shared/src/env_config.rs | 20 +- crates/vite_shared/src/env_vars.rs | 11 +- 8 files changed, 285 insertions(+), 98 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30af104f53..784926c0b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2489,7 +2489,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -3631,6 +3631,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", + "dispatch2", + "objc2", ] [[package]] @@ -3649,6 +3651,37 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -6734,6 +6767,21 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81c645a4de0d803ced6ef0388a2646aa1ef8467173b5d59a2c33c88de4ab76e7" +[[package]] +name = "sysinfo" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14311e7e9a03114cd4b65eedd54e8fed2945e17f08586ae97ef53bc0669f9581" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows 0.62.2", +] + [[package]] name = "tar" version = "0.4.45" @@ -7535,6 +7583,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "sysinfo", "tempfile", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index e24c5d438a..57395e3979 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -266,6 +266,7 @@ simdutf8 = "0.1.5" string_cache = "0.9.0" sugar_path = { version = "2.0.1", features = ["cached_current_dir"] } syn = { version = "2", default-features = false } +sysinfo = "0.39.2" tar = "0.4.43" tempfile = "3.14.0" terminal_size = "0.4.2" diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index ab914d5911..5ce1470def 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -39,6 +39,9 @@ vite_shared = { workspace = true } vite_str = { workspace = true } vite_workspace = { workspace = true } +[target.'cfg(windows)'.dependencies] +sysinfo = { workspace = true } + [dev-dependencies] serial_test = { workspace = true } tempfile = { workspace = true } diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index bd37fa311f..c0aa4dbe1f 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -562,7 +562,7 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { ^vp ...$args return } - let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", VP_SHELL_NU: "1" } { + let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1" } { ^vp ...$args }) let lines = ($out | lines) @@ -625,7 +625,6 @@ function vp { & (Join-Path $__vp_bin "vp") @args; return } $env:VP_ENV_USE_EVAL_ENABLE = "1" - $env:VP_SHELL_PWSH = "1" $output = & (Join-Path $__vp_bin "vp") @args 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { Write-Host $_.Exception.Message @@ -634,7 +633,6 @@ function vp { } } Remove-Item Env:VP_ENV_USE_EVAL_ENABLE -ErrorAction SilentlyContinue - Remove-Item Env:VP_SHELL_PWSH -ErrorAction SilentlyContinue if ($LASTEXITCODE -eq 0 -and $output) { Invoke-Expression ($output -join "`n") } @@ -848,10 +846,6 @@ mod tests { nu_content.contains("VP_COMPLETE=fish"), "env.nu should use dynamic Fish completion delegation" ); - assert!( - nu_content.contains("VP_SHELL_NU"), - "env.nu should use VP_SHELL_NU explicit marker instead of inherited NU_VERSION" - ); assert!(nu_content.contains("load-env"), "env.nu should use load-env to apply exports"); } diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index ed1da3684f..3057dbd13a 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -181,35 +181,33 @@ mod tests { use super::*; #[test] - fn test_detect_shell_pwsh() { + fn test_detect_shell_vp_shell_powershell() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - vp_shell_pwsh: true, + vp_shell: Some("powershell".into()), ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); - assert!(matches!(shell, Shell::PowerShell)); + assert_eq!(shell, Shell::PowerShell); } #[test] - fn test_detect_shell_fish() { + fn test_detect_shell_vp_shell_fish() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - fish_version: Some("3.7.0".into()), + vp_shell: Some("fish".into()), ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); - assert!(matches!(shell, Shell::Fish)); + assert_eq!(shell, Shell::Fish); } #[test] - fn test_detect_shell_fish_and_nushell() { - // Fish takes priority over Nu shell signal + fn test_detect_shell_vp_shell_nu() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - fish_version: Some("3.7.0".into()), - vp_shell_nu: true, + vp_shell: Some("nu".into()), ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); - assert!(matches!(shell, Shell::Fish)); + assert_eq!(shell, Shell::NuShell); } #[test] @@ -218,35 +216,9 @@ mod tests { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_test()); let shell = detect_shell(); #[cfg(not(windows))] - assert!(matches!(shell, Shell::Posix)); + assert_eq!(shell, Shell::Posix); #[cfg(windows)] - assert!(matches!(shell, Shell::Cmd)); - } - - #[test] - fn test_detect_shell_nushell() { - let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - vp_shell_nu: true, - ..vite_shared::EnvConfig::for_test() - }); - let shell = detect_shell(); - assert!(matches!(shell, Shell::NuShell)); - } - - #[test] - fn test_detect_shell_inherited_nu_version_is_posix() { - // NU_VERSION alone (inherited from parent Nushell) must NOT trigger Nu detection. - // Only the explicit VP_SHELL_NU marker set by env.nu wrapper counts. - let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - nu_version: Some("0.111.0".into()), - vp_shell_nu: false, - ..vite_shared::EnvConfig::for_test() - }); - let shell = detect_shell(); - #[cfg(not(windows))] - assert!(matches!(shell, Shell::Posix)); - #[cfg(windows)] - let _ = shell; + assert_eq!(shell, Shell::Cmd); } #[test] @@ -346,7 +318,7 @@ mod tests { let cwd = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { env_use_eval_enable: true, - vp_shell_pwsh: true, + vp_shell: Some("powershell".into()), ..vite_shared::EnvConfig::for_test_with_home(temp_dir.path()) }); diff --git a/crates/vite_global_cli/src/commands/shell.rs b/crates/vite_global_cli/src/commands/shell.rs index b24d83aaf9..92ccad6471 100644 --- a/crates/vite_global_cli/src/commands/shell.rs +++ b/crates/vite_global_cli/src/commands/shell.rs @@ -1,9 +1,14 @@ //! Shared shell detection and profile helpers. +use std::str::FromStr; + use directories::BaseDirs; use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_str::Str; +/// Maximum depth to walk up the process tree for shell inference. +const MAX_PROCESS_DEPTH: u8 = 10; + /// Detected shell type for output formatting. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Shell { @@ -19,23 +24,163 @@ pub enum Shell { Cmd, } -/// Detect the current shell from environment variables. +impl FromStr for Shell { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "sh" | "bash" | "zsh" => Ok(Shell::Posix), + "fish" => Ok(Shell::Fish), + "nu" | "nushell" => Ok(Shell::NuShell), + "pwsh" | "powershell" => Ok(Shell::PowerShell), + "cmd" => Ok(Shell::Cmd), + _ => Err(()), + } + } +} + +/// Detect the current shell: +/// 1. `VP_SHELL` environment variable +/// 2. Process tree inference +/// 3. Platform default #[must_use] pub fn detect_shell() -> Shell { let config = vite_shared::EnvConfig::get(); - if config.fish_version.is_some() { - Shell::Fish - } else if config.vp_shell_nu { - Shell::NuShell - } else if config.vp_shell_pwsh { - Shell::PowerShell - } else if cfg!(windows) { - Shell::Cmd - } else { - Shell::Posix + + // 1. Check VP_SHELL environment variable + if let Some(vp_shell) = &config.vp_shell { + if let Ok(shell) = Shell::from_str(vp_shell) { + return shell; + } + } + + // 2. Infer from process tree + if let Some(shell) = infer_shell_from_process_tree() { + return shell; + } + + // 3. Platform default + if cfg!(windows) { Shell::Cmd } else { Shell::Posix } +} + +/// Infer shell by walking up the process tree. +/// Returns the first known shell. +fn infer_shell_from_process_tree() -> Option { + #[cfg(unix)] + { + infer_shell_unix() + } + #[cfg(windows)] + { + infer_shell_windows() } } +#[cfg(unix)] +fn infer_shell_unix() -> Option { + let mut pid = Some(std::process::id()); + let mut visited = 0u8; + + while let Some(current_pid) = pid { + if visited >= MAX_PROCESS_DEPTH { + return None; + } + + let info = get_process_info(current_pid)?; + let binary = info.command.trim_start_matches('-').split('/').next_back()?; + + if let Ok(shell) = Shell::from_str(binary) { + return Some(shell); + } + + tracing::debug!("binary is not a supported shell: {:?}", binary); + + pid = info.parent_pid; + visited += 1; + } + + None +} + +#[cfg(unix)] +struct ProcessInfo { + parent_pid: Option, + command: String, +} + +/// Get process name and parent PID +#[cfg(unix)] +fn get_process_info(pid: u32) -> Option { + let output = std::process::Command::new("ps") + .args(["-o", "ppid,comm", "-p", &pid.to_string()]) + .output() + .ok()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut lines = stdout.lines(); + + // Skip header line + lines.next()?; + + let line = lines.next()?; + let mut parts = line.split_whitespace(); + + let ppid = parts.next()?.parse().ok(); + let command = parts.next()?.to_string(); + + Some(ProcessInfo { parent_pid: ppid, command }) +} + +/// Get process name and parent PID on Windows using sysinfo. +#[cfg(windows)] +fn infer_shell_windows() -> Option { + use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind}; + + let mut system = System::new(); + let mut current_pid = Some(sysinfo::get_current_pid().ok()?); + + system.refresh_processes_specifics( + ProcessesToUpdate::All, + true, + ProcessRefreshKind::nothing().with_exe(UpdateKind::OnlyIfNotSet), + ); + + let mut visited = 0u8; + + while let Some(pid) = current_pid { + if visited >= MAX_PROCESS_DEPTH { + return None; + } + + if let Some(process) = system.process(pid) { + current_pid = process.parent(); + + let process_name = process + .exe() + .and_then(|x| x.file_stem()) + .and_then(|x| x.to_str()) + .map(str::to_lowercase); + + if let Some(shell) = process_name + .as_ref() + .map(|x| x.as_str()) + .and_then(|name| Shell::from_str(name).ok()) + { + return Some(shell); + } + + tracing::debug!("binary is not a supported shell: {:?}", process_name); + } else { + tracing::debug!("process not found for pid {pid}"); + current_pid = None; + } + + visited += 1; + } + + None +} + /// All shell profile files that interactive terminal sessions may source. /// This matches the files that `install.sh` writes to and `vp implode` cleans. #[cfg(not(windows))] @@ -235,44 +380,84 @@ mod tests { use super::*; #[test] - fn test_detect_shell_pwsh() { + fn test_shell_from_str() { + // POSIX shells + assert_eq!(Shell::from_str("sh"), Ok(Shell::Posix)); + assert_eq!(Shell::from_str("bash"), Ok(Shell::Posix)); + assert_eq!(Shell::from_str("zsh"), Ok(Shell::Posix)); + + // Other shells + assert_eq!(Shell::from_str("fish"), Ok(Shell::Fish)); + assert_eq!(Shell::from_str("nu"), Ok(Shell::NuShell)); + assert_eq!(Shell::from_str("nushell"), Ok(Shell::NuShell)); + assert_eq!(Shell::from_str("powershell"), Ok(Shell::PowerShell)); + assert_eq!(Shell::from_str("pwsh"), Ok(Shell::PowerShell)); + assert_eq!(Shell::from_str("cmd"), Ok(Shell::Cmd)); + + // Case insensitive + assert_eq!(Shell::from_str("SH"), Ok(Shell::Posix)); + assert_eq!(Shell::from_str("BASH"), Ok(Shell::Posix)); + assert_eq!(Shell::from_str("Fish"), Ok(Shell::Fish)); + assert_eq!(Shell::from_str("POWERSHELL"), Ok(Shell::PowerShell)); + assert_eq!(Shell::from_str("Nu"), Ok(Shell::NuShell)); + + // Invalid + assert!(Shell::from_str("posix").is_err()); + assert!(Shell::from_str("invalid").is_err()); + assert!(Shell::from_str("").is_err()); + } + + #[test] + fn test_detect_shell_vp_shell_explicit() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - vp_shell_pwsh: true, + vp_shell: Some("nu".into()), ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); - assert!(matches!(shell, Shell::PowerShell)); + assert_eq!(shell, Shell::NuShell); } #[test] - fn test_detect_shell_fish() { + fn test_detect_shell_vp_shell_case_insensitive() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - fish_version: Some("3.7.0".into()), + vp_shell: Some("POWERSHELL".into()), ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); - assert!(matches!(shell, Shell::Fish)); + assert_eq!(shell, Shell::PowerShell); } #[test] - fn test_detect_shell_nushell() { + fn test_detect_shell_vp_shell_pwsh_alias() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - vp_shell_nu: true, + vp_shell: Some("pwsh".into()), ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); - assert!(matches!(shell, Shell::NuShell)); + assert_eq!(shell, Shell::PowerShell); + } + + #[test] + fn test_detect_shell_vp_shell_invalid_falls_back() { + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + vp_shell: Some("invalid_shell".into()), + ..vite_shared::EnvConfig::for_test() + }); + let shell = detect_shell(); + #[cfg(not(windows))] + assert_eq!(shell, Shell::Posix); + #[cfg(windows)] + assert_eq!(shell, Shell::Cmd); } #[test] - fn test_detect_shell_nushell_wins_over_powershell() { + fn test_detect_shell_vp_shell_fish() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - vp_shell_nu: true, - ps_module_path: Some("/some/path".into()), + vp_shell: Some("fish".into()), ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); - assert!(matches!(shell, Shell::NuShell)); + assert_eq!(shell, Shell::Fish); } #[test] @@ -280,8 +465,8 @@ mod tests { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_test()); let shell = detect_shell(); #[cfg(not(windows))] - assert!(matches!(shell, Shell::Posix)); + assert_eq!(shell, Shell::Posix); #[cfg(windows)] - assert!(matches!(shell, Shell::Cmd)); + assert_eq!(shell, Shell::Cmd); } } diff --git a/crates/vite_shared/src/env_config.rs b/crates/vite_shared/src/env_config.rs index d3fe20c3cc..11e8bee58b 100644 --- a/crates/vite_shared/src/env_config.rs +++ b/crates/vite_shared/src/env_config.rs @@ -128,18 +128,10 @@ pub struct EnvConfig { /// Env: `NU_VERSION` pub nu_version: Option, - /// Explicit Nu shell eval signal set by the `env.nu` wrapper. + /// Explicitly specify the current shell. /// - /// Unlike `NU_VERSION`, this is not inherited by child processes — it is only - /// present when the Nushell wrapper explicitly passes it via `with-env`. - /// - /// Env: `VP_SHELL_NU` - pub vp_shell_nu: bool, - - /// Explicit `PowerShell` eval signal set by the `env.ps1` wrapper. - /// - /// Env: `VP_SHELL_PWSH` - pub vp_shell_pwsh: bool, + /// Env: `VP_SHELL` + pub vp_shell: Option, } impl EnvConfig { @@ -170,8 +162,7 @@ impl EnvConfig { fish_version: std::env::var("FISH_VERSION").ok(), ps_module_path: std::env::var("PSModulePath").ok(), nu_version: std::env::var("NU_VERSION").ok(), - vp_shell_nu: std::env::var(env_vars::VP_SHELL_NU).is_ok(), - vp_shell_pwsh: std::env::var(env_vars::VP_SHELL_PWSH).is_ok(), + vp_shell: std::env::var(env_vars::VP_SHELL).ok(), } } @@ -257,8 +248,7 @@ impl EnvConfig { fish_version: None, ps_module_path: None, nu_version: None, - vp_shell_nu: false, - vp_shell_pwsh: false, + vp_shell: None, } } diff --git a/crates/vite_shared/src/env_vars.rs b/crates/vite_shared/src/env_vars.rs index a0f1a3e6d9..40fa82ee7e 100644 --- a/crates/vite_shared/src/env_vars.rs +++ b/crates/vite_shared/src/env_vars.rs @@ -36,15 +36,8 @@ pub const VP_DEBUG_SHIM: &str = "VP_DEBUG_SHIM"; /// Enable eval mode for `vp env use`. pub const VP_ENV_USE_EVAL_ENABLE: &str = "VP_ENV_USE_EVAL_ENABLE"; -/// Explicit signal set by the Nushell wrapper to indicate Nu shell eval context. -/// -/// Unlike `NU_VERSION` (which is inherited by child processes), this is only set -/// by the `with-env` block in `env.nu`, so it cannot cause false detection when -/// bash/zsh is launched from a Nushell session. -pub const VP_SHELL_NU: &str = "VP_SHELL_NU"; - -/// Explicit signal set by the `PowerShell` wrapper to indicate `PowerShell` eval context. -pub const VP_SHELL_PWSH: &str = "VP_SHELL_PWSH"; +/// Explicitly specify the current shell. +pub const VP_SHELL: &str = "VP_SHELL"; /// Filter for update task types. pub const VITE_UPDATE_TASK_TYPES: &str = "VITE_UPDATE_TASK_TYPES"; From cbad4a108651c5c01850e55437b04f1b6d64bfce Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 02:12:39 +0800 Subject: [PATCH 02/10] fix: remove shell detection tests that depend on process tree inference These tests assert on `detect_shell()` default fallback, but process tree inference varies across environments (CI uses `shell: bash` on Windows, local machines may have Git Bash), making the result non-deterministic. --- .../vite_global_cli/src/commands/env/use.rs | 10 --------- crates/vite_global_cli/src/commands/shell.rs | 22 +------------------ 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index 3057dbd13a..8855a38e4e 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -210,16 +210,6 @@ mod tests { assert_eq!(shell, Shell::NuShell); } - #[test] - fn test_detect_shell_posix_default() { - // All shell detection fields None → defaults - let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_test()); - let shell = detect_shell(); - #[cfg(not(windows))] - assert_eq!(shell, Shell::Posix); - #[cfg(windows)] - assert_eq!(shell, Shell::Cmd); - } #[test] fn test_format_export_posix() { diff --git a/crates/vite_global_cli/src/commands/shell.rs b/crates/vite_global_cli/src/commands/shell.rs index 92ccad6471..49cb44f420 100644 --- a/crates/vite_global_cli/src/commands/shell.rs +++ b/crates/vite_global_cli/src/commands/shell.rs @@ -437,18 +437,6 @@ mod tests { assert_eq!(shell, Shell::PowerShell); } - #[test] - fn test_detect_shell_vp_shell_invalid_falls_back() { - let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - vp_shell: Some("invalid_shell".into()), - ..vite_shared::EnvConfig::for_test() - }); - let shell = detect_shell(); - #[cfg(not(windows))] - assert_eq!(shell, Shell::Posix); - #[cfg(windows)] - assert_eq!(shell, Shell::Cmd); - } #[test] fn test_detect_shell_vp_shell_fish() { @@ -460,13 +448,5 @@ mod tests { assert_eq!(shell, Shell::Fish); } - #[test] - fn test_detect_shell_posix_default() { - let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_test()); - let shell = detect_shell(); - #[cfg(not(windows))] - assert_eq!(shell, Shell::Posix); - #[cfg(windows)] - assert_eq!(shell, Shell::Cmd); - } + } From 00720a401140e21988e4937ab856ce1bf4bf1ec2 Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 02:24:06 +0800 Subject: [PATCH 03/10] test(cli): add snap test for shell environment detection --- .../command-env-use-shells/package.json | 5 +++ .../command-env-use-shells/snap.txt | 39 +++++++++++++++++++ .../command-env-use-shells/steps.json | 18 +++++++++ 3 files changed, 62 insertions(+) create mode 100644 packages/cli/snap-tests-global/command-env-use-shells/package.json create mode 100644 packages/cli/snap-tests-global/command-env-use-shells/snap.txt create mode 100644 packages/cli/snap-tests-global/command-env-use-shells/steps.json diff --git a/packages/cli/snap-tests-global/command-env-use-shells/package.json b/packages/cli/snap-tests-global/command-env-use-shells/package.json new file mode 100644 index 0000000000..cafde4c542 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-use-shells/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-env-use-shells", + "version": "1.0.0", + "private": true +} diff --git a/packages/cli/snap-tests-global/command-env-use-shells/snap.txt b/packages/cli/snap-tests-global/command-env-use-shells/snap.txt new file mode 100644 index 0000000000..92f39a8584 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-use-shells/snap.txt @@ -0,0 +1,39 @@ +> VP_SHELL=bash vp env use 20.18.0 --no-install # should detect bash and output posix export +export VP_NODE_VERSION=20.18.0 +Using Node.js v (resolved from ) + +> VP_SHELL=zsh vp env use 20.18.0 --no-install # should detect zsh and output posix export +export VP_NODE_VERSION=20.18.0 +Using Node.js v (resolved from ) + +> VP_SHELL=fish vp env use 20.18.0 --no-install # should detect fish and output fish export +set -gx VP_NODE_VERSION +Using Node.js v (resolved from ) + +> VP_SHELL=nu vp env use 20.18.0 --no-install # should detect nushell and output nushell export +$env.VP_NODE_VERSION = "20.18.0" +Using Node.js v (resolved from ) + +> VP_SHELL=pwsh vp env use 20.18.0 --no-install # should detect powershell and output powershell export +$env:VP_NODE_VERSION = "20.18.0" +Using Node.js v (resolved from ) + +> VP_SHELL=cmd vp env use 20.18.0 --no-install # should detect cmd and output cmd export +set VP_NODE_VERSION=20.18.0 +Using Node.js v (resolved from ) + +> VP_SHELL=BASH vp env use 20.18.0 --no-install # should detect case-insensitive bash +export VP_NODE_VERSION=20.18.0 +Using Node.js v (resolved from ) + +> VP_SHELL=FISH vp env use 20.18.0 --no-install # should detect case-insensitive fish +set -gx VP_NODE_VERSION +Using Node.js v (resolved from ) + +> VP_SHELL=POWERSHELL vp env use 20.18.0 --no-install # should detect case-insensitive powershell +$env:VP_NODE_VERSION = "20.18.0" +Using Node.js v (resolved from ) + +> VP_SHELL=invalid vp env use 20.18.0 --no-install # should fallback to platform default +export VP_NODE_VERSION=20.18.0 +Using Node.js v (resolved from ) diff --git a/packages/cli/snap-tests-global/command-env-use-shells/steps.json b/packages/cli/snap-tests-global/command-env-use-shells/steps.json new file mode 100644 index 0000000000..d4bd1c6431 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-use-shells/steps.json @@ -0,0 +1,18 @@ +{ + "env": { + "VP_ENV_USE_EVAL_ENABLE": "1" + }, + "ignoredPlatforms": ["win32"], + "commands": [ + "VP_SHELL=bash vp env use 20.18.0 --no-install # should detect bash and output posix export", + "VP_SHELL=zsh vp env use 20.18.0 --no-install # should detect zsh and output posix export", + "VP_SHELL=fish vp env use 20.18.0 --no-install # should detect fish and output fish export", + "VP_SHELL=nu vp env use 20.18.0 --no-install # should detect nushell and output nushell export", + "VP_SHELL=pwsh vp env use 20.18.0 --no-install # should detect powershell and output powershell export", + "VP_SHELL=cmd vp env use 20.18.0 --no-install # should detect cmd and output cmd export", + "VP_SHELL=BASH vp env use 20.18.0 --no-install # should detect case-insensitive bash", + "VP_SHELL=FISH vp env use 20.18.0 --no-install # should detect case-insensitive fish", + "VP_SHELL=POWERSHELL vp env use 20.18.0 --no-install # should detect case-insensitive powershell", + "VP_SHELL=invalid vp env use 20.18.0 --no-install # should fallback to platform default" + ] +} \ No newline at end of file From fd034f3e3d917f58e27bc53f4dda05d80d02463d Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 02:42:06 +0800 Subject: [PATCH 04/10] chore: remove blank lines --- crates/vite_global_cli/src/commands/shell.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/vite_global_cli/src/commands/shell.rs b/crates/vite_global_cli/src/commands/shell.rs index 49cb44f420..45219f8375 100644 --- a/crates/vite_global_cli/src/commands/shell.rs +++ b/crates/vite_global_cli/src/commands/shell.rs @@ -437,7 +437,6 @@ mod tests { assert_eq!(shell, Shell::PowerShell); } - #[test] fn test_detect_shell_vp_shell_fish() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { @@ -447,6 +446,4 @@ mod tests { let shell = detect_shell(); assert_eq!(shell, Shell::Fish); } - - } From 7a3ed1ceb93813c337f3c468f7508a24eb8d90a1 Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 03:06:48 +0800 Subject: [PATCH 05/10] lint --- crates/vite_global_cli/src/commands/env/use.rs | 1 - .../cli/snap-tests-global/command-env-use-shells/steps.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index 8855a38e4e..ad5c31fd0f 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -210,7 +210,6 @@ mod tests { assert_eq!(shell, Shell::NuShell); } - #[test] fn test_format_export_posix() { let result = format_export(&Shell::Posix, "20.18.0"); diff --git a/packages/cli/snap-tests-global/command-env-use-shells/steps.json b/packages/cli/snap-tests-global/command-env-use-shells/steps.json index d4bd1c6431..4e1dec799d 100644 --- a/packages/cli/snap-tests-global/command-env-use-shells/steps.json +++ b/packages/cli/snap-tests-global/command-env-use-shells/steps.json @@ -15,4 +15,4 @@ "VP_SHELL=POWERSHELL vp env use 20.18.0 --no-install # should detect case-insensitive powershell", "VP_SHELL=invalid vp env use 20.18.0 --no-install # should fallback to platform default" ] -} \ No newline at end of file +} From 5ca9797e22f2e98886561827c5b2bd981f94d32c Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 03:53:00 +0800 Subject: [PATCH 06/10] fix: pass shell hints from eval wrappers --- crates/vite_global_cli/src/commands/env/setup.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index c0aa4dbe1f..238df6e8c1 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -530,7 +530,8 @@ function vp command vp $argv; return end set -lx VP_ENV_USE_EVAL_ENABLE 1 - set -l __vp_out (env FISH_VERSION=$FISH_VERSION __VP_BIN__/vp $argv); or return $status + set -lx VP_SHELL fish + set -l __vp_out (command vp $argv); or return $status eval (string join ';' $__vp_out) else command vp $argv @@ -562,7 +563,7 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { ^vp ...$args return } - let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1" } { + let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", VP_SHELL: "nu" } { ^vp ...$args }) let lines = ($out | lines) @@ -625,6 +626,7 @@ function vp { & (Join-Path $__vp_bin "vp") @args; return } $env:VP_ENV_USE_EVAL_ENABLE = "1" + $env:VP_SHELL = "pwsh" $output = & (Join-Path $__vp_bin "vp") @args 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { Write-Host $_.Exception.Message @@ -633,6 +635,7 @@ function vp { } } Remove-Item Env:VP_ENV_USE_EVAL_ENABLE -ErrorAction SilentlyContinue + Remove-Item Env:VP_SHELL -ErrorAction SilentlyContinue if ($LASTEXITCODE -eq 0 -and $output) { Invoke-Expression ($output -join "`n") } @@ -1031,10 +1034,6 @@ mod tests { fish_content.contains("\"$argv[2]\" = \"use\""), "env.fish should check for 'use' subcommand" ); - assert!( - fish_content.contains("/vp $argv"), - "env.fish should use absolute path to vp for passthrough" - ); } #[tokio::test] From 9ca020fbbdb608b6a1b66f75f6ac284d6d0360ba Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 03:53:13 +0800 Subject: [PATCH 07/10] fix: remove unstable shell fallback snapshot --- crates/vite_shared/src/env_config.rs | 21 ------------------- .../command-env-use-shells/snap.txt | 4 ---- .../command-env-use-shells/steps.json | 3 +-- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/crates/vite_shared/src/env_config.rs b/crates/vite_shared/src/env_config.rs index 11e8bee58b..2298ab8d17 100644 --- a/crates/vite_shared/src/env_config.rs +++ b/crates/vite_shared/src/env_config.rs @@ -113,21 +113,6 @@ pub struct EnvConfig { /// Env: `HOME` (Unix) / `USERPROFILE` (Windows) pub user_home: Option, - /// Fish shell version (indicates running under fish). - /// - /// Env: `FISH_VERSION` - pub fish_version: Option, - - /// `PowerShell` module path (indicates running under `PowerShell` on Windows). - /// - /// Env: `PSModulePath` - pub ps_module_path: Option, - - /// Nu shell version (indicates running under Nu shell). - /// - /// Env: `NU_VERSION` - pub nu_version: Option, - /// Explicitly specify the current shell. /// /// Env: `VP_SHELL` @@ -159,9 +144,6 @@ impl EnvConfig { .or_else(|_| std::env::var("USERPROFILE")) .ok() .map(PathBuf::from), - fish_version: std::env::var("FISH_VERSION").ok(), - ps_module_path: std::env::var("PSModulePath").ok(), - nu_version: std::env::var("NU_VERSION").ok(), vp_shell: std::env::var(env_vars::VP_SHELL).ok(), } } @@ -245,9 +227,6 @@ impl EnvConfig { update_task_types: None, node_version: None, user_home: None, - fish_version: None, - ps_module_path: None, - nu_version: None, vp_shell: None, } } diff --git a/packages/cli/snap-tests-global/command-env-use-shells/snap.txt b/packages/cli/snap-tests-global/command-env-use-shells/snap.txt index 92f39a8584..13999014b3 100644 --- a/packages/cli/snap-tests-global/command-env-use-shells/snap.txt +++ b/packages/cli/snap-tests-global/command-env-use-shells/snap.txt @@ -33,7 +33,3 @@ Using Node.js v (resolved from ) > VP_SHELL=POWERSHELL vp env use 20.18.0 --no-install # should detect case-insensitive powershell $env:VP_NODE_VERSION = "20.18.0" Using Node.js v (resolved from ) - -> VP_SHELL=invalid vp env use 20.18.0 --no-install # should fallback to platform default -export VP_NODE_VERSION=20.18.0 -Using Node.js v (resolved from ) diff --git a/packages/cli/snap-tests-global/command-env-use-shells/steps.json b/packages/cli/snap-tests-global/command-env-use-shells/steps.json index 4e1dec799d..a71df8844a 100644 --- a/packages/cli/snap-tests-global/command-env-use-shells/steps.json +++ b/packages/cli/snap-tests-global/command-env-use-shells/steps.json @@ -12,7 +12,6 @@ "VP_SHELL=cmd vp env use 20.18.0 --no-install # should detect cmd and output cmd export", "VP_SHELL=BASH vp env use 20.18.0 --no-install # should detect case-insensitive bash", "VP_SHELL=FISH vp env use 20.18.0 --no-install # should detect case-insensitive fish", - "VP_SHELL=POWERSHELL vp env use 20.18.0 --no-install # should detect case-insensitive powershell", - "VP_SHELL=invalid vp env use 20.18.0 --no-install # should fallback to platform default" + "VP_SHELL=POWERSHELL vp env use 20.18.0 --no-install # should detect case-insensitive powershell" ] } From 271fbd296ed04bd8c1e1534ea04bec4fabc7e4ca Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 03:53:13 +0800 Subject: [PATCH 08/10] fix: ignore failed ps shell probes --- crates/vite_global_cli/src/commands/shell.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/vite_global_cli/src/commands/shell.rs b/crates/vite_global_cli/src/commands/shell.rs index 45219f8375..ac5c36f836 100644 --- a/crates/vite_global_cli/src/commands/shell.rs +++ b/crates/vite_global_cli/src/commands/shell.rs @@ -116,6 +116,14 @@ fn get_process_info(pid: u32) -> Option { .output() .ok()?; + if !output.status.success() { + tracing::debug!( + "ps failed while reading process info for pid {pid}: {}", + String::from_utf8_lossy(&output.stderr) + ); + return None; + } + let stdout = String::from_utf8_lossy(&output.stdout); let mut lines = stdout.lines(); From 59c29214c80beff436ae812e66c4f05ca099a319 Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 11:45:50 +0800 Subject: [PATCH 09/10] fix: force POSIX shell for env wrapper --- crates/vite_global_cli/src/commands/env/setup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 238df6e8c1..a060a06fde 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -493,7 +493,7 @@ unset __vp_bin vp() { if [ "$1" = "env" ] && [ "$2" = "use" ]; then case " $* " in *" -h "*|*" --help "*) command vp "$@"; return; esac - __vp_out="$(VP_ENV_USE_EVAL_ENABLE=1 command vp "$@")" || return $? + __vp_out="$(VP_ENV_USE_EVAL_ENABLE=1 VP_SHELL=sh command vp "$@")" || return $? eval "$__vp_out" else command vp "$@" From e1ab2e01d29990ab7130dc0e130557193f19b48a Mon Sep 17 00:00:00 2001 From: nekomoyi Date: Fri, 22 May 2026 14:54:19 +0800 Subject: [PATCH 10/10] fix: remove unused shell infer --- Cargo.lock | 51 +----- Cargo.toml | 1 - crates/vite_global_cli/Cargo.toml | 3 - .../vite_global_cli/src/commands/env/use.rs | 10 ++ crates/vite_global_cli/src/commands/shell.rs | 167 ++++-------------- 5 files changed, 41 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 784926c0b2..30af104f53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2489,7 +2489,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3631,8 +3631,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", - "dispatch2", - "objc2", ] [[package]] @@ -3651,37 +3649,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.11.0", - "objc2", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-open-directory" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" -dependencies = [ - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -6767,21 +6734,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81c645a4de0d803ced6ef0388a2646aa1ef8467173b5d59a2c33c88de4ab76e7" -[[package]] -name = "sysinfo" -version = "0.39.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14311e7e9a03114cd4b65eedd54e8fed2945e17f08586ae97ef53bc0669f9581" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "objc2-open-directory", - "windows 0.62.2", -] - [[package]] name = "tar" version = "0.4.45" @@ -7583,7 +7535,6 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sysinfo", "tempfile", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 57395e3979..e24c5d438a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -266,7 +266,6 @@ simdutf8 = "0.1.5" string_cache = "0.9.0" sugar_path = { version = "2.0.1", features = ["cached_current_dir"] } syn = { version = "2", default-features = false } -sysinfo = "0.39.2" tar = "0.4.43" tempfile = "3.14.0" terminal_size = "0.4.2" diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index 5ce1470def..ab914d5911 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -39,9 +39,6 @@ vite_shared = { workspace = true } vite_str = { workspace = true } vite_workspace = { workspace = true } -[target.'cfg(windows)'.dependencies] -sysinfo = { workspace = true } - [dev-dependencies] serial_test = { workspace = true } tempfile = { workspace = true } diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index ad5c31fd0f..57e0e7d572 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -210,6 +210,16 @@ mod tests { assert_eq!(shell, Shell::NuShell); } + #[test] + fn test_detect_shell_posix_default() { + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_test()); + let shell = detect_shell(); + #[cfg(not(windows))] + assert_eq!(shell, Shell::Posix); + #[cfg(windows)] + assert_eq!(shell, Shell::Cmd); + } + #[test] fn test_format_export_posix() { let result = format_export(&Shell::Posix, "20.18.0"); diff --git a/crates/vite_global_cli/src/commands/shell.rs b/crates/vite_global_cli/src/commands/shell.rs index ac5c36f836..9d6824ccc1 100644 --- a/crates/vite_global_cli/src/commands/shell.rs +++ b/crates/vite_global_cli/src/commands/shell.rs @@ -6,9 +6,6 @@ use directories::BaseDirs; use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_str::Str; -/// Maximum depth to walk up the process tree for shell inference. -const MAX_PROCESS_DEPTH: u8 = 10; - /// Detected shell type for output formatting. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Shell { @@ -41,8 +38,7 @@ impl FromStr for Shell { /// Detect the current shell: /// 1. `VP_SHELL` environment variable -/// 2. Process tree inference -/// 3. Platform default +/// 2. Platform default #[must_use] pub fn detect_shell() -> Shell { let config = vite_shared::EnvConfig::get(); @@ -54,141 +50,10 @@ pub fn detect_shell() -> Shell { } } - // 2. Infer from process tree - if let Some(shell) = infer_shell_from_process_tree() { - return shell; - } - - // 3. Platform default + // 2. Platform default if cfg!(windows) { Shell::Cmd } else { Shell::Posix } } -/// Infer shell by walking up the process tree. -/// Returns the first known shell. -fn infer_shell_from_process_tree() -> Option { - #[cfg(unix)] - { - infer_shell_unix() - } - #[cfg(windows)] - { - infer_shell_windows() - } -} - -#[cfg(unix)] -fn infer_shell_unix() -> Option { - let mut pid = Some(std::process::id()); - let mut visited = 0u8; - - while let Some(current_pid) = pid { - if visited >= MAX_PROCESS_DEPTH { - return None; - } - - let info = get_process_info(current_pid)?; - let binary = info.command.trim_start_matches('-').split('/').next_back()?; - - if let Ok(shell) = Shell::from_str(binary) { - return Some(shell); - } - - tracing::debug!("binary is not a supported shell: {:?}", binary); - - pid = info.parent_pid; - visited += 1; - } - - None -} - -#[cfg(unix)] -struct ProcessInfo { - parent_pid: Option, - command: String, -} - -/// Get process name and parent PID -#[cfg(unix)] -fn get_process_info(pid: u32) -> Option { - let output = std::process::Command::new("ps") - .args(["-o", "ppid,comm", "-p", &pid.to_string()]) - .output() - .ok()?; - - if !output.status.success() { - tracing::debug!( - "ps failed while reading process info for pid {pid}: {}", - String::from_utf8_lossy(&output.stderr) - ); - return None; - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let mut lines = stdout.lines(); - - // Skip header line - lines.next()?; - - let line = lines.next()?; - let mut parts = line.split_whitespace(); - - let ppid = parts.next()?.parse().ok(); - let command = parts.next()?.to_string(); - - Some(ProcessInfo { parent_pid: ppid, command }) -} - -/// Get process name and parent PID on Windows using sysinfo. -#[cfg(windows)] -fn infer_shell_windows() -> Option { - use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind}; - - let mut system = System::new(); - let mut current_pid = Some(sysinfo::get_current_pid().ok()?); - - system.refresh_processes_specifics( - ProcessesToUpdate::All, - true, - ProcessRefreshKind::nothing().with_exe(UpdateKind::OnlyIfNotSet), - ); - - let mut visited = 0u8; - - while let Some(pid) = current_pid { - if visited >= MAX_PROCESS_DEPTH { - return None; - } - - if let Some(process) = system.process(pid) { - current_pid = process.parent(); - - let process_name = process - .exe() - .and_then(|x| x.file_stem()) - .and_then(|x| x.to_str()) - .map(str::to_lowercase); - - if let Some(shell) = process_name - .as_ref() - .map(|x| x.as_str()) - .and_then(|name| Shell::from_str(name).ok()) - { - return Some(shell); - } - - tracing::debug!("binary is not a supported shell: {:?}", process_name); - } else { - tracing::debug!("process not found for pid {pid}"); - current_pid = None; - } - - visited += 1; - } - - None -} - /// All shell profile files that interactive terminal sessions may source. /// This matches the files that `install.sh` writes to and `vp implode` cleans. #[cfg(not(windows))] @@ -454,4 +319,32 @@ mod tests { let shell = detect_shell(); assert_eq!(shell, Shell::Fish); } + + #[test] + fn test_detect_shell_defaults_without_vp_shell() { + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + vp_shell: None, + ..vite_shared::EnvConfig::for_test() + }); + let shell = detect_shell(); + if cfg!(windows) { + assert_eq!(shell, Shell::Cmd); + } else { + assert_eq!(shell, Shell::Posix); + } + } + + #[test] + fn test_detect_shell_invalid_vp_shell_falls_back_to_default() { + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + vp_shell: Some("invalid".into()), + ..vite_shared::EnvConfig::for_test() + }); + let shell = detect_shell(); + if cfg!(windows) { + assert_eq!(shell, Shell::Cmd); + } else { + assert_eq!(shell, Shell::Posix); + } + } }