@@ -388,25 +388,31 @@ async fn cleanup_legacy_completion_dir(vite_plus_home: &vite_path::AbsolutePath)
388388/// Creates:
389389/// - `~/.vite-plus/env` (POSIX shell — bash/zsh) with `vp()` wrapper function
390390/// - `~/.vite-plus/env.fish` (fish shell) with `vp` wrapper function
391+ /// - `~/.vite-plus/env.nu` (Nushell) with PATH setup + `vp` wrapper function
391392/// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function
392393/// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`)
393394async fn create_env_files ( vite_plus_home : & vite_path:: AbsolutePath ) -> Result < ( ) , Error > {
394395 let bin_path = vite_plus_home. join ( "bin" ) ;
395396
396397 // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env)
397398 // This makes the env file portable across sessions where HOME may differ
398- let home_dir = vite_shared:: EnvConfig :: get ( ) . user_home ;
399- let to_ref = |path : & vite_path:: AbsolutePath | -> String {
400- home_dir
399+ let ( bin_path_ref, bin_path_nu) = {
400+ let bin_path_ref = & bin_path;
401+ vite_shared:: EnvConfig :: get ( )
402+ . user_home
401403 . as_ref ( )
402- . and_then ( |h| path. as_path ( ) . strip_prefix ( h) . ok ( ) )
403- . map ( |s| {
404- // Normalize to forward slashes for $HOME/... paths (POSIX-style)
405- format ! ( "$HOME/{}" , s. display( ) . to_string( ) . replace( '\\' , "/" ) )
406- } )
407- . unwrap_or_else ( || path. as_path ( ) . display ( ) . to_string ( ) )
404+ . and_then ( |h| bin_path_ref. as_path ( ) . strip_prefix ( h) . ok ( ) )
405+ . map_or_else (
406+ || ( bin_path_ref. as_path ( ) . display ( ) . to_string ( ) , None ) ,
407+ |s| {
408+ (
409+ // Normalize to forward slashes for $HOME/... paths (POSIX-style)
410+ format ! ( "$HOME/{}" , s. display( ) . to_string( ) . replace( '\\' , "/" ) ) ,
411+ Some ( format ! ( "~/{}" , s. display( ) ) ) ,
412+ )
413+ } ,
414+ )
408415 } ;
409- let bin_path_ref = to_ref ( & bin_path) ;
410416
411417 // POSIX env file (bash/zsh)
412418 // When sourced multiple times, removes existing entry and re-prepends to front
@@ -499,6 +505,75 @@ complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)"
499505 let env_fish_file = vite_plus_home. join ( "env.fish" ) ;
500506 tokio:: fs:: write ( & env_fish_file, env_fish_content) . await ?;
501507
508+ // Nushell env file with vp wrapper function
509+ let env_nu_content = r#"# Vite+ environment setup (https://viteplus.dev)
510+ let __vp_bin = $"__VP_BIN_NU__"
511+ $env.PATH = (($env.PATH | where {|entry| $entry != $__vp_bin }) | prepend $__vp_bin)
512+
513+ # Helper function to process `vp env use` stdout payload
514+ # to set/unset VP_NODE_VERSION in the current shell session.
515+ def --env __vp_apply_env_use_output [payload: string] {
516+ let __vp_payload = ($payload | str trim)
517+ if (($__vp_payload | str length) == 0) {
518+ return
519+ }
520+
521+ # `vp env use` emits JSONL for Nushell so we can apply it without string parsing.
522+ for __line in ($__vp_payload | lines) {
523+ let __vp_data = try {
524+ $__line | from json
525+ } catch {
526+ error make { msg: $"Invalid Vite+ env payload: ($__vp_payload)" }
527+ }
528+
529+ if 'set' in $__vp_data {
530+ load-env $__vp_data.set
531+ } else if 'unset' in $__vp_data {
532+ for name in $__vp_data.unset {
533+ hide-env -i $name
534+ }
535+ } else {
536+ error make { msg: $"Unsupported Vite+ env payload: ($__vp_payload)" }
537+ }
538+ }
539+ }
540+
541+ # Shell function wrapper: intercepts `vp env use` to consume its stdout,
542+ # Which then used to set/unset VP_NODE_VERSION in the current shell session.
543+ def --env vp [...args: string] {
544+ if (($args | length) >= 2) and $args.0 == "env" and $args.1 == "use" {
545+ if ($args | any {|arg| $arg == "-h" or $arg == "--help"}) {
546+ ^vp ...$args
547+ return
548+ }
549+
550+ let __vp_result = (with-env { VP_ENV_USE_EVAL_ENABLE: "1" } {
551+ do { ^vp ...$args } | complete
552+ })
553+
554+ if (($__vp_result.stderr | str length) > 0) {
555+ print --stderr --raw $__vp_result.stderr
556+ }
557+
558+ if $__vp_result.exit_code != 0 {
559+ let __vp_error = ($__vp_result.stderr | str trim)
560+ if (($__vp_error | str length) > 0) {
561+ error make { msg: $__vp_error }
562+ } else {
563+ error make { msg: $"vp env use exited with code ($__vp_result.exit_code)" }
564+ }
565+ }
566+
567+ __vp_apply_env_use_output $__vp_result.stdout
568+ } else {
569+ ^vp ...$args
570+ }
571+ }
572+ "#
573+ . replace ( "__VP_BIN_NU__" , & bin_path_nu. unwrap_or ( bin_path_ref) ) ;
574+ let env_nu_file = vite_plus_home. join ( "env.nu" ) ;
575+ tokio:: fs:: write ( & env_nu_file, env_nu_content) . await ?;
576+
502577 // PowerShell env file
503578 let env_ps1_content = r#"# Vite+ environment setup (https://viteplus.dev)
504579$__vp_bin = "__VP_BIN_WIN__"
@@ -577,20 +652,21 @@ Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp
577652
578653/// Print instructions for adding bin directory to PATH.
579654fn print_path_instructions ( bin_dir : & vite_path:: AbsolutePath ) {
580- // Derive vite_plus_home from bin_dir (parent), using $ HOME prefix for readability
655+ // Derive vite_plus_home from bin_dir (parent), using a HOME-relative path for readability.
581656 let home_path = bin_dir
582657 . parent ( )
583658 . map ( |p| p. as_path ( ) . display ( ) . to_string ( ) )
584659 . unwrap_or_else ( || bin_dir. as_path ( ) . display ( ) . to_string ( ) ) ;
585- let home_path = if let Ok ( home_dir) = std:: env:: var ( "HOME" ) {
660+ let ( home_path, nu_home_path ) = if let Ok ( home_dir) = std:: env:: var ( "HOME" ) {
586661 if let Some ( suffix) = home_path. strip_prefix ( & home_dir) {
587- format ! ( "$HOME{suffix}" )
662+ ( format ! ( "$HOME{suffix}" ) , Some ( format ! ( "~{suffix}" ) ) )
588663 } else {
589- home_path
664+ ( home_path. clone ( ) , None )
590665 }
591666 } else {
592- home_path
667+ ( home_path. clone ( ) , None )
593668 } ;
669+ let nu_home_path = nu_home_path. unwrap_or ( home_path. clone ( ) ) ;
594670
595671 println ! ( "{}" , help:: render_heading( "Next Steps" ) ) ;
596672 println ! ( " Add to your shell profile (~/.zshrc, ~/.bashrc, etc.):" ) ;
@@ -601,6 +677,10 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) {
601677 println ! ( ) ;
602678 println ! ( " source \" {home_path}/env.fish\" " ) ;
603679 println ! ( ) ;
680+ println ! ( " For Nushell, add to ~/.config/nushell/config.nu:" ) ;
681+ println ! ( ) ;
682+ println ! ( " source \" {nu_home_path}/env.nu\" " ) ;
683+ println ! ( ) ;
604684 println ! ( " For PowerShell, add to your $PROFILE:" ) ;
605685 println ! ( ) ;
606686 println ! ( " . \" {home_path}/env.ps1\" " ) ;
@@ -654,9 +734,11 @@ mod tests {
654734
655735 let env_path = home. join ( "env" ) ;
656736 let env_fish_path = home. join ( "env.fish" ) ;
737+ let env_nu_path = home. join ( "env.nu" ) ;
657738 let env_ps1_path = home. join ( "env.ps1" ) ;
658739 assert ! ( env_path. as_path( ) . exists( ) , "env file should be created" ) ;
659740 assert ! ( env_fish_path. as_path( ) . exists( ) , "env.fish file should be created" ) ;
741+ assert ! ( env_nu_path. as_path( ) . exists( ) , "env.nu file should be created" ) ;
660742 assert ! ( env_ps1_path. as_path( ) . exists( ) , "env.ps1 file should be created" ) ;
661743 }
662744
@@ -670,6 +752,7 @@ mod tests {
670752
671753 let env_content = tokio:: fs:: read_to_string ( home. join ( "env" ) ) . await . unwrap ( ) ;
672754 let fish_content = tokio:: fs:: read_to_string ( home. join ( "env.fish" ) ) . await . unwrap ( ) ;
755+ let nu_content = tokio:: fs:: read_to_string ( home. join ( "env.nu" ) ) . await . unwrap ( ) ;
673756
674757 // Placeholder should be fully replaced
675758 assert ! (
@@ -680,6 +763,10 @@ mod tests {
680763 !fish_content. contains( "__VP_BIN__" ) ,
681764 "env.fish file should not contain __VP_BIN__ placeholder"
682765 ) ;
766+ assert ! (
767+ !nu_content. contains( "__VP_BIN_NU__" ) ,
768+ "env.nu file should not contain __VP_BIN_NU__ placeholder"
769+ ) ;
683770
684771 // Should use $HOME-relative path since install dir is under HOME
685772 assert ! (
@@ -690,6 +777,10 @@ mod tests {
690777 fish_content. contains( "$HOME/bin" ) ,
691778 "env.fish file should reference $HOME/bin, got: {fish_content}"
692779 ) ;
780+ assert ! (
781+ nu_content. contains( "~/bin" ) ,
782+ "env.nu file should reference ~/bin, got: {nu_content}"
783+ ) ;
693784 }
694785
695786 #[ tokio:: test]
@@ -703,6 +794,7 @@ mod tests {
703794
704795 let env_content = tokio:: fs:: read_to_string ( home. join ( "env" ) ) . await . unwrap ( ) ;
705796 let fish_content = tokio:: fs:: read_to_string ( home. join ( "env.fish" ) ) . await . unwrap ( ) ;
797+ let nu_content = tokio:: fs:: read_to_string ( home. join ( "env.nu" ) ) . await . unwrap ( ) ;
706798
707799 // Should use absolute path since install dir is not under HOME
708800 let expected_bin = home. join ( "bin" ) ;
@@ -715,9 +807,14 @@ mod tests {
715807 fish_content. contains( & expected_str) ,
716808 "env.fish file should use absolute path {expected_str}, got: {fish_content}"
717809 ) ;
810+ assert ! (
811+ nu_content. contains( & expected_str) ,
812+ "env.nu file should use absolute path {expected_str}, got: {nu_content}"
813+ ) ;
718814
719815 // Should NOT use $HOME-relative path
720816 assert ! ( !env_content. contains( "$HOME/bin" ) , "env file should not reference $HOME/bin" ) ;
817+ assert ! ( !nu_content. contains( "~/bin" ) , "env.nu file should not reference ~/bin" ) ;
721818 }
722819
723820 #[ tokio:: test]
@@ -773,6 +870,23 @@ mod tests {
773870 assert ! ( fish_content. contains( "set -gx PATH" ) , "env.fish should set PATH globally" ) ;
774871 }
775872
873+ #[ tokio:: test]
874+ async fn test_create_env_files_nushell_contains_path_guard ( ) {
875+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
876+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
877+ let _guard = home_guard ( temp_dir. path ( ) ) ;
878+
879+ create_env_files ( & home) . await . unwrap ( ) ;
880+
881+ let nu_content = tokio:: fs:: read_to_string ( home. join ( "env.nu" ) ) . await . unwrap ( ) ;
882+
883+ // Verify Nushell PATH guard
884+ assert ! (
885+ nu_content. contains( "$env.PATH = (($env.PATH | where {|entry| $entry != $__vp_bin }) | prepend $__vp_bin)" ) ,
886+ "env.nu should dedupe and prepend the bin path"
887+ ) ;
888+ }
889+
776890 #[ tokio:: test]
777891 async fn test_create_env_files_is_idempotent ( ) {
778892 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
@@ -783,15 +897,18 @@ mod tests {
783897 create_env_files ( & home) . await . unwrap ( ) ;
784898 let first_env = tokio:: fs:: read_to_string ( home. join ( "env" ) ) . await . unwrap ( ) ;
785899 let first_fish = tokio:: fs:: read_to_string ( home. join ( "env.fish" ) ) . await . unwrap ( ) ;
900+ let first_nu = tokio:: fs:: read_to_string ( home. join ( "env.nu" ) ) . await . unwrap ( ) ;
786901 let first_ps1 = tokio:: fs:: read_to_string ( home. join ( "env.ps1" ) ) . await . unwrap ( ) ;
787902
788903 create_env_files ( & home) . await . unwrap ( ) ;
789904 let second_env = tokio:: fs:: read_to_string ( home. join ( "env" ) ) . await . unwrap ( ) ;
790905 let second_fish = tokio:: fs:: read_to_string ( home. join ( "env.fish" ) ) . await . unwrap ( ) ;
906+ let second_nu = tokio:: fs:: read_to_string ( home. join ( "env.nu" ) ) . await . unwrap ( ) ;
791907 let second_ps1 = tokio:: fs:: read_to_string ( home. join ( "env.ps1" ) ) . await . unwrap ( ) ;
792908
793909 assert_eq ! ( first_env, second_env, "env file should be identical after second write" ) ;
794910 assert_eq ! ( first_fish, second_fish, "env.fish file should be identical after second write" ) ;
911+ assert_eq ! ( first_nu, second_nu, "env.nu file should be identical after second write" ) ;
795912 assert_eq ! ( first_ps1, second_ps1, "env.ps1 file should be identical after second write" ) ;
796913 }
797914
@@ -868,6 +985,36 @@ mod tests {
868985 ) ;
869986 }
870987
988+ #[ tokio:: test]
989+ async fn test_create_env_files_nushell_contains_vp_function ( ) {
990+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
991+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
992+ let _guard = home_guard ( temp_dir. path ( ) ) ;
993+
994+ create_env_files ( & home) . await . unwrap ( ) ;
995+
996+ let nu_content = tokio:: fs:: read_to_string ( home. join ( "env.nu" ) ) . await . unwrap ( ) ;
997+
998+ // Verify Nushell vp function wrapper is present
999+ assert ! ( nu_content. contains( "def --env vp" ) , "env.nu should contain vp function" ) ;
1000+ assert ! (
1001+ nu_content. contains( "do { ^vp ...$args } | complete" ) ,
1002+ "env.nu should capture stdout/stderr from vp env use"
1003+ ) ;
1004+ assert ! (
1005+ nu_content. contains( "from json" ) ,
1006+ "env.nu should parse the env use payload as JSON"
1007+ ) ;
1008+ assert ! (
1009+ nu_content. contains( "load-env $__vp_data.set" ) ,
1010+ "env.nu should load env changes from the payload record"
1011+ ) ;
1012+ assert ! (
1013+ nu_content. contains( "hide-env -i $name" ) ,
1014+ "env.nu should hide env vars based on the payload list"
1015+ ) ;
1016+ }
1017+
8711018 #[ tokio:: test]
8721019 async fn test_execute_env_only_creates_home_dir_and_env_files ( ) {
8731020 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
@@ -888,6 +1035,7 @@ mod tests {
8881035 // Env files should be written
8891036 assert ! ( fresh_home. join( "env" ) . exists( ) , "env file should be created" ) ;
8901037 assert ! ( fresh_home. join( "env.fish" ) . exists( ) , "env.fish file should be created" ) ;
1038+ assert ! ( fresh_home. join( "env.nu" ) . exists( ) , "env.nu file should be created" ) ;
8911039 assert ! ( fresh_home. join( "env.ps1" ) . exists( ) , "env.ps1 file should be created" ) ;
8921040 }
8931041
0 commit comments