Skip to content

Commit 7e2243b

Browse files
committed
feat(cli): more Nushell support
1 parent 2e2c4c6 commit 7e2243b

8 files changed

Lines changed: 984 additions & 334 deletions

File tree

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

Lines changed: 176 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

@@ -166,10 +167,16 @@ async fn print_env(cwd: AbsolutePathBuf) -> Result<ExitStatus, Error> {
166167
.await?;
167168

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

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

174181
Ok(ExitStatus::default())
175182
}

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 cfg!(windows) && config.ps_module_path.is_some() {
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: 85 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-
}
128-
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-
}
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);
138100

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,22 @@ 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+
(trimmed.starts_with(". ") || trimmed.starts_with("source "))
270+
&& ["env", "env.fish", "env.nu"].iter().any(|env_file| {
271+
trimmed.contains(&format!(".vite-plus/{env_file}\""))
272+
|| trimmed.contains(&format!(".vite-plus\\{env_file}\""))
273+
})
309274
}
310275

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

317281
for (i, line) in lines.iter().enumerate() {
318-
if line.contains(pattern) {
282+
if is_vite_plus_source_line(line) {
319283
remove_indices.push(i);
320284
// Also remove the comment line above
321285
if i > 0 && lines[i - 1].contains(VITE_PLUS_COMMENT) {
@@ -396,6 +360,27 @@ mod tests {
396360
assert_eq!(&*result, "# existing\n");
397361
}
398362

363+
#[test]
364+
fn test_remove_vite_plus_lines_fish() {
365+
let content = "# existing config\n\n# Vite+ bin (https://viteplus.dev)\nsource \"$HOME/.vite-plus/env.fish\"\n";
366+
let result = remove_vite_plus_lines(content);
367+
assert_eq!(&*result, "# existing config\n");
368+
}
369+
370+
#[test]
371+
fn test_remove_vite_plus_lines_nushell() {
372+
let content = "# existing config\n\n# Vite+ bin (https://viteplus.dev)\nsource \"~/.vite-plus/env.nu\"\n";
373+
let result = remove_vite_plus_lines(content);
374+
assert_eq!(&*result, "# existing config\n");
375+
}
376+
377+
#[test]
378+
fn test_remove_vite_plus_lines_nushell_windows_path() {
379+
let content = "# existing config\nsource \"~\\.vite-plus\\env.nu\"\n";
380+
let result = remove_vite_plus_lines(content);
381+
assert_eq!(&*result, "# existing config\n");
382+
}
383+
399384
#[test]
400385
fn test_remove_vite_plus_lines_preserves_surrounding() {
401386
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 +461,8 @@ mod tests {
476461
let temp_dir = tempfile::tempdir().unwrap();
477462
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
478463

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

482467
// Main profile with vite-plus line
483468
std::fs::write(home.join(".zshrc"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
@@ -494,19 +479,25 @@ mod tests {
494479
assert!(matches!(&profiles[1].kind, AffectedProfileKind::Snippet));
495480
}
496481

497-
/// Guard that saves and restores ZDOTDIR and XDG_CONFIG_HOME env vars.
482+
/// Guard that saves and restores profile-related env vars.
498483
#[cfg(not(windows))]
499484
struct ProfileEnvGuard {
500485
original_zdotdir: Option<std::ffi::OsString>,
501486
original_xdg_config: Option<std::ffi::OsString>,
487+
original_xdg_data: Option<std::ffi::OsString>,
502488
}
503489

504490
#[cfg(not(windows))]
505491
impl ProfileEnvGuard {
506-
fn new(zdotdir: Option<&std::path::Path>, xdg_config: Option<&std::path::Path>) -> Self {
492+
fn new(
493+
zdotdir: Option<&std::path::Path>,
494+
xdg_config: Option<&std::path::Path>,
495+
xdg_data: Option<&std::path::Path>,
496+
) -> Self {
507497
let guard = Self {
508498
original_zdotdir: std::env::var_os("ZDOTDIR"),
509499
original_xdg_config: std::env::var_os("XDG_CONFIG_HOME"),
500+
original_xdg_data: std::env::var_os("XDG_DATA_HOME"),
510501
};
511502
unsafe {
512503
match zdotdir {
@@ -517,6 +508,10 @@ mod tests {
517508
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
518509
None => std::env::remove_var("XDG_CONFIG_HOME"),
519510
}
511+
match xdg_data {
512+
Some(v) => std::env::set_var("XDG_DATA_HOME", v),
513+
None => std::env::remove_var("XDG_DATA_HOME"),
514+
}
520515
}
521516
guard
522517
}
@@ -534,6 +529,10 @@ mod tests {
534529
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
535530
None => std::env::remove_var("XDG_CONFIG_HOME"),
536531
}
532+
match &self.original_xdg_data {
533+
Some(v) => std::env::set_var("XDG_DATA_HOME", v),
534+
None => std::env::remove_var("XDG_DATA_HOME"),
535+
}
537536
}
538537
}
539538
}
@@ -550,7 +549,7 @@ mod tests {
550549

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

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

555554
let profiles = collect_affected_profiles(&home);
556555
let zdotdir_profiles: Vec<_> =
@@ -572,7 +571,7 @@ mod tests {
572571

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

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

577576
let profiles = collect_affected_profiles(&home);
578577
let xdg_profiles: Vec<_> =
@@ -581,6 +580,29 @@ mod tests {
581580
assert!(matches!(&xdg_profiles[0].kind, AffectedProfileKind::Snippet));
582581
}
583582

583+
#[test]
584+
#[serial]
585+
#[cfg(not(windows))]
586+
fn test_collect_affected_profiles_xdg_data() {
587+
let temp_dir = tempfile::tempdir().unwrap();
588+
let home = AbsolutePathBuf::new(temp_dir.path().join("home")).unwrap();
589+
let xdg_data = temp_dir.path().join("xdg_data");
590+
let nushell_dir = xdg_data.join("nushell/vendor/autoload");
591+
std::fs::create_dir_all(&home).unwrap();
592+
std::fs::create_dir_all(&nushell_dir).unwrap();
593+
594+
std::fs::write(nushell_dir.join("vite-plus.nu"), "source \"~/.vite-plus/env.nu\"\n")
595+
.unwrap();
596+
597+
let _guard = ProfileEnvGuard::new(None, None, Some(&xdg_data));
598+
599+
let profiles = collect_affected_profiles(&home);
600+
let xdg_profiles: Vec<_> =
601+
profiles.iter().filter(|p| p.path.as_path().starts_with(&xdg_data)).collect();
602+
assert_eq!(xdg_profiles.len(), 1);
603+
assert!(matches!(&xdg_profiles[0].kind, AffectedProfileKind::Snippet));
604+
}
605+
584606
#[test]
585607
fn test_execute_not_installed() {
586608
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
@@ -171,6 +171,7 @@ pub mod version;
171171

172172
// Category D: Environment Management
173173
pub mod env;
174+
pub mod shell;
174175

175176
// Standalone binary commands
176177
pub mod vpr;

0 commit comments

Comments
 (0)