Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
18e5d59
feat(env): add support for Nu shell version in EnvConfig
naokihaba Apr 5, 2026
fc9d457
feat(shell): add support for NuShell in shell detection and command f…
naokihaba Apr 5, 2026
e17d514
feat(env): add Nushell support with completion and environment setup
naokihaba Apr 5, 2026
f339b45
feat(nushell): add Nushell completion support in main function
naokihaba Apr 5, 2026
babc76d
feat(nushell): add clap_complete_nushell dependency for Nushell support
naokihaba Apr 5, 2026
840bf1e
feat(nushell): add clap_complete_nushell dependency for Nushell compl…
naokihaba Apr 5, 2026
4e4d495
chore: remove clap_complete_nushell dependency from Cargo.toml files
naokihaba Apr 5, 2026
d55632e
feat(nushell): update completion generation to use Fish format for Nu…
naokihaba Apr 5, 2026
c060095
refactor(nushell): remove direct Nushell completion handling from mai…
naokihaba Apr 5, 2026
d39efa0
Merge branch 'main' into feat/nushell-support
naokihaba Apr 5, 2026
aa170c4
chore: remove unused import of CompleteEnv from main.rs
naokihaba Apr 5, 2026
5e0bf75
Merge branch 'feat/nushell-support' of github.com:naokihaba/vite-plus…
naokihaba Apr 5, 2026
f39e459
fix(nushell): ensure environment mutations are applied in emission order
naokihaba Apr 5, 2026
2e8fc7b
fix(nushell): ensure environment key is checked before hiding
naokihaba Apr 5, 2026
08a8e9d
fix(nushell): update environment variable assignment syntax in setup …
naokihaba Apr 5, 2026
371d6ef
feat(nushell): introduce VP_SHELL_NU for explicit Nu shell detection …
naokihaba Apr 5, 2026
6fede95
test(nushell): simplify assertion for load-env in nu_content check
naokihaba Apr 5, 2026
5938405
test(nushell): add windows shell detection assertion in tests
naokihaba Apr 5, 2026
3170641
Merge branch 'main' into feat/nushell-support
fengmk2 Apr 6, 2026
9da10f5
fix(setup): update Nushell path handling in print_path_instructions
naokihaba Apr 9, 2026
e4d445d
feat(nushell): enhance shell completion for vp and vpr commands
naokihaba Apr 9, 2026
d834f63
feat(nushell): improve Fish completion delegation for Nushell integra…
naokihaba Apr 9, 2026
6178f08
refactor(nushell): remove Fish completion generation from setup command
naokihaba Apr 10, 2026
b8693b9
fix(nushell): update PATH handling for Nushell to use `~` instead of …
naokihaba Apr 10, 2026
ae84b6b
style(tests): format assertion for Nushell path reference in env.nu
naokihaba Apr 10, 2026
f7b6740
Merge branch 'main' into feat/nushell-support
naokihaba Apr 10, 2026
6a5c199
Merge branch 'main' into feat/nushell-support
naokihaba Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions crates/vite_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
// Create env files with PATH guard (prevents duplicate PATH entries)
create_env_files(&vite_plus_home).await?;

// 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 fish_completions_path = vite_plus_home.join("vp.fish").as_path().to_path_buf();
Comment thread
naokihaba marked this conversation as resolved.
Outdated
std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(move || -> std::io::Result<()> {
let mut file = std::fs::File::create(&fish_completions_path)?;
let mut cmd = crate::cli::command_with_help();
clap_complete::generate(clap_complete::Shell::Fish, &mut cmd, "vp", &mut file);
Ok(())
})
.map_err(Error::CommandExecution)?
.join()
.map_err(|_| Error::Other("Fish completion generation failed".into()))?
.map_err(Error::CommandExecution)?;

if env_only {
println!("{}", help::render_heading("Setup"));
println!(" Updated shell environment files.");
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -499,6 +517,54 @@ 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.
// 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__")
Comment thread
fengmk2 marked this conversation as resolved.
Comment thread
naokihaba marked this conversation as resolved.

# 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 }
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
} else {
^vp ...$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 }
Comment thread
naokihaba marked this conversation as resolved.
Outdated
"#
.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__"
Expand Down Expand Up @@ -601,6 +667,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\"");
Comment thread
naokihaba marked this conversation as resolved.
Outdated
println!();
println!(" For PowerShell, add to your $PROFILE:");
println!();
println!(" . \"{home_path}/env.ps1\"");
Expand Down Expand Up @@ -654,12 +724,38 @@ 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_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.fish"),
"env.nu should reference the Fish completion file for Nushell delegation"
);
}

#[tokio::test]
async fn test_create_env_files_replaces_placeholder_with_home_relative_path() {
let temp_dir = TempDir::new().unwrap();
Expand Down
38 changes: 38 additions & 0 deletions crates/vite_global_cli/src/commands/env/use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ enum Shell {
PowerShell,
/// Windows cmd.exe
Cmd,
/// Nushell
NuShell,
}

/// Detect the current shell from environment variables.
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
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
} else if cfg!(windows) && config.ps_module_path.is_some() {
Shell::PowerShell
} else if cfg!(windows) {
Expand All @@ -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}\""),
}
}

Expand All @@ -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}"),
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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");
Expand Down Expand Up @@ -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");
}
}
1 change: 1 addition & 0 deletions crates/vite_global_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use std::{

use clap::error::{ContextKind, ContextValue};
use clap_complete::env::CompleteEnv;

use owo_colors::OwoColorize;
use vite_shared::output;

Expand Down
7 changes: 7 additions & 0 deletions crates/vite_shared/src/env_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ pub struct EnvConfig {
///
/// Env: `PSModulePath`
pub ps_module_path: Option<String>,

/// Nu shell version (indicates running under Nu shell).
///
/// Env: `NU_VERSION`
pub nu_version: Option<String>,
}

impl EnvConfig {
Expand Down Expand Up @@ -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(),
}
}

Expand Down Expand Up @@ -233,6 +239,7 @@ impl EnvConfig {
user_home: None,
fish_version: None,
ps_module_path: None,
nu_version: None,
}
}

Expand Down
Loading