From 18e5d594d96bc2470978fb8dd257543e55839752 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:00:58 +0900 Subject: [PATCH 01/16] feat(env): add support for Nu shell version in EnvConfig --- crates/vite_shared/src/env_config.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/vite_shared/src/env_config.rs b/crates/vite_shared/src/env_config.rs index fcfa047c2f..6e63629c6b 100644 --- a/crates/vite_shared/src/env_config.rs +++ b/crates/vite_shared/src/env_config.rs @@ -122,6 +122,11 @@ pub struct EnvConfig { /// /// Env: `PSModulePath` pub ps_module_path: Option, + + /// Nu shell version (indicates running under Nu shell). + /// + /// Env: `NU_VERSION` + pub nu_version: Option, } impl EnvConfig { @@ -151,6 +156,7 @@ impl EnvConfig { .map(PathBuf::from), fish_version: std::env::var("FISH_VERSION").ok(), ps_module_path: std::env::var("PSModulePath").ok(), + nu_version: std::env::var("NU_VERSION").ok(), } } @@ -233,6 +239,7 @@ impl EnvConfig { user_home: None, fish_version: None, ps_module_path: None, + nu_version: None, } } From fc9d45704bca9c0849832d4bb3576cfc1b831fa9 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:09:39 +0900 Subject: [PATCH 02/16] feat(shell): add support for NuShell in shell detection and command formatting --- .../vite_global_cli/src/commands/env/use.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index 398fb57379..2a7893dda0 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -25,6 +25,8 @@ enum Shell { PowerShell, /// Windows cmd.exe Cmd, + /// Nushell + NuShell, } /// Detect the current shell from environment variables. @@ -32,6 +34,8 @@ fn detect_shell() -> Shell { let config = vite_shared::EnvConfig::get(); if config.fish_version.is_some() { Shell::Fish + } else if config.nu_version.is_some() { + Shell::NuShell } else if cfg!(windows) && config.ps_module_path.is_some() { Shell::PowerShell } else if cfg!(windows) { @@ -48,6 +52,7 @@ fn format_export(shell: &Shell, value: &str) -> String { Shell::Fish => format!("set -gx {VERSION_ENV_VAR} {value}"), Shell::PowerShell => format!("$env:{VERSION_ENV_VAR} = \"{value}\""), Shell::Cmd => format!("set {VERSION_ENV_VAR}={value}"), + Shell::NuShell => format!("$env.{VERSION_ENV_VAR} = \"{value}\""), } } @@ -60,6 +65,7 @@ fn format_unset(shell: &Shell) -> String { format!("Remove-Item Env:{VERSION_ENV_VAR} -ErrorAction SilentlyContinue") } Shell::Cmd => format!("set {VERSION_ENV_VAR}="), + Shell::NuShell => format!("hide-env {VERSION_ENV_VAR}"), } } @@ -194,6 +200,17 @@ mod tests { assert!(matches!(shell, Shell::Fish)); } + #[test] + fn test_detect_shell_fish_and_nushell() { + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + fish_version: Some("3.7.0".into()), + nu_version: Some("0.91.0".into()), + ..vite_shared::EnvConfig::for_test() + }); + let shell = detect_shell(); + assert!(matches!(shell, Shell::Fish)); + } + #[test] fn test_detect_shell_posix_default() { // All shell detection fields None → defaults @@ -205,6 +222,16 @@ mod tests { assert!(matches!(shell, Shell::Cmd)); } + #[test] + fn test_detect_shell_nushell() { + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + nu_version: Some("0.91.0".into()), + ..vite_shared::EnvConfig::for_test() + }); + let shell = detect_shell(); + assert!(matches!(shell, Shell::NuShell)); + } + #[test] fn test_format_export_posix() { let result = format_export(&Shell::Posix, "20.18.0"); @@ -252,4 +279,15 @@ mod tests { let result = format_unset(&Shell::Cmd); assert_eq!(result, "set VP_NODE_VERSION="); } + #[test] + fn test_format_export_nushell() { + let result = format_export(&Shell::NuShell, "20.18.0"); + assert_eq!(result, "$env.VP_NODE_VERSION = \"20.18.0\""); + } + + #[test] + fn test_format_unset_nushell() { + let result = format_unset(&Shell::NuShell); + assert_eq!(result, "hide-env VP_NODE_VERSION"); + } } From e17d514919d21e69cb4bebe489508200ff966f79 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:20:27 +0900 Subject: [PATCH 03/16] feat(env): add Nushell support with completion and environment setup --- .../vite_global_cli/src/commands/env/setup.rs | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 45718c87cf..9cef70faa8 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -46,6 +46,23 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result // Create env files with PATH guard (prevents duplicate PATH entries) create_env_files(&vite_plus_home).await?; + // Generate native Nushell completion file at ~/.vite-plus/vp.nu. + // This allows env.nu to source completions without requiring Fish to be installed. + // command_with_help() uses a deep call stack, so we spawn a thread with a larger stack. + let nu_completions_path: std::path::PathBuf = vite_plus_home.join("vp.nu").as_path().to_path_buf(); + std::thread::Builder::new() + .stack_size(8 * 1024 * 1024) + .spawn(move || -> std::io::Result<()> { + let mut file = std::fs::File::create(&nu_completions_path)?; + let mut cmd = crate::cli::command_with_help(); + clap_complete::generate(clap_complete_nushell::Nushell, &mut cmd, "vp", &mut file); + Ok(()) + }) + .map_err(Error::CommandExecution)? + .join() + .map_err(|_| Error::Other("Nushell completion generation failed".into()))? + .map_err(Error::CommandExecution)?; + if env_only { println!("{}", help::render_heading("Setup")); println!(" Updated shell environment files."); @@ -388,6 +405,7 @@ async fn cleanup_legacy_completion_dir(vite_plus_home: &vite_path::AbsolutePath) /// Creates: /// - `~/.vite-plus/env` (POSIX shell — bash/zsh) with `vp()` wrapper function /// - `~/.vite-plus/env.fish` (fish shell) with `vp` wrapper function +/// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function /// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function /// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`) async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> { @@ -497,6 +515,38 @@ complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)" let env_fish_file = vite_plus_home.join("env.fish"); tokio::fs::write(&env_fish_file, env_fish_content).await?; + // Nushell env file with vp wrapper function + let env_nu_content = r#"# Vite+ environment setup (https://viteplus.dev) +$env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__") + +# Load native Nushell completions for vp (generated by clap_complete_nushell). +source "__VP_HOME__/vp.nu" + +# Intercept `vp env use` to apply environment changes to the current shell session. +# Overrides the extern defined in vp.nu for execution; vp.nu still provides completions. +def --env --wrapped "vp env use" [...args: string] { + if ("-h" in $args) or ("--help" in $args) { + ^vp env use ...$args + return + } + let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", NU_VERSION: "1" } { + ^vp env use ...$args + }) + let exports = ($out | lines | where { $in =~ '^\$env\.' } | parse '$env.{key} = "{value}"') + if ($exports | is-not-empty) { + load-env ($exports | reduce -f {} {|it, acc| $acc | insert $it.key $it.value}) + } + let unsets = ($out | lines | where { $in =~ '^hide-env ' } | parse 'hide-env {key}' | get key) + for key in $unsets { hide-env $key } +} + +export def --env --wrapped vpr [...args: string] { ^vp run ...$args } +"# + .replace("__VP_BIN__", &bin_path_ref) + .replace("__VP_HOME__", &vite_plus_home.as_path().display().to_string()); + let env_nu_file = vite_plus_home.join("env.nu"); + tokio::fs::write(&env_nu_file, env_nu_content).await?; + // PowerShell env file let env_ps1_content = r#"# Vite+ environment setup (https://viteplus.dev) $__vp_bin = "__VP_BIN_WIN__" @@ -599,6 +649,10 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) { println!(); println!(" source \"{home_path}/env.fish\""); println!(); + println!(" For Nushell, add to ~/.config/nushell/config.nu:"); + println!(); + println!(" source \"{home_path}/env.nu\""); + println!(); println!(" For PowerShell, add to your $PROFILE:"); println!(); println!(" . \"{home_path}/env.ps1\""); @@ -652,20 +706,44 @@ mod tests { let env_path = home.join("env"); let env_fish_path = home.join("env.fish"); + let env_nu_path = home.join("env.nu"); let env_ps1_path = home.join("env.ps1"); assert!(env_path.as_path().exists(), "env file should be created"); assert!(env_fish_path.as_path().exists(), "env.fish file should be created"); + assert!(env_nu_path.as_path().exists(), "env.nu file should be created"); assert!(env_ps1_path.as_path().exists(), "env.ps1 file should be created"); } #[tokio::test] - async fn test_create_env_files_replaces_placeholder_with_home_relative_path() { + async fn test_create_env_files_nu_contains_path_guard() { let temp_dir = TempDir::new().unwrap(); let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); let _guard = home_guard(temp_dir.path()); create_env_files(&home).await.unwrap(); + let nu_content = tokio::fs::read_to_string(home.join("env.nu")).await.unwrap(); + assert!( + !nu_content.contains("__VP_BIN__"), + "env.nu should not contain __VP_BIN__ placeholder" + ); + assert!(nu_content.contains("$HOME/bin"), "env.nu should reference $HOME/bin"); + assert!( + nu_content.contains("VP_ENV_USE_EVAL_ENABLE"), + "env.nu should set VP_ENV_USE_EVAL_ENABLE" + ); + assert!( + nu_content.contains("vp.nu"), + "env.nu should source the native Nushell completion file" + ); + } + + #[tokio::test] + async fn test_create_env_files_replaces_placeholder_with_home_relative_path() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let _guard = home_guard(temp_dir.path()); + let env_content = tokio::fs::read_to_string(home.join("env")).await.unwrap(); let fish_content = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap(); From f339b453404f1378f0d150ff0edb9522612582f9 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:23:05 +0900 Subject: [PATCH 04/16] feat(nushell): add Nushell completion support in main function --- crates/vite_global_cli/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index f339b037f4..2aff9137f3 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -25,6 +25,7 @@ use std::{ use clap::error::{ContextKind, ContextValue}; use clap_complete::env::CompleteEnv; +use clap_complete_nushell::Nushell; use owo_colors::OwoColorize; use vite_shared::output; @@ -239,6 +240,13 @@ async fn main() -> ExitCode { return ExitCode::SUCCESS; } + // Handle Nushell completion (clap_complete_nushell uses generate() directly) + if env::var_os("VP_COMPLETE").is_some_and(|shell| shell == "nushell") { + let mut cmd = command_with_help(); + clap_complete::generate(Nushell, &mut cmd, "vp", &mut std::io::stdout()); + return ExitCode::SUCCESS; + } + // Handle shell completion CompleteEnv::with_factory(command_with_help).var("VP_COMPLETE").complete(); From babc76d325ebf20ba6f7149f279ffdbc411c8692 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:23:15 +0900 Subject: [PATCH 05/16] feat(nushell): add clap_complete_nushell dependency for Nushell support --- crates/vite_global_cli/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index fa1b693d69..e8a3f6c2ce 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -16,6 +16,7 @@ base64-simd = { workspace = true } chrono = { workspace = true } clap = { workspace = true, features = ["derive"] } clap_complete = { workspace = true, features = ["unstable-dynamic"] } +clap_complete_nushell = { workspace = true } directories = { workspace = true } flate2 = { workspace = true } serde = { workspace = true } From 840bf1e00e713b4cce2a51a4edbe1ee4d520bb4a Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:23:24 +0900 Subject: [PATCH 06/16] feat(nushell): add clap_complete_nushell dependency for Nushell completion support --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + 2 files changed, 12 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 738eba4aa1..65f55e819d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,6 +886,16 @@ dependencies = [ "shlex", ] +[[package]] +name = "clap_complete_nushell" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897" +dependencies = [ + "clap", + "clap_complete", +] + [[package]] name = "clap_derive" version = "4.6.0" @@ -7334,6 +7344,7 @@ dependencies = [ "chrono", "clap", "clap_complete", + "clap_complete_nushell", "crossterm", "directories", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 2d71b26139..9aa2d7978e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ blake3 = "1.8.2" chrono = { version = "0.4", features = ["serde"] } clap = "4.5.40" clap_complete = "4.6.0" +clap_complete_nushell = "4.6.0" commondir = "1.0.0" cow-utils = "0.1.3" criterion = { version = "0.7", features = ["html_reports"] } From 4e4d49566f2a33f03f484490ac900415ec606608 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:48:27 +0900 Subject: [PATCH 07/16] chore: remove clap_complete_nushell dependency from Cargo.toml files --- Cargo.lock | 11 ----------- Cargo.toml | 1 - crates/vite_global_cli/Cargo.toml | 1 - 3 files changed, 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65f55e819d..738eba4aa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,16 +886,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "clap_complete_nushell" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897" -dependencies = [ - "clap", - "clap_complete", -] - [[package]] name = "clap_derive" version = "4.6.0" @@ -7344,7 +7334,6 @@ dependencies = [ "chrono", "clap", "clap_complete", - "clap_complete_nushell", "crossterm", "directories", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 9aa2d7978e..2d71b26139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,6 @@ blake3 = "1.8.2" chrono = { version = "0.4", features = ["serde"] } clap = "4.5.40" clap_complete = "4.6.0" -clap_complete_nushell = "4.6.0" commondir = "1.0.0" cow-utils = "0.1.3" criterion = { version = "0.7", features = ["html_reports"] } diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index e8a3f6c2ce..fa1b693d69 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -16,7 +16,6 @@ base64-simd = { workspace = true } chrono = { workspace = true } clap = { workspace = true, features = ["derive"] } clap_complete = { workspace = true, features = ["unstable-dynamic"] } -clap_complete_nushell = { workspace = true } directories = { workspace = true } flate2 = { workspace = true } serde = { workspace = true } From d55632e54474dc393a5a9b921497a4e94e776f32 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:48:33 +0900 Subject: [PATCH 08/16] feat(nushell): update completion generation to use Fish format for Nushell support --- .../vite_global_cli/src/commands/env/setup.rs | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 9cef70faa8..881d6157f5 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -46,21 +46,21 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result // Create env files with PATH guard (prevents duplicate PATH entries) create_env_files(&vite_plus_home).await?; - // Generate native Nushell completion file at ~/.vite-plus/vp.nu. - // This allows env.nu to source completions without requiring Fish to be installed. + // Generate Fish completion file at ~/.vite-plus/vp.fish for use by the Nushell completer. + // Fish completions are generated statically so that Nushell can delegate to Fish at runtime. // command_with_help() uses a deep call stack, so we spawn a thread with a larger stack. - let nu_completions_path: std::path::PathBuf = vite_plus_home.join("vp.nu").as_path().to_path_buf(); + let fish_completions_path = vite_plus_home.join("vp.fish").as_path().to_path_buf(); std::thread::Builder::new() .stack_size(8 * 1024 * 1024) .spawn(move || -> std::io::Result<()> { - let mut file = std::fs::File::create(&nu_completions_path)?; + let mut file = std::fs::File::create(&fish_completions_path)?; let mut cmd = crate::cli::command_with_help(); - clap_complete::generate(clap_complete_nushell::Nushell, &mut cmd, "vp", &mut file); + clap_complete::generate(clap_complete::Shell::Fish, &mut cmd, "vp", &mut file); Ok(()) }) .map_err(Error::CommandExecution)? .join() - .map_err(|_| Error::Other("Nushell completion generation failed".into()))? + .map_err(|_| Error::Other("Fish completion generation failed".into()))? .map_err(Error::CommandExecution)?; if env_only { @@ -515,32 +515,48 @@ complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)" let env_fish_file = vite_plus_home.join("env.fish"); tokio::fs::write(&env_fish_file, env_fish_content).await?; - // Nushell env file with vp wrapper function + // Nushell env file with vp wrapper function. + // Completions delegate to Fish via vp.fish because clap_complete_nushell generates + // multiple rest params (e.g. for `vp install`), which Nushell does not support. let env_nu_content = r#"# Vite+ environment setup (https://viteplus.dev) $env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__") -# Load native Nushell completions for vp (generated by clap_complete_nushell). -source "__VP_HOME__/vp.nu" - -# Intercept `vp env use` to apply environment changes to the current shell session. -# Overrides the extern defined in vp.nu for execution; vp.nu still provides completions. -def --env --wrapped "vp env use" [...args: string] { - if ("-h" in $args) or ("--help" in $args) { - ^vp env use ...$args - return - } - let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", NU_VERSION: "1" } { - ^vp env use ...$args - }) - let exports = ($out | lines | where { $in =~ '^\$env\.' } | parse '$env.{key} = "{value}"') - if ($exports | is-not-empty) { - load-env ($exports | reduce -f {} {|it, acc| $acc | insert $it.key $it.value}) - } - let unsets = ($out | lines | where { $in =~ '^hide-env ' } | parse 'hide-env {key}' | get key) - for key in $unsets { hide-env $key } +# Shell function wrapper: intercepts `vp env use` to parse its stdout, +# which sets/unsets VP_NODE_VERSION in the current shell session. +def --env --wrapped vp [...args: string@"nu-complete vp"] { + if ($args | length) >= 2 and $args.0 == "env" and $args.1 == "use" { + if ("-h" in $args) or ("--help" in $args) { + ^vp ...$args + return + } + let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", NU_VERSION: "1" } { + ^vp ...$args + }) + let exports = ($out | lines | where { $in =~ '^\$env\.' } | parse '$env.{key} = "{value}"') + if ($exports | is-not-empty) { + load-env ($exports | reduce -f {} {|it, acc| $acc | insert $it.key $it.value}) + } + let unsets = ($out | lines | where { $in =~ '^hide-env ' } | parse 'hide-env {key}' | get key) + for key in $unsets { hide-env $key } + } else { + ^vp ...$args + } } -export def --env --wrapped vpr [...args: string] { ^vp run ...$args } +# Shell completion for nushell (delegates to fish completions) +def "nu-complete vp" [context: string] { + let fish_comp_path = "__VP_HOME__/vp.fish" + let fish_cmd = $"source '($fish_comp_path)'; complete '--do-complete=($context)'" + fish --command $fish_cmd | from tsv --flexible --noheaders --no-infer | rename value description | update value {|row| + let value = $row.value + let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | any {$in in $value} + if ($need_quote and ($value | path exists)) { + let expanded_path = if ($value starts-with ~) {$value | path expand --no-symlink} else {$value} + $'"($expanded_path | str replace --all "\"" "\\\"")"' + } else {$value} + } +} +export def --env --wrapped vpr [...args: string@"nu-complete vp"] { ^vp run ...$args } "# .replace("__VP_BIN__", &bin_path_ref) .replace("__VP_HOME__", &vite_plus_home.as_path().display().to_string()); @@ -733,8 +749,8 @@ mod tests { "env.nu should set VP_ENV_USE_EVAL_ENABLE" ); assert!( - nu_content.contains("vp.nu"), - "env.nu should source the native Nushell completion file" + nu_content.contains("vp.fish"), + "env.nu should reference the Fish completion file for Nushell delegation" ); } @@ -744,6 +760,8 @@ mod tests { let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); let _guard = home_guard(temp_dir.path()); + create_env_files(&home).await.unwrap(); + let env_content = tokio::fs::read_to_string(home.join("env")).await.unwrap(); let fish_content = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap(); From c060095cbf20a16d81a78039a3ac8159b2b2396b Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:48:38 +0900 Subject: [PATCH 09/16] refactor(nushell): remove direct Nushell completion handling from main function --- crates/vite_global_cli/src/main.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 2aff9137f3..c4549455e7 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -25,7 +25,7 @@ use std::{ use clap::error::{ContextKind, ContextValue}; use clap_complete::env::CompleteEnv; -use clap_complete_nushell::Nushell; + use owo_colors::OwoColorize; use vite_shared::output; @@ -240,13 +240,6 @@ async fn main() -> ExitCode { return ExitCode::SUCCESS; } - // Handle Nushell completion (clap_complete_nushell uses generate() directly) - if env::var_os("VP_COMPLETE").is_some_and(|shell| shell == "nushell") { - let mut cmd = command_with_help(); - clap_complete::generate(Nushell, &mut cmd, "vp", &mut std::io::stdout()); - return ExitCode::SUCCESS; - } - // Handle shell completion CompleteEnv::with_factory(command_with_help).var("VP_COMPLETE").complete(); From aa170c403476ca9f12ff5bf7be132c313bb3f3c3 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:59:57 +0900 Subject: [PATCH 10/16] chore: remove unused import of CompleteEnv from main.rs --- crates/vite_global_cli/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index c4549455e7..f339b037f4 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -25,7 +25,6 @@ use std::{ use clap::error::{ContextKind, ContextValue}; use clap_complete::env::CompleteEnv; - use owo_colors::OwoColorize; use vite_shared::output; From f39e459efc5c12a2d7d5c0705b4ee4f8f92268d3 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:08:52 +0900 Subject: [PATCH 11/16] fix(nushell): ensure environment mutations are applied in emission order --- .../vite_global_cli/src/commands/env/setup.rs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index deee7b8c57..3ce2496432 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -534,12 +534,21 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", NU_VERSION: "1" } { ^vp ...$args }) - let exports = ($out | lines | where { $in =~ '^\$env\.' } | parse '$env.{key} = "{value}"') - if ($exports | is-not-empty) { - load-env ($exports | reduce -f {} {|it, acc| $acc | insert $it.key $it.value}) + # Apply mutations in emission order so that hide-env followed by $env.KEY = "..." + # correctly results in the variable being set (not unset). + for line in ($out | lines) { + if ($line =~ '^\$env\.') { + let parsed = ($line | parse '$env.{key} = "{value}"') + if ($parsed | is-not-empty) { + load-env { ($parsed.0.key): ($parsed.0.value) } + } + } else if ($line =~ '^hide-env ') { + let parsed = ($line | parse 'hide-env {key}') + if ($parsed | is-not-empty) { + hide-env ($parsed.0.key) + } + } } - let unsets = ($out | lines | where { $in =~ '^hide-env ' } | parse 'hide-env {key}' | get key) - for key in $unsets { hide-env $key } } else { ^vp ...$args } @@ -754,6 +763,10 @@ mod tests { nu_content.contains("vp.fish"), "env.nu should reference the Fish completion file for Nushell delegation" ); + assert!( + nu_content.contains("for line in ($out | lines)"), + "env.nu should apply mutations in emission order" + ); } #[tokio::test] From 2e8fc7bcf28ae2f48b108e97ff58e405bab942ce Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:23:22 +0900 Subject: [PATCH 12/16] fix(nushell): ensure environment key is checked before hiding --- crates/vite_global_cli/src/commands/env/setup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 3ce2496432..500a24574d 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -544,7 +544,7 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { } } else if ($line =~ '^hide-env ') { let parsed = ($line | parse 'hide-env {key}') - if ($parsed | is-not-empty) { + if ($parsed | is-not-empty) and ($parsed.0.key in $env) { hide-env ($parsed.0.key) } } From 08a8e9de56635fa33d9c931ca59fa888095ab5ed Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:40:42 +0900 Subject: [PATCH 13/16] fix(nushell): update environment variable assignment syntax in setup command --- crates/vite_global_cli/src/commands/env/setup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 500a24574d..c0b51ad7fb 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -540,7 +540,7 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { if ($line =~ '^\$env\.') { let parsed = ($line | parse '$env.{key} = "{value}"') if ($parsed | is-not-empty) { - load-env { ($parsed.0.key): ($parsed.0.value) } + $env.($parsed.0.key) = $parsed.0.value } } else if ($line =~ '^hide-env ') { let parsed = ($line | parse 'hide-env {key}') From 371d6efdd10a3b94cfbeecc8a8b0093ae0217b8b Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:55:18 +0900 Subject: [PATCH 14/16] feat(nushell): introduce VP_SHELL_NU for explicit Nu shell detection and update environment handling --- .../vite_global_cli/src/commands/env/setup.rs | 36 ++++++++++--------- .../vite_global_cli/src/commands/env/use.rs | 21 +++++++++-- crates/vite_shared/src/env_config.rs | 10 ++++++ crates/vite_shared/src/env_vars.rs | 7 ++++ 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index c0b51ad7fb..d10190e9f3 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -531,23 +531,21 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { ^vp ...$args return } - let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", NU_VERSION: "1" } { + let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", VP_SHELL_NU: "1" } { ^vp ...$args }) - # Apply mutations in emission order so that hide-env followed by $env.KEY = "..." - # correctly results in the variable being set (not unset). - for line in ($out | lines) { - if ($line =~ '^\$env\.') { - let parsed = ($line | parse '$env.{key} = "{value}"') - if ($parsed | is-not-empty) { - $env.($parsed.0.key) = $parsed.0.value - } - } else if ($line =~ '^hide-env ') { - let parsed = ($line | parse 'hide-env {key}') - if ($parsed | is-not-empty) and ($parsed.0.key in $env) { - hide-env ($parsed.0.key) - } - } + let lines = ($out | lines) + let exports = ($lines | where { $in =~ '^\$env\.' } | parse '$env.{key} = "{value}"') + let export_keys = ($exports | get key? | default []) + # Exclude keys that also appear in exports: when vp emits `hide-env X` then + # `$env.X = "v"` (e.g. `vp env use` with no args resolving from .node-version), + # the set should win. + let unsets = ($lines | where { $in =~ '^hide-env ' } | parse 'hide-env {key}' | get key? | default [] | where { $in not-in $export_keys }) + if ($exports | is-not-empty) { + load-env ($exports | reduce -f {} {|it, acc| $acc | insert $it.key $it.value}) + } + for key in $unsets { + if ($key in $env) { hide-env $key } } } else { ^vp ...$args @@ -764,8 +762,12 @@ mod tests { "env.nu should reference the Fish completion file for Nushell delegation" ); assert!( - nu_content.contains("for line in ($out | lines)"), - "env.nu should apply mutations in emission order" + nu_content.contains("VP_SHELL_NU"), + "env.nu should use VP_SHELL_NU explicit marker instead of inherited NU_VERSION" + ); + assert!( + nu_content.contains("load-env"), + "env.nu should use load-env to apply exports" ); } diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index 2a7893dda0..b04ec6698e 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -34,7 +34,7 @@ fn detect_shell() -> Shell { let config = vite_shared::EnvConfig::get(); if config.fish_version.is_some() { Shell::Fish - } else if config.nu_version.is_some() { + } else if config.vp_shell_nu { Shell::NuShell } else if cfg!(windows) && config.ps_module_path.is_some() { Shell::PowerShell @@ -202,9 +202,10 @@ mod tests { #[test] fn test_detect_shell_fish_and_nushell() { + // Fish takes priority over Nu shell signal let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { fish_version: Some("3.7.0".into()), - nu_version: Some("0.91.0".into()), + vp_shell_nu: true, ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); @@ -225,13 +226,27 @@ mod tests { #[test] fn test_detect_shell_nushell() { let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { - nu_version: Some("0.91.0".into()), + vp_shell_nu: true, ..vite_shared::EnvConfig::for_test() }); let shell = detect_shell(); assert!(matches!(shell, Shell::NuShell)); } + #[test] + fn test_detect_shell_inherited_nu_version_is_posix() { + // NU_VERSION alone (inherited from parent Nushell) must NOT trigger Nu detection. + // Only the explicit VP_SHELL_NU marker set by env.nu wrapper counts. + let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig { + nu_version: Some("0.111.0".into()), + vp_shell_nu: false, + ..vite_shared::EnvConfig::for_test() + }); + let shell = detect_shell(); + #[cfg(not(windows))] + assert!(matches!(shell, Shell::Posix)); + } + #[test] fn test_format_export_posix() { let result = format_export(&Shell::Posix, "20.18.0"); diff --git a/crates/vite_shared/src/env_config.rs b/crates/vite_shared/src/env_config.rs index 524c55cdaa..6818058c5b 100644 --- a/crates/vite_shared/src/env_config.rs +++ b/crates/vite_shared/src/env_config.rs @@ -127,6 +127,14 @@ pub struct EnvConfig { /// /// Env: `NU_VERSION` pub nu_version: Option, + + /// Explicit Nu shell eval signal set by the `env.nu` wrapper. + /// + /// Unlike `NU_VERSION`, this is not inherited by child processes — it is only + /// present when the Nushell wrapper explicitly passes it via `with-env`. + /// + /// Env: `VP_SHELL_NU` + pub vp_shell_nu: bool, } impl EnvConfig { @@ -157,6 +165,7 @@ impl EnvConfig { fish_version: std::env::var("FISH_VERSION").ok(), ps_module_path: std::env::var("PSModulePath").ok(), nu_version: std::env::var("NU_VERSION").ok(), + vp_shell_nu: std::env::var(env_vars::VP_SHELL_NU).is_ok(), } } @@ -240,6 +249,7 @@ impl EnvConfig { fish_version: None, ps_module_path: None, nu_version: None, + vp_shell_nu: false, } } diff --git a/crates/vite_shared/src/env_vars.rs b/crates/vite_shared/src/env_vars.rs index 6a3ecd6b49..5618e416fc 100644 --- a/crates/vite_shared/src/env_vars.rs +++ b/crates/vite_shared/src/env_vars.rs @@ -36,6 +36,13 @@ pub const VP_DEBUG_SHIM: &str = "VP_DEBUG_SHIM"; /// Enable eval mode for `vp env use`. pub const VP_ENV_USE_EVAL_ENABLE: &str = "VP_ENV_USE_EVAL_ENABLE"; +/// Explicit signal set by the Nushell wrapper to indicate Nu shell eval context. +/// +/// Unlike `NU_VERSION` (which is inherited by child processes), this is only set +/// by the `with-env` block in `env.nu`, so it cannot cause false detection when +/// bash/zsh is launched from a Nushell session. +pub const VP_SHELL_NU: &str = "VP_SHELL_NU"; + /// Filter for update task types. pub const VITE_UPDATE_TASK_TYPES: &str = "VITE_UPDATE_TASK_TYPES"; From 6fede95734f0e4eb3737a85527e8360f554816d3 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:57:41 +0900 Subject: [PATCH 15/16] test(nushell): simplify assertion for load-env in nu_content check --- crates/vite_global_cli/src/commands/env/setup.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index d10190e9f3..b41ebef6bd 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -765,10 +765,7 @@ mod tests { nu_content.contains("VP_SHELL_NU"), "env.nu should use VP_SHELL_NU explicit marker instead of inherited NU_VERSION" ); - assert!( - nu_content.contains("load-env"), - "env.nu should use load-env to apply exports" - ); + assert!(nu_content.contains("load-env"), "env.nu should use load-env to apply exports"); } #[tokio::test] From 59384050e142dead522476e113cea88c9d1a4aaf Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Mon, 6 Apr 2026 04:02:45 +0900 Subject: [PATCH 16/16] test(nushell): add windows shell detection assertion in tests --- crates/vite_global_cli/src/commands/env/use.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/vite_global_cli/src/commands/env/use.rs b/crates/vite_global_cli/src/commands/env/use.rs index b04ec6698e..27ee6dc819 100644 --- a/crates/vite_global_cli/src/commands/env/use.rs +++ b/crates/vite_global_cli/src/commands/env/use.rs @@ -245,6 +245,8 @@ mod tests { let shell = detect_shell(); #[cfg(not(windows))] assert!(matches!(shell, Shell::Posix)); + #[cfg(windows)] + let _ = shell; } #[test]