Skip to content

Commit c0fcf31

Browse files
authored
feat(cli): more Nushell support (#1305)
This PR adds more Nushell support to the Vite+ global CLI setup and environment commands. It introduces: - Nushell profile detection in `vp env doctor` - Nushell cleanup support in uninstall-related flows - Installer updates so Nushell can be configured automatically when available It also refactors shared shell/profile handling to keep setup, doctor, and cleanup behavior consistent across shells and platforms.
1 parent cece6a4 commit c0fcf31

10 files changed

Lines changed: 991 additions & 335 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_global_cli/src/commands/env/doctor.rs

Lines changed: 177 additions & 115 deletions
Large diffs are not rendered by default.

crates/vite_global_cli/src/commands/env/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use vite_path::AbsolutePathBuf;
2828

2929
use crate::{
3030
cli::{EnvArgs, EnvSubcommands},
31+
commands::shell::{Shell, detect_shell},
3132
error::Error,
3233
};
3334

@@ -164,10 +165,16 @@ async fn print_env(cwd: AbsolutePathBuf) -> Result<ExitStatus, Error> {
164165
.await?;
165166

166167
let bin_dir = runtime.get_bin_prefix();
168+
let snippet = match detect_shell() {
169+
Shell::NuShell => {
170+
format!("$env.PATH = ($env.PATH | prepend \"{}\")", bin_dir.as_path().display())
171+
}
172+
_ => format!("export PATH=\"{}:$PATH\"", bin_dir.as_path().display()),
173+
};
167174

168175
// Print shell snippet
169176
println!("# Add to your shell to use this Node.js version for this session:");
170-
println!("export PATH=\"{}:$PATH\"", bin_dir.as_path().display());
177+
println!("{snippet}");
171178

172179
Ok(ExitStatus::default())
173180
}

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) {
737737
println!();
738738
println!(" For Nushell, add to ~/.config/nushell/config.nu:");
739739
println!();
740-
println!(" source \"{nu_home_path}/env.nu\"");
740+
println!(" source '{nu_home_path}/env.nu'");
741741
println!();
742742
println!(" For PowerShell, add to your $PROFILE:");
743743
println!();

crates/vite_global_cli/src/commands/env/use.rs

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,10 @@ use std::process::ExitStatus;
1313
use vite_path::AbsolutePathBuf;
1414

1515
use super::config::{self, VERSION_ENV_VAR};
16-
use crate::error::Error;
17-
18-
/// Detected shell type for output formatting.
19-
enum Shell {
20-
/// POSIX shell (bash, zsh, sh)
21-
Posix,
22-
/// Fish shell
23-
Fish,
24-
/// PowerShell
25-
PowerShell,
26-
/// Windows cmd.exe
27-
Cmd,
28-
/// Nushell
29-
NuShell,
30-
}
31-
32-
/// Detect the current shell from environment variables.
33-
fn detect_shell() -> Shell {
34-
let config = vite_shared::EnvConfig::get();
35-
if config.fish_version.is_some() {
36-
Shell::Fish
37-
} else if config.vp_shell_nu {
38-
Shell::NuShell
39-
} else if config.vp_shell_pwsh {
40-
Shell::PowerShell
41-
} else if cfg!(windows) {
42-
Shell::Cmd
43-
} else {
44-
Shell::Posix
45-
}
46-
}
16+
use crate::{
17+
commands::shell::{Shell, detect_shell},
18+
error::Error,
19+
};
4720

4821
/// Format a shell export command for the detected shell.
4922
fn format_export(shell: &Shell, value: &str) -> String {

crates/vite_global_cli/src/commands/implode.rs

Lines changed: 88 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,17 @@ use std::{
77

88
use directories::BaseDirs;
99
use owo_colors::OwoColorize;
10-
use vite_path::{AbsolutePath, AbsolutePathBuf};
10+
use vite_path::AbsolutePathBuf;
1111
use vite_shared::output;
1212
use vite_str::Str;
1313

14-
use crate::{cli::exit_status, error::Error};
15-
16-
/// All shell profile paths to check, with `is_snippet` flag.
17-
const SHELL_PROFILES: &[(&str, bool)] = &[
18-
(".zshenv", false),
19-
(".zshrc", false),
20-
(".bash_profile", false),
21-
(".bashrc", false),
22-
(".profile", false),
23-
(".config/fish/conf.d/vite-plus.fish", true),
24-
];
25-
26-
/// Abbreviate a path for display: replace `$HOME` prefix with `~`.
27-
fn abbreviate_home_path(path: &AbsolutePath, user_home: &AbsolutePath) -> Str {
28-
match path.strip_prefix(user_home) {
29-
Ok(Some(suffix)) => vite_str::format!("~/{suffix}"),
30-
_ => Str::from(path.to_string()),
31-
}
32-
}
14+
use crate::{
15+
cli::exit_status,
16+
commands::shell::{
17+
ALL_SHELL_PROFILES, ShellProfileKind, abbreviate_home_path, resolve_profile_path,
18+
},
19+
error::Error,
20+
};
3321

3422
/// Comment marker written by the install script above the sourcing line.
3523
const VITE_PLUS_COMMENT: &str = "# Vite+ bin";
@@ -106,39 +94,12 @@ enum AffectedProfileKind {
10694
fn collect_affected_profiles(user_home: &AbsolutePathBuf) -> Vec<AffectedProfile> {
10795
let mut affected = Vec::new();
10896

109-
// Build full list of (display_name, path, is_snippet) from the base set
110-
let mut profiles: Vec<(Str, AbsolutePathBuf, bool)> = SHELL_PROFILES
111-
.iter()
112-
.map(|&(name, is_snippet)| {
113-
(vite_str::format!("~/{name}"), user_home.join(name), is_snippet)
114-
})
115-
.collect();
116-
117-
// If ZDOTDIR is set and differs from $HOME, also check there.
118-
if let Ok(zdotdir) = std::env::var("ZDOTDIR")
119-
&& let Some(zdotdir_path) = AbsolutePathBuf::new(zdotdir.into())
120-
&& zdotdir_path != *user_home
121-
{
122-
for name in [".zshenv", ".zshrc"] {
123-
let path = zdotdir_path.join(name);
124-
let display = abbreviate_home_path(&path, user_home);
125-
profiles.push((display, path, false));
126-
}
127-
}
97+
for profile in ALL_SHELL_PROFILES {
98+
let path = resolve_profile_path(profile, user_home);
99+
let name = abbreviate_home_path(&path, user_home);
128100

129-
// If XDG_CONFIG_HOME is set and differs from $HOME/.config, also check there.
130-
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME")
131-
&& let Some(xdg_path) = AbsolutePathBuf::new(xdg_config.into())
132-
&& xdg_path != user_home.join(".config")
133-
{
134-
let path = xdg_path.join("fish/conf.d/vite-plus.fish");
135-
let display = abbreviate_home_path(&path, user_home);
136-
profiles.push((display, path, true));
137-
}
138-
139-
for (name, path, is_snippet) in profiles {
140101
// For snippets, check if the file exists only
141-
if is_snippet {
102+
if matches!(profile.kind, ShellProfileKind::Snippet) {
142103
if let Ok(true) = std::fs::exists(&path) {
143104
affected.push(AffectedProfile { name, path, kind: AffectedProfileKind::Snippet })
144105
}
@@ -147,7 +108,7 @@ fn collect_affected_profiles(user_home: &AbsolutePathBuf) -> Vec<AffectedProfile
147108
// Read directly — if the file doesn't exist, read_to_string returns Err
148109
// which .ok().filter() handles gracefully (no redundant exists() check).
149110
if let Some(content) =
150-
std::fs::read_to_string(&path).ok().filter(|c| has_vite_plus_lines(c))
111+
std::fs::read_to_string(&path).ok().filter(|c| c.lines().any(is_vite_plus_source_line))
151112
{
152113
affected.push(AffectedProfile {
153114
name,
@@ -303,19 +264,26 @@ fn spawn_deferred_delete(trash_path: &std::path::Path) -> std::io::Result<std::p
303264
}
304265

305266
/// Check if file content contains Vite+ sourcing lines.
306-
fn has_vite_plus_lines(content: &str) -> bool {
307-
let pattern = ".vite-plus/env\"";
308-
content.lines().any(|line| line.contains(pattern))
267+
fn is_vite_plus_source_line(line: &str) -> bool {
268+
let trimmed = line.trim_start();
269+
[
270+
(". ", ".vite-plus/env\""),
271+
("source ", ".vite-plus/env\""),
272+
("source ", ".vite-plus/env.fish\""),
273+
("source ", ".vite-plus/env.nu'"),
274+
("source ", ".vite-plus\\env.nu'"),
275+
]
276+
.iter()
277+
.any(|(prefix, suffix)| trimmed.starts_with(prefix) && trimmed.contains(suffix))
309278
}
310279

311280
/// Remove Vite+ lines from content, returning the cleaned string.
312281
fn remove_vite_plus_lines(content: &str) -> Str {
313-
let pattern = ".vite-plus/env\"";
314282
let lines: Vec<&str> = content.lines().collect();
315283
let mut remove_indices = Vec::new();
316284

317285
for (i, line) in lines.iter().enumerate() {
318-
if line.contains(pattern) {
286+
if is_vite_plus_source_line(line) {
319287
remove_indices.push(i);
320288
// Also remove the comment line above
321289
if i > 0 && lines[i - 1].contains(VITE_PLUS_COMMENT) {
@@ -396,6 +364,27 @@ mod tests {
396364
assert_eq!(&*result, "# existing\n");
397365
}
398366

367+
#[test]
368+
fn test_remove_vite_plus_lines_fish() {
369+
let content = "# existing config\n\n# Vite+ bin (https://viteplus.dev)\nsource \"$HOME/.vite-plus/env.fish\"\n";
370+
let result = remove_vite_plus_lines(content);
371+
assert_eq!(&*result, "# existing config\n");
372+
}
373+
374+
#[test]
375+
fn test_remove_vite_plus_lines_nushell() {
376+
let content = "# existing config\n\n# Vite+ bin (https://viteplus.dev)\nsource '~/.vite-plus/env.nu'\n";
377+
let result = remove_vite_plus_lines(content);
378+
assert_eq!(&*result, "# existing config\n");
379+
}
380+
381+
#[test]
382+
fn test_remove_vite_plus_lines_nushell_windows_path() {
383+
let content = "# existing config\nsource '~\\.vite-plus\\env.nu'\n";
384+
let result = remove_vite_plus_lines(content);
385+
assert_eq!(&*result, "# existing config\n");
386+
}
387+
399388
#[test]
400389
fn test_remove_vite_plus_lines_preserves_surrounding() {
401390
let content = "# before\nexport A=1\n\n# Vite+ bin (https://viteplus.dev)\n. \"$HOME/.vite-plus/env\"\n# after\nexport B=2\n";
@@ -476,8 +465,8 @@ mod tests {
476465
let temp_dir = tempfile::tempdir().unwrap();
477466
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
478467

479-
// Clear ZDOTDIR/XDG_CONFIG_HOME so the test environment doesn't affect results
480-
let _guard = ProfileEnvGuard::new(None, None);
468+
// Clear env overrides so the test environment doesn't affect results
469+
let _guard = ProfileEnvGuard::new(None, None, None);
481470

482471
// Main profile with vite-plus line
483472
std::fs::write(home.join(".zshrc"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
@@ -494,19 +483,25 @@ mod tests {
494483
assert!(matches!(&profiles[1].kind, AffectedProfileKind::Snippet));
495484
}
496485

497-
/// Guard that saves and restores ZDOTDIR and XDG_CONFIG_HOME env vars.
486+
/// Guard that saves and restores profile-related env vars.
498487
#[cfg(not(windows))]
499488
struct ProfileEnvGuard {
500489
original_zdotdir: Option<std::ffi::OsString>,
501490
original_xdg_config: Option<std::ffi::OsString>,
491+
original_xdg_data: Option<std::ffi::OsString>,
502492
}
503493

504494
#[cfg(not(windows))]
505495
impl ProfileEnvGuard {
506-
fn new(zdotdir: Option<&std::path::Path>, xdg_config: Option<&std::path::Path>) -> Self {
496+
fn new(
497+
zdotdir: Option<&std::path::Path>,
498+
xdg_config: Option<&std::path::Path>,
499+
xdg_data: Option<&std::path::Path>,
500+
) -> Self {
507501
let guard = Self {
508502
original_zdotdir: std::env::var_os("ZDOTDIR"),
509503
original_xdg_config: std::env::var_os("XDG_CONFIG_HOME"),
504+
original_xdg_data: std::env::var_os("XDG_DATA_HOME"),
510505
};
511506
unsafe {
512507
match zdotdir {
@@ -517,6 +512,10 @@ mod tests {
517512
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
518513
None => std::env::remove_var("XDG_CONFIG_HOME"),
519514
}
515+
match xdg_data {
516+
Some(v) => std::env::set_var("XDG_DATA_HOME", v),
517+
None => std::env::remove_var("XDG_DATA_HOME"),
518+
}
520519
}
521520
guard
522521
}
@@ -534,6 +533,10 @@ mod tests {
534533
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
535534
None => std::env::remove_var("XDG_CONFIG_HOME"),
536535
}
536+
match &self.original_xdg_data {
537+
Some(v) => std::env::set_var("XDG_DATA_HOME", v),
538+
None => std::env::remove_var("XDG_DATA_HOME"),
539+
}
537540
}
538541
}
539542
}
@@ -550,7 +553,7 @@ mod tests {
550553

551554
std::fs::write(zdotdir.join(".zshenv"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
552555

553-
let _guard = ProfileEnvGuard::new(Some(&zdotdir), None);
556+
let _guard = ProfileEnvGuard::new(Some(&zdotdir), None, None);
554557

555558
let profiles = collect_affected_profiles(&home);
556559
let zdotdir_profiles: Vec<_> =
@@ -572,7 +575,7 @@ mod tests {
572575

573576
std::fs::write(fish_dir.join("vite-plus.fish"), "").unwrap();
574577

575-
let _guard = ProfileEnvGuard::new(None, Some(&xdg_config));
578+
let _guard = ProfileEnvGuard::new(None, Some(&xdg_config), None);
576579

577580
let profiles = collect_affected_profiles(&home);
578581
let xdg_profiles: Vec<_> =
@@ -581,6 +584,28 @@ mod tests {
581584
assert!(matches!(&xdg_profiles[0].kind, AffectedProfileKind::Snippet));
582585
}
583586

587+
#[test]
588+
#[serial]
589+
#[cfg(not(windows))]
590+
fn test_collect_affected_profiles_xdg_data() {
591+
let temp_dir = tempfile::tempdir().unwrap();
592+
let home = AbsolutePathBuf::new(temp_dir.path().join("home")).unwrap();
593+
let xdg_data = temp_dir.path().join("xdg_data");
594+
let nushell_dir = xdg_data.join("nushell/vendor/autoload");
595+
std::fs::create_dir_all(&home).unwrap();
596+
std::fs::create_dir_all(&nushell_dir).unwrap();
597+
598+
std::fs::write(nushell_dir.join("vite-plus.nu"), "source '~/.vite-plus/env.nu'\n").unwrap();
599+
600+
let _guard = ProfileEnvGuard::new(None, None, Some(&xdg_data));
601+
602+
let profiles = collect_affected_profiles(&home);
603+
let xdg_profiles: Vec<_> =
604+
profiles.iter().filter(|p| p.path.as_path().starts_with(&xdg_data)).collect();
605+
assert_eq!(xdg_profiles.len(), 1);
606+
assert!(matches!(&xdg_profiles[0].kind, AffectedProfileKind::Snippet));
607+
}
608+
584609
#[test]
585610
fn test_execute_not_installed() {
586611
let temp_dir = tempfile::tempdir().unwrap();

crates/vite_global_cli/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub mod version;
9494

9595
// Category D: Environment Management
9696
pub mod env;
97+
pub mod shell;
9798

9899
// Standalone binary commands
99100
pub mod vpr;

0 commit comments

Comments
 (0)