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 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..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 @@ -53,6 +53,35 @@ 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" +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" @@ -68,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", "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", "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"], + ["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_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_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_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..a906ca553f --- /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,83 @@ +# command_self_setup_mixed_shim_refresh + +## `vpt mkdir -p external home/bin user-bin` + + +## `vpt cp $VP_HOME/bin/vp external/vp` + + +## `vpt chmod +x external/vp` + + +## `vpt write-file home/bin/node old-node-shim` + + +## `vpt write-file home/bin/npm old-npm-shim` + + +## `vpt write-file home/bin/pnpm old-pnpm-shim` + + +## `vpt write-file home/bin/pnpx old-pnpx-shim` + + +## `vpt write-file user-bin/node user-node-shim` + + +## `vpt write-file user-bin/pnpm user-pnpm-shim` + + +## `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` + +``` +home/bin/node: symlink +``` + +## `vpt stat-file home/bin/npm --assert symlink` + +``` +home/bin/npm: symlink +``` + +## `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 print-file user-bin/node` + +``` +user-node-shim +``` + +## `vpt print-file user-bin/pnpm` + +``` +user-pnpm-shim +``` + +## `vpt print-file home/config.json` + +``` +{ + "nodeShimMode": "system_first", + "packageManagerShimModes": { + "bun": "system_first", + "npm": "system_first", + "pnpm": "managed", + "yarn": "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..4602925911 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}; @@ -119,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)) @@ -210,14 +215,22 @@ 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 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) = node_mode { + settings.node_shim_mode = mode; + } + 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(pm_mode) { + settings.set_package_manager_shim_mode(family, mode); + } + } config::save_config(&settings).await?; } @@ -234,12 +247,10 @@ 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 refresh = node_manager != NodeManager::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?; + // 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 .as_path() @@ -283,19 +294,15 @@ 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), + 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), - 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(); @@ -307,18 +314,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. diff --git a/crates/vp_installer/src/main.rs b/crates/vp_installer/src/main.rs index e72f1503ec..6f08b88e86 100644 --- a/crates/vp_installer/src/main.rs +++ b/crates/vp_installer/src/main.rs @@ -161,11 +161,19 @@ async fn do_install(opts: &cli::Options, dirs: &VpDirs) -> Result<(), Box