From 2d9a0568348a16bdad6f3eb14b17b21120477626 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 27 Jun 2026 01:41:10 +0800 Subject: [PATCH 1/3] feat(cli): advertise VP_USER_AGENT to child processes Set `VP_USER_AGENT=vp/` once at startup so spawned tools can detect vp as the package manager. Underlying package managers overwrite `npm_config_user_agent`, so a dedicated variable is needed. Enables `vp dlx create-vite` to scaffold with vp commands. --- crates/vite_global_cli/src/main.rs | 24 ++++++++++++++++++++++-- crates/vite_shared/src/env_vars.rs | 7 +++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 1fdd658238..891f0c39be 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -294,8 +294,28 @@ fn print_unknown_argument_error(error: &clap::Error) -> bool { true } -#[tokio::main] -async fn main() -> ExitCode { +fn main() -> ExitCode { + // Advertise that this process tree is running under Vite+ so the tools we + // invoke (e.g. `vp dlx create-vite`) can detect vp as the package manager. + // The underlying package managers overwrite `npm_config_user_agent` with + // their own value, so we expose a dedicated variable that is passed through + // untouched and inherited by every child process (including the `npx` + // fallback used when there is no local `package.json`). + // + // SAFETY: `set_var` must run while the process is still single-threaded. + // This is the first statement in `main`, before the async runtime (and its + // worker threads) are created. + unsafe { + std::env::set_var( + vite_shared::env_vars::VP_USER_AGENT, + concat!("vp/", env!("CARGO_PKG_VERSION")), + ); + } + + tokio::runtime::Runtime::new().expect("failed to build tokio runtime").block_on(run()) +} + +async fn run() -> ExitCode { // Initialize tracing vite_shared::init_tracing(); diff --git a/crates/vite_shared/src/env_vars.rs b/crates/vite_shared/src/env_vars.rs index 7e3de5b0dd..55b24e30cd 100644 --- a/crates/vite_shared/src/env_vars.rs +++ b/crates/vite_shared/src/env_vars.rs @@ -80,6 +80,13 @@ pub const VP_CLI_BIN: &str = "VP_CLI_BIN"; /// Global CLI version, passed from Rust binary to JS for --version display. pub const VP_GLOBAL_VERSION: &str = "VP_GLOBAL_VERSION"; +/// Advertises that the process tree is running under Vite+ (value: `vp/`). +/// +/// Set once at startup and inherited by every child process. Tools we invoke +/// (e.g. `vp dlx create-vite`) read it to detect vp as the package manager, +/// since the underlying package manager overwrites `npm_config_user_agent`. +pub const VP_USER_AGENT: &str = "VP_USER_AGENT"; + // ── HTTP client TLS / CA configuration ────────────────────────────────── /// Path to a PEM bundle of extra CA certificates to trust for HTTPS. From deeab22f9900c6585cf8a15653cadd6ab9450258 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 27 Jun 2026 10:10:55 +0800 Subject: [PATCH 2/3] fix(cli): don't advertise VP_USER_AGENT in shim mode When vp runs as a managed npm/npx/node shim, the user is invoking that tool, not vp, so clear VP_USER_AGENT before dispatching the shim. Otherwise a child like create-vite would wrongly scaffold vp commands. --- crates/vite_global_cli/src/main.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 891f0c39be..441d567f1d 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -300,7 +300,8 @@ fn main() -> ExitCode { // The underlying package managers overwrite `npm_config_user_agent` with // their own value, so we expose a dedicated variable that is passed through // untouched and inherited by every child process (including the `npx` - // fallback used when there is no local `package.json`). + // fallback used when there is no local `package.json`). Shim mode clears it + // again below, since there the user invokes the wrapped tool, not vp. // // SAFETY: `set_var` must run while the process is still single-threaded. // This is the first statement in `main`, before the async runtime (and its @@ -337,6 +338,11 @@ async fn run() -> ExitCode { if let Some(tool) = shim::detect_shim_tool(argv0) { // Shim mode - dispatch to the appropriate tool. stdout belongs to the // wrapped tool; route vp's own output to stderr. + // + // The user is invoking the wrapped tool (e.g. `npm`/`npx`), not vp, so + // don't advertise vp to it or its children. Clear the marker set in + // `main` before dispatching. SAFETY: see the `set_var` in `main`. + unsafe { std::env::remove_var(vite_shared::env_vars::VP_USER_AGENT) }; output::route_user_output_to_stderr(); let exit_code = shim::dispatch(&tool, &args[1..]).await; return ExitCode::from(exit_code as u8); From 529ccb84fabc4f2266da427906e087a99509ca51 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 27 Jun 2026 17:07:31 +0800 Subject: [PATCH 3/3] refactor(cli): set VP_USER_AGENT only for real vp invocations Detect shim mode in `main` before the runtime starts and set VP_USER_AGENT only when not a shim, instead of setting it unconditionally and clearing it in the shim branch. Also makes `detect_shim_tool` run genuinely single-threaded (it clears VP_SHIM_TOOL), matching its own SAFETY note. --- crates/vite_global_cli/src/main.rs | 48 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 441d567f1d..ea6c6da866 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -295,33 +295,44 @@ fn print_unknown_argument_error(error: &clap::Error) -> bool { } fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + + // Detect shim mode (vp invoked as node/npm/npx/...) up front. `detect_shim_tool` + // reads and clears the VP_SHIM_TOOL env vars, so it must run exactly once and + // while the process is still single-threaded, before the async runtime starts. + let argv0 = args.first().map(|s| s.as_str()).unwrap_or("vp"); + let shim_tool = shim::detect_shim_tool(argv0); + // Advertise that this process tree is running under Vite+ so the tools we // invoke (e.g. `vp dlx create-vite`) can detect vp as the package manager. // The underlying package managers overwrite `npm_config_user_agent` with // their own value, so we expose a dedicated variable that is passed through // untouched and inherited by every child process (including the `npx` - // fallback used when there is no local `package.json`). Shim mode clears it - // again below, since there the user invokes the wrapped tool, not vp. + // fallback used when there is no local `package.json`). + // + // Only set it for a real `vp` invocation, not when vp is acting as a shim: + // there the user is invoking the wrapped tool (e.g. `npm`/`npx`), not vp. // // SAFETY: `set_var` must run while the process is still single-threaded. - // This is the first statement in `main`, before the async runtime (and its - // worker threads) are created. - unsafe { - std::env::set_var( - vite_shared::env_vars::VP_USER_AGENT, - concat!("vp/", env!("CARGO_PKG_VERSION")), - ); + // This runs before the async runtime (and its worker threads) are created. + if shim_tool.is_none() { + unsafe { + std::env::set_var( + vite_shared::env_vars::VP_USER_AGENT, + concat!("vp/", env!("CARGO_PKG_VERSION")), + ); + } } - tokio::runtime::Runtime::new().expect("failed to build tokio runtime").block_on(run()) + tokio::runtime::Runtime::new() + .expect("failed to build tokio runtime") + .block_on(run(args, shim_tool)) } -async fn run() -> ExitCode { +async fn run(mut args: Vec, shim_tool: Option) -> ExitCode { // Initialize tracing vite_shared::init_tracing(); - let mut args: Vec = std::env::args().collect(); - // Replace bash completion script to fix completion for items containing ':' if env::var_os("VP_COMPLETE").is_some_and(|shell| shell == "bash") && args.len() == 1 { print!("{}", include_str!("../completion-register.bash")); @@ -331,18 +342,9 @@ async fn run() -> ExitCode { // Handle shell completion CompleteEnv::with_factory(command_with_help).var("VP_COMPLETE").complete(); - // Check for shim mode (invoked as node, npm, or npx) - let argv0 = args.first().map(|s| s.as_str()).unwrap_or("vp"); - tracing::debug!("argv0: {argv0}"); - - if let Some(tool) = shim::detect_shim_tool(argv0) { + if let Some(tool) = shim_tool { // Shim mode - dispatch to the appropriate tool. stdout belongs to the // wrapped tool; route vp's own output to stderr. - // - // The user is invoking the wrapped tool (e.g. `npm`/`npx`), not vp, so - // don't advertise vp to it or its children. Clear the marker set in - // `main` before dispatching. SAFETY: see the `set_var` in `main`. - unsafe { std::env::remove_var(vite_shared::env_vars::VP_USER_AGENT) }; output::route_user_output_to_stderr(); let exit_code = shim::dispatch(&tool, &args[1..]).await; return ExitCode::from(exit_code as u8);