From 1233c47e397a7136cb282efface58104ed36072d Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 29 Apr 2026 17:46:53 +0900 Subject: [PATCH 01/16] fix(command): route Windows .cmd shims through PowerShell for pm commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same .cmd → .ps1 rewrite to `vite_command::run_command` (and `run_command_with_fspy`) so package-manager-routed commands (`vp dlx`, `vp add`, `vp remove`, `vp update`, `vp install`, …) stop spawning `cmd.exe` on Windows and Ctrl+C no longer leaves the terminal in a broken state. The task-layer fix (`vite_task_plan::ps1_shim`) only covered `node_modules/.bin/*.cmd` — too narrow for pm shims like `npm.cmd` / `pnpm.cmd` / `yarn.cmd` which live in `~/.vite-plus/js_runtime/...` and system PATH. The new module drops the `node_modules/.bin` check: if a `.cmd` has a sibling `.ps1`, route through PowerShell. Closes #1489 --- crates/vite_command/src/lib.rs | 30 +++-- crates/vite_command/src/ps1_shim.rs | 197 ++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 crates/vite_command/src/ps1_shim.rs diff --git a/crates/vite_command/src/lib.rs b/crates/vite_command/src/lib.rs index 9f724e3aac..7eae766b95 100644 --- a/crates/vite_command/src/lib.rs +++ b/crates/vite_command/src/lib.rs @@ -12,6 +12,8 @@ use tokio_util::sync::CancellationToken; use vite_error::Error; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; +mod ps1_shim; + /// Result of running a command with fspy tracking. #[derive(Debug)] pub struct FspyCommandResult { @@ -142,8 +144,12 @@ where let cwd = cwd.as_ref(); let paths = envs.get("PATH"); let bin_path = resolve_bin(bin_name, paths.map(|p| OsStr::new(p.as_str())), cwd)?; - let mut cmd = build_command(&bin_path, cwd); - cmd.args(args).envs(envs); + let (program, prefix_args) = match ps1_shim::rewrite_cmd_to_powershell(&bin_path) { + Some(rewritten) => rewritten, + None => (bin_path, Vec::new()), + }; + let mut cmd = build_command(&program, cwd); + cmd.args(&prefix_args).args(args).envs(envs); let status = cmd.status().await?; Ok(status) } @@ -171,8 +177,15 @@ where S: AsRef, { let cwd = cwd.as_ref(); - let mut cmd = fspy::Command::new(bin_name); - cmd.args(args) + let bin_path = resolve_bin(bin_name, envs.get("PATH").map(|p| OsStr::new(p.as_str())), cwd)?; + let (program, prefix_args) = match ps1_shim::rewrite_cmd_to_powershell(&bin_path) { + Some(rewritten) => rewritten, + None => (bin_path, Vec::new()), + }; + + let mut cmd = fspy::Command::new(program.as_path()); + cmd.args(&prefix_args) + .args(args) // set system environment variables first .envs(std::env::vars_os()) // then set custom environment variables @@ -454,12 +467,9 @@ mod tests { run_command_with_fspy("npm-not-exists", &["--version"], &envs, &temp_dir_path) .await; assert!(result.is_err(), "Should not find binary path, but got: {:?}", result); - assert!( - result - .err() - .unwrap() - .to_string() - .contains("could not resolve the full path of program '\"npm-not-exists\"'") + assert_eq!( + result.unwrap_err().to_string(), + "Cannot find binary path for command 'npm-not-exists'" ); } } diff --git a/crates/vite_command/src/ps1_shim.rs b/crates/vite_command/src/ps1_shim.rs new file mode 100644 index 0000000000..6712d31890 --- /dev/null +++ b/crates/vite_command/src/ps1_shim.rs @@ -0,0 +1,197 @@ +//! Windows-specific: when a resolved binary is a `.cmd` shim with a sibling +//! `.ps1`, rewrite the spawn to go through `powershell.exe -File `. +//! +//! Running a `.cmd` from any shell makes `cmd.exe` prompt "Terminate batch +//! job (Y/N)?" on Ctrl+C, which leaves the terminal corrupt. Routing through +//! `PowerShell` sidesteps the prompt and lets Ctrl+C propagate cleanly. +//! +//! Unlike the task-layer rewrite (`vite_task_plan::ps1_shim`, scoped to +//! `node_modules/.bin/*.cmd` inside the workspace), this one applies to any +//! `.cmd` whose `.ps1` sibling exists. Package manager shims (`npm.cmd`, +//! `pnpm.cmd`, `yarn.cmd`, `npx.cmd`) live in `~/.vite-plus/js_runtime/...` +//! or system PATH — outside any `node_modules/.bin` — so the structural +//! check there is too narrow for this code path. If a `.ps1` sibling exists, +//! the same tool that emitted the `.cmd` (npm cmd-shim, pnpm, yarn) emitted +//! the `.ps1` with equivalent semantics. +//! +//! See +//! and . + +use std::ffi::OsString; + +use vite_path::{AbsolutePath, AbsolutePathBuf}; + +/// Fixed arguments prepended before the `.ps1` path. `-NoProfile`/`-NoLogo` +/// skip user profile loading; `-ExecutionPolicy Bypass` allows running the +/// unsigned shims that npm/pnpm/yarn install. +#[cfg(any(windows, test))] +const POWERSHELL_PREFIX: &[&str] = + &["-NoProfile", "-NoLogo", "-ExecutionPolicy", "Bypass", "-File"]; + +/// Rewrite a resolved `.cmd` invocation to go through PowerShell. +/// +/// Returns `Some((powershell_host, prefix_args))` when the rewrite applies, +/// where `prefix_args` is `["-NoProfile", "-NoLogo", "-ExecutionPolicy", +/// "Bypass", "-File", ]`. Caller prepends `prefix_args` to the +/// user args and spawns `powershell_host`. +/// +/// Returns `None` when: +/// - not on Windows, +/// - no PowerShell host (`pwsh.exe` or `powershell.exe`) is on PATH, +/// - the resolved path is not a `.cmd` (case-insensitive), +/// - the `.cmd` has no sibling `.ps1`. +#[cfg(windows)] +#[must_use] +pub fn rewrite_cmd_to_powershell( + resolved: &AbsolutePath, +) -> Option<(AbsolutePathBuf, Vec)> { + let host = powershell_host()?; + rewrite_with_host(resolved, host) +} + +#[cfg(not(windows))] +#[must_use] +pub const fn rewrite_cmd_to_powershell( + _resolved: &AbsolutePath, +) -> Option<(AbsolutePathBuf, Vec)> { + None +} + +/// Cached location of the PowerShell host. Prefers cross-platform `pwsh.exe` +/// when present, falling back to the Windows built-in `powershell.exe`. +#[cfg(windows)] +fn powershell_host() -> Option<&'static AbsolutePathBuf> { + use std::sync::LazyLock; + + static POWERSHELL_HOST: LazyLock> = LazyLock::new(|| { + let resolved = which::which("pwsh.exe").or_else(|_| which::which("powershell.exe")).ok()?; + AbsolutePathBuf::new(resolved) + }); + POWERSHELL_HOST.as_ref() +} + +/// Pure rewrite logic. Factored out so tests can drive it on any platform +/// without depending on a real `powershell.exe`. +#[cfg(any(windows, test))] +fn rewrite_with_host( + resolved: &AbsolutePath, + host: &AbsolutePathBuf, +) -> Option<(AbsolutePathBuf, Vec)> { + let ps1 = find_ps1_sibling(resolved)?; + + tracing::debug!( + "rewriting .cmd to powershell: {} -> {} -File {}", + resolved.as_path().display(), + host.as_path().display(), + ps1.as_path().display(), + ); + + let mut prefix_args: Vec = + POWERSHELL_PREFIX.iter().copied().map(OsString::from).collect(); + prefix_args.push(ps1.as_path().as_os_str().to_owned()); + + Some((host.clone(), prefix_args)) +} + +#[cfg(any(windows, test))] +fn find_ps1_sibling(resolved: &AbsolutePath) -> Option { + let ext = resolved.as_path().extension().and_then(|e| e.to_str())?; + if !ext.eq_ignore_ascii_case("cmd") { + return None; + } + + let ps1 = resolved.with_extension("ps1"); + if !ps1.as_path().is_file() { + return None; + } + + Some(ps1) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::*; + + #[expect(clippy::disallowed_types, reason = "tempdir bridges std PathBuf into AbsolutePath")] + fn abs(buf: std::path::PathBuf) -> AbsolutePathBuf { + AbsolutePathBuf::new(buf).unwrap() + } + + #[test] + fn rewrites_cmd_to_powershell_with_sibling_ps1() { + let dir = tempdir().unwrap(); + let root = abs(dir.path().canonicalize().unwrap()); + fs::write(root.as_path().join("npm.cmd"), "").unwrap(); + fs::write(root.as_path().join("npm.ps1"), "").unwrap(); + + let host = abs(root.as_path().join("powershell.exe")); + let resolved = abs(root.as_path().join("npm.cmd")); + + let (program, prefix_args) = rewrite_with_host(&resolved, &host).expect("should rewrite"); + + assert_eq!(program.as_path(), host.as_path()); + let as_strs: Vec<&str> = prefix_args.iter().filter_map(|a| a.to_str()).collect(); + let ps1_path = root.as_path().join("npm.ps1"); + let ps1_str = ps1_path.to_str().unwrap(); + assert_eq!( + as_strs, + vec!["-NoProfile", "-NoLogo", "-ExecutionPolicy", "Bypass", "-File", ps1_str] + ); + } + + #[test] + fn rewrites_uppercase_cmd_extension() { + let dir = tempdir().unwrap(); + let root = abs(dir.path().canonicalize().unwrap()); + fs::write(root.as_path().join("pnpm.CMD"), "").unwrap(); + fs::write(root.as_path().join("pnpm.ps1"), "").unwrap(); + + let host = abs(root.as_path().join("powershell.exe")); + let resolved = abs(root.as_path().join("pnpm.CMD")); + + let result = rewrite_with_host(&resolved, &host); + assert!(result.is_some(), "case-insensitive .CMD should still rewrite"); + } + + #[test] + fn returns_none_when_no_ps1_sibling() { + let dir = tempdir().unwrap(); + let root = abs(dir.path().canonicalize().unwrap()); + fs::write(root.as_path().join("npm.cmd"), "").unwrap(); + + let host = abs(root.as_path().join("powershell.exe")); + let resolved = abs(root.as_path().join("npm.cmd")); + + assert!(rewrite_with_host(&resolved, &host).is_none()); + } + + #[test] + fn returns_none_for_exe() { + let dir = tempdir().unwrap(); + let root = abs(dir.path().canonicalize().unwrap()); + fs::write(root.as_path().join("bun.exe"), "").unwrap(); + fs::write(root.as_path().join("bun.ps1"), "").unwrap(); + + let host = abs(root.as_path().join("powershell.exe")); + let resolved = abs(root.as_path().join("bun.exe")); + + assert!(rewrite_with_host(&resolved, &host).is_none()); + } + + #[test] + fn returns_none_for_no_extension() { + let dir = tempdir().unwrap(); + let root = abs(dir.path().canonicalize().unwrap()); + fs::write(root.as_path().join("node"), "").unwrap(); + fs::write(root.as_path().join("node.ps1"), "").unwrap(); + + let host = abs(root.as_path().join("powershell.exe")); + let resolved = abs(root.as_path().join("node")); + + assert!(rewrite_with_host(&resolved, &host).is_none()); + } +} From ebbace435e463995c318f3127cd303cdfc62d140 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 29 Apr 2026 17:51:51 +0900 Subject: [PATCH 02/16] refactor(command): extract resolve_program to dedupe ps1-rewrite plumbing Both `run_command` and `run_command_with_fspy` were doing the same resolve_bin + rewrite_cmd_to_powershell match. Pull that into a private helper so each call site is one line. --- crates/vite_command/src/lib.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/vite_command/src/lib.rs b/crates/vite_command/src/lib.rs index 7eae766b95..d0e136e867 100644 --- a/crates/vite_command/src/lib.rs +++ b/crates/vite_command/src/lib.rs @@ -45,6 +45,21 @@ pub fn resolve_bin( AbsolutePathBuf::new(path).ok_or_else(|| Error::CannotFindBinaryPath(bin_name.into())) } +/// Resolve `bin_name` to a path and apply the Windows `.cmd` → PowerShell +/// rewrite. Returns the program to spawn and the arg prefix to prepend +/// before the user args (empty when no rewrite applies). +fn resolve_program( + bin_name: &str, + envs: &HashMap, + cwd: &AbsolutePath, +) -> Result<(AbsolutePathBuf, Vec), Error> { + let bin_path = resolve_bin(bin_name, envs.get("PATH").map(|p| OsStr::new(p.as_str())), cwd)?; + Ok(match ps1_shim::rewrite_cmd_to_powershell(&bin_path) { + Some(rewritten) => rewritten, + None => (bin_path, Vec::new()), + }) +} + /// Build a `tokio::process::Command` for a pre-resolved binary path. /// Sets inherited stdio and `fix_stdio_streams` (Unix pre_exec). /// Callers can further customize (add args, envs, override stdio, etc.). @@ -142,12 +157,7 @@ where S: AsRef, { let cwd = cwd.as_ref(); - let paths = envs.get("PATH"); - let bin_path = resolve_bin(bin_name, paths.map(|p| OsStr::new(p.as_str())), cwd)?; - let (program, prefix_args) = match ps1_shim::rewrite_cmd_to_powershell(&bin_path) { - Some(rewritten) => rewritten, - None => (bin_path, Vec::new()), - }; + let (program, prefix_args) = resolve_program(bin_name, envs, cwd)?; let mut cmd = build_command(&program, cwd); cmd.args(&prefix_args).args(args).envs(envs); let status = cmd.status().await?; @@ -177,11 +187,7 @@ where S: AsRef, { let cwd = cwd.as_ref(); - let bin_path = resolve_bin(bin_name, envs.get("PATH").map(|p| OsStr::new(p.as_str())), cwd)?; - let (program, prefix_args) = match ps1_shim::rewrite_cmd_to_powershell(&bin_path) { - Some(rewritten) => rewritten, - None => (bin_path, Vec::new()), - }; + let (program, prefix_args) = resolve_program(bin_name, envs, cwd)?; let mut cmd = fspy::Command::new(program.as_path()); cmd.args(&prefix_args) From 86258423e6931989f7bc32af0fcee46d7ef43a86 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 29 Apr 2026 20:28:56 +0900 Subject: [PATCH 03/16] refactor(command): use shared vite_powershell crate for ps1 rewrite Switch `vite_command::ps1_shim` to depend on the new `vite_powershell` crate (companion change in voidzero-dev/vite-task#368) for the PowerShell host lookup, the fixed argument prefix, and the sibling-`.ps1` discovery. This module now just composes those primitives with vp's own conventions: absolute `.ps1` path in args and `Vec` return type to match the spawn API. Bumps the vite-task git rev across all pinned crates to pick up the extraction. --- Cargo.lock | 53 ++++++++------ Cargo.toml | 14 ++-- crates/vite_command/Cargo.toml | 1 + crates/vite_command/src/ps1_shim.rs | 110 +++++----------------------- 4 files changed, 58 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f3786249e..54d9016a2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1872,7 +1872,7 @@ dependencies = [ [[package]] name = "fspy" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "allocator-api2", "anyhow", @@ -1908,7 +1908,7 @@ dependencies = [ [[package]] name = "fspy_detours_sys" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "cc", "winapi", @@ -1917,7 +1917,7 @@ dependencies = [ [[package]] name = "fspy_preload_unix" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "anyhow", "bstr", @@ -1932,7 +1932,7 @@ dependencies = [ [[package]] name = "fspy_preload_windows" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "constcat", "fspy_detours_sys", @@ -1948,7 +1948,7 @@ dependencies = [ [[package]] name = "fspy_seccomp_unotify" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "futures-util", "libc", @@ -1965,7 +1965,7 @@ dependencies = [ [[package]] name = "fspy_shared" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "allocator-api2", "bitflags 2.11.0", @@ -1984,7 +1984,7 @@ dependencies = [ [[package]] name = "fspy_shared_unix" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "anyhow", "base64 0.22.1", @@ -3124,7 +3124,7 @@ dependencies = [ [[package]] name = "materialized_artifact" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "tempfile", ] @@ -3132,7 +3132,7 @@ dependencies = [ [[package]] name = "materialized_artifact_build" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "xxhash-rust", ] @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "native_str" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "allocator-api2", "bytemuck", @@ -4821,7 +4821,7 @@ dependencies = [ [[package]] name = "pty_terminal_test_client" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" [[package]] name = "quote" @@ -7467,6 +7467,7 @@ dependencies = [ "tracing", "vite_error", "vite_path", + "vite_powershell", "which", ] @@ -7496,7 +7497,7 @@ dependencies = [ [[package]] name = "vite_glob" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "thiserror 2.0.18", "vite_path", @@ -7536,7 +7537,7 @@ dependencies = [ [[package]] name = "vite_graph_ser" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "petgraph 0.8.3", "serde", @@ -7635,7 +7636,7 @@ dependencies = [ [[package]] name = "vite_path" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "diff-struct", "path-clean", @@ -7646,10 +7647,19 @@ dependencies = [ "wincode", ] +[[package]] +name = "vite_powershell" +version = "0.1.0" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +dependencies = [ + "vite_path", + "which", +] + [[package]] name = "vite_select" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "anyhow", "crossterm", @@ -7699,7 +7709,7 @@ dependencies = [ [[package]] name = "vite_shell" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "brush-parser 0.3.0 (git+https://github.com/reubeno/brush?rev=dcb760933b10ee0433d7b740a5709b06f5c67c6b)", "diff-struct", @@ -7726,7 +7736,7 @@ dependencies = [ [[package]] name = "vite_str" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "compact_str", "diff-struct", @@ -7737,7 +7747,7 @@ dependencies = [ [[package]] name = "vite_task" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "anyhow", "async-trait", @@ -7775,7 +7785,7 @@ dependencies = [ [[package]] name = "vite_task_graph" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "anyhow", "async-trait", @@ -7797,7 +7807,7 @@ dependencies = [ [[package]] name = "vite_task_plan" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "anyhow", "async-trait", @@ -7815,6 +7825,7 @@ dependencies = [ "vite_glob", "vite_graph_ser", "vite_path", + "vite_powershell", "vite_shell", "vite_str", "vite_task_graph", @@ -7829,7 +7840,7 @@ version = "0.0.0" [[package]] name = "vite_workspace" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef#d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" dependencies = [ "clap", "petgraph 0.8.3", diff --git a/Cargo.toml b/Cargo.toml index 02932f7530..07c5320a55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,7 @@ dunce = "1.0.5" fast-glob = "1.0.0" flate2 = { version = "=1.1.9", features = ["zlib-rs"] } form_urlencoded = "1.2.1" -fspy = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" } +fspy = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } futures = "0.3.31" futures-util = "0.3.31" glob = "0.3.2" @@ -194,16 +194,17 @@ vfs = "0.13.0" vite_command = { path = "crates/vite_command" } vite_error = { path = "crates/vite_error" } vite_js_runtime = { path = "crates/vite_js_runtime" } -vite_glob = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" } +vite_glob = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } vite_install = { path = "crates/vite_install" } vite_migration = { path = "crates/vite_migration" } vite_setup = { path = "crates/vite_setup" } vite_shared = { path = "crates/vite_shared" } vite_static_config = { path = "crates/vite_static_config" } -vite_path = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" } -vite_str = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" } -vite_task = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" } -vite_workspace = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" } +vite_path = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } +vite_powershell = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } +vite_str = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } +vite_task = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } +vite_workspace = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } walkdir = "2.5.0" wax = "0.6.0" which = "8.0.0" @@ -314,6 +315,7 @@ string_wizard = { path = "./rolldown/crates/string_wizard", features = ["serde"] # fspy = { path = "../vite-task/crates/fspy" } # vite_glob = { path = "../vite-task/crates/vite_glob" } # vite_path = { path = "../vite-task/crates/vite_path" } +# vite_powershell = { path = "../vite-task/crates/vite_powershell" } # vite_str = { path = "../vite-task/crates/vite_str" } # vite_task = { path = "../vite-task/crates/vite_task" } # vite_workspace = { path = "../vite-task/crates/vite_workspace" } diff --git a/crates/vite_command/Cargo.toml b/crates/vite_command/Cargo.toml index 4d031bfe42..e1cd5a971d 100644 --- a/crates/vite_command/Cargo.toml +++ b/crates/vite_command/Cargo.toml @@ -14,6 +14,7 @@ tokio-util = { workspace = true } tracing = { workspace = true } vite_error = { workspace = true } vite_path = { workspace = true } +vite_powershell = { workspace = true } which = { workspace = true, features = ["tracing"] } [target.'cfg(not(target_os = "windows"))'.dependencies] diff --git a/crates/vite_command/src/ps1_shim.rs b/crates/vite_command/src/ps1_shim.rs index 6712d31890..59a6ee3c25 100644 --- a/crates/vite_command/src/ps1_shim.rs +++ b/crates/vite_command/src/ps1_shim.rs @@ -10,23 +10,21 @@ //! `.cmd` whose `.ps1` sibling exists. Package manager shims (`npm.cmd`, //! `pnpm.cmd`, `yarn.cmd`, `npx.cmd`) live in `~/.vite-plus/js_runtime/...` //! or system PATH — outside any `node_modules/.bin` — so the structural -//! check there is too narrow for this code path. If a `.ps1` sibling exists, -//! the same tool that emitted the `.cmd` (npm cmd-shim, pnpm, yarn) emitted -//! the `.ps1` with equivalent semantics. +//! check there is too narrow for this code path. +//! +//! The cross-platform primitives (`POWERSHELL_PREFIX`, `powershell_host`, +//! `find_ps1_sibling`) live in the `vite_powershell` crate and are shared +//! with `vite_task_plan::ps1_shim`. This module composes them with vp's +//! own conventions: absolute `.ps1` path in args (no cache fingerprint to +//! keep portable) and `Vec` return type (matches the spawn API). //! //! See //! and . -use std::ffi::OsString; +use std::{ffi::OsString, sync::Arc}; use vite_path::{AbsolutePath, AbsolutePathBuf}; - -/// Fixed arguments prepended before the `.ps1` path. `-NoProfile`/`-NoLogo` -/// skip user profile loading; `-ExecutionPolicy Bypass` allows running the -/// unsigned shims that npm/pnpm/yarn install. -#[cfg(any(windows, test))] -const POWERSHELL_PREFIX: &[&str] = - &["-NoProfile", "-NoLogo", "-ExecutionPolicy", "Bypass", "-File"]; +use vite_powershell::{POWERSHELL_PREFIX, find_ps1_sibling, powershell_host}; /// Rewrite a resolved `.cmd` invocation to go through PowerShell. /// @@ -40,7 +38,6 @@ const POWERSHELL_PREFIX: &[&str] = /// - no PowerShell host (`pwsh.exe` or `powershell.exe`) is on PATH, /// - the resolved path is not a `.cmd` (case-insensitive), /// - the `.cmd` has no sibling `.ps1`. -#[cfg(windows)] #[must_use] pub fn rewrite_cmd_to_powershell( resolved: &AbsolutePath, @@ -49,33 +46,11 @@ pub fn rewrite_cmd_to_powershell( rewrite_with_host(resolved, host) } -#[cfg(not(windows))] -#[must_use] -pub const fn rewrite_cmd_to_powershell( - _resolved: &AbsolutePath, -) -> Option<(AbsolutePathBuf, Vec)> { - None -} - -/// Cached location of the PowerShell host. Prefers cross-platform `pwsh.exe` -/// when present, falling back to the Windows built-in `powershell.exe`. -#[cfg(windows)] -fn powershell_host() -> Option<&'static AbsolutePathBuf> { - use std::sync::LazyLock; - - static POWERSHELL_HOST: LazyLock> = LazyLock::new(|| { - let resolved = which::which("pwsh.exe").or_else(|_| which::which("powershell.exe")).ok()?; - AbsolutePathBuf::new(resolved) - }); - POWERSHELL_HOST.as_ref() -} - /// Pure rewrite logic. Factored out so tests can drive it on any platform /// without depending on a real `powershell.exe`. -#[cfg(any(windows, test))] fn rewrite_with_host( resolved: &AbsolutePath, - host: &AbsolutePathBuf, + host: &Arc, ) -> Option<(AbsolutePathBuf, Vec)> { let ps1 = find_ps1_sibling(resolved)?; @@ -90,22 +65,7 @@ fn rewrite_with_host( POWERSHELL_PREFIX.iter().copied().map(OsString::from).collect(); prefix_args.push(ps1.as_path().as_os_str().to_owned()); - Some((host.clone(), prefix_args)) -} - -#[cfg(any(windows, test))] -fn find_ps1_sibling(resolved: &AbsolutePath) -> Option { - let ext = resolved.as_path().extension().and_then(|e| e.to_str())?; - if !ext.eq_ignore_ascii_case("cmd") { - return None; - } - - let ps1 = resolved.with_extension("ps1"); - if !ps1.as_path().is_file() { - return None; - } - - Some(ps1) + Some((host.to_absolute_path_buf(), prefix_args)) } #[cfg(test)] @@ -121,6 +81,10 @@ mod tests { AbsolutePathBuf::new(buf).unwrap() } + fn host_arc(root: &AbsolutePath) -> Arc { + Arc::::from(abs(root.as_path().join("powershell.exe"))) + } + #[test] fn rewrites_cmd_to_powershell_with_sibling_ps1() { let dir = tempdir().unwrap(); @@ -128,7 +92,7 @@ mod tests { fs::write(root.as_path().join("npm.cmd"), "").unwrap(); fs::write(root.as_path().join("npm.ps1"), "").unwrap(); - let host = abs(root.as_path().join("powershell.exe")); + let host = host_arc(&root); let resolved = abs(root.as_path().join("npm.cmd")); let (program, prefix_args) = rewrite_with_host(&resolved, &host).expect("should rewrite"); @@ -143,55 +107,15 @@ mod tests { ); } - #[test] - fn rewrites_uppercase_cmd_extension() { - let dir = tempdir().unwrap(); - let root = abs(dir.path().canonicalize().unwrap()); - fs::write(root.as_path().join("pnpm.CMD"), "").unwrap(); - fs::write(root.as_path().join("pnpm.ps1"), "").unwrap(); - - let host = abs(root.as_path().join("powershell.exe")); - let resolved = abs(root.as_path().join("pnpm.CMD")); - - let result = rewrite_with_host(&resolved, &host); - assert!(result.is_some(), "case-insensitive .CMD should still rewrite"); - } - #[test] fn returns_none_when_no_ps1_sibling() { let dir = tempdir().unwrap(); let root = abs(dir.path().canonicalize().unwrap()); fs::write(root.as_path().join("npm.cmd"), "").unwrap(); - let host = abs(root.as_path().join("powershell.exe")); + let host = host_arc(&root); let resolved = abs(root.as_path().join("npm.cmd")); assert!(rewrite_with_host(&resolved, &host).is_none()); } - - #[test] - fn returns_none_for_exe() { - let dir = tempdir().unwrap(); - let root = abs(dir.path().canonicalize().unwrap()); - fs::write(root.as_path().join("bun.exe"), "").unwrap(); - fs::write(root.as_path().join("bun.ps1"), "").unwrap(); - - let host = abs(root.as_path().join("powershell.exe")); - let resolved = abs(root.as_path().join("bun.exe")); - - assert!(rewrite_with_host(&resolved, &host).is_none()); - } - - #[test] - fn returns_none_for_no_extension() { - let dir = tempdir().unwrap(); - let root = abs(dir.path().canonicalize().unwrap()); - fs::write(root.as_path().join("node"), "").unwrap(); - fs::write(root.as_path().join("node.ps1"), "").unwrap(); - - let host = abs(root.as_path().join("powershell.exe")); - let resolved = abs(root.as_path().join("node")); - - assert!(rewrite_with_host(&resolved, &host).is_none()); - } } From 121a5449191e54e0fb0fe8562f3541c42ef3ec30 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 29 Apr 2026 20:33:19 +0900 Subject: [PATCH 04/16] chore(deps): bump vite-task pin to 957278df Picks up the test-only AbsolutePathBuf import move from voidzero-dev/vite-task#368 review feedback. --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- Cargo.toml | 14 +++++++------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54d9016a2e..9a3061876e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1872,7 +1872,7 @@ dependencies = [ [[package]] name = "fspy" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "allocator-api2", "anyhow", @@ -1908,7 +1908,7 @@ dependencies = [ [[package]] name = "fspy_detours_sys" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "cc", "winapi", @@ -1917,7 +1917,7 @@ dependencies = [ [[package]] name = "fspy_preload_unix" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "anyhow", "bstr", @@ -1932,7 +1932,7 @@ dependencies = [ [[package]] name = "fspy_preload_windows" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "constcat", "fspy_detours_sys", @@ -1948,7 +1948,7 @@ dependencies = [ [[package]] name = "fspy_seccomp_unotify" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "futures-util", "libc", @@ -1965,7 +1965,7 @@ dependencies = [ [[package]] name = "fspy_shared" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "allocator-api2", "bitflags 2.11.0", @@ -1984,7 +1984,7 @@ dependencies = [ [[package]] name = "fspy_shared_unix" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "anyhow", "base64 0.22.1", @@ -3124,7 +3124,7 @@ dependencies = [ [[package]] name = "materialized_artifact" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "tempfile", ] @@ -3132,7 +3132,7 @@ dependencies = [ [[package]] name = "materialized_artifact_build" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "xxhash-rust", ] @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "native_str" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "allocator-api2", "bytemuck", @@ -4821,7 +4821,7 @@ dependencies = [ [[package]] name = "pty_terminal_test_client" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" [[package]] name = "quote" @@ -7497,7 +7497,7 @@ dependencies = [ [[package]] name = "vite_glob" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "thiserror 2.0.18", "vite_path", @@ -7537,7 +7537,7 @@ dependencies = [ [[package]] name = "vite_graph_ser" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "petgraph 0.8.3", "serde", @@ -7636,7 +7636,7 @@ dependencies = [ [[package]] name = "vite_path" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "diff-struct", "path-clean", @@ -7650,7 +7650,7 @@ dependencies = [ [[package]] name = "vite_powershell" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "vite_path", "which", @@ -7659,7 +7659,7 @@ dependencies = [ [[package]] name = "vite_select" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "anyhow", "crossterm", @@ -7709,7 +7709,7 @@ dependencies = [ [[package]] name = "vite_shell" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "brush-parser 0.3.0 (git+https://github.com/reubeno/brush?rev=dcb760933b10ee0433d7b740a5709b06f5c67c6b)", "diff-struct", @@ -7736,7 +7736,7 @@ dependencies = [ [[package]] name = "vite_str" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "compact_str", "diff-struct", @@ -7747,7 +7747,7 @@ dependencies = [ [[package]] name = "vite_task" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "anyhow", "async-trait", @@ -7785,7 +7785,7 @@ dependencies = [ [[package]] name = "vite_task_graph" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "anyhow", "async-trait", @@ -7807,7 +7807,7 @@ dependencies = [ [[package]] name = "vite_task_plan" version = "0.1.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "anyhow", "async-trait", @@ -7840,7 +7840,7 @@ version = "0.0.0" [[package]] name = "vite_workspace" version = "0.0.0" -source = "git+https://github.com/voidzero-dev/vite-task.git?rev=1e2b7913840c15aa670d702f8bf4c7fdcd45c56f#1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" +source = "git+https://github.com/voidzero-dev/vite-task.git?rev=957278dfba4eac6756a564372c0bb26df0870707#957278dfba4eac6756a564372c0bb26df0870707" dependencies = [ "clap", "petgraph 0.8.3", diff --git a/Cargo.toml b/Cargo.toml index 07c5320a55..fa90ec27fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,7 @@ dunce = "1.0.5" fast-glob = "1.0.0" flate2 = { version = "=1.1.9", features = ["zlib-rs"] } form_urlencoded = "1.2.1" -fspy = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } +fspy = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "957278dfba4eac6756a564372c0bb26df0870707" } futures = "0.3.31" futures-util = "0.3.31" glob = "0.3.2" @@ -194,17 +194,17 @@ vfs = "0.13.0" vite_command = { path = "crates/vite_command" } vite_error = { path = "crates/vite_error" } vite_js_runtime = { path = "crates/vite_js_runtime" } -vite_glob = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } +vite_glob = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "957278dfba4eac6756a564372c0bb26df0870707" } vite_install = { path = "crates/vite_install" } vite_migration = { path = "crates/vite_migration" } vite_setup = { path = "crates/vite_setup" } vite_shared = { path = "crates/vite_shared" } vite_static_config = { path = "crates/vite_static_config" } -vite_path = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } -vite_powershell = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } -vite_str = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } -vite_task = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } -vite_workspace = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "1e2b7913840c15aa670d702f8bf4c7fdcd45c56f" } +vite_path = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "957278dfba4eac6756a564372c0bb26df0870707" } +vite_powershell = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "957278dfba4eac6756a564372c0bb26df0870707" } +vite_str = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "957278dfba4eac6756a564372c0bb26df0870707" } +vite_task = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "957278dfba4eac6756a564372c0bb26df0870707" } +vite_workspace = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "957278dfba4eac6756a564372c0bb26df0870707" } walkdir = "2.5.0" wax = "0.6.0" which = "8.0.0" From fbf831a97c1dc3759a6fae06eb5932381cd99f5e Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 29 Apr 2026 20:38:26 +0900 Subject: [PATCH 05/16] style(command): import OsString and drop Arc turbofish in ps1_shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two minor cleanups from review: - Hoist `OsString` into the existing `std::ffi` `use` so the `resolve_program` return type reads `Vec` instead of the inlined `Vec`. - Drop the `Arc::::from(...)` turbofish in the `host_arc` test helper — type inference handles it. --- crates/vite_command/src/lib.rs | 4 ++-- crates/vite_command/src/ps1_shim.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/vite_command/src/lib.rs b/crates/vite_command/src/lib.rs index d0e136e867..19efcabb54 100644 --- a/crates/vite_command/src/lib.rs +++ b/crates/vite_command/src/lib.rs @@ -2,7 +2,7 @@ use std::os::fd::{BorrowedFd, RawFd}; use std::{ collections::HashMap, - ffi::OsStr, + ffi::{OsStr, OsString}, process::{ExitStatus, Stdio}, }; @@ -52,7 +52,7 @@ fn resolve_program( bin_name: &str, envs: &HashMap, cwd: &AbsolutePath, -) -> Result<(AbsolutePathBuf, Vec), Error> { +) -> Result<(AbsolutePathBuf, Vec), Error> { let bin_path = resolve_bin(bin_name, envs.get("PATH").map(|p| OsStr::new(p.as_str())), cwd)?; Ok(match ps1_shim::rewrite_cmd_to_powershell(&bin_path) { Some(rewritten) => rewritten, diff --git a/crates/vite_command/src/ps1_shim.rs b/crates/vite_command/src/ps1_shim.rs index 59a6ee3c25..5b206da3f1 100644 --- a/crates/vite_command/src/ps1_shim.rs +++ b/crates/vite_command/src/ps1_shim.rs @@ -82,7 +82,7 @@ mod tests { } fn host_arc(root: &AbsolutePath) -> Arc { - Arc::::from(abs(root.as_path().join("powershell.exe"))) + Arc::from(abs(root.as_path().join("powershell.exe"))) } #[test] From 221b88a62062a12a9b26ddedff224241b6842b10 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 29 Apr 2026 21:14:56 +0900 Subject: [PATCH 06/16] fix(command): look up PATH case-insensitively in resolve_program MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows the canonical key is `Path`, and the TS layer's `prependToPathToEnvs` preserves whatever casing the process started with when adding the downloaded package-manager bin dir. Hard-coding `envs.get("PATH")` missed a `Path`-keyed override and silently fell back to the process PATH — where the prepended shim is absent — making `vp create` (and any other flow routed through `runCommandAndDetectProjectDir` / `run_command_with_fspy`) fail with `Cannot find binary path...` for `npx`/`pnpm`. Match the case-insensitive lookup `fspy::Command::resolve_program` already does, so the previously-correct behavior of the fspy path holds after vp-side resolution moved up a layer in 1b77707e (refactor(command): extract resolve_program to dedupe ps1-rewrite plumbing). Adds a Windows-only regression test (`resolve_program_tests`) that writes a `.exe` into a tempdir and resolves via a `Path`-keyed env map; without the fix the test fails because resolution falls back to the process PATH. --- crates/vite_command/src/lib.rs | 55 +++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/vite_command/src/lib.rs b/crates/vite_command/src/lib.rs index 19efcabb54..2d7d041ef6 100644 --- a/crates/vite_command/src/lib.rs +++ b/crates/vite_command/src/lib.rs @@ -53,7 +53,16 @@ fn resolve_program( envs: &HashMap, cwd: &AbsolutePath, ) -> Result<(AbsolutePathBuf, Vec), Error> { - let bin_path = resolve_bin(bin_name, envs.get("PATH").map(|p| OsStr::new(p.as_str())), cwd)?; + // Look up PATH case-insensitively: on Windows the canonical key is `Path` + // and the TS layer's `prependToPathToEnvs` preserves whatever casing the + // process started with. Hard-coding `"PATH"` would miss a `Path`-keyed + // override and silently fall back to the process PATH (where the + // prepended package-manager bin dir is absent). + let path_env = envs + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case("path")) + .map(|(_, v)| OsStr::new(v.as_str())); + let bin_path = resolve_bin(bin_name, path_env, cwd)?; Ok(match ps1_shim::rewrite_cmd_to_powershell(&bin_path) { Some(rewritten) => rewritten, None => (bin_path, Vec::new()), @@ -369,6 +378,50 @@ mod tests { } } + #[cfg(windows)] + mod resolve_program_tests { + use super::*; + + // Regression for voidzero-dev/vite-plus#1489 follow-up: on Windows the + // PATH environment variable is conventionally keyed `Path` (and the + // TS layer's `prependToPathToEnvs` preserves that casing when adding + // the downloaded package-manager bin dir). `resolve_program` must + // look up PATH case-insensitively or it falls back to the process + // environment and can't find the prepended shim. + #[test] + fn resolve_program_finds_binary_via_lowercase_path_key() { + let temp_dir = create_temp_dir(); + let temp_dir_path = + AbsolutePathBuf::new(temp_dir.path().canonicalize().unwrap().to_path_buf()) + .unwrap(); + + // Unique name so it can't accidentally resolve via the process PATH. + let bin_name = "vp_test_resolve_program_path_casing"; + let bin_path = temp_dir.path().join(format!("{bin_name}.exe")); + std::fs::write(&bin_path, b"").unwrap(); + + // `Path`, not `PATH` — the real-world Windows casing. + let envs = HashMap::from([( + "Path".to_string(), + temp_dir_path.as_path().to_string_lossy().into_owned(), + )]); + + let result = resolve_program(bin_name, &envs, &temp_dir_path); + assert!( + result.is_ok(), + "resolve_program must find the binary via the `Path` env key on Windows; got: {:?}", + result.err() + ); + let (program, prefix_args) = result.unwrap(); + assert!(prefix_args.is_empty(), "no .ps1 sibling, no PowerShell prefix expected"); + assert_eq!( + program.as_path().file_stem().and_then(|s| s.to_str()), + Some(bin_name), + "resolved program should be the .exe in the tempdir" + ); + } + } + mod run_command_with_fspy_tests { use super::*; From 466b3a571bfbb5f8b0fc59f5c7a089ae0eeedc5a Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 29 Apr 2026 23:31:29 +0900 Subject: [PATCH 07/16] fix(command): restrict .cmd to PowerShell rewrite to vp-managed shims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the Codex adversarial review: the previous implementation rewrote any `.cmd` with a sibling `.ps1` to spawn through `PowerShell -ExecutionPolicy Bypass`. That covers vp's own pm shims but also silently retargets unrelated PATH commands — system tools, globally-installed npm wrappers, third-party CLIs whose `.cmd`/`.ps1` wrappers may not be semantically equivalent — and broadens execution semantics on locked-down hosts that intentionally block unsigned `.ps1` execution. Gate the rewrite on `resolved.starts_with($VP_HOME)`, where `$VP_HOME` is the vp install root (`~/.vite-plus` by default). That covers the legitimate cases (`$VP_HOME/js_runtime/node//npm.cmd` and `$VP_HOME/package_manager////bin/.cmd`) and leaves anything else alone. `$VP_HOME` is read once via `vite_shared::get_vp_home()` and cached in a `LazyLock`. Adds a regression test (`returns_none_for_cmd_outside_vp_home`) that puts a matching `.cmd`+`.ps1` pair outside the scope and asserts no rewrite happens. --- Cargo.lock | 1 + crates/vite_command/Cargo.toml | 1 + crates/vite_command/src/ps1_shim.rs | 127 ++++++++++++++++++++-------- 3 files changed, 95 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a3061876e..8377b47639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7468,6 +7468,7 @@ dependencies = [ "vite_error", "vite_path", "vite_powershell", + "vite_shared", "which", ] diff --git a/crates/vite_command/Cargo.toml b/crates/vite_command/Cargo.toml index e1cd5a971d..b2bc76aead 100644 --- a/crates/vite_command/Cargo.toml +++ b/crates/vite_command/Cargo.toml @@ -15,6 +15,7 @@ tracing = { workspace = true } vite_error = { workspace = true } vite_path = { workspace = true } vite_powershell = { workspace = true } +vite_shared = { workspace = true } which = { workspace = true, features = ["tracing"] } [target.'cfg(not(target_os = "windows"))'.dependencies] diff --git a/crates/vite_command/src/ps1_shim.rs b/crates/vite_command/src/ps1_shim.rs index 5b206da3f1..1814a8b8bc 100644 --- a/crates/vite_command/src/ps1_shim.rs +++ b/crates/vite_command/src/ps1_shim.rs @@ -1,22 +1,28 @@ -//! Windows-specific: when a resolved binary is a `.cmd` shim with a sibling -//! `.ps1`, rewrite the spawn to go through `powershell.exe -File `. +//! Windows-specific: when a vp-managed package-manager `.cmd` shim has a +//! sibling `.ps1`, rewrite the spawn to go through +//! `powershell.exe -File `. //! //! Running a `.cmd` from any shell makes `cmd.exe` prompt "Terminate batch //! job (Y/N)?" on Ctrl+C, which leaves the terminal corrupt. Routing through //! `PowerShell` sidesteps the prompt and lets Ctrl+C propagate cleanly. //! -//! Unlike the task-layer rewrite (`vite_task_plan::ps1_shim`, scoped to -//! `node_modules/.bin/*.cmd` inside the workspace), this one applies to any -//! `.cmd` whose `.ps1` sibling exists. Package manager shims (`npm.cmd`, -//! `pnpm.cmd`, `yarn.cmd`, `npx.cmd`) live in `~/.vite-plus/js_runtime/...` -//! or system PATH — outside any `node_modules/.bin` — so the structural -//! check there is too narrow for this code path. +//! The rewrite is intentionally **scoped to paths inside `$VP_HOME`** +//! (`~/.vite-plus` by default). vp's managed shims live there: +//! - `$VP_HOME/js_runtime/node//{npm,npx}.cmd` (npm/npx shipped with +//! the managed Node.js), +//! - `$VP_HOME/package_manager////bin/.cmd` (downloaded +//! pnpm/yarn/bun). //! -//! The cross-platform primitives (`POWERSHELL_PREFIX`, `powershell_host`, -//! `find_ps1_sibling`) live in the `vite_powershell` crate and are shared -//! with `vite_task_plan::ps1_shim`. This module composes them with vp's -//! own conventions: absolute `.ps1` path in args (no cache fingerprint to -//! keep portable) and `Vec` return type (matches the spawn API). +//! Anything outside `$VP_HOME` — system tools, globally-installed npm +//! shims, third-party CLIs whose `.cmd` and `.ps1` wrappers may not be +//! semantically equivalent, hosts that intentionally block unsigned +//! `.ps1` execution — is left alone. Stricter scoping means the prior +//! `.cmd` path still runs there, but that matches the pre-fix status quo +//! and avoids broadening execution semantics for unrelated commands. +//! +//! The task-layer rewrite (`vite_task_plan::ps1_shim`) is scoped +//! differently — to `node_modules/.bin/*.cmd` inside the workspace — and +//! covers `vp run