From 8aa8bf04c0df7f92b4182dba59ff846e0f6a3c22 Mon Sep 17 00:00:00 2001 From: Liang Date: Sat, 17 Oct 2026 22:27:39 +0800 Subject: [PATCH] fix(env): honor system-first mode in `vp env exec` and `vp node` --- .../shim_system_first_fallback/snapshots.toml | 2 ++ .../snapshots/placement_and_path_precedence.md | 16 ++++++++++++++++ crates/vp_global_cli/src/commands/env/config.rs | 12 ++++++++++++ crates/vp_global_cli/src/commands/env/exec.rs | 14 +++++++++++--- crates/vp_global_cli/src/commands/env/setup.rs | 9 +-------- 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots.toml index 557ff7c1b0..5975e30399 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots.toml @@ -13,6 +13,7 @@ steps = [ { argv = ["node", "list-shims.cjs", "$VP_HOME/bin", "$VP_HOME/fallback-bin"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${VP_HOME}/fallback-bin${PATH_SEPARATOR}${PATH}"]], comment = "Only Node moves to fallback." }, { argv = ["node", "-p", "process.execPath.includes('system-bin')"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], comment = "The shell selects system Node before fallback." }, { argv = ["node", "assert-node-resolution.cjs"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], comment = "Internal Node resolution agrees with PATH." }, + { argv = ["vp", "node", "-p", "process.execPath.includes('system-bin')"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], comment = "vp node (env exec node) selects system Node like the shell." }, # Move the pnpm family together; refresh must preserve both preferences. ["vp", "env", "off", "pnpm"], @@ -23,6 +24,7 @@ steps = [ # Reverse PATH: reaching the Vite+ shim first must select managed Node. { argv = ["node", "-p", "process.execPath.includes('system-bin')"], envs = [["PATH", "${VP_HOME}/fallback-bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], comment = "A fallback shim before system Node selects managed Node." }, { argv = ["node", "assert-node-resolution.cjs"], envs = [["PATH", "${VP_HOME}/fallback-bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], comment = "Internal resolution must also stop at the first Vite+ shim." }, + { argv = ["vp", "node", "-p", "process.execPath.includes('system-bin')"], envs = [["PATH", "${VP_HOME}/fallback-bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], comment = "vp node also stops at the first Vite+ shim." }, # Restore each family independently; fallback should end up empty. ["vp", "env", "on", "node"], diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots/placement_and_path_precedence.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots/placement_and_path_precedence.md index 88891e2551..6f10353cc1 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots/placement_and_path_precedence.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shim_system_first_fallback/snapshots/placement_and_path_precedence.md @@ -52,6 +52,14 @@ Internal Node resolution agrees with PATH. env which selects the same Node as PATH ``` +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} vp node -p process.execPath.includes('system-bin')` + +vp node (env exec node) selects system Node like the shell. + +``` +true +``` + ## `vp env off pnpm` ``` @@ -101,6 +109,14 @@ Internal resolution must also stop at the first Vite+ shim. env which selects the same Node as PATH ``` +## `PATH=${VP_HOME}/fallback-bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} vp node -p process.execPath.includes('system-bin')` + +vp node also stops at the first Vite+ shim. + +``` +false +``` + ## `vp env on node` ``` diff --git a/crates/vp_global_cli/src/commands/env/config.rs b/crates/vp_global_cli/src/commands/env/config.rs index cdb370b428..32b495090e 100644 --- a/crates/vp_global_cli/src/commands/env/config.rs +++ b/crates/vp_global_cli/src/commands/env/config.rs @@ -89,6 +89,18 @@ impl Config { self.configured_package_manager_shim_mode_for(package_manager).unwrap_or_default() } + /// Node.js and each package-manager family have their own mode; package binaries are always managed. + #[must_use] + pub fn shim_mode_for_tool(&self, tool: &str) -> ShimMode { + if tool == "node" { + self.node_shim_mode + } else if let Some(kind) = PackageManagerType::from_tool(tool) { + self.package_manager_shim_mode_for(kind) + } else { + ShimMode::Managed + } + } + pub fn set_shim_modes(&mut self, node: bool, package_manager: bool, mode: ShimMode) { if node { self.node_shim_mode = mode; diff --git a/crates/vp_global_cli/src/commands/env/exec.rs b/crates/vp_global_cli/src/commands/env/exec.rs index 162854fa1c..c310555556 100644 --- a/crates/vp_global_cli/src/commands/env/exec.rs +++ b/crates/vp_global_cli/src/commands/env/exec.rs @@ -4,7 +4,7 @@ //! 1. Explicit version: `vp env exec --node [--npm ] ` //! 2. Shim mode: `vp env exec [args...]` where tool is node/npm/npx or a global package binary //! -//! Direct invocations resolve a fresh tool selection through shim dispatch. +//! Direct invocations prefer system-first tools from PATH, then resolve a fresh selection through shim dispatch. //! Windows .cmd wrappers and Git Bash shell scripts inherit tools like Unix shims. use std::process::ExitStatus; @@ -21,7 +21,7 @@ use super::{ use crate::{ cli::exit_status, error::Error, - shim::{dispatch as shim_dispatch, is_shim_tool}, + shim::{dispatch as shim_dispatch, is_core_shim_tool, is_shim_tool}, }; /// Execute the exec command. @@ -67,7 +67,6 @@ pub async fn execute( // - Core tools: Version resolved from .node-version/package.json/default // - Package binaries: Uses Node.js version from package metadata // - Automatic Node.js download if needed - // - Shim mode checking (managed vs system-first) // Explicit env exec starts a new selection; wrappers inherit like shims. let env = if from_wrapper { ToolPathEnv::from_env() @@ -77,6 +76,15 @@ pub async fn execute( let args: Vec = command[1..].to_vec(); // stdout belongs to the dispatched tool; route vp's own output to stderr. vp_shared::output::route_user_output_to_stderr(); + // Dispatch assumes the shell's PATH lookup already preferred system-first tools, so repeat it here. + if !from_wrapper + && is_core_shim_tool(tool) + && config::load_config().await?.shim_mode_for_tool(tool) + == config::ShimMode::SystemFirst + && let Some(path) = crate::shim::dispatch::find_system_tool(tool) + { + return Ok(exit_status(crate::shim::exec::exec_tool(&path, &args, env))); + } let exit_code = shim_dispatch(tool, &args, env).await; return Ok(exit_status(exit_code)); } diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 52ae6487db..e27fab6546 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -217,14 +217,7 @@ fn is_owned_shim(path: &vt_path::AbsolutePath, current_exe: &std::path::Path) -> /// Resolve placement independently for Node.js and each package-manager family. pub(super) fn shim_dir(settings: &super::config::Config, tool: &str) -> vt_path::AbsolutePathBuf { let dirs = &vp_shared::EnvConfig::get().dirs; - let mode = if tool == "node" { - settings.node_shim_mode - } else if let Some(kind) = vp_pm_cli::PackageManagerType::from_tool(tool) { - settings.package_manager_shim_mode_for(kind) - } else { - super::config::ShimMode::Managed - }; - match mode { + match settings.shim_mode_for_tool(tool) { super::config::ShimMode::Managed => dirs.bin.clone(), super::config::ShimMode::SystemFirst => dirs.fallback_bin(), }