diff --git a/crates/vite_global_cli/src/commands/env/bin_config.rs b/crates/vite_global_cli/src/commands/env/bin_config.rs index 76b0198425..3e136c678a 100644 --- a/crates/vite_global_cli/src/commands/env/bin_config.rs +++ b/crates/vite_global_cli/src/commands/env/bin_config.rs @@ -137,11 +137,25 @@ impl BinConfig { Ok(()) } + /// Find all binaries with `Vp` source (installed via `vp install -g`). + /// + /// Used during shim refresh to discover package shims that need their + /// trampoline executables updated after a vite-plus upgrade. + #[cfg_attr(not(windows), allow(dead_code))] // Only called from #[cfg(windows)] refresh_package_shims + pub async fn find_all_vp_source() -> Result, Error> { + Self::find_bins_where(|config| config.source == BinSource::Vp).await + } + /// Find all binaries installed by a package. /// /// This is used as a fallback during uninstall when PackageMetadata is missing /// (orphan recovery). pub async fn find_by_package(package_name: &str) -> Result, Error> { + Self::find_bins_where(|config| config.package == package_name).await + } + + /// Scan `~/.vite-plus/bins/` and return names of binaries matching a predicate. + async fn find_bins_where(predicate: impl Fn(&BinConfig) -> bool) -> Result, Error> { let bins_dir = Self::bins_dir()?; if !tokio::fs::try_exists(&bins_dir).await.unwrap_or(false) { return Ok(Vec::new()); @@ -155,7 +169,7 @@ impl BinConfig { if path.extension().is_some_and(|e| e == "json") { if let Ok(content) = tokio::fs::read_to_string(&path).await { if let Ok(config) = serde_json::from_str::(&content) { - if config.package == package_name { + if predicate(&config) { bins.push(config.name); } } @@ -346,4 +360,53 @@ mod tests { // Delete again should not error BinConfig::delete_sync("codex").unwrap(); } + + #[tokio::test] + async fn test_find_all_vp_source() { + let temp_dir = TempDir::new().unwrap(); + let _guard = vite_shared::EnvConfig::test_guard( + vite_shared::EnvConfig::for_test_with_home(temp_dir.path()), + ); + + // Create Vp-source configs + let tsc = BinConfig::new( + "tsc".to_string(), + "typescript".to_string(), + "5.0.0".to_string(), + "20.18.0".to_string(), + ); + tsc.save().await.unwrap(); + + let corepack = BinConfig::new( + "corepack".to_string(), + "corepack".to_string(), + "0.20.0".to_string(), + "20.18.0".to_string(), + ); + corepack.save().await.unwrap(); + + // Create Npm-source config (should be excluded) + let codex = BinConfig::new_npm( + "codex".to_string(), + "@openai/codex".to_string(), + "22.22.0".to_string(), + ); + codex.save().await.unwrap(); + + let mut vp_bins = BinConfig::find_all_vp_source().await.unwrap(); + vp_bins.sort(); + assert_eq!(vp_bins.len(), 2); + assert_eq!(vp_bins, vec!["corepack", "tsc"]); + } + + #[tokio::test] + async fn test_find_all_vp_source_empty_bins_dir() { + let temp_dir = TempDir::new().unwrap(); + let _guard = vite_shared::EnvConfig::test_guard( + vite_shared::EnvConfig::for_test_with_home(temp_dir.path()), + ); + + let vp_bins = BinConfig::find_all_vp_source().await.unwrap(); + assert!(vp_bins.is_empty()); + } } diff --git a/crates/vite_global_cli/src/commands/env/doctor.rs b/crates/vite_global_cli/src/commands/env/doctor.rs index f54fe09ae9..619943d9d3 100644 --- a/crates/vite_global_cli/src/commands/env/doctor.rs +++ b/crates/vite_global_cli/src/commands/env/doctor.rs @@ -62,8 +62,7 @@ const KNOWN_VERSION_MANAGERS: &[(&str, &str)] = &[ ("n", "N_PREFIX"), ]; -/// Tools that should have shims -const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"]; +use super::setup::SHIM_TOOLS; /// Column width for left-side keys in aligned output const KEY_WIDTH: usize = 18; diff --git a/crates/vite_global_cli/src/commands/env/global_install.rs b/crates/vite_global_cli/src/commands/env/global_install.rs index a3ea85ae38..327478d37c 100644 --- a/crates/vite_global_cli/src/commands/env/global_install.rs +++ b/crates/vite_global_cli/src/commands/env/global_install.rs @@ -408,10 +408,8 @@ async fn create_package_shim( { let shim_path = bin_dir.join(format!("{}.exe", bin_name)); - // Skip if already exists (e.g., re-installing the same package) - if tokio::fs::try_exists(&shim_path).await.unwrap_or(false) { - return Ok(()); - } + // Delete before overwrite; falls back to rename if the exe is locked. + super::setup::remove_or_rename_to_old(&shim_path).await; // Copy the trampoline binary as .exe. // The trampoline detects the tool name from its own filename and sets diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 1e5b7b5aed..1748a9b8fd 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -23,7 +23,7 @@ use super::config::{get_bin_dir, get_vp_home}; use crate::{error::Error, help}; /// Tools to create shims for (node, npm, npx, vpx, vpr) -const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"]; +pub(crate) const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"]; fn accent_command(command: &str) -> String { if help::should_style_help() { @@ -82,6 +82,13 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result } } + #[cfg(windows)] + if refresh { + if let Err(e) = refresh_package_shims(&bin_dir).await { + tracing::warn!("Failed to refresh package shims: {}", e); + } + } + // Best-effort cleanup of .old files from rename-before-copy on Windows #[cfg(windows)] if refresh { @@ -192,11 +199,8 @@ async fn create_shim( if !refresh { return Ok(false); } - // Remove existing shim for refresh. - // On Windows, .exe files may be locked (by antivirus, indexer, or - // still-running processes), so rename to .old first instead of deleting. #[cfg(windows)] - rename_to_old(&shim_path).await; + remove_or_rename_to_old(&shim_path).await; #[cfg(not(windows))] { tokio::fs::remove_file(&shim_path).await?; @@ -273,6 +277,46 @@ async fn create_windows_shim( Ok(()) } +/// Refresh trampoline `.exe` files for package shims installed via `vp install -g`. +/// +/// Discovers all package binaries tracked by BinConfig with `source: Vp` +/// and replaces their `.exe` with the current trampoline. +#[cfg(windows)] +async fn refresh_package_shims(bin_dir: &vite_path::AbsolutePath) -> Result<(), Error> { + use super::bin_config::BinConfig; + + let package_bins = BinConfig::find_all_vp_source().await?; + + if package_bins.is_empty() { + return Ok(()); + } + + let trampoline_src = get_trampoline_path()?; + + for bin_name in &package_bins { + // Core shims (SHIM_TOOLS + vp) are already refreshed by the main loop. + if bin_name == "vp" || SHIM_TOOLS.contains(&bin_name.as_str()) { + continue; + } + + let shim_path = bin_dir.join(format!("{bin_name}.exe")); + + remove_or_rename_to_old(&shim_path).await; + + if let Err(e) = tokio::fs::copy(trampoline_src.as_path(), &shim_path).await { + tracing::warn!("Failed to refresh package shim {}: {}", bin_name, e); + continue; + } + + // Remove legacy .cmd/shell wrappers that could shadow the .exe in Git Bash. + cleanup_legacy_windows_shim(bin_dir, bin_name).await; + + tracing::debug!("Refreshed package trampoline shim {:?}", shim_path); + } + + Ok(()) +} + /// Get the path to the trampoline template binary (vp-shim.exe). /// /// The trampoline binary is distributed alongside vp.exe in the same directory. @@ -309,6 +353,22 @@ pub(crate) fn get_trampoline_path() -> Result .ok_or_else(|| Error::ConfigError("Invalid trampoline path".into())) } +/// Try to delete an `.exe` file; if deletion fails (e.g., file is locked by a +/// running process), fall back to renaming it to `.old`. +/// +/// This avoids accumulating `.old` files when the exe is not in use. +#[cfg(windows)] +pub(crate) async fn remove_or_rename_to_old(path: &vite_path::AbsolutePath) { + match tokio::fs::remove_file(path).await { + Ok(()) => return, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, + Err(e) => { + tracing::debug!("remove_file failed ({}), attempting rename", e); + } + } + rename_to_old(path).await; +} + /// Rename an existing `.exe` to a timestamped `.old` file instead of deleting. /// /// On Windows, running `.exe` files can't be deleted or overwritten, but they can diff --git a/rfcs/trampoline-exe-for-shims.md b/rfcs/trampoline-exe-for-shims.md index 620ab61ab4..7f7f656b9c 100644 --- a/rfcs/trampoline-exe-for-shims.md +++ b/rfcs/trampoline-exe-for-shims.md @@ -207,6 +207,17 @@ When `vp env setup --refresh` is invoked through the trampoline (`~/.vite-plus/b 2. Copy new trampoline to `vp.exe` 3. Best-effort cleanup of all `*.old` files in the bin directory +### Upgrade Refresh + +During `vp upgrade`, after the `current` link is swapped to the new version, `vp env setup --refresh` is invoked to regenerate all trampoline `.exe` files. This ensures that when the trampoline binary (`vp-shim.exe`) changes between versions, all shims pick up the new version: + +1. **Core shims** (`vp.exe`, `node.exe`, `npm.exe`, `npx.exe`, `vpx.exe`, `vpr.exe`) are refreshed by the standard `--refresh` logic. +2. **Package shims** (e.g., `corepack.exe`, `tsc.exe`, installed via `vp install -g`) are discovered by scanning `~/.vite-plus/bins/` for `BinConfig` entries with `source: Vp`, and each `.exe` is replaced with the new trampoline. + +Package shims installed via npm interception (`source: Npm`) use `.cmd` wrappers, not trampoline `.exe` files, and are not affected by this refresh. + +Additionally, re-installing a global package (`vp install -g `) always re-copies the current trampoline, ensuring the shim stays up to date even without a full upgrade. + ### Distribution The trampoline binary (`vp-shim.exe`) is distributed alongside `vp.exe`: diff --git a/rfcs/upgrade-command.md b/rfcs/upgrade-command.md index 7780e02342..6e379119a3 100644 --- a/rfcs/upgrade-command.md +++ b/rfcs/upgrade-command.md @@ -302,7 +302,7 @@ Key differences on Windows: After the symlink swap (the **point of no return**), post-update operations are treated as non-fatal. Errors are printed to stderr as warnings but do not trigger the outer error handler (which would delete the now-active version directory). -1. **Refresh shims**: Run the equivalent of `vp env setup --refresh` to ensure node/npm/npx shims point to the new version. If this fails, the user can run it manually. +1. **Refresh shims**: Run the equivalent of `vp env setup --refresh` to ensure node/npm/npx shims point to the new version. This also refreshes trampoline `.exe` files for globally installed package shims (e.g., `corepack.exe`, `tsc.exe`) by scanning `BinConfig` entries. If this fails, the user can run it manually. 2. **Cleanup old versions**: Remove old version directories, keeping the 5 most recent by **creation time** (matching `install.sh` behavior). The new version and the previous version are always protected from cleanup, even if they fall outside the top 5 (e.g., after a downgrade via `--rollback`). #### Step 7: Running Binary Consideration