From 0cbad6e7c9aa43d34f825a0a3df31c25dbabdde5 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 06:40:51 +0800 Subject: [PATCH 01/10] feat(installer): separate Node and package-manager preferences --- .../command_self_setup/snapshots.toml | 17 +++++ .../command_self_setup_bootstrap_options.md | 8 +-- ...mand_self_setup_package_manager_choices.md | 66 +++++++++++++++++++ crates/vp_global_cli/src/self_setup.rs | 30 +++++++-- docs/guide/installer-env-vars.md | 31 ++++++++- 5 files changed, 140 insertions(+), 12 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_package_manager_choices.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml index 393c78215c..56bbfcbeba 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml @@ -53,6 +53,23 @@ steps = [ ["vpt", "stat-file", "user/.zshenv", "--assert", "missing"], ] +[[case]] +name = "command_self_setup_package_manager_choices" +vp = "global" +skip-platforms = ["windows"] +env = { VP_SKIP_DEPS_INSTALL = "1", VP_SELF_SETUP_NO_MODIFY_PATH = "1", VP_NODE_MANAGER = "no" } +steps = [ + { argv = ["vpt", "mkdir", "-p", "external", "home"], snapshot = false }, + { argv = ["vpt", "cp", "$VP_HOME/bin/vp", "external/vp"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "external/vp"], snapshot = false }, + { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "pm-default"], ["VP_PM_MANAGER", "yes"], ["VP_PNPM_MANAGER", "no"], ["VP_YARN_MANAGER", "no"]], comment = "Package-manager choices are independent of Node; pnpm and Yarn override the group default", snapshot = false }, + ["vpt", "print-file", "home/config.json"], + { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "pm-overrides"], ["VP_PM_MANAGER", "no"], ["VP_NPM_MANAGER", "yes"], ["VP_BUN_MANAGER", "yes"]], comment = "npm and Bun can opt into management while the other families prefer system tools", snapshot = false }, + ["vpt", "print-file", "home/config.json"], + { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "pm-preserved"], ["VP_NODE_MANAGER", "yes"]], comment = "Changing only Node management preserves all saved package-manager choices", snapshot = false }, + ["vpt", "print-file", "home/config.json"], +] + [[case]] name = "command_self_setup_shell_warning" vp = "global" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_bootstrap_options.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_bootstrap_options.md index 5995aeac43..0c026c81ef 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_bootstrap_options.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_bootstrap_options.md @@ -28,13 +28,7 @@ home/current/bin/.vp-setup-complete: file ``` { - "nodeShimMode": "system_first", - "packageManagerShimModes": { - "bun": "system_first", - "npm": "system_first", - "pnpm": "system_first", - "yarn": "system_first" - } + "nodeShimMode": "system_first" } ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_package_manager_choices.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_package_manager_choices.md new file mode 100644 index 0000000000..e998f4498b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_package_manager_choices.md @@ -0,0 +1,66 @@ +# command_self_setup_package_manager_choices + +## `vpt mkdir -p external home` + + +## `vpt cp $VP_HOME/bin/vp external/vp` + + +## `vpt chmod +x external/vp` + + +## `VP_HOME=${workspace}/home VP_VERSION=pm-default VP_PM_MANAGER=yes VP_PNPM_MANAGER=no VP_YARN_MANAGER=no ./external/vp` + +Package-manager choices are independent of Node; pnpm and Yarn override the group default + + +## `vpt print-file home/config.json` + +``` +{ + "nodeShimMode": "system_first", + "packageManagerShimModes": { + "bun": "managed", + "npm": "managed", + "pnpm": "system_first", + "yarn": "system_first" + } +} +``` + +## `VP_HOME=${workspace}/home VP_VERSION=pm-overrides VP_PM_MANAGER=no VP_NPM_MANAGER=yes VP_BUN_MANAGER=yes ./external/vp` + +npm and Bun can opt into management while the other families prefer system tools + + +## `vpt print-file home/config.json` + +``` +{ + "nodeShimMode": "system_first", + "packageManagerShimModes": { + "bun": "managed", + "npm": "managed", + "pnpm": "system_first", + "yarn": "system_first" + } +} +``` + +## `VP_HOME=${workspace}/home VP_VERSION=pm-preserved VP_NODE_MANAGER=yes ./external/vp` + +Changing only Node management preserves all saved package-manager choices + + +## `vpt print-file home/config.json` + +``` +{ + "packageManagerShimModes": { + "bun": "managed", + "npm": "managed", + "pnpm": "system_first", + "yarn": "system_first" + } +} +``` diff --git a/crates/vp_global_cli/src/self_setup.rs b/crates/vp_global_cli/src/self_setup.rs index 04ad24c24c..8813eeb0e8 100644 --- a/crates/vp_global_cli/src/self_setup.rs +++ b/crates/vp_global_cli/src/self_setup.rs @@ -5,6 +5,7 @@ mod shell; use std::{path::Path, process::ExitCode}; use dialoguer::{Confirm, theme::ColorfulTheme}; +use vp_pm_cli::PackageManagerType; use vp_setup::{SELF_SETUP_MARKER, VP_BINARY_NAME, install}; use vp_shared::{EnvConfig, env_vars, output}; use vt_path::{AbsolutePath, AbsolutePathBuf}; @@ -215,9 +216,23 @@ async fn run(source: &Path) -> Result { NodeManager::SystemFirst => Some(config::ShimMode::SystemFirst), NodeManager::Refresh => None, }; - if let Some(mode) = mode { + if !in_place { let mut settings = config::load_config().await?; - settings.set_shim_modes(true, true, mode); + if let Some(mode) = mode { + settings.node_shim_mode = mode; + } + // Family-specific choices override the group default; unset choices preserve saved preferences. + let default = manager_mode("VP_PM_MANAGER"); + for (family, variable) in [ + (PackageManagerType::Npm, "VP_NPM_MANAGER"), + (PackageManagerType::Pnpm, "VP_PNPM_MANAGER"), + (PackageManagerType::Yarn, "VP_YARN_MANAGER"), + (PackageManagerType::Bun, "VP_BUN_MANAGER"), + ] { + if let Some(mode) = manager_mode(variable).or(default) { + settings.set_package_manager_shim_mode(family, mode); + } + } config::save_config(&settings).await?; } @@ -290,6 +305,14 @@ enum NodeManager { Enable, } +fn manager_mode(variable: &str) -> Option { + match std::env::var(variable).as_deref() { + Ok("yes") => Some(config::ShimMode::Managed), + Ok("no") => Some(config::ShimMode::SystemFirst), + _ => None, + } +} + fn node_manager() -> Result { match std::env::var("VP_NODE_MANAGER").as_deref() { Ok("yes") => return Ok(NodeManager::Enable), @@ -316,8 +339,7 @@ fn node_manager() -> Result { if !exists && automatic { return Ok(NodeManager::Enable); } - let enable = - confirm("Would you like Vite+ to manage your Node.js and package-manager versions?", true)?; + let enable = confirm("Would you like Vite+ to manage your Node.js versions?", true)?; Ok(if enable { NodeManager::Enable } else { NodeManager::SystemFirst }) } diff --git a/docs/guide/installer-env-vars.md b/docs/guide/installer-env-vars.md index 802af64636..201c943130 100644 --- a/docs/guide/installer-env-vars.md +++ b/docs/guide/installer-env-vars.md @@ -81,7 +81,8 @@ These variables control the installer scripts and the standalone Windows install ### `VP_NODE_MANAGER` -- **Purpose**: Control Node.js version manager setup during installation +- **Purpose**: Control Node.js version manager setup during installation. This + does not change package-manager preferences. - **Values**: `yes` or `no` - **Default**: Auto-detected - **CLI equivalent**: `--no-node-manager` (inverted) @@ -91,6 +92,34 @@ These variables control the installer scripts and the standalone Windows install curl -fsSL https://vite.plus | VP_NODE_MANAGER=no bash ``` +### `VP_PM_MANAGER` + +- **Purpose**: Set the management preference for all four package-manager + families: npm, pnpm, Yarn, and Bun. +- **Values**: `yes` uses Vite+ management; `no` prefers system tools, with + managed tools as a fallback when a system tool is unavailable. +- **Default**: Unset. Existing preferences are preserved; on a fresh install, + each family follows the normal first-use selection. + +### `VP_NPM_MANAGER` / `VP_PNPM_MANAGER` / `VP_YARN_MANAGER` / `VP_BUN_MANAGER` + +- **Purpose**: Set the management preference for an individual package-manager + family. Each variable overrides `VP_PM_MANAGER` for that family. +- **Values**: `yes` or `no`, with the same meaning as `VP_PM_MANAGER`. +- **Default**: Unset (use `VP_PM_MANAGER`, or preserve the existing preference). +- **Example**: + + ```bash + # Keep system Node.js and package managers, but let Vite+ manage pnpm. + curl -fsSL https://vite.plus | VP_NODE_MANAGER=no VP_PM_MANAGER=no VP_PNPM_MANAGER=yes bash + ``` + +These management variables are installation choices, saved in Vite+'s config. +In-place upgrades preserve the saved choices. Unrecognized values are ignored. +They select management behavior, not package-manager versions, and do not +prevent the installer from creating shims. Older releases installed through +the legacy installer retain their original behavior. + ### `VP_PR_VERSION` - **Purpose**: Install a preview build from a pull request or commit SHA From 7953a7589853afcaf45f2d5086a7939d83ba0199 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 06:45:32 +0800 Subject: [PATCH 02/10] fix(installer): preserve combined management choices --- .../command_self_setup/snapshots.toml | 12 ++++++++ .../command_self_setup_combined_default.md | 28 +++++++++++++++++++ crates/vp_global_cli/src/self_setup.rs | 8 ++++-- crates/vp_installer/src/cli.rs | 4 +++ crates/vp_installer/src/main.rs | 12 +++++++- docs/guide/installer-env-vars.md | 11 ++++++-- 6 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_combined_default.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml index 56bbfcbeba..b79aa272c7 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml @@ -53,6 +53,18 @@ steps = [ ["vpt", "stat-file", "user/.zshenv", "--assert", "missing"], ] +[[case]] +name = "command_self_setup_combined_default" +vp = "global" +skip-platforms = ["windows"] +steps = [ + { argv = ["vpt", "mkdir", "-p", "external", "home"], snapshot = false }, + { argv = ["vpt", "cp", "$VP_HOME/bin/vp", "external/vp"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "external/vp"], snapshot = false }, + { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_SKIP_DEPS_INSTALL", "1"], ["VP_VERSION", "combined-default"], ["VP_SELF_SETUP_NO_MODIFY_PATH", "1"], ["CI", "true"], ["VP_PNPM_MANAGER", "no"]], comment = "Without an explicit Node override, automatic setup still enables both; a family override takes precedence", snapshot = false }, + ["vpt", "print-file", "home/config.json"], +] + [[case]] name = "command_self_setup_package_manager_choices" vp = "global" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_combined_default.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_combined_default.md new file mode 100644 index 0000000000..6727cb4e1a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_combined_default.md @@ -0,0 +1,28 @@ +# command_self_setup_combined_default + +## `vpt mkdir -p external home` + + +## `vpt cp $VP_HOME/bin/vp external/vp` + + +## `vpt chmod +x external/vp` + + +## `VP_HOME=${workspace}/home VP_SKIP_DEPS_INSTALL=1 VP_VERSION=combined-default VP_SELF_SETUP_NO_MODIFY_PATH=1 CI=true VP_PNPM_MANAGER=no ./external/vp` + +Without an explicit Node override, automatic setup still enables both; a family override takes precedence + + +## `vpt print-file home/config.json` + +``` +{ + "packageManagerShimModes": { + "bun": "managed", + "npm": "managed", + "pnpm": "system_first", + "yarn": "managed" + } +} +``` diff --git a/crates/vp_global_cli/src/self_setup.rs b/crates/vp_global_cli/src/self_setup.rs index 8813eeb0e8..08e858d3c2 100644 --- a/crates/vp_global_cli/src/self_setup.rs +++ b/crates/vp_global_cli/src/self_setup.rs @@ -221,8 +221,9 @@ async fn run(source: &Path) -> Result { if let Some(mode) = mode { settings.node_shim_mode = mode; } - // Family-specific choices override the group default; unset choices preserve saved preferences. - let default = manager_mode("VP_PM_MANAGER"); + // The combined prompt still controls both; an explicit Node variable only controls Node. + let default = manager_mode("VP_PM_MANAGER") + .or_else(|| if manager_mode("VP_NODE_MANAGER").is_none() { mode } else { None }); for (family, variable) in [ (PackageManagerType::Npm, "VP_NPM_MANAGER"), (PackageManagerType::Pnpm, "VP_PNPM_MANAGER"), @@ -339,7 +340,8 @@ fn node_manager() -> Result { if !exists && automatic { return Ok(NodeManager::Enable); } - let enable = confirm("Would you like Vite+ to manage your Node.js versions?", true)?; + let enable = + confirm("Would you like Vite+ to manage your Node.js and package-manager versions?", true)?; Ok(if enable { NodeManager::Enable } else { NodeManager::SystemFirst }) } diff --git a/crates/vp_installer/src/cli.rs b/crates/vp_installer/src/cli.rs index db5f8a4eec..8e274c10a3 100644 --- a/crates/vp_installer/src/cli.rs +++ b/crates/vp_installer/src/cli.rs @@ -30,6 +30,10 @@ pub struct Options { #[arg(long = "no-node-manager")] pub no_node_manager: bool, + // A choice in the combined menu applies to both, independently of VP_NODE_MANAGER. + #[arg(skip)] + pub management_choice: bool, + /// Do not modify the User PATH #[arg(long = "no-modify-path")] pub no_modify_path: bool, diff --git a/crates/vp_installer/src/main.rs b/crates/vp_installer/src/main.rs index e72f1503ec..2807a7bd27 100644 --- a/crates/vp_installer/src/main.rs +++ b/crates/vp_installer/src/main.rs @@ -173,6 +173,13 @@ async fn do_install(opts: &cli::Options, dirs: &VpDirs) -> Result<(), Box opts.no_node_manager = !opts.no_node_manager, + "3" => { + opts.no_node_manager = !opts.no_node_manager; + opts.management_choice = true; + } "4" => opts.no_modify_path = !opts.no_modify_path, _ => println!(" Invalid option."), } diff --git a/docs/guide/installer-env-vars.md b/docs/guide/installer-env-vars.md index 201c943130..cbe287bf0c 100644 --- a/docs/guide/installer-env-vars.md +++ b/docs/guide/installer-env-vars.md @@ -98,15 +98,17 @@ These variables control the installer scripts and the standalone Windows install families: npm, pnpm, Yarn, and Bun. - **Values**: `yes` uses Vite+ management; `no` prefers system tools, with managed tools as a fallback when a system tool is unavailable. -- **Default**: Unset. Existing preferences are preserved; on a fresh install, - each family follows the normal first-use selection. +- **Default**: Unset. The installer's combined Node.js and package-manager + choice remains the default. When `VP_NODE_MANAGER` is explicitly set, it + only changes Node.js; existing package-manager preferences are preserved. ### `VP_NPM_MANAGER` / `VP_PNPM_MANAGER` / `VP_YARN_MANAGER` / `VP_BUN_MANAGER` - **Purpose**: Set the management preference for an individual package-manager family. Each variable overrides `VP_PM_MANAGER` for that family. - **Values**: `yes` or `no`, with the same meaning as `VP_PM_MANAGER`. -- **Default**: Unset (use `VP_PM_MANAGER`, or preserve the existing preference). +- **Default**: Unset (use `VP_PM_MANAGER`, then the combined installer choice, + or preserve the existing preference). - **Example**: ```bash @@ -115,6 +117,9 @@ These variables control the installer scripts and the standalone Windows install ``` These management variables are installation choices, saved in Vite+'s config. +The interactive prompt still controls both Node.js and package managers; +explicit package-manager variables override that combined choice. The Windows +installer forwards the same choices to the downloaded binary. In-place upgrades preserve the saved choices. Unrecognized values are ignored. They select management behavior, not package-manager versions, and do not prevent the installer from creating shims. Older releases installed through From d4e9a42dc75087b140af5a6e5d680c5494050011 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 06:48:01 +0800 Subject: [PATCH 03/10] refactor(installer): forward management defaults without new option state --- crates/vp_installer/src/cli.rs | 4 ---- crates/vp_installer/src/main.rs | 9 +++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/vp_installer/src/cli.rs b/crates/vp_installer/src/cli.rs index 8e274c10a3..db5f8a4eec 100644 --- a/crates/vp_installer/src/cli.rs +++ b/crates/vp_installer/src/cli.rs @@ -30,10 +30,6 @@ pub struct Options { #[arg(long = "no-node-manager")] pub no_node_manager: bool, - // A choice in the combined menu applies to both, independently of VP_NODE_MANAGER. - #[arg(skip)] - pub management_choice: bool, - /// Do not modify the User PATH #[arg(long = "no-modify-path")] pub no_modify_path: bool, diff --git a/crates/vp_installer/src/main.rs b/crates/vp_installer/src/main.rs index 2807a7bd27..acbc089e31 100644 --- a/crates/vp_installer/src/main.rs +++ b/crates/vp_installer/src/main.rs @@ -173,11 +173,11 @@ async fn do_install(opts: &cli::Options, dirs: &VpDirs) -> Result<(), Box { - opts.no_node_manager = !opts.no_node_manager; - opts.management_choice = true; - } + "3" => opts.no_node_manager = !opts.no_node_manager, "4" => opts.no_modify_path = !opts.no_modify_path, _ => println!(" Invalid option."), } From 7b2d980febb0b607208ac2289ec1374f1e367566 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 06:50:07 +0800 Subject: [PATCH 04/10] refactor(installer): directly forward combined management defaults --- crates/vp_installer/src/main.rs | 14 ++++++-------- docs/guide/installer-env-vars.md | 9 +++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/vp_installer/src/main.rs b/crates/vp_installer/src/main.rs index acbc089e31..7dd6b1c759 100644 --- a/crates/vp_installer/src/main.rs +++ b/crates/vp_installer/src/main.rs @@ -161,11 +161,16 @@ async fn do_install(opts: &cli::Options, dirs: &VpDirs) -> Result<(), Box Result<(), Box Date: Sun, 13 Sep 2026 06:51:16 +0800 Subject: [PATCH 05/10] wip --- docs/guide/installer-env-vars.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guide/installer-env-vars.md b/docs/guide/installer-env-vars.md index 0713bbccc1..c6cad0c25a 100644 --- a/docs/guide/installer-env-vars.md +++ b/docs/guide/installer-env-vars.md @@ -81,8 +81,7 @@ These variables control the installer scripts and the standalone Windows install ### `VP_NODE_MANAGER` -- **Purpose**: Control Node.js version manager setup during installation. This - does not change package-manager preferences. +- **Purpose**: Control Node.js version manager setup during installation. - **Values**: `yes` or `no` - **Default**: Auto-detected - **CLI equivalent**: `--no-node-manager` (inverted) From ea29c6665c1fb38ff9cb924e13f7e658b3262e5a Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 06:53:54 +0800 Subject: [PATCH 06/10] refactor(installer): separate received overrides from setup defaults --- crates/vp_global_cli/src/self_setup.rs | 41 ++++++++------------------ 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/crates/vp_global_cli/src/self_setup.rs b/crates/vp_global_cli/src/self_setup.rs index 08e858d3c2..d542ea7b63 100644 --- a/crates/vp_global_cli/src/self_setup.rs +++ b/crates/vp_global_cli/src/self_setup.rs @@ -120,7 +120,11 @@ async fn run(source: &Path) -> Result { )); } let previous_install = previous_install()?; - let node_manager = if in_place { NodeManager::Refresh } else { node_manager()? }; + let node_override = manager_mode("VP_NODE_MANAGER"); + // A supplied Node choice skips the combined prompt; upgrades preserve all saved choices. + let default_mode = + if in_place || node_override.is_some() { None } else { management_default()? }; + let node_mode = if in_place { None } else { node_override.or(default_mode) }; let version = env!("CARGO_PKG_VERSION"); let registry = std::env::var(env_vars::NPM_CONFIG_REGISTRY_UPPER) .or_else(|_| std::env::var(env_vars::NPM_CONFIG_REGISTRY)) @@ -211,26 +215,19 @@ async fn run(source: &Path) -> Result { } } } - let mode = match node_manager { - NodeManager::Enable => Some(config::ShimMode::Managed), - NodeManager::SystemFirst => Some(config::ShimMode::SystemFirst), - NodeManager::Refresh => None, - }; if !in_place { let mut settings = config::load_config().await?; - if let Some(mode) = mode { + if let Some(mode) = node_mode { settings.node_shim_mode = mode; } - // The combined prompt still controls both; an explicit Node variable only controls Node. - let default = manager_mode("VP_PM_MANAGER") - .or_else(|| if manager_mode("VP_NODE_MANAGER").is_none() { mode } else { None }); + let pm_mode = manager_mode("VP_PM_MANAGER").or(default_mode); for (family, variable) in [ (PackageManagerType::Npm, "VP_NPM_MANAGER"), (PackageManagerType::Pnpm, "VP_PNPM_MANAGER"), (PackageManagerType::Yarn, "VP_YARN_MANAGER"), (PackageManagerType::Bun, "VP_BUN_MANAGER"), ] { - if let Some(mode) = manager_mode(variable).or(default) { + if let Some(mode) = manager_mode(variable).or(pm_mode) { settings.set_package_manager_shim_mode(family, mode); } } @@ -253,7 +250,7 @@ async fn run(source: &Path) -> Result { // Declining management preserves regular files, but create_shim can still replace foreign Unix symlinks. // The default bin directory is private to Vite+, so we accept this limitation for custom shared directories // rather than add the complexity of reliably identifying which symlinks belong to Vite+. - let refresh = node_manager != NodeManager::SystemFirst; + let refresh = node_mode != Some(config::ShimMode::SystemFirst); // Windows entrypoints must point at this installation even when Node management is declined. setup::execute_for_binary(binary.as_path(), refresh, cfg!(windows) || refresh, false).await?; if !in_place { @@ -299,13 +296,6 @@ fn confirm(prompt: &str, default: bool) -> Result { .map_err(|error| Error::Other(error.to_string().into())) } -#[derive(PartialEq, Eq)] -enum NodeManager { - SystemFirst, - Refresh, - Enable, -} - fn manager_mode(variable: &str) -> Option { match std::env::var(variable).as_deref() { Ok("yes") => Some(config::ShimMode::Managed), @@ -314,12 +304,7 @@ fn manager_mode(variable: &str) -> Option { } } -fn node_manager() -> Result { - match std::env::var("VP_NODE_MANAGER").as_deref() { - Ok("yes") => return Ok(NodeManager::Enable), - Ok("no") => return Ok(NodeManager::SystemFirst), - _ => {} - } +fn management_default() -> Result, Error> { let dirs = &EnvConfig::get().dirs; let node = dirs.bin.join(setup::shim_filename("node")); let exists = std::fs::symlink_metadata(&node).is_ok(); @@ -331,18 +316,18 @@ fn node_manager() -> Result { let owned = exists && dirs.owns_windows_trampoline(node.as_path()); if owned { // Refresh existing shims without undoing a user's `vp env off` preference. - return Ok(NodeManager::Refresh); + return Ok(None); } let automatic = ["CI", "CODESPACES", "REMOTE_CONTAINERS", "DEVPOD"] .iter() .any(|name| std::env::var_os(name).is_some()) || find_on_path("node").is_none(); if !exists && automatic { - return Ok(NodeManager::Enable); + return Ok(Some(config::ShimMode::Managed)); } let enable = confirm("Would you like Vite+ to manage your Node.js and package-manager versions?", true)?; - Ok(if enable { NodeManager::Enable } else { NodeManager::SystemFirst }) + Ok(Some(if enable { config::ShimMode::Managed } else { config::ShimMode::SystemFirst })) } // PATH discovery only offers cleanup after an explicit move; VpDirs remains the authority for the target. From 97dca640052a103c5330338d9f8a97815f3e997c Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 07:03:40 +0800 Subject: [PATCH 07/10] fix(installer): apply manager choices to each tool shim --- .../command_self_setup/snapshots.toml | 25 +++++++ .../command_self_setup_mixed_shim_refresh.md | 74 +++++++++++++++++++ .../vp_global_cli/src/commands/env/setup.rs | 30 +++++--- crates/vp_global_cli/src/self_setup.rs | 12 ++- crates/vp_installer/src/main.rs | 5 +- 5 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml index b79aa272c7..29335f9ea5 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml @@ -97,3 +97,28 @@ steps = [ ["vpt", "stat-file", "user/.bashrc", "--assert", "dir"], { argv = ["./home/bin/vp", "--help"], envs = [["VP_HOME", "${workspace}/home"]], comment = "The installed CLI accepts commands after the warning", snapshot = false }, ] + +[[case]] +name = "command_self_setup_mixed_shim_refresh" +vp = "global" +skip-platforms = ["windows"] +env = { VP_SKIP_DEPS_INSTALL = "1", VP_SELF_SETUP_NO_MODIFY_PATH = "1" } +steps = [ + { argv = ["vpt", "mkdir", "-p", "external", "home/bin", "other/bin"], snapshot = false }, + { argv = ["vpt", "cp", "$VP_HOME/bin/vp", "external/vp"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "external/vp"], snapshot = false }, + { argv = ["vpt", "write-file", "home/bin/node", "existing-node"], snapshot = false }, + { argv = ["vpt", "write-file", "home/bin/npm", "existing-npm"], snapshot = false }, + { argv = ["vpt", "write-file", "home/bin/pnpm", "existing-pnpm"], snapshot = false }, + { argv = ["vpt", "write-file", "home/bin/pnpx", "existing-pnpx"], snapshot = false }, + { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "mixed-shims"], ["VP_NODE_MANAGER", "no"], ["VP_PM_MANAGER", "no"], ["VP_PNPM_MANAGER", "yes"]], comment = "Managing pnpm replaces its existing commands while preserving system-first Node and npm", snapshot = false }, + ["vpt", "print-file", "home/bin/node"], + ["vpt", "print-file", "home/bin/npm"], + ["vpt", "stat-file", "home/bin/pnpm", "--assert", "symlink"], + ["vpt", "stat-file", "home/bin/pnpx", "--assert", "symlink"], + { argv = ["vpt", "write-file", "other/bin/node", "existing-node"], snapshot = false }, + { argv = ["vpt", "write-file", "other/bin/npm", "existing-npm"], snapshot = false }, + { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/other"], ["VP_VERSION", "mixed-shims"], ["VP_NODE_MANAGER", "yes"], ["VP_PM_MANAGER", "no"]], comment = "Managing Node preserves an existing system-first npm command", snapshot = false }, + ["vpt", "stat-file", "other/bin/node", "--assert", "symlink"], + ["vpt", "print-file", "other/bin/npm"], +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md new file mode 100644 index 0000000000..33c19ab001 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md @@ -0,0 +1,74 @@ +# command_self_setup_mixed_shim_refresh + +## `vpt mkdir -p external home/bin other/bin` + + +## `vpt cp $VP_HOME/bin/vp external/vp` + + +## `vpt chmod +x external/vp` + + +## `vpt write-file home/bin/node existing-node` + + +## `vpt write-file home/bin/npm existing-npm` + + +## `vpt write-file home/bin/pnpm existing-pnpm` + + +## `vpt write-file home/bin/pnpx existing-pnpx` + + +## `VP_HOME=${workspace}/home VP_VERSION=mixed-shims VP_NODE_MANAGER=no VP_PM_MANAGER=no VP_PNPM_MANAGER=yes ./external/vp` + +Managing pnpm replaces its existing commands while preserving system-first Node and npm + + +## `vpt print-file home/bin/node` + +``` +existing-node +``` + +## `vpt print-file home/bin/npm` + +``` +existing-npm +``` + +## `vpt stat-file home/bin/pnpm --assert symlink` + +``` +home/bin/pnpm: symlink +``` + +## `vpt stat-file home/bin/pnpx --assert symlink` + +``` +home/bin/pnpx: symlink +``` + +## `vpt write-file other/bin/node existing-node` + + +## `vpt write-file other/bin/npm existing-npm` + + +## `VP_HOME=${workspace}/other VP_VERSION=mixed-shims VP_NODE_MANAGER=yes VP_PM_MANAGER=no ./external/vp` + +Managing Node preserves an existing system-first npm command + + +## `vpt stat-file other/bin/node --assert symlink` + +``` +other/bin/node: symlink +``` + +## `vpt print-file other/bin/npm` + +``` +existing-npm +``` diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 77add589f3..cac9cebd34 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -55,13 +55,13 @@ impl EnvShell { /// Execute the setup command. pub async fn execute(refresh: bool, env_only: bool) -> Result { - execute_for_binary(&std::env::current_exe()?, refresh, refresh, env_only).await + execute_for_binary(&std::env::current_exe()?, |_| refresh, refresh, env_only).await } // Self-setup must create shims for the deployed binary, not the temporary download. pub(crate) async fn execute_for_binary( current_exe: &std::path::Path, - refresh: bool, + refresh: impl Fn(&str) -> bool, refresh_entrypoints: bool, env_only: bool, ) -> Result { @@ -90,9 +90,7 @@ pub(crate) async fn execute_for_binary( // Ensure bin directory exists tokio::fs::create_dir_all(bin_dir).await?; - if refresh { - cleanup_legacy_package_manager_installs(&bin_dir).await; - } + cleanup_legacy_package_manager_installs(&bin_dir, &refresh).await; #[cfg(windows)] tokio::fs::write(bin_dir.join("vp-use.cmd"), vp_use_cmd_content(&config)).await?; @@ -106,7 +104,7 @@ pub(crate) async fn execute_for_binary( for tool in crate::shim::DEFAULT_SHIM_TOOLS { let refresh_tool = - if matches!(*tool, "vpx" | "vpr") { refresh_entrypoints } else { refresh }; + if matches!(*tool, "vpx" | "vpr") { refresh_entrypoints } else { refresh(tool) }; let result = create_shim(current_exe, bin_dir, tool, refresh_tool).await?; if result { created.push(*tool); @@ -131,7 +129,7 @@ pub(crate) async fn execute_for_binary( } #[cfg(windows)] - if refresh { + if refresh("node") { if let Err(e) = refresh_package_shims(current_exe, bin_dir).await { tracing::warn!("Failed to refresh package shims: {}", e); } @@ -139,7 +137,7 @@ pub(crate) async fn execute_for_binary( // Best-effort cleanup of .old files from rename-before-copy on Windows #[cfg(windows)] - if refresh || refresh_entrypoints { + if refresh("node") || refresh_entrypoints { cleanup_old_files(bin_dir).await; } @@ -152,7 +150,7 @@ pub(crate) async fn execute_for_binary( } } - if !skipped.is_empty() && !refresh { + if !skipped.is_empty() { if !created.is_empty() { output::raw(""); } @@ -172,8 +170,14 @@ pub(crate) async fn execute_for_binary( } /// Remove legacy managed installs left by versions that did not expose package-manager shims. -async fn cleanup_legacy_package_manager_installs(bin_dir: &vt_path::AbsolutePath) { +async fn cleanup_legacy_package_manager_installs( + bin_dir: &vt_path::AbsolutePath, + refresh: &impl Fn(&str) -> bool, +) { for package_name in LEGACY_PACKAGE_MANAGER_PACKAGES { + if !refresh(package_name) { + continue; + } let has_metadata = match PackageMetadata::load(package_name).await { Ok(metadata) => metadata.is_some(), Err(error) => { @@ -202,6 +206,10 @@ async fn cleanup_legacy_package_manager_installs(bin_dir: &vt_path::AbsolutePath } } + if !refresh("corepack") { + return; + } + // Corepack is no longer exposed, so remove its old default shim even when no package metadata remains. #[cfg(unix)] { @@ -1816,7 +1824,7 @@ mod tests { .await .unwrap(); } - execute_for_binary(&std::env::current_exe().unwrap(), false, true, false) + execute_for_binary(&std::env::current_exe().unwrap(), |_| false, true, false) .await .unwrap(); let dirs = &vp_shared::EnvConfig::get().dirs; diff --git a/crates/vp_global_cli/src/self_setup.rs b/crates/vp_global_cli/src/self_setup.rs index d542ea7b63..8a2c3ee3a0 100644 --- a/crates/vp_global_cli/src/self_setup.rs +++ b/crates/vp_global_cli/src/self_setup.rs @@ -250,9 +250,17 @@ async fn run(source: &Path) -> Result { // Declining management preserves regular files, but create_shim can still replace foreign Unix symlinks. // The default bin directory is private to Vite+, so we accept this limitation for custom shared directories // rather than add the complexity of reliably identifying which symlinks belong to Vite+. - let refresh = node_mode != Some(config::ShimMode::SystemFirst); + let settings = config::load_config().await?; + // Each managed tool replaces its own shim, independently of the Node preference. + let refresh = |tool: &str| { + let mode = PackageManagerType::from_tool(tool) + .map(|family| settings.package_manager_shim_mode_for(family)) + .unwrap_or(settings.node_shim_mode); + mode != config::ShimMode::SystemFirst + }; // Windows entrypoints must point at this installation even when Node management is declined. - setup::execute_for_binary(binary.as_path(), refresh, cfg!(windows) || refresh, false).await?; + setup::execute_for_binary(binary.as_path(), refresh, cfg!(windows) || refresh("node"), false) + .await?; if !in_place { let name = version_dir .as_path() diff --git a/crates/vp_installer/src/main.rs b/crates/vp_installer/src/main.rs index 7dd6b1c759..6f08b88e86 100644 --- a/crates/vp_installer/src/main.rs +++ b/crates/vp_installer/src/main.rs @@ -169,7 +169,10 @@ async fn do_install(opts: &cli::Options, dirs: &VpDirs) -> Result<(), Box Date: Sun, 13 Sep 2026 20:49:35 +0800 Subject: [PATCH 08/10] fix(installer): refresh shims independently of management preferences --- .../command_self_setup/snapshots.toml | 26 ++++----- .../command_self_setup_mixed_shim_refresh.md | 57 +++++++++++-------- .../vp_global_cli/src/commands/env/setup.rs | 30 ++++------ crates/vp_global_cli/src/self_setup.rs | 19 ++----- 4 files changed, 62 insertions(+), 70 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml index 29335f9ea5..6de6bfdd25 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots.toml @@ -104,21 +104,21 @@ vp = "global" skip-platforms = ["windows"] env = { VP_SKIP_DEPS_INSTALL = "1", VP_SELF_SETUP_NO_MODIFY_PATH = "1" } steps = [ - { argv = ["vpt", "mkdir", "-p", "external", "home/bin", "other/bin"], snapshot = false }, + { argv = ["vpt", "mkdir", "-p", "external", "home/bin", "user-bin"], snapshot = false }, { argv = ["vpt", "cp", "$VP_HOME/bin/vp", "external/vp"], snapshot = false }, { argv = ["vpt", "chmod", "+x", "external/vp"], snapshot = false }, - { argv = ["vpt", "write-file", "home/bin/node", "existing-node"], snapshot = false }, - { argv = ["vpt", "write-file", "home/bin/npm", "existing-npm"], snapshot = false }, - { argv = ["vpt", "write-file", "home/bin/pnpm", "existing-pnpm"], snapshot = false }, - { argv = ["vpt", "write-file", "home/bin/pnpx", "existing-pnpx"], snapshot = false }, - { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "mixed-shims"], ["VP_NODE_MANAGER", "no"], ["VP_PM_MANAGER", "no"], ["VP_PNPM_MANAGER", "yes"]], comment = "Managing pnpm replaces its existing commands while preserving system-first Node and npm", snapshot = false }, - ["vpt", "print-file", "home/bin/node"], - ["vpt", "print-file", "home/bin/npm"], + { argv = ["vpt", "write-file", "home/bin/node", "old-node-shim"], snapshot = false }, + { argv = ["vpt", "write-file", "home/bin/npm", "old-npm-shim"], snapshot = false }, + { argv = ["vpt", "write-file", "home/bin/pnpm", "old-pnpm-shim"], snapshot = false }, + { argv = ["vpt", "write-file", "home/bin/pnpx", "old-pnpx-shim"], snapshot = false }, + { argv = ["vpt", "write-file", "user-bin/node", "user-node-shim"], snapshot = false }, + { argv = ["vpt", "write-file", "user-bin/pnpm", "user-pnpm-shim"], snapshot = false }, + { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "mixed-shims"], ["VP_NODE_MANAGER", "no"], ["VP_PM_MANAGER", "no"], ["VP_PNPM_MANAGER", "yes"], ["PATH", "${workspace}/user-bin${PATH_SEPARATOR}${PATH}"]], comment = "Installation refreshes every Vite+ shim regardless of management preferences, leaving user tools elsewhere on PATH untouched", snapshot = false }, + ["vpt", "stat-file", "home/bin/node", "--assert", "symlink"], + ["vpt", "stat-file", "home/bin/npm", "--assert", "symlink"], ["vpt", "stat-file", "home/bin/pnpm", "--assert", "symlink"], ["vpt", "stat-file", "home/bin/pnpx", "--assert", "symlink"], - { argv = ["vpt", "write-file", "other/bin/node", "existing-node"], snapshot = false }, - { argv = ["vpt", "write-file", "other/bin/npm", "existing-npm"], snapshot = false }, - { argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/other"], ["VP_VERSION", "mixed-shims"], ["VP_NODE_MANAGER", "yes"], ["VP_PM_MANAGER", "no"]], comment = "Managing Node preserves an existing system-first npm command", snapshot = false }, - ["vpt", "stat-file", "other/bin/node", "--assert", "symlink"], - ["vpt", "print-file", "other/bin/npm"], + ["vpt", "print-file", "user-bin/node"], + ["vpt", "print-file", "user-bin/pnpm"], + ["vpt", "print-file", "home/config.json"], ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md index 33c19ab001..a906ca553f 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_self_setup/snapshots/command_self_setup_mixed_shim_refresh.md @@ -1,6 +1,6 @@ # command_self_setup_mixed_shim_refresh -## `vpt mkdir -p external home/bin other/bin` +## `vpt mkdir -p external home/bin user-bin` ## `vpt cp $VP_HOME/bin/vp external/vp` @@ -9,33 +9,39 @@ ## `vpt chmod +x external/vp` -## `vpt write-file home/bin/node existing-node` +## `vpt write-file home/bin/node old-node-shim` -## `vpt write-file home/bin/npm existing-npm` +## `vpt write-file home/bin/npm old-npm-shim` -## `vpt write-file home/bin/pnpm existing-pnpm` +## `vpt write-file home/bin/pnpm old-pnpm-shim` -## `vpt write-file home/bin/pnpx existing-pnpx` +## `vpt write-file home/bin/pnpx old-pnpx-shim` -## `VP_HOME=${workspace}/home VP_VERSION=mixed-shims VP_NODE_MANAGER=no VP_PM_MANAGER=no VP_PNPM_MANAGER=yes ./external/vp` +## `vpt write-file user-bin/node user-node-shim` -Managing pnpm replaces its existing commands while preserving system-first Node and npm +## `vpt write-file user-bin/pnpm user-pnpm-shim` -## `vpt print-file home/bin/node` + +## `VP_HOME=${workspace}/home VP_VERSION=mixed-shims VP_NODE_MANAGER=no VP_PM_MANAGER=no VP_PNPM_MANAGER=yes PATH=${workspace}/user-bin${PATH_SEPARATOR}${PATH} ./external/vp` + +Installation refreshes every Vite+ shim regardless of management preferences, leaving user tools elsewhere on PATH untouched + + +## `vpt stat-file home/bin/node --assert symlink` ``` -existing-node +home/bin/node: symlink ``` -## `vpt print-file home/bin/npm` +## `vpt stat-file home/bin/npm --assert symlink` ``` -existing-npm +home/bin/npm: symlink ``` ## `vpt stat-file home/bin/pnpm --assert symlink` @@ -50,25 +56,28 @@ home/bin/pnpm: symlink home/bin/pnpx: symlink ``` -## `vpt write-file other/bin/node existing-node` - - -## `vpt write-file other/bin/npm existing-npm` - - -## `VP_HOME=${workspace}/other VP_VERSION=mixed-shims VP_NODE_MANAGER=yes VP_PM_MANAGER=no ./external/vp` - -Managing Node preserves an existing system-first npm command +## `vpt print-file user-bin/node` +``` +user-node-shim +``` -## `vpt stat-file other/bin/node --assert symlink` +## `vpt print-file user-bin/pnpm` ``` -other/bin/node: symlink +user-pnpm-shim ``` -## `vpt print-file other/bin/npm` +## `vpt print-file home/config.json` ``` -existing-npm +{ + "nodeShimMode": "system_first", + "packageManagerShimModes": { + "bun": "system_first", + "npm": "system_first", + "pnpm": "managed", + "yarn": "system_first" + } +} ``` diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index cac9cebd34..77add589f3 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -55,13 +55,13 @@ impl EnvShell { /// Execute the setup command. pub async fn execute(refresh: bool, env_only: bool) -> Result { - execute_for_binary(&std::env::current_exe()?, |_| refresh, refresh, env_only).await + execute_for_binary(&std::env::current_exe()?, refresh, refresh, env_only).await } // Self-setup must create shims for the deployed binary, not the temporary download. pub(crate) async fn execute_for_binary( current_exe: &std::path::Path, - refresh: impl Fn(&str) -> bool, + refresh: bool, refresh_entrypoints: bool, env_only: bool, ) -> Result { @@ -90,7 +90,9 @@ pub(crate) async fn execute_for_binary( // Ensure bin directory exists tokio::fs::create_dir_all(bin_dir).await?; - cleanup_legacy_package_manager_installs(&bin_dir, &refresh).await; + if refresh { + cleanup_legacy_package_manager_installs(&bin_dir).await; + } #[cfg(windows)] tokio::fs::write(bin_dir.join("vp-use.cmd"), vp_use_cmd_content(&config)).await?; @@ -104,7 +106,7 @@ pub(crate) async fn execute_for_binary( for tool in crate::shim::DEFAULT_SHIM_TOOLS { let refresh_tool = - if matches!(*tool, "vpx" | "vpr") { refresh_entrypoints } else { refresh(tool) }; + if matches!(*tool, "vpx" | "vpr") { refresh_entrypoints } else { refresh }; let result = create_shim(current_exe, bin_dir, tool, refresh_tool).await?; if result { created.push(*tool); @@ -129,7 +131,7 @@ pub(crate) async fn execute_for_binary( } #[cfg(windows)] - if refresh("node") { + if refresh { if let Err(e) = refresh_package_shims(current_exe, bin_dir).await { tracing::warn!("Failed to refresh package shims: {}", e); } @@ -137,7 +139,7 @@ pub(crate) async fn execute_for_binary( // Best-effort cleanup of .old files from rename-before-copy on Windows #[cfg(windows)] - if refresh("node") || refresh_entrypoints { + if refresh || refresh_entrypoints { cleanup_old_files(bin_dir).await; } @@ -150,7 +152,7 @@ pub(crate) async fn execute_for_binary( } } - if !skipped.is_empty() { + if !skipped.is_empty() && !refresh { if !created.is_empty() { output::raw(""); } @@ -170,14 +172,8 @@ pub(crate) async fn execute_for_binary( } /// Remove legacy managed installs left by versions that did not expose package-manager shims. -async fn cleanup_legacy_package_manager_installs( - bin_dir: &vt_path::AbsolutePath, - refresh: &impl Fn(&str) -> bool, -) { +async fn cleanup_legacy_package_manager_installs(bin_dir: &vt_path::AbsolutePath) { for package_name in LEGACY_PACKAGE_MANAGER_PACKAGES { - if !refresh(package_name) { - continue; - } let has_metadata = match PackageMetadata::load(package_name).await { Ok(metadata) => metadata.is_some(), Err(error) => { @@ -206,10 +202,6 @@ async fn cleanup_legacy_package_manager_installs( } } - if !refresh("corepack") { - return; - } - // Corepack is no longer exposed, so remove its old default shim even when no package metadata remains. #[cfg(unix)] { @@ -1824,7 +1816,7 @@ mod tests { .await .unwrap(); } - execute_for_binary(&std::env::current_exe().unwrap(), |_| false, true, false) + execute_for_binary(&std::env::current_exe().unwrap(), false, true, false) .await .unwrap(); let dirs = &vp_shared::EnvConfig::get().dirs; diff --git a/crates/vp_global_cli/src/self_setup.rs b/crates/vp_global_cli/src/self_setup.rs index 8a2c3ee3a0..0f2d619f89 100644 --- a/crates/vp_global_cli/src/self_setup.rs +++ b/crates/vp_global_cli/src/self_setup.rs @@ -247,20 +247,11 @@ async fn run(source: &Path) -> Result { // 3. Run setup in this process. Spawning the unmarked binary here would reenter self-setup. tokio::fs::create_dir_all(&dirs.bin).await?; - // Declining management preserves regular files, but create_shim can still replace foreign Unix symlinks. - // The default bin directory is private to Vite+, so we accept this limitation for custom shared directories - // rather than add the complexity of reliably identifying which symlinks belong to Vite+. - let settings = config::load_config().await?; - // Each managed tool replaces its own shim, independently of the Node preference. - let refresh = |tool: &str| { - let mode = PackageManagerType::from_tool(tool) - .map(|family| settings.package_manager_shim_mode_for(family)) - .unwrap_or(settings.node_shim_mode); - mode != config::ShimMode::SystemFirst - }; - // Windows entrypoints must point at this installation even when Node management is declined. - setup::execute_for_binary(binary.as_path(), refresh, cfg!(windows) || refresh("node"), false) - .await?; + // Installation runs setup --refresh: management preferences select tools at runtime, not which shims are refreshed. + // Refresh only Vite+'s configured bin directory; never replace tools discovered elsewhere on PATH. + // This directory is private by default; using a shared directory requires explicit directory overrides. + // Keep all shims, legacy cleanup and Windows package trampolines current even in system-first mode. + setup::execute_for_binary(binary.as_path(), true, true, false).await?; if !in_place { let name = version_dir .as_path() From fee57fceef445ae706c686f1d814ed3857b5f74e Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 21:03:04 +0800 Subject: [PATCH 09/10] docs(installer): explain unconditional shim refresh --- crates/vp_global_cli/src/self_setup.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/vp_global_cli/src/self_setup.rs b/crates/vp_global_cli/src/self_setup.rs index 0f2d619f89..4602925911 100644 --- a/crates/vp_global_cli/src/self_setup.rs +++ b/crates/vp_global_cli/src/self_setup.rs @@ -247,10 +247,9 @@ async fn run(source: &Path) -> Result { // 3. Run setup in this process. Spawning the unmarked binary here would reenter self-setup. tokio::fs::create_dir_all(&dirs.bin).await?; - // Installation runs setup --refresh: management preferences select tools at runtime, not which shims are refreshed. - // Refresh only Vite+'s configured bin directory; never replace tools discovered elsewhere on PATH. - // This directory is private by default; using a shared directory requires explicit directory overrides. - // Keep all shims, legacy cleanup and Windows package trampolines current even in system-first mode. + // Always create and refresh shims, even in system-first mode; `vp env off` and per-tool preferences control runtime dispatch. + // VpDirs::bin is private by default, so replacing its shims leaves system-first tools elsewhere on PATH intact. + // Users explicitly pointing VpDirs::bin at a shared directory accept replacement of conflicting entries there. setup::execute_for_binary(binary.as_path(), true, true, false).await?; if !in_place { let name = version_dir From 8fb38b9c050dd8c259d7ba47ddfb9263eedcbb3a Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 13 Sep 2026 21:11:26 +0800 Subject: [PATCH 10/10] test(installer): expect shared-bin shim refresh --- .github/workflows/test-standalone-install.yml | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test-standalone-install.yml b/.github/workflows/test-standalone-install.yml index 0fef8d42f5..94f6ae7465 100644 --- a/.github/workflows/test-standalone-install.yml +++ b/.github/workflows/test-standalone-install.yml @@ -361,14 +361,15 @@ jobs: test "$CONFIG_DIR" = "$(dump_dir config)" test "$STATE_DIR" = "$(dump_dir state)" - - name: Custom shared bin preserves an unrelated Node executable + - name: Custom shared bin refreshes Node even in system-first mode run: | set -euo pipefail FOREIGN=$(mktemp -d) FAKE_TGZ=$(mktemp) - mkdir -p "$FOREIGN/.local/bin" + mkdir -p "$FOREIGN/.local/bin" "$FOREIGN/user-bin" printf '#!/bin/sh\necho foreign-node\n' > "$FOREIGN/.local/bin/node" chmod +x "$FOREIGN/.local/bin/node" + cp "$FOREIGN/.local/bin/node" "$FOREIGN/user-bin/node" export HOME="$FOREIGN" export USERPROFILE="$FOREIGN" export GITHUB_ACTION_REPOSITORY=voidzero-dev/setup-vp @@ -376,17 +377,17 @@ jobs: export VP_BIN_DIR="$FOREIGN/.local/bin" export VP_DATA_DIR="$FOREIGN/.local/share/vite-plus" export VP_CACHE_DIR="$FOREIGN/.cache/vite-plus" - # Override the setting for this job. A custom bin can be shared. CI - # must not replace a Node executable that Vite+ does not own. - export VP_NODE_MANAGER= + # Explicitly sharing the Vite+ bin directory accepts replacement there; system-first tools elsewhere stay intact. + export VP_NODE_MANAGER=no + export PATH="$FOREIGN/user-bin:$PATH" unset VP_HOME unset XDG_DATA_HOME XDG_CACHE_HOME XDG_CONFIG_HOME XDG_STATE_HOME VP_LOCAL_TGZ="$FAKE_TGZ" VP_VERSION=local-foreign-node bash packages/cli/install.sh test ! -d "$FOREIGN/.vite-plus" test -f "$FOREIGN/.local/bin/node" - test ! -L "$FOREIGN/.local/bin/node" - test "$("$FOREIGN/.local/bin/node")" = "foreign-node" + test -L "$FOREIGN/.local/bin/node" + test "$("$FOREIGN/user-bin/node")" = "foreign-node" - name: Existing ~/.vite-plus is reused run: | @@ -1522,7 +1523,7 @@ jobs: if (Test-Path $legacyRoot) { throw "implode left the monolithic root" } if ($removingRoots.Count -ne 0) { throw "implode left a renamed monolithic root" } - - name: PowerShell installer rejects an unversioned Node sidecar + - name: PowerShell installer refreshes shared-bin Node even in system-first mode shell: pwsh run: | $ErrorActionPreference = "Stop" @@ -1532,10 +1533,13 @@ jobs: $cache = Join-Path $root "cache" $node = Join-Path $bin "node.exe" $pointer = Join-Path $bin "node.shim" + $externalNode = Join-Path $root "user-bin/node.exe" $fakeTgz = Join-Path $root "vite-plus.tgz" Remove-Item -Recurse -Force $root -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $bin | Out-Null + New-Item -ItemType Directory -Force -Path (Split-Path $externalNode) | Out-Null [System.IO.File]::WriteAllText($node, "foreign-node") + [System.IO.File]::WriteAllText($externalNode, "external-node") [System.IO.File]::WriteAllText($pointer, "$data`n") New-Item -ItemType File -Force -Path $fakeTgz | Out-Null @@ -1547,16 +1551,21 @@ jobs: $env:VP_LOCAL_TGZ = $fakeTgz $env:VP_SKIP_DEPS_INSTALL = "1" $env:VP_VERSION = "local-foreign-node" - $env:VP_NODE_MANAGER = "" + # Explicitly sharing the Vite+ bin directory accepts replacement there; system-first tools elsewhere stay intact. + $env:VP_NODE_MANAGER = "no" $env:CI = "true" & ./packages/cli/install.ps1 - if ([System.IO.File]::ReadAllText($node) -ne "foreign-node") { - Write-Error "install.ps1 replaced an unrelated node.exe" + $trampoline = Join-Path $data "current/bin/vp-shim.exe" + if ((Get-FileHash $node).Hash -ne (Get-FileHash $trampoline).Hash) { + Write-Error "install.ps1 did not refresh node.exe in the configured bin directory" + } + if ([System.IO.File]::ReadAllText($pointer) -ne [System.IO.File]::ReadAllText((Join-Path $bin "vp.shim"))) { + Write-Error "install.ps1 did not refresh the Node shim pointer" } - if ([System.IO.File]::ReadAllText($pointer) -ne "$data`n") { - Write-Error "install.ps1 changed the unversioned node.shim" + if ([System.IO.File]::ReadAllText($externalNode) -ne "external-node") { + Write-Error "install.ps1 replaced node.exe outside the configured bin directory" } - name: Complete directory overrides keep the split layout through the trampoline