From c64eeda31714d2fab01f02e1088d7bc5405845ea Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Thu, 14 May 2026 16:28:25 +0800 Subject: [PATCH 01/14] fix(cli): keep env use session-scoped on Windows --- crates/vite_global_cli/src/cli.rs | 27 +++++- .../src/commands/env/config.rs | 66 +++++++++++-- .../src/commands/env/doctor.rs | 41 ++++++-- .../vite_global_cli/src/commands/env/mod.rs | 30 +++++- .../vite_global_cli/src/commands/env/setup.rs | 6 +- .../vite_global_cli/src/commands/env/use.rs | 94 ++++++++++++++++++- crates/vite_global_cli/src/js_executor.rs | 21 +++-- crates/vite_global_cli/src/shim/dispatch.rs | 26 ++--- docs/guide/env.md | 12 +++ .../cli-helper-message/snap.txt | 2 +- 10 files changed, 279 insertions(+), 46 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 4e864f555b..73eef0e652 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -5,7 +5,7 @@ use std::{ffi::OsStr, process::ExitStatus}; -use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; +use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum}; use clap_complete::ArgValueCompleter; use tokio::runtime::Runtime; use vite_path::AbsolutePathBuf; @@ -243,6 +243,7 @@ impl Commands { Examples: Setup: vp env setup # Create shims for node, npm, npx + vp env --shell powershell # Print PowerShell setup code vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session @@ -269,11 +270,35 @@ Related Commands: vp update -g [package] # Update global packages vp list -g [package] # List global packages")] pub struct EnvArgs { + /// Shell syntax to print when no subcommand is provided + #[arg(long, value_enum)] + pub shell: Option, + + /// Print setup without directory-change hooks + #[arg(long)] + pub use_no_cd: bool, + /// Subcommand (e.g., 'default', 'setup', 'doctor', 'which') #[command(subcommand)] pub command: Option, } +/// Shell syntax for `vp env` output. +#[derive(Clone, Copy, Debug, ValueEnum)] +pub enum EnvShell { + /// POSIX shell (bash, zsh, sh) + #[value(alias = "bash", alias = "zsh", alias = "sh")] + Posix, + /// Fish shell + Fish, + /// Nushell + #[value(alias = "nushell")] + Nu, + /// PowerShell + #[value(alias = "pwsh", alias = "power-shell")] + Powershell, +} + /// Subcommands for the `env` command #[derive(clap::Subcommand, Debug)] pub enum EnvSubcommands { diff --git a/crates/vite_global_cli/src/commands/env/config.rs b/crates/vite_global_cli/src/commands/env/config.rs index bc0f248f27..8162799dce 100644 --- a/crates/vite_global_cli/src/commands/env/config.rs +++ b/crates/vite_global_cli/src/commands/env/config.rs @@ -198,7 +198,7 @@ pub async fn delete_session_version() -> Result<(), Error> { /// /// Resolution order: /// 0. `VP_NODE_VERSION` env var (session override from `vp env use`) -/// 1. `.session-node-version` file (session override written by `vp env use` for shell-wrapper-less environments) +/// 1. `.session-node-version` file (non-Windows fallback for shell-wrapper-less environments) /// 2. `.node-version` file in current or parent directories /// 3. `package.json#engines.node` in current or parent directories /// 4. `package.json#devEngines.runtime` in current or parent directories @@ -219,15 +219,18 @@ pub async fn resolve_version(cwd: &AbsolutePath) -> Result Result Result) -> Result { + let vite_plus_home = config::get_vp_home()?; + setup::create_env_files(&vite_plus_home).await?; + + let shell = shell.unwrap_or_else(|| match detect_shell() { + Shell::Fish => EnvShell::Fish, + Shell::NuShell => EnvShell::Nu, + Shell::PowerShell => EnvShell::Powershell, + Shell::Posix | Shell::Cmd => EnvShell::Posix, + }); + + let env_file = match shell { + EnvShell::Posix => "env", + EnvShell::Fish => "env.fish", + EnvShell::Nu => "env.nu", + EnvShell::Powershell => "env.ps1", + }; + + let content = tokio::fs::read_to_string(vite_plus_home.join(env_file)).await?; + print!("{content}"); + + Ok(ExitStatus::default()) +} + /// Print shell snippet for setting environment (`vp env print`) async fn print_env(cwd: AbsolutePathBuf) -> Result { // Resolve the Node.js version for the current directory diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 7fb6813956..acc6d10342 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -451,7 +451,9 @@ async fn cleanup_legacy_completion_dir(vite_plus_home: &vite_path::AbsolutePath) /// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function /// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function /// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`) -async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> { +pub(crate) async fn create_env_files( + vite_plus_home: &vite_path::AbsolutePath, +) -> Result<(), Error> { let bin_path = vite_plus_home.join("bin"); // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env) @@ -741,7 +743,7 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) { println!(); println!(" For PowerShell, add to your $PROFILE:"); println!(); - println!(" . \"{home_path}/env.ps1\""); + println!(" vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression"); println!(); println!(" For IDE support (VS Code, Cursor), ensure bin directory is in system PATH:"); diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index 5076aa849c..ae45d16bb4 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -12,7 +12,10 @@ use std::process::ExitStatus; use vite_path::AbsolutePathBuf; -use super::config::{self, VERSION_ENV_VAR}; +use super::{ + config::{self, VERSION_ENV_VAR}, + exit_status, +}; use crate::{ commands::shell::{Shell, detect_shell}, error::Error, @@ -49,6 +52,19 @@ fn has_eval_wrapper() -> bool { vite_shared::EnvConfig::get().env_use_eval_enable } +fn can_use_session_file() -> bool { + cfg!(not(windows)) || vite_shared::EnvConfig::get().is_ci +} + +fn print_windows_eval_wrapper_required() { + eprintln!( + "vp env use on Windows requires the Vite+ PowerShell wrapper to affect only the current shell session." + ); + eprintln!("Run this in PowerShell first:"); + eprintln!(" vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression"); + eprintln!("Or add that line to your PowerShell $PROFILE."); +} + /// Execute the `vp env use` command. pub async fn execute( cwd: AbsolutePathBuf, @@ -61,10 +77,17 @@ pub async fn execute( // Handle --unset: remove session override if unset { + // Clean up legacy file state even when eval mode is active. On Windows, + // this file is shared through VP_HOME and can leak across shell windows. + config::delete_session_version().await?; if has_eval_wrapper() { println!("{}", format_unset(&shell)); - } else { + } else if can_use_session_file() { config::delete_session_version().await?; + } else { + eprintln!("Removed file-based Node.js version override if it existed."); + print_windows_eval_wrapper_required(); + return Ok(ExitStatus::default()); } eprintln!("Reverted to file-based Node.js version resolution"); return Ok(ExitStatus::default()); @@ -79,10 +102,15 @@ pub async fn execute( (resolved, format!("{ver}")) } else { // No version argument - unset session override first + config::delete_session_version().await?; if has_eval_wrapper() { println!("{}", format_unset(&shell)); - } else { + } else if can_use_session_file() { config::delete_session_version().await?; + } else { + eprintln!("Reverted to file-based Node.js version resolution"); + print_windows_eval_wrapper_required(); + return Ok(ExitStatus::default()); } // Now resolve from project files (not from session override) let resolution = config::resolve_version_from_files(&cwd).await?; @@ -101,8 +129,13 @@ pub async fn execute( if current.as_deref() == Some(&resolved_version) { // Already active — idempotent, skip stderr status message if has_eval_wrapper() { + config::delete_session_version().await?; println!("{}", format_export(&shell, &resolved_version)); } else { + if !can_use_session_file() { + print_windows_eval_wrapper_required(); + return Ok(exit_status(1)); + } config::write_session_version(&resolved_version).await?; } return Ok(ExitStatus::default()); @@ -133,10 +166,15 @@ pub async fn execute( } if has_eval_wrapper() { + config::delete_session_version().await?; // Output the shell command to stdout (consumed by shell wrapper's eval) println!("{}", format_export(&shell, &resolved_version)); } else { // No eval wrapper (CI or direct invocation) — write session file so shims can read it + if !can_use_session_file() { + print_windows_eval_wrapper_required(); + return Ok(exit_status(1)); + } config::write_session_version(&resolved_version).await?; } @@ -277,4 +315,54 @@ mod tests { let result = format_unset(&Shell::NuShell); assert_eq!(result, "hide-env VP_NODE_VERSION"); } + + #[cfg(windows)] + #[tokio::test] + async fn test_windows_direct_use_without_eval_wrapper_does_not_write_session_file() { + let temp_dir = tempfile::TempDir::new().unwrap(); + let cwd = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let _guard = vite_shared::EnvConfig::test_guard( + vite_shared::EnvConfig::for_test_with_home(temp_dir.path()), + ); + + let status = execute(cwd, Some("20.18.0".into()), false, true, false).await.unwrap(); + + assert_eq!(status.code(), Some(1)); + assert!(config::read_session_version().await.is_none()); + } + + #[cfg(windows)] + #[tokio::test] + async fn test_windows_ci_direct_use_writes_session_file() { + let temp_dir = tempfile::TempDir::new().unwrap(); + let cwd = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + is_ci: true, + ..vite_shared::EnvConfig::for_test_with_home(temp_dir.path()) + }); + + let status = execute(cwd, Some("20.18.0".into()), false, true, false).await.unwrap(); + + assert!(status.success()); + assert_eq!(config::read_session_version().await.as_deref(), Some("20.18.0")); + } + + #[cfg(windows)] + #[tokio::test] + async fn test_windows_eval_wrapper_cleans_legacy_session_file() { + let temp_dir = tempfile::TempDir::new().unwrap(); + 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, + ..vite_shared::EnvConfig::for_test_with_home(temp_dir.path()) + }); + + config::write_session_version("22.0.0").await.unwrap(); + + let status = execute(cwd, Some("20.18.0".into()), false, true, false).await.unwrap(); + + assert!(status.success()); + assert!(config::read_session_version().await.is_none()); + } } diff --git a/crates/vite_global_cli/src/js_executor.rs b/crates/vite_global_cli/src/js_executor.rs index 3d1e7f5806..2c926b0b72 100644 --- a/crates/vite_global_cli/src/js_executor.rs +++ b/crates/vite_global_cli/src/js_executor.rs @@ -188,15 +188,18 @@ impl JsExecutor { self.check_runtime_compatibility(&session_version, Some(VP_NODE_VERSION), false) .await?; Some(session_version) - } else if let Some(session_version) = config::read_session_version().await { - // Read from file - self.check_runtime_compatibility( - &session_version, - Some(SESSION_VERSION_FILE), - false, - ) - .await?; - Some(session_version) + } else if cfg!(not(windows)) || vite_shared::EnvConfig::get().is_ci { + if let Some(session_version) = config::read_session_version().await { + self.check_runtime_compatibility( + &session_version, + Some(SESSION_VERSION_FILE), + false, + ) + .await?; + Some(session_version) + } else { + None + } } else { None }; diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 44813c261e..b4f5810595 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -1038,17 +1038,21 @@ async fn resolve_with_cache(cwd: &AbsolutePathBuf) -> Result` prints shell setup code for the current session - `vp env on` enables managed mode so shims always use Vite+-managed Node.js - `vp env off` enables system-first mode so shims prefer system Node.js first - `vp env print` prints the shell snippet for the current session +PowerShell needs to evaluate the setup code in the current shell before `vp env use` can affect only that shell session. This is the same pattern used by tools such as fnm: + +```powershell +vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression +``` + +Add that line to your PowerShell `$PROFILE` to apply it automatically in new shells. It does not require elevated privileges. + +In CI, `vp env use` can still run without shell initialization. It writes a temporary session file under `VP_HOME` so later shim calls in the same job can resolve the selected Node.js version. + ### Manage - `vp env default` sets or shows the global default Node.js version @@ -63,6 +74,7 @@ This switches to system-first mode, where the shims prefer your system Node.js a ```bash # Setup vp env setup # Create shims for node, npm, npx +vp env --shell powershell # Print PowerShell setup code vp env on # Use Vite+ managed Node.js vp env print # Print shell snippet for this session diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index a8ef8dd79e..f6b9fc9034 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -367,6 +367,7 @@ Inspect: Examples: Setup: vp env setup # Create shims for node, npm, npx + vp env --shell powershell # Print PowerShell setup code vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session @@ -414,4 +415,3 @@ Options: -h, --help Print help Documentation: https://viteplus.dev/guide/upgrade - From 6d8e660017fb9d781db4646e63b6c825e43e5d25 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Thu, 14 May 2026 21:33:27 +0800 Subject: [PATCH 02/14] test(cli): update helper snapshot --- packages/cli/snap-tests-global/cli-helper-message/snap.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index f6b9fc9034..a8ef8dd79e 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -367,7 +367,6 @@ Inspect: Examples: Setup: vp env setup # Create shims for node, npm, npx - vp env --shell powershell # Print PowerShell setup code vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session @@ -415,3 +414,4 @@ Options: -h, --help Print help Documentation: https://viteplus.dev/guide/upgrade + From bd45495c616e7a702f69842ee7b88c03e501f130 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Thu, 14 May 2026 21:38:22 +0800 Subject: [PATCH 03/14] docs(env): clarify powershell session setup --- docs/guide/env.md | 4 ++-- rfcs/env-command.md | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/guide/env.md b/docs/guide/env.md index 239ec271f0..4493d2be68 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -34,7 +34,7 @@ This switches to system-first mode, where the shims prefer your system Node.js a - `vp env off` enables system-first mode so shims prefer system Node.js first - `vp env print` prints the shell snippet for the current session -PowerShell needs to evaluate the setup code in the current shell before `vp env use` can affect only that shell session. This is the same pattern used by tools such as fnm: +PowerShell needs to evaluate the setup code in the current shell before `vp env use` can affect only that shell session: ```powershell vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression @@ -74,7 +74,7 @@ In CI, `vp env use` can still run without shell initialization. It writes a temp ```bash # Setup vp env setup # Create shims for node, npm, npx -vp env --shell powershell # Print PowerShell setup code +vp env --use-no-cd --shell powershell # Print PowerShell setup code vp env on # Use Vite+ managed Node.js vp env print # Print shell snippet for this session diff --git a/rfcs/env-command.md b/rfcs/env-command.md index af61b74f2c..21220c1a1d 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -50,6 +50,9 @@ vp env default # Control shim mode vp env on # Enable managed mode (shims always use vite-plus Node.js) vp env off # Enable system-first mode (shims prefer system Node.js) + +# Print shell setup code for the current session +vp env --use-no-cd --shell powershell ``` ### Diagnostic Commands @@ -122,12 +125,18 @@ vp env use --silent-if-unchanged # Suppress output if version already active 1. `~/.vite-plus/env` includes a `vp()` shell function that intercepts `vp env use` calls 2. The wrapper sets `VITE_PLUS_ENV_USE_EVAL_ENABLE=1` before calling `command vp env use ...` 3. When the env var is present (wrapper active), `vp env use` outputs shell commands to stdout for eval -4. When the env var is absent (CI, direct invocation), `vp env use` writes a session file (`~/.vite-plus/.session-node-version`) instead +4. When the env var is absent in CI, `vp env use` writes a session file (`~/.vite-plus/.session-node-version`) instead 5. The shim dispatch checks `VITE_PLUS_NODE_VERSION` env var first, then the session file, in the resolution chain -**Automatic session file (for CI / wrapper-less environments):** +On Windows interactive shells, `vp env use` requires the PowerShell setup to be evaluated in the current shell so the selected version stays session-scoped: + +```powershell +vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression +``` + +**Automatic session file (for CI):** -When `vp env use` detects that the shell eval wrapper is not active (i.e., `VITE_PLUS_ENV_USE_EVAL_ENABLE` is not set), it automatically writes the resolved version to `~/.vite-plus/.session-node-version`. Shims read this file directly from disk, so `vp env use` works without the shell wrapper — no extra flags needed. The env var still takes priority when set, so the shell wrapper experience is unchanged. +When `vp env use` detects a CI environment and the shell eval wrapper is not active (i.e., `VITE_PLUS_ENV_USE_EVAL_ENABLE` is not set), it automatically writes the resolved version to `~/.vite-plus/.session-node-version`. Shims read this file directly from disk, so CI jobs can keep using `vp env use` without shell setup. The env var still takes priority when set, so the shell wrapper experience is unchanged. ```bash # GitHub Actions example (no shell wrapper, session file written automatically) @@ -519,8 +528,8 @@ When resolving which Node.js version to use, vite-plus checks the following sour - Overrides all file-based resolution 1. **`.session-node-version`** file (session override) - - Written by `vp env use` to `~/.vite-plus/.session-node-version` - - Works without shell eval wrapper (CI environments) + - Written by `vp env use` to `~/.vite-plus/.session-node-version` in CI + - Preserves wrapper-less CI behavior without making Windows interactive shells global - Deleted by `vp env use --unset` 2. **`.node-version`** file From b87ff81dd02653999ccda43a09f6f5852b3ed0e9 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Thu, 14 May 2026 21:39:40 +0800 Subject: [PATCH 04/14] docs(env): document powershell profile setup --- docs/guide/env.md | 14 +++++++++++++- rfcs/env-command.md | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/guide/env.md b/docs/guide/env.md index 4493d2be68..1d8ac3560e 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -40,7 +40,19 @@ PowerShell needs to evaluate the setup code in the current shell before `vp env vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression ``` -Add that line to your PowerShell `$PROFILE` to apply it automatically in new shells. It does not require elevated privileges. +Add that line to the end of your PowerShell `$PROFILE` to apply it automatically in new shells. It does not require elevated privileges. + +Create the profile file if it does not already exist: + +```powershell +if (-not (Test-Path $PROFILE)) { New-Item $PROFILE -Force } +``` + +Open the profile file for editing: + +```powershell +Invoke-Item $PROFILE +``` In CI, `vp env use` can still run without shell initialization. It writes a temporary session file under `VP_HOME` so later shim calls in the same job can resolve the selected Node.js version. diff --git a/rfcs/env-command.md b/rfcs/env-command.md index 21220c1a1d..4891563f5c 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -134,6 +134,13 @@ On Windows interactive shells, `vp env use` requires the PowerShell setup to be vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression ``` +Add that line to the end of the PowerShell `$PROFILE` to apply it automatically in new shells: + +```powershell +if (-not (Test-Path $PROFILE)) { New-Item $PROFILE -Force } +Invoke-Item $PROFILE +``` + **Automatic session file (for CI):** When `vp env use` detects a CI environment and the shell eval wrapper is not active (i.e., `VITE_PLUS_ENV_USE_EVAL_ENABLE` is not set), it automatically writes the resolved version to `~/.vite-plus/.session-node-version`. Shims read this file directly from disk, so CI jobs can keep using `vp env use` without shell setup. The env var still takes priority when set, so the shell wrapper experience is unchanged. From 7166761fcea000eb88b59b835ee51297ea21349c Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Thu, 14 May 2026 21:53:05 +0800 Subject: [PATCH 05/14] fix(env): address powershell session review --- .../src/commands/env/config.rs | 18 ++++---- .../src/commands/env/doctor.rs | 41 ++++--------------- docs/guide/env.md | 1 - 3 files changed, 17 insertions(+), 43 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/config.rs b/crates/vite_global_cli/src/commands/env/config.rs index 8162799dce..06e8902711 100644 --- a/crates/vite_global_cli/src/commands/env/config.rs +++ b/crates/vite_global_cli/src/commands/env/config.rs @@ -198,7 +198,7 @@ pub async fn delete_session_version() -> Result<(), Error> { /// /// Resolution order: /// 0. `VP_NODE_VERSION` env var (session override from `vp env use`) -/// 1. `.session-node-version` file (non-Windows fallback for shell-wrapper-less environments) +/// 1. `.session-node-version` file (session override written by `vp env use` for shell-wrapper-less environments) /// 2. `.node-version` file in current or parent directories /// 3. `package.json#engines.node` in current or parent directories /// 4. `package.json#devEngines.runtime` in current or parent directories @@ -958,14 +958,14 @@ mod tests { assert!(result.is_ok()); } - #[cfg(not(windows))] #[tokio::test] async fn test_resolve_version_session_file_takes_priority_over_node_version() { let temp_dir = TempDir::new().unwrap(); let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let _guard = vite_shared::EnvConfig::test_guard( - vite_shared::EnvConfig::for_test_with_home(temp_dir.path()), - ); + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + is_ci: cfg!(windows), + ..vite_shared::EnvConfig::for_test_with_home(temp_dir.path()) + }); // Create .node-version file tokio::fs::write(temp_path.join(".node-version"), "20.18.0\n").await.unwrap(); @@ -1070,14 +1070,14 @@ mod tests { /// Verify that the session file source is accepted by `vp env install` (no-arg) source validation. /// This is a regression test ensuring `vp env use 24` followed by `vp env install` /// works when the session file is the resolution source. - #[cfg(not(windows))] #[tokio::test] async fn test_session_file_source_accepted_by_install_validation() { let temp_dir = TempDir::new().unwrap(); let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let _guard = vite_shared::EnvConfig::test_guard( - vite_shared::EnvConfig::for_test_with_home(temp_dir.path()), - ); + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + is_ci: cfg!(windows), + ..vite_shared::EnvConfig::for_test_with_home(temp_dir.path()) + }); // Write session version file write_session_version("22.0.0").await.unwrap(); diff --git a/crates/vite_global_cli/src/commands/env/doctor.rs b/crates/vite_global_cli/src/commands/env/doctor.rs index 5890930e2b..5dbf3da638 100644 --- a/crates/vite_global_cli/src/commands/env/doctor.rs +++ b/crates/vite_global_cli/src/commands/env/doctor.rs @@ -323,39 +323,14 @@ fn check_session_override() { } } - #[cfg(not(windows))] - { - if let Some(version) = config::read_session_version_sync() { - print_check( - &output::WARN_SIGN.yellow().to_string(), - "Session override (file)", - &format!("{}={version}", config::SESSION_VERSION_FILE).yellow().to_string(), - ); - print_hint("Written by 'vp env use'. Run 'vp env use --unset' to remove."); - } - } - - #[cfg(windows)] - { - if let Some(version) = config::read_session_version_sync() { - if vite_shared::EnvConfig::get().is_ci { - print_check( - &output::WARN_SIGN.yellow().to_string(), - "Session override (file)", - &format!("{}={version}", config::SESSION_VERSION_FILE).yellow().to_string(), - ); - print_hint("Written by 'vp env use'. Run 'vp env use --unset' to remove."); - } else { - print_check( - &output::WARN_SIGN.yellow().to_string(), - "Legacy override file", - &format!("{} ignored on Windows", config::SESSION_VERSION_FILE) - .yellow() - .to_string(), - ); - print_hint("Run 'vp env use --unset' to remove it."); - } - } + // Also check session version file + if let Some(version) = config::read_session_version_sync() { + print_check( + &output::WARN_SIGN.yellow().to_string(), + "Session override (file)", + &format!("{}={version}", config::SESSION_VERSION_FILE).yellow().to_string(), + ); + print_hint("Written by 'vp env use'. Run 'vp env use --unset' to remove."); } } diff --git a/docs/guide/env.md b/docs/guide/env.md index 1d8ac3560e..5ea5330119 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -86,7 +86,6 @@ In CI, `vp env use` can still run without shell initialization. It writes a temp ```bash # Setup vp env setup # Create shims for node, npm, npx -vp env --use-no-cd --shell powershell # Print PowerShell setup code vp env on # Use Vite+ managed Node.js vp env print # Print shell snippet for this session From 8ff2fac57532b335da4884372e32aa7e6a0e36ea Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Thu, 14 May 2026 22:03:15 +0800 Subject: [PATCH 06/14] fix(env): preserve session file readers --- .../src/commands/env/config.rs | 62 +++---------------- crates/vite_global_cli/src/js_executor.rs | 21 +++---- crates/vite_global_cli/src/shim/dispatch.rs | 26 ++++---- 3 files changed, 29 insertions(+), 80 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/config.rs b/crates/vite_global_cli/src/commands/env/config.rs index 06e8902711..45e955a797 100644 --- a/crates/vite_global_cli/src/commands/env/config.rs +++ b/crates/vite_global_cli/src/commands/env/config.rs @@ -219,18 +219,15 @@ pub async fn resolve_version(cwd: &AbsolutePath) -> Result Result Date: Thu, 14 May 2026 22:41:21 +0800 Subject: [PATCH 07/14] refactor(env): drop unused --use-no-cd flag and dedup session-file deletes - Remove the --use-no-cd flag: it was wired into the parser but never read by print_shell_env, and env.ps1 has no directory-change hooks for it to opt out of. Update the recommended PowerShell setup line everywhere (cli help, print_path_instructions, wrapper-required warning, docs/RFC). - Drop the duplicated config::delete_session_version() calls in use.rs: the unconditional delete at the top of each branch already covers all three sub-cases (eval-wrapper, session-file-allowed, Windows non-CI), so the inner delete inside `else if can_use_session_file()` was a guaranteed-NotFound syscall. - Flatten three `else { if ... }` blocks into `else if ...` (silent-if- unchanged, tail write block, unset branch) and drop the redundant "Removed file-based ... if it existed" line that was superseded by the existing "Reverted to file-based Node.js version resolution" message. - Sync the `vp env --help` example for the PowerShell setup into help.rs's hand-maintained mirror; the clap after_help in cli.rs is not rendered for `vp env --help` (the unified renderer wins), so the PowerShell example was previously invisible to users. --- crates/vite_global_cli/src/cli.rs | 6 +--- .../vite_global_cli/src/commands/env/mod.rs | 2 +- .../vite_global_cli/src/commands/env/setup.rs | 2 +- .../vite_global_cli/src/commands/env/use.rs | 32 +++++++------------ crates/vite_global_cli/src/help.rs | 1 + docs/guide/env.md | 2 +- rfcs/env-command.md | 4 +-- 7 files changed, 19 insertions(+), 30 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 73eef0e652..5bf493b63b 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -243,7 +243,7 @@ impl Commands { Examples: Setup: vp env setup # Create shims for node, npm, npx - vp env --shell powershell # Print PowerShell setup code + vp env --shell powershell | Out-String | Invoke-Expression # PowerShell session setup vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session @@ -274,10 +274,6 @@ pub struct EnvArgs { #[arg(long, value_enum)] pub shell: Option, - /// Print setup without directory-change hooks - #[arg(long)] - pub use_no_cd: bool, - /// Subcommand (e.g., 'default', 'setup', 'doctor', 'which') #[command(subcommand)] pub command: Option, diff --git a/crates/vite_global_cli/src/commands/env/mod.rs b/crates/vite_global_cli/src/commands/env/mod.rs index 178f46c065..89e2abf3e9 100644 --- a/crates/vite_global_cli/src/commands/env/mod.rs +++ b/crates/vite_global_cli/src/commands/env/mod.rs @@ -136,7 +136,7 @@ pub async fn execute(cwd: AbsolutePathBuf, args: EnvArgs) -> Result Result { let shell = detect_shell(); - // Handle --unset: remove session override + // Handle --unset: remove session override. + // Always delete the session file: on Windows it lives under VP_HOME and can + // leak across shell windows, so even eval mode must clean it up. if unset { - // Clean up legacy file state even when eval mode is active. On Windows, - // this file is shared through VP_HOME and can leak across shell windows. config::delete_session_version().await?; if has_eval_wrapper() { println!("{}", format_unset(&shell)); - } else if can_use_session_file() { - config::delete_session_version().await?; - } else { - eprintln!("Removed file-based Node.js version override if it existed."); + } else if !can_use_session_file() { print_windows_eval_wrapper_required(); - return Ok(ExitStatus::default()); } eprintln!("Reverted to file-based Node.js version resolution"); return Ok(ExitStatus::default()); @@ -105,9 +101,7 @@ pub async fn execute( config::delete_session_version().await?; if has_eval_wrapper() { println!("{}", format_unset(&shell)); - } else if can_use_session_file() { - config::delete_session_version().await?; - } else { + } else if !can_use_session_file() { eprintln!("Reverted to file-based Node.js version resolution"); print_windows_eval_wrapper_required(); return Ok(ExitStatus::default()); @@ -131,11 +125,10 @@ pub async fn execute( if has_eval_wrapper() { config::delete_session_version().await?; println!("{}", format_export(&shell, &resolved_version)); + } else if !can_use_session_file() { + print_windows_eval_wrapper_required(); + return Ok(exit_status(1)); } else { - if !can_use_session_file() { - print_windows_eval_wrapper_required(); - return Ok(exit_status(1)); - } config::write_session_version(&resolved_version).await?; } return Ok(ExitStatus::default()); @@ -169,12 +162,11 @@ pub async fn execute( config::delete_session_version().await?; // Output the shell command to stdout (consumed by shell wrapper's eval) println!("{}", format_export(&shell, &resolved_version)); + } else if !can_use_session_file() { + print_windows_eval_wrapper_required(); + return Ok(exit_status(1)); } else { // No eval wrapper (CI or direct invocation) — write session file so shims can read it - if !can_use_session_file() { - print_windows_eval_wrapper_required(); - return Ok(exit_status(1)); - } config::write_session_version(&resolved_version).await?; } diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index ed70359bae..46c85e5bac 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -546,6 +546,7 @@ fn env_help_doc() -> HelpDoc { vec![ " Setup:", " vp env setup # Create shims for node, npm, npx", + " vp env --shell powershell | Out-String | Invoke-Expression # PowerShell session setup", " vp env on # Use vite-plus managed Node.js", " vp env print # Print shell snippet for this session", "", diff --git a/docs/guide/env.md b/docs/guide/env.md index 5ea5330119..51044c7a39 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -37,7 +37,7 @@ This switches to system-first mode, where the shims prefer your system Node.js a PowerShell needs to evaluate the setup code in the current shell before `vp env use` can affect only that shell session: ```powershell -vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression +vp env --shell powershell | Out-String | Invoke-Expression ``` Add that line to the end of your PowerShell `$PROFILE` to apply it automatically in new shells. It does not require elevated privileges. diff --git a/rfcs/env-command.md b/rfcs/env-command.md index 4891563f5c..03c0d4b012 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -52,7 +52,7 @@ vp env on # Enable managed mode (shims always use vite-plus Node.js) vp env off # Enable system-first mode (shims prefer system Node.js) # Print shell setup code for the current session -vp env --use-no-cd --shell powershell +vp env --shell powershell ``` ### Diagnostic Commands @@ -131,7 +131,7 @@ vp env use --silent-if-unchanged # Suppress output if version already active On Windows interactive shells, `vp env use` requires the PowerShell setup to be evaluated in the current shell so the selected version stays session-scoped: ```powershell -vp env --use-no-cd --shell powershell | Out-String | Invoke-Expression +vp env --shell powershell | Out-String | Invoke-Expression ``` Add that line to the end of the PowerShell `$PROFILE` to apply it automatically in new shells: From b617644e4655126570b13e6d54405c95534466aa Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 14 May 2026 22:49:33 +0800 Subject: [PATCH 08/14] docs(env): split PowerShell setup into its own help subsection The full `vp env --shell powershell | Out-String | Invoke-Expression` line is much longer than the other Setup examples, so leaving it inline broke the `#`-comment column alignment. Move it into its own `PowerShell (add to $PROFILE):` subsection (matches the existing Setup/Manage/Inspect/Execute structure) and drop the inline comment. Update the cli-helper-message snapshot to match. --- crates/vite_global_cli/src/cli.rs | 4 +++- crates/vite_global_cli/src/help.rs | 4 +++- packages/cli/snap-tests-global/cli-helper-message/snap.txt | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 5bf493b63b..a6c9a6f22d 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -243,10 +243,12 @@ impl Commands { Examples: Setup: vp env setup # Create shims for node, npm, npx - vp env --shell powershell | Out-String | Invoke-Expression # PowerShell session setup vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session + PowerShell (add to $PROFILE): + vp env --shell powershell | Out-String | Invoke-Expression + Manage: vp env pin lts # Pin to latest LTS version vp env install # Install version from .node-version / package.json diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index 46c85e5bac..9f9fa42fb1 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -546,10 +546,12 @@ fn env_help_doc() -> HelpDoc { vec![ " Setup:", " vp env setup # Create shims for node, npm, npx", - " vp env --shell powershell | Out-String | Invoke-Expression # PowerShell session setup", " vp env on # Use vite-plus managed Node.js", " vp env print # Print shell snippet for this session", "", + " PowerShell (add to $PROFILE):", + " vp env --shell powershell | Out-String | Invoke-Expression", + "", " Manage:", " vp env pin lts # Pin to latest LTS version", " vp env install # Install version from .node-version / package.json", diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index a8ef8dd79e..eb8be03394 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -370,6 +370,9 @@ Examples: vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session + PowerShell (add to $PROFILE): + vp env --shell powershell | Out-String | Invoke-Expression + Manage: vp env pin lts # Pin to latest LTS version vp env install # Install version from .node-version / package.json From f25d3217ced1d3a119498ca7305b11397f219268 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 14 May 2026 23:08:14 +0800 Subject: [PATCH 09/14] refactor(env): replace `--shell` flag with `vp env profile` subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote the previous top-level `--shell ` flag on `vp env` into a proper `Profile { shell: Option }` subcommand so the new capability fits the existing noun-verb subcommand pattern (current, print, default, on, off, setup, doctor, which, pin, unpin, list, ...) instead of being the only top-level flag on the command. The recommended PowerShell setup invocation becomes: vp env profile --shell powershell | Out-String | Invoke-Expression Side-effect-free implementation: - Lift the four shell env-file templates (POSIX/Fish/Nushell/PowerShell) and the cmd.exe wrapper to module-scope constants in setup.rs. - Add `setup::render_env_content(shell, vite_plus_home) -> String` as a pure helper that performs the placeholder substitution for one shell. - `create_env_files` now loops over the four shells calling the helper, removing the inline-template duplication. - `vp env profile` builds the content directly via `render_env_content` and prints it. No call to `create_env_files`, no read-back of a file we just wrote — important because the recommended `$PROFILE` line runs on every PowerShell startup, which previously rewrote all five env files (~20KB of disk churn) on every shell launch. Also reverts `create_env_files` from `pub(crate)` back to private now that the profile path no longer needs it, syncs help.rs subcommand list + Examples block, and updates the use.rs warning text plus docs/RFC. --- crates/vite_global_cli/src/cli.rs | 15 +- .../vite_global_cli/src/commands/env/mod.rs | 27 +--- .../vite_global_cli/src/commands/env/setup.rs | 153 ++++++++++-------- .../vite_global_cli/src/commands/env/use.rs | 2 +- crates/vite_global_cli/src/help.rs | 6 +- docs/guide/env.md | 4 +- .../cli-helper-message/snap.txt | 11 +- rfcs/env-command.md | 6 +- 8 files changed, 116 insertions(+), 108 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index a6c9a6f22d..cf27f11ca3 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -247,7 +247,7 @@ Examples: vp env print # Print shell snippet for this session PowerShell (add to $PROFILE): - vp env --shell powershell | Out-String | Invoke-Expression + vp env profile --shell powershell | Out-String | Invoke-Expression Manage: vp env pin lts # Pin to latest LTS version @@ -272,16 +272,12 @@ Related Commands: vp update -g [package] # Update global packages vp list -g [package] # List global packages")] pub struct EnvArgs { - /// Shell syntax to print when no subcommand is provided - #[arg(long, value_enum)] - pub shell: Option, - /// Subcommand (e.g., 'default', 'setup', 'doctor', 'which') #[command(subcommand)] pub command: Option, } -/// Shell syntax for `vp env` output. +/// Shell syntax for `vp env profile` output. #[derive(Clone, Copy, Debug, ValueEnum)] pub enum EnvShell { /// POSIX shell (bash, zsh, sh) @@ -310,6 +306,13 @@ pub enum EnvSubcommands { /// Print shell snippet to set environment for current session Print, + /// Print the full shell setup script for `$PROFILE`/rc-file evaluation + Profile { + /// Target shell syntax (defaults to the auto-detected current shell) + #[arg(long, value_enum)] + shell: Option, + }, + /// Set or show the global default Node.js version Default { /// Version to set as default (e.g., "20.18.0", "lts", "latest") diff --git a/crates/vite_global_cli/src/commands/env/mod.rs b/crates/vite_global_cli/src/commands/env/mod.rs index 89e2abf3e9..3c7db7c37e 100644 --- a/crates/vite_global_cli/src/commands/env/mod.rs +++ b/crates/vite_global_cli/src/commands/env/mod.rs @@ -58,6 +58,7 @@ pub async fn execute(cwd: AbsolutePathBuf, args: EnvArgs) -> Result current::execute(cwd, json).await, crate::cli::EnvSubcommands::Print => print_env(cwd).await, + crate::cli::EnvSubcommands::Profile { shell } => print_profile(shell), crate::cli::EnvSubcommands::Default { version } => default::execute(cwd, version).await, crate::cli::EnvSubcommands::On => on::execute().await, crate::cli::EnvSubcommands::Off => off::execute().await, @@ -136,11 +137,7 @@ pub async fn execute(cwd: AbsolutePathBuf, args: EnvArgs) -> Result Result) -> Result { +/// Print the full shell setup script (`vp env profile`). +/// +/// Pure: no disk I/O, safe to run on every shell startup via +/// `vp env profile --shell powershell | Out-String | Invoke-Expression` in `$PROFILE`. +fn print_profile(shell: Option) -> Result { let vite_plus_home = config::get_vp_home()?; - setup::create_env_files(&vite_plus_home).await?; - let shell = shell.unwrap_or_else(|| match detect_shell() { Shell::Fish => EnvShell::Fish, Shell::NuShell => EnvShell::Nu, Shell::PowerShell => EnvShell::Powershell, Shell::Posix | Shell::Cmd => EnvShell::Posix, }); - - let env_file = match shell { - EnvShell::Posix => "env", - EnvShell::Fish => "env.fish", - EnvShell::Nu => "env.nu", - EnvShell::Powershell => "env.ps1", - }; - - let content = tokio::fs::read_to_string(vite_plus_home.join(env_file)).await?; - print!("{content}"); - + print!("{}", setup::render_env_content(shell, &vite_plus_home)); Ok(ExitStatus::default()) } diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 5f415b08c3..c205b9e12c 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -20,7 +20,7 @@ use std::process::ExitStatus; use owo_colors::OwoColorize; use super::config::{get_bin_dir, get_vp_home}; -use crate::{error::Error, help}; +use crate::{cli::EnvShell, error::Error, help}; /// Tools to create shims for (node, npm, npx, vpx, vpr) pub(crate) const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"]; @@ -443,43 +443,12 @@ async fn cleanup_legacy_completion_dir(vite_plus_home: &vite_path::AbsolutePath) } } -/// Create env files with PATH guard (prevents duplicate PATH entries). -/// -/// Creates: -/// - `~/.vite-plus/env` (POSIX shell — bash/zsh) with `vp()` wrapper function -/// - `~/.vite-plus/env.fish` (fish shell) with `vp` wrapper function -/// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function -/// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function -/// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`) -pub(crate) async fn create_env_files( - vite_plus_home: &vite_path::AbsolutePath, -) -> Result<(), Error> { - let bin_path = vite_plus_home.join("bin"); - - // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env) - // This makes the env file portable across sessions where HOME may differ - let home_dir = vite_shared::EnvConfig::get().user_home; - let to_ref = |path: &vite_path::AbsolutePath| -> String { - home_dir - .as_ref() - .and_then(|h| path.as_path().strip_prefix(h).ok()) - .map(|s| { - // Normalize to forward slashes for $HOME/... paths (POSIX-style) - format!("$HOME/{}", s.display().to_string().replace('\\', "/")) - }) - .unwrap_or_else(|| path.as_path().display().to_string()) - }; - let bin_path_ref = to_ref(&bin_path); - // Nushell requires `~` instead of `$HOME` in string literals — `$HOME` is not expanded - // at parse time, so PATH entries would contain a literal "$HOME/..." segment. - let bin_path_ref_nu = bin_path_ref.replace("$HOME/", "~/"); - - // POSIX env file (bash/zsh) - // When sourced multiple times, removes existing entry and re-prepends to front - // Uses parameter expansion to split PATH around the bin entry in O(1) operations - // Includes vp() shell function wrapper for `vp env use` (evals stdout) - // Includes shell completion support - let env_content = r#"#!/bin/sh +// POSIX env file (bash/zsh) +// When sourced multiple times, removes existing entry and re-prepends to front +// Uses parameter expansion to split PATH around the bin entry in O(1) operations +// Includes vp() shell function wrapper for `vp env use` (evals stdout) +// Includes shell completion support +const ENV_TEMPLATE_POSIX: &str = r#"#!/bin/sh # Vite+ environment setup (https://viteplus.dev) __vp_bin="__VP_BIN__" case ":${PATH}:" in @@ -525,13 +494,9 @@ elif [ -n "$ZSH_VERSION" ] && type compdef >/dev/null 2>&1; then compdef _vpr_complete vpr ' fi -"# - .replace("__VP_BIN__", &bin_path_ref); - let env_file = vite_plus_home.join("env"); - tokio::fs::write(&env_file, env_content).await?; +"#; - // Fish env file with vp wrapper function - let env_fish_content = r#"# Vite+ environment setup (https://viteplus.dev) +const ENV_TEMPLATE_FISH: &str = r#"# Vite+ environment setup (https://viteplus.dev) set -l __vp_idx (contains -i -- __VP_BIN__ $PATH) and set -e PATH[$__vp_idx] set -gx PATH __VP_BIN__ $PATH @@ -560,15 +525,12 @@ function __vpr_complete VP_COMPLETE=fish command vp -- vp run $tokens[2..] $current end complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)" -"# - .replace("__VP_BIN__", &bin_path_ref); - let env_fish_file = vite_plus_home.join("env.fish"); - tokio::fs::write(&env_fish_file, env_fish_content).await?; - - // Nushell env file with vp wrapper function. - // Completions delegate to Fish dynamically (VP_COMPLETE=fish) because clap_complete_nushell - // generates multiple rest params (e.g. for `vp install`), which Nushell does not support. - let env_nu_content = r#"# Vite+ environment setup (https://viteplus.dev) +"#; + +// Nushell env file with vp wrapper function. +// Completions delegate to Fish dynamically (VP_COMPLETE=fish) because clap_complete_nushell +// generates multiple rest params (e.g. for `vp install`), which Nushell does not support. +const ENV_TEMPLATE_NU: &str = r#"# Vite+ environment setup (https://viteplus.dev) $env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__") # Shell function wrapper: intercepts `vp env use` to parse its stdout, @@ -626,13 +588,9 @@ def "nu-complete vpr" [context: string] { } } export extern "vpr" [...args: string@"nu-complete vpr"] -"# - .replace("__VP_BIN__", &bin_path_ref_nu); - let env_nu_file = vite_plus_home.join("env.nu"); - tokio::fs::write(&env_nu_file, env_nu_content).await?; +"#; - // PowerShell env file - let env_ps1_content = r#"# Vite+ environment setup (https://viteplus.dev) +const ENV_TEMPLATE_PS1: &str = r#"# Vite+ environment setup (https://viteplus.dev) $__vp_bin = "__VP_BIN_WIN__" if ($env:Path -split ';' -notcontains $__vp_bin) { $env:Path = "$__vp_bin;$env:Path" @@ -691,19 +649,72 @@ $__vpr_comp = { Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp "#; - // For PowerShell, use the actual absolute path (not $HOME-relative) - let bin_path_win = bin_path.as_path().display().to_string(); - let env_ps1_content = env_ps1_content.replace("__VP_BIN_WIN__", &bin_path_win); - let env_ps1_file = vite_plus_home.join("env.ps1"); - tokio::fs::write(&env_ps1_file, env_ps1_content).await?; +// cmd.exe wrapper for `vp env use` (cmd.exe cannot define shell functions). +// Users run `vp-use 24` in cmd.exe instead of `vp env use 24`. +const VP_USE_CMD_CONTENT: &str = "@echo off\r\nset VP_ENV_USE_EVAL_ENABLE=1\r\nfor /f \"delims=\" %%i in ('%~dp0..\\current\\bin\\vp.exe env use %*') do %%i\r\nset VP_ENV_USE_EVAL_ENABLE=\r\n"; + +/// Render the env-file content for `shell` against `vite_plus_home`. +/// +/// Pure: no disk I/O. Used by both `create_env_files` (writes the result +/// to ~/.vite-plus/env.*) and `vp env profile` (prints to stdout). +pub(crate) fn render_env_content( + shell: EnvShell, + vite_plus_home: &vite_path::AbsolutePath, +) -> String { + let bin_path = vite_plus_home.join("bin"); + + // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env). + // This makes the env file portable across sessions where HOME may differ. + let home_dir = vite_shared::EnvConfig::get().user_home; + let bin_path_ref = home_dir + .as_ref() + .and_then(|h| bin_path.as_path().strip_prefix(h).ok()) + .map(|s| { + // Normalize to forward slashes for $HOME/... paths (POSIX-style) + format!("$HOME/{}", s.display().to_string().replace('\\', "/")) + }) + .unwrap_or_else(|| bin_path.as_path().display().to_string()); + + match shell { + EnvShell::Posix => ENV_TEMPLATE_POSIX.replace("__VP_BIN__", &bin_path_ref), + EnvShell::Fish => ENV_TEMPLATE_FISH.replace("__VP_BIN__", &bin_path_ref), + EnvShell::Nu => { + // Nushell requires `~` instead of `$HOME` in string literals — `$HOME` is not + // expanded at parse time, so PATH entries would contain a literal "$HOME/...". + let bin_path_ref_nu = bin_path_ref.replace("$HOME/", "~/"); + ENV_TEMPLATE_NU.replace("__VP_BIN__", &bin_path_ref_nu) + } + EnvShell::Powershell => { + // PowerShell uses the actual absolute path (not $HOME-relative) + let bin_path_win = bin_path.as_path().display().to_string(); + ENV_TEMPLATE_PS1.replace("__VP_BIN_WIN__", &bin_path_win) + } + } +} + +/// Create env files with PATH guard (prevents duplicate PATH entries). +/// +/// Creates: +/// - `~/.vite-plus/env` (POSIX shell — bash/zsh) with `vp()` wrapper function +/// - `~/.vite-plus/env.fish` (fish shell) with `vp` wrapper function +/// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function +/// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function +/// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`) +async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> { + for (shell, file_name) in [ + (EnvShell::Posix, "env"), + (EnvShell::Fish, "env.fish"), + (EnvShell::Nu, "env.nu"), + (EnvShell::Powershell, "env.ps1"), + ] { + let content = render_env_content(shell, vite_plus_home); + tokio::fs::write(vite_plus_home.join(file_name), content).await?; + } - // cmd.exe wrapper for `vp env use` (cmd.exe cannot define shell functions) - // Users run `vp-use 24` in cmd.exe instead of `vp env use 24` - let vp_use_cmd_content = "@echo off\r\nset VP_ENV_USE_EVAL_ENABLE=1\r\nfor /f \"delims=\" %%i in ('%~dp0..\\current\\bin\\vp.exe env use %*') do %%i\r\nset VP_ENV_USE_EVAL_ENABLE=\r\n"; - // Only write if bin directory exists (it may not during --env-only) + // Only write the cmd wrapper if bin directory exists (it may not during --env-only) + let bin_path = vite_plus_home.join("bin"); if tokio::fs::try_exists(&bin_path).await.unwrap_or(false) { - let vp_use_cmd_file = bin_path.join("vp-use.cmd"); - tokio::fs::write(&vp_use_cmd_file, vp_use_cmd_content).await?; + tokio::fs::write(bin_path.join("vp-use.cmd"), VP_USE_CMD_CONTENT).await?; } Ok(()) @@ -743,7 +754,7 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) { println!(); println!(" For PowerShell, add to your $PROFILE:"); println!(); - println!(" vp env --shell powershell | Out-String | Invoke-Expression"); + println!(" vp env profile --shell powershell | Out-String | Invoke-Expression"); println!(); println!(" For IDE support (VS Code, Cursor), ensure bin directory is in system PATH:"); diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index 6acbccf9c4..e15e43df7f 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -61,7 +61,7 @@ fn print_windows_eval_wrapper_required() { "vp env use on Windows requires the Vite+ PowerShell wrapper to affect only the current shell session." ); eprintln!("Run this in PowerShell first:"); - eprintln!(" vp env --shell powershell | Out-String | Invoke-Expression"); + eprintln!(" vp env profile --shell powershell | Out-String | Invoke-Expression"); eprintln!("Or add that line to your PowerShell $PROFILE."); } diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index 9f9fa42fb1..b26c823b16 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -508,6 +508,10 @@ fn env_help_doc() -> HelpDoc { "Enable system-first mode - shims prefer system Node.js, fallback to managed", ), row("print", "Print shell snippet to set environment for current session"), + row( + "profile", + "Print the full shell setup script for `$PROFILE`/rc-file evaluation", + ), ], ), section_rows( @@ -550,7 +554,7 @@ fn env_help_doc() -> HelpDoc { " vp env print # Print shell snippet for this session", "", " PowerShell (add to $PROFILE):", - " vp env --shell powershell | Out-String | Invoke-Expression", + " vp env profile --shell powershell | Out-String | Invoke-Expression", "", " Manage:", " vp env pin lts # Pin to latest LTS version", diff --git a/docs/guide/env.md b/docs/guide/env.md index 51044c7a39..778bc19c48 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -29,7 +29,7 @@ This switches to system-first mode, where the shims prefer your system Node.js a ### Setup - `vp env setup` creates or updates shims in `VP_HOME/bin` -- `vp env --shell ` prints shell setup code for the current session +- `vp env profile [--shell ]` prints the full shell setup script (intended for `$PROFILE`/rc-file evaluation) - `vp env on` enables managed mode so shims always use Vite+-managed Node.js - `vp env off` enables system-first mode so shims prefer system Node.js first - `vp env print` prints the shell snippet for the current session @@ -37,7 +37,7 @@ This switches to system-first mode, where the shims prefer your system Node.js a PowerShell needs to evaluate the setup code in the current shell before `vp env use` can affect only that shell session: ```powershell -vp env --shell powershell | Out-String | Invoke-Expression +vp env profile --shell powershell | Out-String | Invoke-Expression ``` Add that line to the end of your PowerShell `$PROFILE` to apply it automatically in new shells. It does not require elevated privileges. diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index eb8be03394..4b93541ee4 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -343,10 +343,11 @@ Usage: vp env [COMMAND] Manage Node.js versions Setup: - setup Create or update shims in VP_HOME/bin - on Enable managed mode - shims always use vite-plus managed Node.js - off Enable system-first mode - shims prefer system Node.js, fallback to managed - print Print shell snippet to set environment for current session + setup Create or update shims in VP_HOME/bin + on Enable managed mode - shims always use vite-plus managed Node.js + off Enable system-first mode - shims prefer system Node.js, fallback to managed + print Print shell snippet to set environment for current session + profile Print the full shell setup script for `$PROFILE`/rc-file evaluation Manage: default Set or show the global default Node.js version @@ -371,7 +372,7 @@ Examples: vp env print # Print shell snippet for this session PowerShell (add to $PROFILE): - vp env --shell powershell | Out-String | Invoke-Expression + vp env profile --shell powershell | Out-String | Invoke-Expression Manage: vp env pin lts # Pin to latest LTS version diff --git a/rfcs/env-command.md b/rfcs/env-command.md index 03c0d4b012..ca6729d98c 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -51,8 +51,8 @@ vp env default vp env on # Enable managed mode (shims always use vite-plus Node.js) vp env off # Enable system-first mode (shims prefer system Node.js) -# Print shell setup code for the current session -vp env --shell powershell +# Print the full shell setup script (intended for $PROFILE/rc-file evaluation) +vp env profile --shell powershell ``` ### Diagnostic Commands @@ -131,7 +131,7 @@ vp env use --silent-if-unchanged # Suppress output if version already active On Windows interactive shells, `vp env use` requires the PowerShell setup to be evaluated in the current shell so the selected version stays session-scoped: ```powershell -vp env --shell powershell | Out-String | Invoke-Expression +vp env profile --shell powershell | Out-String | Invoke-Expression ``` Add that line to the end of the PowerShell `$PROFILE` to apply it automatically in new shells: From c462a93a37b00343c3603917d4592116c8fff9c1 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 14 May 2026 23:17:05 +0800 Subject: [PATCH 10/14] refactor(env): add EnvShell::env_file_name and clarify print vs profile - Add `EnvShell::env_file_name() -> &'static str` so the per-shell file name (env, env.fish, env.nu, env.ps1) lives on the enum instead of the inline tuple in `create_env_files`. Removes one source of drift; the rest of the codebase (notably `commands/shell.rs::ALL_SHELL_PROFILES`) can migrate to this method in a follow-up. - Disambiguate the `print` subcommand description against the new `profile` subcommand in both clap (`vp env print --help`) and the unified renderer (`vp env --help`). The contrast between "current session" and "$PROFILE-style setup" is now explicit. --- crates/vite_global_cli/src/cli.rs | 14 +++++++++++++- crates/vite_global_cli/src/commands/env/setup.rs | 9 ++------- crates/vite_global_cli/src/help.rs | 5 ++++- .../snap-tests-global/cli-helper-message/snap.txt | 2 +- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index cf27f11ca3..6a7111155f 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -293,6 +293,18 @@ pub enum EnvShell { Powershell, } +impl EnvShell { + /// File name written under `~/.vite-plus/` for this shell's setup script. + pub(crate) const fn env_file_name(self) -> &'static str { + match self { + EnvShell::Posix => "env", + EnvShell::Fish => "env.fish", + EnvShell::Nu => "env.nu", + EnvShell::Powershell => "env.ps1", + } + } +} + /// Subcommands for the `env` command #[derive(clap::Subcommand, Debug)] pub enum EnvSubcommands { @@ -303,7 +315,7 @@ pub enum EnvSubcommands { json: bool, }, - /// Print shell snippet to set environment for current session + /// Print PATH snippet for the current session (use `profile` for `$PROFILE`-style setup) Print, /// Print the full shell setup script for `$PROFILE`/rc-file evaluation diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index c205b9e12c..2adba5afe5 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -701,14 +701,9 @@ pub(crate) fn render_env_content( /// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function /// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`) async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> { - for (shell, file_name) in [ - (EnvShell::Posix, "env"), - (EnvShell::Fish, "env.fish"), - (EnvShell::Nu, "env.nu"), - (EnvShell::Powershell, "env.ps1"), - ] { + for shell in [EnvShell::Posix, EnvShell::Fish, EnvShell::Nu, EnvShell::Powershell] { let content = render_env_content(shell, vite_plus_home); - tokio::fs::write(vite_plus_home.join(file_name), content).await?; + tokio::fs::write(vite_plus_home.join(shell.env_file_name()), content).await?; } // Only write the cmd wrapper if bin directory exists (it may not during --env-only) diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index b26c823b16..999a38df42 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -507,7 +507,10 @@ fn env_help_doc() -> HelpDoc { "off", "Enable system-first mode - shims prefer system Node.js, fallback to managed", ), - row("print", "Print shell snippet to set environment for current session"), + row( + "print", + "Print PATH snippet for the current session (use `profile` for `$PROFILE`-style setup)", + ), row( "profile", "Print the full shell setup script for `$PROFILE`/rc-file evaluation", diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index 4b93541ee4..f3bacf04e0 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -346,7 +346,7 @@ Setup: setup Create or update shims in VP_HOME/bin on Enable managed mode - shims always use vite-plus managed Node.js off Enable system-first mode - shims prefer system Node.js, fallback to managed - print Print shell snippet to set environment for current session + print Print PATH snippet for the current session (use `profile` for `$PROFILE`-style setup) profile Print the full shell setup script for `$PROFILE`/rc-file evaluation Manage: From 90dfd48c144c6b41417293ecfb4071ecedb62340 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 15 May 2026 09:03:39 +0800 Subject: [PATCH 11/14] refactor(env): drop `vp env profile`, dot-source the generated env.ps1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `vp env setup` already writes `~/.vite-plus/env.ps1` to disk, so the new `vp env profile` subcommand was redundant: the recommended PowerShell `$PROFILE` line just needs to dot-source the existing file. That avoids spawning a `vp` subprocess on every shell startup, removes the chicken-and-egg requirement that `vp` already be on PATH, and matches the pattern used by rustup/mise/fnm/sdkman. . "$env:USERPROFILE\.vite-plus\env.ps1" Changes: - Remove `EnvSubcommands::Profile` and the `print_profile` handler. - Remove the `clap::ValueEnum` derive and `#[value(alias = ...)]` attributes on `EnvShell` — it is no longer exposed via the CLI, only used internally by `setup::create_env_files` / `render_env_content`. - Restore the original `print` subcommand description (the `profile` cross-reference is no longer needed). - Update the recommended PowerShell setup line everywhere (cli help examples, help.rs Examples block, print_path_instructions, the `print_windows_eval_wrapper_required` warning, docs/guide/env.md, rfcs/env-command.md). - Make `render_env_content` private again now that the `vp env profile` caller is gone; keep the lift-templates-to-consts cleanup since `create_env_files` still benefits. --- crates/vite_global_cli/src/cli.rs | 20 +++++-------------- .../vite_global_cli/src/commands/env/mod.rs | 19 +----------------- .../vite_global_cli/src/commands/env/setup.rs | 10 ++-------- .../vite_global_cli/src/commands/env/use.rs | 6 +++--- crates/vite_global_cli/src/help.rs | 11 ++-------- docs/guide/env.md | 7 +++---- .../cli-helper-message/snap.txt | 11 +++++----- rfcs/env-command.md | 8 ++++---- 8 files changed, 25 insertions(+), 67 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 6a7111155f..9bca219143 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -5,7 +5,7 @@ use std::{ffi::OsStr, process::ExitStatus}; -use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum}; +use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; use tokio::runtime::Runtime; use vite_path::AbsolutePathBuf; @@ -247,7 +247,7 @@ Examples: vp env print # Print shell snippet for this session PowerShell (add to $PROFILE): - vp env profile --shell powershell | Out-String | Invoke-Expression + . \"$env:USERPROFILE\\.vite-plus\\env.ps1\" Manage: vp env pin lts # Pin to latest LTS version @@ -277,19 +277,16 @@ pub struct EnvArgs { pub command: Option, } -/// Shell syntax for `vp env profile` output. -#[derive(Clone, Copy, Debug, ValueEnum)] +/// Shells that get a generated `~/.vite-plus/env.*` setup script. +#[derive(Clone, Copy, Debug)] pub enum EnvShell { /// POSIX shell (bash, zsh, sh) - #[value(alias = "bash", alias = "zsh", alias = "sh")] Posix, /// Fish shell Fish, /// Nushell - #[value(alias = "nushell")] Nu, /// PowerShell - #[value(alias = "pwsh", alias = "power-shell")] Powershell, } @@ -315,16 +312,9 @@ pub enum EnvSubcommands { json: bool, }, - /// Print PATH snippet for the current session (use `profile` for `$PROFILE`-style setup) + /// Print shell snippet to set environment for current session Print, - /// Print the full shell setup script for `$PROFILE`/rc-file evaluation - Profile { - /// Target shell syntax (defaults to the auto-detected current shell) - #[arg(long, value_enum)] - shell: Option, - }, - /// Set or show the global default Node.js version Default { /// Version to set as default (e.g., "20.18.0", "lts", "latest") diff --git a/crates/vite_global_cli/src/commands/env/mod.rs b/crates/vite_global_cli/src/commands/env/mod.rs index 3c7db7c37e..6202039cb9 100644 --- a/crates/vite_global_cli/src/commands/env/mod.rs +++ b/crates/vite_global_cli/src/commands/env/mod.rs @@ -27,7 +27,7 @@ use std::process::ExitStatus; use vite_path::AbsolutePathBuf; use crate::{ - cli::{EnvArgs, EnvShell, EnvSubcommands}, + cli::{EnvArgs, EnvSubcommands}, commands::shell::{Shell, detect_shell}, error::Error, }; @@ -58,7 +58,6 @@ pub async fn execute(cwd: AbsolutePathBuf, args: EnvArgs) -> Result current::execute(cwd, json).await, crate::cli::EnvSubcommands::Print => print_env(cwd).await, - crate::cli::EnvSubcommands::Profile { shell } => print_profile(shell), crate::cli::EnvSubcommands::Default { version } => default::execute(cwd, version).await, crate::cli::EnvSubcommands::On => on::execute().await, crate::cli::EnvSubcommands::Off => off::execute().await, @@ -153,22 +152,6 @@ pub async fn execute(cwd: AbsolutePathBuf, args: EnvArgs) -> Result) -> Result { - let vite_plus_home = config::get_vp_home()?; - let shell = shell.unwrap_or_else(|| match detect_shell() { - Shell::Fish => EnvShell::Fish, - Shell::NuShell => EnvShell::Nu, - Shell::PowerShell => EnvShell::Powershell, - Shell::Posix | Shell::Cmd => EnvShell::Posix, - }); - print!("{}", setup::render_env_content(shell, &vite_plus_home)); - Ok(ExitStatus::default()) -} - /// Print shell snippet for setting environment (`vp env print`) async fn print_env(cwd: AbsolutePathBuf) -> Result { // Resolve the Node.js version for the current directory diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 2adba5afe5..47a2683fe1 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -654,13 +654,7 @@ Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp const VP_USE_CMD_CONTENT: &str = "@echo off\r\nset VP_ENV_USE_EVAL_ENABLE=1\r\nfor /f \"delims=\" %%i in ('%~dp0..\\current\\bin\\vp.exe env use %*') do %%i\r\nset VP_ENV_USE_EVAL_ENABLE=\r\n"; /// Render the env-file content for `shell` against `vite_plus_home`. -/// -/// Pure: no disk I/O. Used by both `create_env_files` (writes the result -/// to ~/.vite-plus/env.*) and `vp env profile` (prints to stdout). -pub(crate) fn render_env_content( - shell: EnvShell, - vite_plus_home: &vite_path::AbsolutePath, -) -> String { +fn render_env_content(shell: EnvShell, vite_plus_home: &vite_path::AbsolutePath) -> String { let bin_path = vite_plus_home.join("bin"); // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env). @@ -749,7 +743,7 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) { println!(); println!(" For PowerShell, add to your $PROFILE:"); println!(); - println!(" vp env profile --shell powershell | Out-String | Invoke-Expression"); + println!(" . \"$env:USERPROFILE\\.vite-plus\\env.ps1\""); println!(); println!(" For IDE support (VS Code, Cursor), ensure bin directory is in system PATH:"); diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index e15e43df7f..1306a56e47 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -60,9 +60,9 @@ fn print_windows_eval_wrapper_required() { eprintln!( "vp env use on Windows requires the Vite+ PowerShell wrapper to affect only the current shell session." ); - eprintln!("Run this in PowerShell first:"); - eprintln!(" vp env profile --shell powershell | Out-String | Invoke-Expression"); - eprintln!("Or add that line to your PowerShell $PROFILE."); + eprintln!("Add this line to your PowerShell $PROFILE:"); + eprintln!(" . \"$env:USERPROFILE\\.vite-plus\\env.ps1\""); + eprintln!("Then start a new PowerShell session."); } /// Execute the `vp env use` command. diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index 999a38df42..c6cd0bf1b9 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -507,14 +507,7 @@ fn env_help_doc() -> HelpDoc { "off", "Enable system-first mode - shims prefer system Node.js, fallback to managed", ), - row( - "print", - "Print PATH snippet for the current session (use `profile` for `$PROFILE`-style setup)", - ), - row( - "profile", - "Print the full shell setup script for `$PROFILE`/rc-file evaluation", - ), + row("print", "Print shell snippet to set environment for current session"), ], ), section_rows( @@ -557,7 +550,7 @@ fn env_help_doc() -> HelpDoc { " vp env print # Print shell snippet for this session", "", " PowerShell (add to $PROFILE):", - " vp env profile --shell powershell | Out-String | Invoke-Expression", + " . \"$env:USERPROFILE\\.vite-plus\\env.ps1\"", "", " Manage:", " vp env pin lts # Pin to latest LTS version", diff --git a/docs/guide/env.md b/docs/guide/env.md index 778bc19c48..68b7f97dda 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -28,16 +28,15 @@ This switches to system-first mode, where the shims prefer your system Node.js a ### Setup -- `vp env setup` creates or updates shims in `VP_HOME/bin` -- `vp env profile [--shell ]` prints the full shell setup script (intended for `$PROFILE`/rc-file evaluation) +- `vp env setup` creates or updates shims in `VP_HOME/bin` (and writes the per-shell setup scripts under `~/.vite-plus/`) - `vp env on` enables managed mode so shims always use Vite+-managed Node.js - `vp env off` enables system-first mode so shims prefer system Node.js first - `vp env print` prints the shell snippet for the current session -PowerShell needs to evaluate the setup code in the current shell before `vp env use` can affect only that shell session: +PowerShell needs to dot-source the generated setup script in the current shell before `vp env use` can affect only that shell session: ```powershell -vp env profile --shell powershell | Out-String | Invoke-Expression +. "$env:USERPROFILE\.vite-plus\env.ps1" ``` Add that line to the end of your PowerShell `$PROFILE` to apply it automatically in new shells. It does not require elevated privileges. diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index f3bacf04e0..9c92a0084f 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -343,11 +343,10 @@ Usage: vp env [COMMAND] Manage Node.js versions Setup: - setup Create or update shims in VP_HOME/bin - on Enable managed mode - shims always use vite-plus managed Node.js - off Enable system-first mode - shims prefer system Node.js, fallback to managed - print Print PATH snippet for the current session (use `profile` for `$PROFILE`-style setup) - profile Print the full shell setup script for `$PROFILE`/rc-file evaluation + setup Create or update shims in VP_HOME/bin + on Enable managed mode - shims always use vite-plus managed Node.js + off Enable system-first mode - shims prefer system Node.js, fallback to managed + print Print shell snippet to set environment for current session Manage: default Set or show the global default Node.js version @@ -372,7 +371,7 @@ Examples: vp env print # Print shell snippet for this session PowerShell (add to $PROFILE): - vp env profile --shell powershell | Out-String | Invoke-Expression + . "$env:USERPROFILE\.vite-plus\env.ps1" Manage: vp env pin lts # Pin to latest LTS version diff --git a/rfcs/env-command.md b/rfcs/env-command.md index ca6729d98c..ef0c584356 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -51,8 +51,8 @@ vp env default vp env on # Enable managed mode (shims always use vite-plus Node.js) vp env off # Enable system-first mode (shims prefer system Node.js) -# Print the full shell setup script (intended for $PROFILE/rc-file evaluation) -vp env profile --shell powershell +# PowerShell session setup: dot-source the script generated by `vp env setup` +. "$env:USERPROFILE\.vite-plus\env.ps1" ``` ### Diagnostic Commands @@ -128,10 +128,10 @@ vp env use --silent-if-unchanged # Suppress output if version already active 4. When the env var is absent in CI, `vp env use` writes a session file (`~/.vite-plus/.session-node-version`) instead 5. The shim dispatch checks `VITE_PLUS_NODE_VERSION` env var first, then the session file, in the resolution chain -On Windows interactive shells, `vp env use` requires the PowerShell setup to be evaluated in the current shell so the selected version stays session-scoped: +On Windows interactive shells, `vp env use` requires the PowerShell setup script (`~/.vite-plus/env.ps1`, written by `vp env setup`) to be dot-sourced in the current shell so the selected version stays session-scoped: ```powershell -vp env profile --shell powershell | Out-String | Invoke-Expression +. "$env:USERPROFILE\.vite-plus\env.ps1" ``` Add that line to the end of the PowerShell `$PROFILE` to apply it automatically in new shells: From 0c3e9aa9a1d3259c5b4fb56c6edcf0f151552c95 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 15 May 2026 09:23:21 +0800 Subject: [PATCH 12/14] refactor(env): move EnvShell into setup.rs and tighten wrapper-required hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Relocate `EnvShell` enum and `env_file_name()` from `cli.rs` into `commands/env/setup.rs`. With `vp env profile` gone, the type is no longer CLI surface — only `create_env_files` and `render_env_content` use it. Drop `pub`/`pub(crate)` since callers are now same-module. - Reword `print_windows_eval_wrapper_required`: the previous "Then start a new PowerShell session" hint left the user's current PowerShell still wrapper-less. Updated to "dot-source it now (or open a new PowerShell session)" so the user can re-run `vp env use` without restarting the shell. --- crates/vite_global_cli/src/cli.rs | 25 ------------------- .../vite_global_cli/src/commands/env/setup.rs | 23 ++++++++++++++++- .../vite_global_cli/src/commands/env/use.rs | 2 +- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 9bca219143..4b5d5fd11e 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -277,31 +277,6 @@ pub struct EnvArgs { pub command: Option, } -/// Shells that get a generated `~/.vite-plus/env.*` setup script. -#[derive(Clone, Copy, Debug)] -pub enum EnvShell { - /// POSIX shell (bash, zsh, sh) - Posix, - /// Fish shell - Fish, - /// Nushell - Nu, - /// PowerShell - Powershell, -} - -impl EnvShell { - /// File name written under `~/.vite-plus/` for this shell's setup script. - pub(crate) const fn env_file_name(self) -> &'static str { - match self { - EnvShell::Posix => "env", - EnvShell::Fish => "env.fish", - EnvShell::Nu => "env.nu", - EnvShell::Powershell => "env.ps1", - } - } -} - /// Subcommands for the `env` command #[derive(clap::Subcommand, Debug)] pub enum EnvSubcommands { diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 47a2683fe1..9aa22b176c 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -20,7 +20,28 @@ use std::process::ExitStatus; use owo_colors::OwoColorize; use super::config::{get_bin_dir, get_vp_home}; -use crate::{cli::EnvShell, error::Error, help}; +use crate::{error::Error, help}; + +/// Shells that get a generated `~/.vite-plus/env.*` setup script. +#[derive(Clone, Copy, Debug)] +enum EnvShell { + Posix, + Fish, + Nu, + Powershell, +} + +impl EnvShell { + /// File name written under `~/.vite-plus/` for this shell's setup script. + const fn env_file_name(self) -> &'static str { + match self { + EnvShell::Posix => "env", + EnvShell::Fish => "env.fish", + EnvShell::Nu => "env.nu", + EnvShell::Powershell => "env.ps1", + } + } +} /// Tools to create shims for (node, npm, npx, vpx, vpr) pub(crate) const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"]; diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index 1306a56e47..ed1da3684f 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -62,7 +62,7 @@ fn print_windows_eval_wrapper_required() { ); eprintln!("Add this line to your PowerShell $PROFILE:"); eprintln!(" . \"$env:USERPROFILE\\.vite-plus\\env.ps1\""); - eprintln!("Then start a new PowerShell session."); + eprintln!("Then dot-source it now (or open a new PowerShell session) to load the wrapper."); } /// Execute the `vp env use` command. From 6e3806950004bc66d6ad34d4c12eb6b7cba76c76 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 15 May 2026 09:46:45 +0800 Subject: [PATCH 13/14] docs(env): drop PowerShell $PROFILE block from env help examples The PowerShell setup line is documented in docs/guide/env.md and the RFC; including it inline in the `vp env --help` Examples is noise for the common (POSIX) audience and was the only Windows-specific example in the block. --- crates/vite_global_cli/src/cli.rs | 3 --- crates/vite_global_cli/src/help.rs | 3 --- packages/cli/snap-tests-global/cli-helper-message/snap.txt | 3 --- 3 files changed, 9 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 4b5d5fd11e..4e864f555b 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -246,9 +246,6 @@ Examples: vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session - PowerShell (add to $PROFILE): - . \"$env:USERPROFILE\\.vite-plus\\env.ps1\" - Manage: vp env pin lts # Pin to latest LTS version vp env install # Install version from .node-version / package.json diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index c6cd0bf1b9..ed70359bae 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -549,9 +549,6 @@ fn env_help_doc() -> HelpDoc { " vp env on # Use vite-plus managed Node.js", " vp env print # Print shell snippet for this session", "", - " PowerShell (add to $PROFILE):", - " . \"$env:USERPROFILE\\.vite-plus\\env.ps1\"", - "", " Manage:", " vp env pin lts # Pin to latest LTS version", " vp env install # Install version from .node-version / package.json", diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index 9c92a0084f..a8ef8dd79e 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -370,9 +370,6 @@ Examples: vp env on # Use vite-plus managed Node.js vp env print # Print shell snippet for this session - PowerShell (add to $PROFILE): - . "$env:USERPROFILE\.vite-plus\env.ps1" - Manage: vp env pin lts # Pin to latest LTS version vp env install # Install version from .node-version / package.json From 56d76bd034d14542538e81c30679767b6bb0feb0 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 15 May 2026 10:09:49 +0800 Subject: [PATCH 14/14] fix(env): use computed home_path for PowerShell setup instructions `vp env setup` writes `env.ps1` under the actual `vite_plus_home`, but the printed PowerShell `$PROFILE` instruction was hardcoded to `$env:USERPROFILE\.vite-plus\env.ps1`. With a custom `VP_HOME` the file written and the file the user is told to dot-source did not match, so subsequent `vp env use` calls kept hitting the wrapper-required error even though setup succeeded. Use the same `home_path` already computed for the POSIX/Fish/Nushell instructions. --- 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 9aa22b176c..bd37fa311f 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -764,7 +764,7 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) { println!(); println!(" For PowerShell, add to your $PROFILE:"); println!(); - println!(" . \"$env:USERPROFILE\\.vite-plus\\env.ps1\""); + println!(" . \"{home_path}/env.ps1\""); println!(); println!(" For IDE support (VS Code, Cursor), ensure bin directory is in system PATH:");