Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
122 changes: 122 additions & 0 deletions crates/vite_global_cli/src/commands/env/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,33 @@ fn check_profile_files(vite_plus_home: &str) -> Option<String> {
}
}

// If ZDOTDIR is set and differs from $HOME, also check $ZDOTDIR/.zshenv and .zshrc
if let Ok(zdotdir) = std::env::var("ZDOTDIR") {
if !zdotdir.is_empty() && zdotdir != home_dir {
for file in [".zshenv", ".zshrc"] {
let path = format!("{zdotdir}/{file}");
if let Ok(content) = std::fs::read_to_string(&path) {
if search_strings.iter().any(|s| content.contains(s)) {
return Some(abbreviate_home(&path));
}
}
}
}
}

// If XDG_CONFIG_HOME is set and differs from default, also check fish conf.d
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
let default_config = format!("{home_dir}/.config");
if !xdg_config.is_empty() && xdg_config != default_config {
let path = format!("{xdg_config}/fish/conf.d/vite-plus.fish");
if let Ok(content) = std::fs::read_to_string(&path) {
if search_strings.iter().any(|s| content.contains(s)) {
return Some(abbreviate_home(&path));
}
}
}
}

None
}

Expand Down Expand Up @@ -766,4 +793,99 @@ mod tests {
assert_eq!(abbreviate_home("/usr/local/bin"), "/usr/local/bin");
}
}

/// Guard for env vars used by profile file tests.
#[cfg(not(windows))]
struct ProfileEnvGuard {
original_home: Option<std::ffi::OsString>,
original_zdotdir: Option<std::ffi::OsString>,
original_xdg_config: Option<std::ffi::OsString>,
}

