diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 9c3c078a61..88cd8ce9fd 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -542,7 +542,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 "$@" @@ -579,7 +579,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 @@ -611,7 +612,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_SHELL: "nu" } { ^vp ...$args }) let lines = ($out | lines) @@ -674,7 +675,7 @@ function vp { & (Join-Path $__vp_bin "vp") @args; return } $env:VP_ENV_USE_EVAL_ENABLE = "1" - $env:VP_SHELL_PWSH = "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 @@ -683,7 +684,7 @@ function vp { } } Remove-Item Env:VP_ENV_USE_EVAL_ENABLE -ErrorAction SilentlyContinue - Remove-Item Env:VP_SHELL_PWSH -ErrorAction SilentlyContinue + Remove-Item Env:VP_SHELL -ErrorAction SilentlyContinue if ($LASTEXITCODE -eq 0 -and $output) { Invoke-Expression ($output -join "`n") } @@ -897,10 +898,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"); } @@ -1086,10 +1083,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] diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index ed1da3684f..57e0e7d572 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -181,72 +181,43 @@ 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] 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!(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 +317,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..9d6824ccc1 100644 --- a/crates/vite_global_cli/src/commands/shell.rs +++ b/crates/vite_global_cli/src/commands/shell.rs @@ -1,5 +1,7 @@ //! Shared shell detection and profile helpers. +use std::str::FromStr; + use directories::BaseDirs; use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_str::Str; @@ -19,21 +21,37 @@ 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. 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. Platform default + if cfg!(windows) { Shell::Cmd } else { Shell::Posix } } /// All shell profile files that interactive terminal sessions may source. @@ -235,53 +253,98 @@ 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_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] - fn test_detect_shell_posix_default() { - let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_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(); - #[cfg(not(windows))] - assert!(matches!(shell, Shell::Posix)); - #[cfg(windows)] - assert!(matches!(shell, Shell::Cmd)); + if cfg!(windows) { + assert_eq!(shell, Shell::Cmd); + } else { + assert_eq!(shell, Shell::Posix); + } } } diff --git a/crates/vite_shared/src/env_config.rs b/crates/vite_shared/src/env_config.rs index d3fe20c3cc..2298ab8d17 100644 --- a/crates/vite_shared/src/env_config.rs +++ b/crates/vite_shared/src/env_config.rs @@ -113,33 +113,10 @@ pub struct EnvConfig { /// Env: `HOME` (Unix) / `USERPROFILE` (Windows) pub user_home: Option, - /// Fish shell version (indicates running under fish). + /// Explicitly specify the current shell. /// - /// 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, - - /// Explicit Nu shell eval signal set by the `env.nu` wrapper. - /// - /// 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 { @@ -167,11 +144,7 @@ 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_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(), } } @@ -254,11 +227,7 @@ impl EnvConfig { update_task_types: None, node_version: None, user_home: None, - 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"; 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..13999014b3 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-use-shells/snap.txt @@ -0,0 +1,35 @@ +> 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 ) 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..a71df8844a --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-use-shells/steps.json @@ -0,0 +1,17 @@ +{ + "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" + ] +}