mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(misc): broaden CLI version detection to PATH and Windows managers
Extend scan_cli_version search paths with the PATH environment variable and common Windows Node/version-manager locations (pnpm, Volta, nvm, Scoop, Yarn). Entries from PATH are skipped when they point at the Microsoft\WindowsApps App Execution Alias directory, which is what previously caused bogus launches on Windows. Add unit tests for PATH dedup, child-dir suffix expansion, and the WindowsApps alias skip.
This commit is contained in:
@@ -785,6 +785,114 @@ fn extend_from_path_list(
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_from_cli_path_env(
|
||||
paths: &mut Vec<std::path::PathBuf>,
|
||||
value: Option<std::ffi::OsString>,
|
||||
) {
|
||||
if let Some(raw) = value {
|
||||
for p in std::env::split_paths(&raw) {
|
||||
if should_skip_cli_path_env_dir(&p) {
|
||||
continue;
|
||||
}
|
||||
push_unique_path(paths, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_cli_path_env_dir(path: &Path) -> bool {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
is_windows_app_execution_alias_dir(path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = path;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn is_windows_app_execution_alias_dir(path: &Path) -> bool {
|
||||
let normalized = path
|
||||
.to_string_lossy()
|
||||
.replace('/', "\\")
|
||||
.to_ascii_lowercase();
|
||||
normalized
|
||||
.trim_end_matches('\\')
|
||||
.ends_with("\\microsoft\\windowsapps")
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn push_env_child_dir(
|
||||
paths: &mut Vec<std::path::PathBuf>,
|
||||
value: Option<std::ffi::OsString>,
|
||||
child: &str,
|
||||
) {
|
||||
if let Some(raw) = value {
|
||||
push_unique_path(paths, std::path::PathBuf::from(raw).join(child));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn extend_existing_child_search_paths(
|
||||
paths: &mut Vec<std::path::PathBuf>,
|
||||
base: &Path,
|
||||
suffix: Option<&str>,
|
||||
) {
|
||||
if !base.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(base) {
|
||||
for entry in entries.flatten() {
|
||||
let path = match suffix {
|
||||
Some(suffix) => entry.path().join(suffix),
|
||||
None => entry.path(),
|
||||
};
|
||||
if path.exists() {
|
||||
push_unique_path(paths, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn extend_windows_cli_manager_search_paths(paths: &mut Vec<std::path::PathBuf>, home: &Path) {
|
||||
push_env_single_dir(paths, std::env::var_os("PNPM_HOME"));
|
||||
push_env_child_dir(paths, std::env::var_os("VOLTA_HOME"), "bin");
|
||||
push_env_single_dir(paths, std::env::var_os("NVM_SYMLINK"));
|
||||
push_env_child_dir(paths, std::env::var_os("SCOOP"), "shims");
|
||||
push_env_child_dir(paths, std::env::var_os("SCOOP_GLOBAL"), "shims");
|
||||
|
||||
if let Some(nvm_home) = std::env::var_os("NVM_HOME") {
|
||||
let nvm_home = std::path::PathBuf::from(nvm_home);
|
||||
push_unique_path(paths, nvm_home.clone());
|
||||
extend_existing_child_search_paths(paths, &nvm_home, None);
|
||||
}
|
||||
|
||||
if let Some(appdata) = dirs::data_dir() {
|
||||
let nvm_home = appdata.join("nvm");
|
||||
push_unique_path(paths, nvm_home.clone());
|
||||
extend_existing_child_search_paths(paths, &nvm_home, None);
|
||||
}
|
||||
|
||||
if !home.as_os_str().is_empty() {
|
||||
push_unique_path(paths, home.join("scoop").join("shims"));
|
||||
}
|
||||
|
||||
if let Some(local_data) = dirs::data_local_dir() {
|
||||
push_unique_path(paths, local_data.join("pnpm"));
|
||||
push_unique_path(paths, local_data.join("Volta").join("bin"));
|
||||
push_unique_path(paths, local_data.join("Yarn").join("bin"));
|
||||
}
|
||||
|
||||
let program_data = std::env::var_os("ProgramData")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("C:\\ProgramData"));
|
||||
push_unique_path(paths, program_data.join("scoop").join("shims"));
|
||||
}
|
||||
|
||||
/// OpenCode install.sh 路径优先级(见 https://github.com/anomalyco/opencode README):
|
||||
/// $OPENCODE_INSTALL_DIR > $XDG_BIN_DIR > $HOME/bin > $HOME/.opencode/bin
|
||||
/// 额外扫描 Bun 默认全局安装路径(~/.bun/bin)
|
||||
@@ -936,6 +1044,7 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
&mut search_paths,
|
||||
std::path::PathBuf::from("C:\\Program Files\\nodejs"),
|
||||
);
|
||||
extend_windows_cli_manager_search_paths(&mut search_paths, &home);
|
||||
}
|
||||
|
||||
let fnm_base = home.join(".local/state/fnm_multishells");
|
||||
@@ -975,7 +1084,12 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
let current_path = std::env::var("PATH").unwrap_or_default();
|
||||
let path_env = std::env::var_os("PATH");
|
||||
extend_from_cli_path_env(&mut search_paths, path_env.clone());
|
||||
let current_path = path_env
|
||||
.as_ref()
|
||||
.map(|value| value.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
for path in &search_paths {
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -2061,6 +2175,59 @@ mod tests {
|
||||
assert_eq!(count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_path_env_search_paths_include_path_entries_and_dedupe() {
|
||||
let temp = tempfile::tempdir().expect("temp dir should be created");
|
||||
let first = temp.path().join("first");
|
||||
let second = temp.path().join("second");
|
||||
std::fs::create_dir_all(&first).expect("first dir should be created");
|
||||
std::fs::create_dir_all(&second).expect("second dir should be created");
|
||||
|
||||
let path_env = std::env::join_paths([first.clone(), second.clone(), first.clone()])
|
||||
.expect("test path env should be joinable");
|
||||
let mut paths = vec![first.clone()];
|
||||
|
||||
extend_from_cli_path_env(&mut paths, Some(path_env));
|
||||
|
||||
assert!(paths.contains(&second));
|
||||
assert_eq!(paths.iter().filter(|path| *path == &first).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_search_paths_include_existing_children_with_suffix() {
|
||||
let temp = tempfile::tempdir().expect("temp dir should be created");
|
||||
let base = temp.path().join("node");
|
||||
let bin = base.join("25.8.0").join("bin");
|
||||
std::fs::create_dir_all(&bin).expect("version bin should be created");
|
||||
|
||||
let mut paths = Vec::new();
|
||||
extend_existing_child_search_paths(&mut paths, &base, Some("bin"));
|
||||
|
||||
assert!(paths.contains(&bin));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_child_dir_appends_child_and_dedupes() {
|
||||
let base = std::ffi::OsString::from("/custom/toolchain");
|
||||
let mut paths = Vec::new();
|
||||
|
||||
push_env_child_dir(&mut paths, Some(base.clone()), "bin");
|
||||
push_env_child_dir(&mut paths, Some(base), "bin");
|
||||
|
||||
assert_eq!(paths, vec![PathBuf::from("/custom/toolchain").join("bin")]);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn cli_path_env_skips_windows_apps_alias_dir() {
|
||||
assert!(is_windows_app_execution_alias_dir(Path::new(
|
||||
r"C:\Users\tester\AppData\Local\Microsoft\WindowsApps"
|
||||
)));
|
||||
assert!(!is_windows_app_execution_alias_dir(Path::new(
|
||||
r"C:\Users\tester\AppData\Roaming\npm"
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mise_node_search_paths_include_shims_and_installed_node_bins() {
|
||||
let temp = tempfile::tempdir().expect("temp dir should be created");
|
||||
|
||||
Reference in New Issue
Block a user