#[cfg(not(windows))]
impl ProfileEnvGuard {
fn new(
home: &std::path::Path,
zdotdir: Option<&std::path::Path>,
xdg_config: Option<&std::path::Path>,
) -> Self {
let guard = Self {
original_home: std::env::var_os("HOME"),
original_zdotdir: std::env::var_os("ZDOTDIR"),
original_xdg_config: std::env::var_os("XDG_CONFIG_HOME"),
};
unsafe {
std::env::set_var("HOME", home);
match zdotdir {
Some(v) => std::env::set_var("ZDOTDIR", v),
None => std::env::remove_var("ZDOTDIR"),
}
match xdg_config {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
guard
}
}

#[cfg(not(windows))]
impl Drop for ProfileEnvGuard {
fn drop(&mut self) {
unsafe {
match &self.original_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match &self.original_zdotdir {
Some(v) => std::env::set_var("ZDOTDIR", v),
None => std::env::remove_var("ZDOTDIR"),
}
match &self.original_xdg_config {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
}
}

#[test]
#[serial]
#[cfg(not(windows))]
fn test_check_profile_files_finds_zdotdir() {
let temp = TempDir::new().unwrap();
let fake_home = temp.path().join("home");
let zdotdir = temp.path().join("zdotdir");
std::fs::create_dir_all(&fake_home).unwrap();
std::fs::create_dir_all(&zdotdir).unwrap();

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

let _guard = ProfileEnvGuard::new(&fake_home, Some(&zdotdir), None);

let result = check_profile_files("$HOME/.vite-plus");
assert!(result.is_some(), "Should find .zshenv in ZDOTDIR");
assert!(result.unwrap().ends_with(".zshenv"));
}

#[test]
#[serial]
#[cfg(not(windows))]
fn test_check_profile_files_finds_xdg_fish() {
let temp = TempDir::new().unwrap();
let fake_home = temp.path().join("home");
let xdg_config = temp.path().join("xdg_config");
let fish_dir = xdg_config.join("fish/conf.d");
std::fs::create_dir_all(&fake_home).unwrap();
std::fs::create_dir_all(&fish_dir).unwrap();

std::fs::write(fish_dir.join("vite-plus.fish"), "source \"$HOME/.vite-plus/env.fish\"\n")
.unwrap();

let _guard = ProfileEnvGuard::new(&fake_home, None, Some(&xdg_config));

let result = check_profile_files("$HOME/.vite-plus");
assert!(result.is_some(), "Should find vite-plus.fish in XDG_CONFIG_HOME");
assert!(result.unwrap().contains("vite-plus.fish"));
}
}
193 changes: 177 additions & 16 deletions crates/vite_global_cli/src/commands/implode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::{

use directories::BaseDirs;
use owo_colors::OwoColorize;
use vite_path::AbsolutePathBuf;
use vite_path::{AbsolutePath, AbsolutePathBuf};
use vite_shared::output;
use vite_str::Str;

Expand All @@ -23,6 +23,14 @@ const SHELL_PROFILES: &[(&str, bool)] = &[
(".config/fish/conf.d/vite-plus.fish", true),
];

/// Abbreviate a path for display: replace `$HOME` prefix with `~`.
fn abbreviate_home_path(path: &AbsolutePath, user_home: &AbsolutePath) -> Str {
match path.strip_prefix(user_home) {
Ok(Some(suffix)) => vite_str::format!("~/{suffix}"),
_ => Str::from(path.to_string()),
}
}

/// Comment marker written by the install script above the sourcing line.
const VITE_PLUS_COMMENT: &str = "# Vite+ bin";

Expand Down Expand Up @@ -97,26 +105,52 @@ enum AffectedProfileKind {
/// Content is cached so we don't need to re-read during cleaning.
fn collect_affected_profiles(user_home: &AbsolutePathBuf) -> Vec<AffectedProfile> {
let mut affected = Vec::new();
for &(name, is_snippet) in SHELL_PROFILES {
let path = user_home.join(name);

// Build full list of (display_name, path, is_snippet) from the base set
let mut profiles: Vec<(Str, AbsolutePathBuf, bool)> = SHELL_PROFILES
.iter()
.map(|&(name, is_snippet)| {
(vite_str::format!("~/{name}"), user_home.join(name), is_snippet)
})
.collect();

// If ZDOTDIR is set and differs from $HOME, also check there.
if let Ok(zdotdir) = std::env::var("ZDOTDIR")
&& let Some(zdotdir_path) = AbsolutePathBuf::new(zdotdir.into())
&& zdotdir_path != *user_home
{
for name in [".zshenv", ".zshrc"] {
let path = zdotdir_path.join(name);
let display = abbreviate_home_path(&path, user_home);
profiles.push((display, path, false));
}
}

// If XDG_CONFIG_HOME is set and differs from $HOME/.config, also check there.
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME")
&& let Some(xdg_path) = AbsolutePathBuf::new(xdg_config.into())
&& xdg_path != user_home.join(".config")
{
let path = xdg_path.join("fish/conf.d/vite-plus.fish");
let display = abbreviate_home_path(&path, user_home);
profiles.push((display, path, true));
}

for (name, path, is_snippet) in profiles {
// For snippets, check if the file exists only
if is_snippet {
if let Some(true) = std::fs::exists(&path).ok() {
affected.push(AffectedProfile {
name: Str::from(name),
path,
kind: AffectedProfileKind::Snippet,
})
if let Ok(true) = std::fs::exists(&path) {
affected.push(AffectedProfile { name, path, kind: AffectedProfileKind::Snippet })
}
continue;
}
// Read directly — if the file doesn't exist, read_to_string returns Err
// which is_ok_and handles gracefully (no redundant exists() check).
// which .ok().filter() handles gracefully (no redundant exists() check).
if let Some(content) =
std::fs::read_to_string(&path).ok().filter(|c| has_vite_plus_lines(c))
{
affected.push(AffectedProfile {
name: Str::from(name),
name,
path,
kind: AffectedProfileKind::Main { content: Str::from(content) },
});
Expand Down Expand Up @@ -144,7 +178,7 @@ fn confirm_implode(
if !affected_profiles.is_empty() {
output::raw(" Shell profiles to clean:");
for profile in affected_profiles {
output::raw(&vite_str::format!(" - ~/{}", profile.name));
output::raw(&vite_str::format!(" - {}", profile.name));
}
}
output::raw("");
Expand All @@ -171,16 +205,16 @@ fn clean_affected_profiles(affected_profiles: &[AffectedProfile]) {
AffectedProfileKind::Main { content } => {
let cleaned = remove_vite_plus_lines(content);
match std::fs::write(&profile.path, cleaned.as_bytes()) {
Ok(()) => output::success(&vite_str::format!("Cleaned ~/{}", profile.name)),
Ok(()) => output::success(&vite_str::format!("Cleaned {}", profile.name)),
Err(e) => {
output::warn(&vite_str::format!("Failed to clean ~/{}: {e}", profile.name));
output::warn(&vite_str::format!("Failed to clean {}: {e}", profile.name));
}
}
}
AffectedProfileKind::Snippet => match std::fs::remove_file(&profile.path) {
Ok(()) => output::success(&vite_str::format!("Removed ~/{}", profile.name)),
Ok(()) => output::success(&vite_str::format!("Removed {}", profile.name)),
Err(e) => {
output::warn(&vite_str::format!("Failed to remove ~/{}: {e}", profile.name));
output::warn(&vite_str::format!("Failed to remove {}: {e}", profile.name));
}
},
}
Expand Down Expand Up @@ -336,6 +370,9 @@ fn remove_windows_path_entry(bin_path: &vite_path::AbsolutePath) -> std::io::Res

#[cfg(test)]
mod tests {
#[cfg(not(windows))]
use serial_test::serial;

use super::*;

#[test]
Expand Down Expand Up @@ -420,6 +457,130 @@ mod tests {
assert!(script.contains("timeout /T 1 /NOBREAK"));
}

#[test]
#[cfg(not(windows))]
fn test_abbreviate_home_path() {
let home = AbsolutePathBuf::new("/home/user".into()).unwrap();
// Under home → ~/...
let under = AbsolutePathBuf::new("/home/user/.zshrc".into()).unwrap();
assert_eq!(&*abbreviate_home_path(&under, &home), "~/.zshrc");
// Outside home → absolute path as-is
let outside = AbsolutePathBuf::new("/opt/zdotdir/.zshenv".into()).unwrap();
assert_eq!(&*abbreviate_home_path(&outside, &home), "/opt/zdotdir/.zshenv");
}

#[test]
#[serial]
#[cfg(not(windows))]
fn test_collect_affected_profiles() {
let temp_dir = tempfile::tempdir().unwrap();
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();

// Clear ZDOTDIR/XDG_CONFIG_HOME so the test environment doesn't affect results
let _guard = ProfileEnvGuard::new(None, None);

// Main profile with vite-plus line
std::fs::write(home.join(".zshrc"), ". \"$HOME/.vite-plus/env\"\n").unwrap();
// Unrelated profile (should be ignored)
std::fs::write(home.join(".bashrc"), "export PATH=/usr/bin\n").unwrap();
// Snippet file (just needs to exist)
let fish_dir = home.join(".config/fish/conf.d");
std::fs::create_dir_all(&fish_dir).unwrap();
std::fs::write(fish_dir.join("vite-plus.fish"), "source ~/.vite-plus/env.fish\n").unwrap();

let profiles = collect_affected_profiles(&home);
assert_eq!(profiles.len(), 2);
assert!(matches!(&profiles[0].kind, AffectedProfileKind::Main { .. }));
assert!(matches!(&profiles[1].kind, AffectedProfileKind::Snippet));
}

/// Guard that saves and restores ZDOTDIR and XDG_CONFIG_HOME env vars.
#[cfg(not(windows))]
struct ProfileEnvGuard {
original_zdotdir: Option<std::ffi::OsString>,
original_xdg_config: Option<std::ffi::OsString>,
}

#[cfg(not(windows))]
impl ProfileEnvGuard {
fn new(zdotdir: Option<&std::path::Path>, xdg_config: Option<&std::path::Path>) -> Self {
let guard = Self {
original_zdotdir: std::env::var_os("ZDOTDIR"),
original_xdg_config: std::env::var_os("XDG_CONFIG_HOME"),
};
unsafe {
match zdotdir {
Some(v) => std::env::set_var("ZDOTDIR", v),
None => std::env::remove_var("ZDOTDIR"),
}
match xdg_config {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
guard
}
}

#[cfg(not(windows))]
impl Drop for ProfileEnvGuard {
fn drop(&mut self) {
unsafe {
match &self.original_zdotdir {
Some(v) => std::env::set_var("ZDOTDIR", v),
None => std::env::remove_var("ZDOTDIR"),
}
match &self.original_xdg_config {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
}
}

#[test]
#[serial]
#[cfg(not(windows))]
fn test_collect_affected_profiles_zdotdir() {
let temp_dir = tempfile::tempdir().unwrap();
let home = AbsolutePathBuf::new(temp_dir.path().join("home")).unwrap();
let zdotdir = temp_dir.path().join("zdotdir");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&zdotdir).unwrap();

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

let _guard = ProfileEnvGuard::new(Some(&zdotdir), None);

let profiles = collect_affected_profiles(&home);
let zdotdir_profiles: Vec<_> =
profiles.iter().filter(|p| p.path.as_path().starts_with(&zdotdir)).collect();
assert_eq!(zdotdir_profiles.len(), 1);
assert!(matches!(&zdotdir_profiles[0].kind, AffectedProfileKind::Main { .. }));
}

#[test]
#[serial]
#[cfg(not(windows))]
fn test_collect_affected_profiles_xdg_config() {
let temp_dir = tempfile::tempdir().unwrap();
let home = AbsolutePathBuf::new(temp_dir.path().join("home")).unwrap();
let xdg_config = temp_dir.path().join("xdg_config");
let fish_dir = xdg_config.join("fish/conf.d");
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&fish_dir).unwrap();

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

let _guard = ProfileEnvGuard::new(None, Some(&xdg_config));

let profiles = collect_affected_profiles(&home);
let xdg_profiles: Vec<_> =
profiles.iter().filter(|p| p.path.as_path().starts_with(&xdg_config)).collect();
assert_eq!(xdg_profiles.len(), 1);
assert!(matches!(&xdg_profiles[0].kind, AffectedProfileKind::Snippet));
}

#[test]
fn test_execute_not_installed() {
let temp_dir = tempfile::tempdir().unwrap();
Expand Down
Loading
Loading