From 32ea6624a973ff230d247d3d4ba3e2e23af32e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CFardeen?= Date: Sun, 11 Jan 2026 00:46:52 +0530 Subject: [PATCH 1/2] fix: detect Claude Code installed via asdf Add support for detecting Claude Code when installed via asdf version manager. - Add find_asdf_installations() for Unix and Windows platforms - Check ASDF_DIR env var and default ~/.asdf/shims location - Add asdf to source_preference() ranking - Pass ASDF_DIR, ASDF_DATA_DIR, ASDF_CONFIG_FILE env vars to subprocess - Augment PATH with ~/.asdf/bin and ~/.asdf/shims - Detect symlinks from /usr/local/bin/claude pointing to asdf shims This fixes detection for users who install Claude Code via: asdf plugin add claude-code asdf install claude-code latest asdf global claude-code latest --- src-tauri/src/claude_binary.rs | 175 ++++++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 0ce4ce48..ca47e932 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -131,15 +131,16 @@ fn source_preference(installation: &ClaudeInstallation) -> u8 { "system" => 3, "nvm-active" => 4, source if source.starts_with("nvm") => 5, - "local-bin" => 6, - "claude-local" => 7, - "npm-global" => 8, - "yarn" | "yarn-global" => 9, - "bun" => 10, - "node-modules" => 11, - "home-bin" => 12, - "PATH" => 13, - _ => 14, + "asdf" => 6, + "local-bin" => 7, + "claude-local" => 8, + "npm-global" => 9, + "yarn" | "yarn-global" => 10, + "bun" => 11, + "node-modules" => 12, + "home-bin" => 13, + "PATH" => 14, + _ => 15, } } @@ -152,10 +153,13 @@ fn discover_system_installations() -> Vec { installations.push(installation); } - // 2. Check NVM paths (includes current active NVM) + // 2. Check asdf shims first (before NVM) + installations.extend(find_asdf_installations()); + + // 3. Check NVM paths (includes current active NVM) installations.extend(find_nvm_installations()); - // 3. Check standard paths + // 4. Check standard paths installations.extend(find_standard_installations()); // Remove duplicates by path @@ -251,6 +255,106 @@ fn try_which_command() -> Option { } } +/// Find Claude installations in asdf shims directories +#[cfg(unix)] +fn find_asdf_installations() -> Vec { + let mut installations = Vec::new(); + + // Check ASDF_DIR environment variable first + if let Ok(asdf_dir) = std::env::var("ASDF_DIR") { + let claude_path = PathBuf::from(&asdf_dir).join("shims").join("claude"); + if claude_path.exists() && claude_path.is_file() { + debug!("Found Claude via ASDF_DIR: {:?}", claude_path); + let version = get_claude_version(&claude_path.to_string_lossy()) + .ok() + .flatten(); + installations.push(ClaudeInstallation { + path: claude_path.to_string_lossy().to_string(), + version, + source: "asdf".to_string(), + installation_type: InstallationType::System, + }); + } + } + + // Then check default ~/.asdf location + if let Ok(home) = std::env::var("HOME") { + let asdf_shims_path = PathBuf::from(&home) + .join(".asdf") + .join("shims") + .join("claude"); + + debug!("Checking asdf shims directory: {:?}", asdf_shims_path); + + if asdf_shims_path.exists() && asdf_shims_path.is_file() { + let path_str = asdf_shims_path.to_string_lossy().to_string(); + + debug!("Found Claude in asdf shims: {}", path_str); + + // Get Claude version + let version = get_claude_version(&path_str).ok().flatten(); + + installations.push(ClaudeInstallation { + path: path_str, + version, + source: "asdf".to_string(), + installation_type: InstallationType::System, + }); + } + } + + installations +} + +#[cfg(windows)] +fn find_asdf_installations() -> Vec { + let mut installations = Vec::new(); + + // Check ASDF_DIR environment variable first + if let Ok(asdf_dir) = std::env::var("ASDF_DIR") { + let claude_path = PathBuf::from(&asdf_dir).join("shims").join("claude.exe"); + if claude_path.exists() && claude_path.is_file() { + debug!("Found Claude via ASDF_DIR: {:?}", claude_path); + let version = get_claude_version(&claude_path.to_string_lossy()) + .ok() + .flatten(); + installations.push(ClaudeInstallation { + path: claude_path.to_string_lossy().to_string(), + version, + source: "asdf".to_string(), + installation_type: InstallationType::System, + }); + } + } + + // Then check default location + if let Ok(user_profile) = std::env::var("USERPROFILE") { + let asdf_shims_path = PathBuf::from(&user_profile) + .join(".asdf") + .join("shims") + .join("claude.exe"); + + debug!("Checking asdf shims directory: {:?}", asdf_shims_path); + + if asdf_shims_path.exists() && asdf_shims_path.is_file() { + let path_str = asdf_shims_path.to_string_lossy().to_string(); + + debug!("Found Claude in asdf shims: {}", path_str); + + let version = get_claude_version(&path_str).ok().flatten(); + + installations.push(ClaudeInstallation { + path: path_str, + version, + source: "asdf".to_string(), + installation_type: InstallationType::System, + }); + } + } + + installations +} + /// Find Claude installations in NVM directories #[cfg(unix)] fn find_nvm_installations() -> Vec { @@ -390,6 +494,8 @@ fn find_standard_installations() -> Vec { format!("{}/.config/yarn/global/node_modules/.bin/claude", home), "yarn-global".to_string(), ), + // Check asdf shims directory + (format!("{}/.asdf/shims/claude", home), "asdf".to_string()), ]); } @@ -638,6 +744,10 @@ pub fn create_command_with_env(program: &str) -> Command { || key == "NVM_BIN" || key == "HOMEBREW_PREFIX" || key == "HOMEBREW_CELLAR" + // Add asdf environment variables + || key == "ASDF_DIR" + || key == "ASDF_DATA_DIR" + || key == "ASDF_CONFIG_FILE" // Add proxy environment variables (only uppercase) || key == "HTTP_PROXY" || key == "HTTPS_PROXY" @@ -689,5 +799,48 @@ pub fn create_command_with_env(program: &str) -> Command { } } + // Add asdf support if the program is in an asdf shims directory + // Also check if the program is a symlink pointing to asdf shims + let is_asdf_program = program.contains("/.asdf/shims/") + || program.contains("/asdf/shims/") + || std::fs::read_link(program) + .map(|target| target.to_string_lossy().contains("/.asdf/shims/")) + .unwrap_or(false); + + if is_asdf_program { + if let Ok(home) = std::env::var("HOME") { + let asdf_bin_dir = format!("{}/.asdf/bin", home); + let asdf_shims_dir = format!("{}/.asdf/shims", home); + let current_path = std::env::var("PATH").unwrap_or_default(); + + let mut new_path = current_path.clone(); + + // Add asdf bin directory if not already in PATH + if !current_path.contains(&asdf_bin_dir) { + new_path = format!("{}:{}", asdf_bin_dir, new_path); + debug!("Adding asdf bin directory to PATH: {}", asdf_bin_dir); + } + + // Add asdf shims directory if not already in PATH + if !current_path.contains(&asdf_shims_dir) { + new_path = format!("{}:{}", asdf_shims_dir, new_path); + debug!("Adding asdf shims directory to PATH: {}", asdf_shims_dir); + } + + if new_path != current_path { + cmd.env("PATH", new_path); + } + + // Set ASDF_DIR if not already set + if std::env::var("ASDF_DIR").is_err() { + let asdf_dir = format!("{}/.asdf", home); + if std::path::Path::new(&asdf_dir).exists() { + debug!("Setting ASDF_DIR to: {}", asdf_dir); + cmd.env("ASDF_DIR", asdf_dir); + } + } + } + } + cmd } From 2029cdaf25e96252ac46bbf0d54428850be53b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CFardeen?= Date: Sun, 11 Jan 2026 01:04:59 +0530 Subject: [PATCH 2/2] fix: eliminate duplicate asdf detection paths Prevent duplicate detections when ASDF_DIR points to default location: - Track checked paths in HashSet to skip duplicates - Only check default ~/.asdf if not already found via ASDF_DIR - Remove duplicate standard path check (already handled by find_asdf_installations) - Remove overly broad /asdf/shims/ substring check (keep /.asdf/shims/ only) Applies to both Unix and Windows implementations. --- src-tauri/src/claude_binary.rs | 79 +++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index ca47e932..f24874e5 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -259,17 +259,21 @@ fn try_which_command() -> Option { #[cfg(unix)] fn find_asdf_installations() -> Vec { let mut installations = Vec::new(); + let mut checked_paths = std::collections::HashSet::new(); // Check ASDF_DIR environment variable first if let Ok(asdf_dir) = std::env::var("ASDF_DIR") { let claude_path = PathBuf::from(&asdf_dir).join("shims").join("claude"); if claude_path.exists() && claude_path.is_file() { debug!("Found Claude via ASDF_DIR: {:?}", claude_path); - let version = get_claude_version(&claude_path.to_string_lossy()) + let path_str = claude_path.to_string_lossy().to_string(); + checked_paths.insert(path_str.clone()); + + let version = get_claude_version(&path_str) .ok() .flatten(); installations.push(ClaudeInstallation { - path: claude_path.to_string_lossy().to_string(), + path: path_str, version, source: "asdf".to_string(), installation_type: InstallationType::System, @@ -277,29 +281,32 @@ fn find_asdf_installations() -> Vec { } } - // Then check default ~/.asdf location + // Then check default ~/.asdf location (skip if already found via ASDF_DIR) if let Ok(home) = std::env::var("HOME") { let asdf_shims_path = PathBuf::from(&home) .join(".asdf") .join("shims") .join("claude"); - debug!("Checking asdf shims directory: {:?}", asdf_shims_path); + let path_str = asdf_shims_path.to_string_lossy().to_string(); + + // Skip if we already found this path via ASDF_DIR + if !checked_paths.contains(&path_str) { + debug!("Checking asdf shims directory: {:?}", asdf_shims_path); - if asdf_shims_path.exists() && asdf_shims_path.is_file() { - let path_str = asdf_shims_path.to_string_lossy().to_string(); + if asdf_shims_path.exists() && asdf_shims_path.is_file() { + debug!("Found Claude in asdf shims: {}", path_str); - debug!("Found Claude in asdf shims: {}", path_str); + // Get Claude version + let version = get_claude_version(&path_str).ok().flatten(); - // Get Claude version - let version = get_claude_version(&path_str).ok().flatten(); - - installations.push(ClaudeInstallation { - path: path_str, - version, - source: "asdf".to_string(), - installation_type: InstallationType::System, - }); + installations.push(ClaudeInstallation { + path: path_str, + version, + source: "asdf".to_string(), + installation_type: InstallationType::System, + }); + } } } @@ -309,17 +316,21 @@ fn find_asdf_installations() -> Vec { #[cfg(windows)] fn find_asdf_installations() -> Vec { let mut installations = Vec::new(); + let mut checked_paths = std::collections::HashSet::new(); // Check ASDF_DIR environment variable first if let Ok(asdf_dir) = std::env::var("ASDF_DIR") { let claude_path = PathBuf::from(&asdf_dir).join("shims").join("claude.exe"); if claude_path.exists() && claude_path.is_file() { debug!("Found Claude via ASDF_DIR: {:?}", claude_path); - let version = get_claude_version(&claude_path.to_string_lossy()) + let path_str = claude_path.to_string_lossy().to_string(); + checked_paths.insert(path_str.clone()); + + let version = get_claude_version(&path_str) .ok() .flatten(); installations.push(ClaudeInstallation { - path: claude_path.to_string_lossy().to_string(), + path: path_str, version, source: "asdf".to_string(), installation_type: InstallationType::System, @@ -327,28 +338,31 @@ fn find_asdf_installations() -> Vec { } } - // Then check default location + // Then check default location (skip if already found via ASDF_DIR) if let Ok(user_profile) = std::env::var("USERPROFILE") { let asdf_shims_path = PathBuf::from(&user_profile) .join(".asdf") .join("shims") .join("claude.exe"); - debug!("Checking asdf shims directory: {:?}", asdf_shims_path); + let path_str = asdf_shims_path.to_string_lossy().to_string(); + + // Skip if we already found this path via ASDF_DIR + if !checked_paths.contains(&path_str) { + debug!("Checking asdf shims directory: {:?}", asdf_shims_path); - if asdf_shims_path.exists() && asdf_shims_path.is_file() { - let path_str = asdf_shims_path.to_string_lossy().to_string(); + if asdf_shims_path.exists() && asdf_shims_path.is_file() { + debug!("Found Claude in asdf shims: {}", path_str); - debug!("Found Claude in asdf shims: {}", path_str); + let version = get_claude_version(&path_str).ok().flatten(); - let version = get_claude_version(&path_str).ok().flatten(); - - installations.push(ClaudeInstallation { - path: path_str, - version, - source: "asdf".to_string(), - installation_type: InstallationType::System, - }); + installations.push(ClaudeInstallation { + path: path_str, + version, + source: "asdf".to_string(), + installation_type: InstallationType::System, + }); + } } } @@ -494,8 +508,6 @@ fn find_standard_installations() -> Vec { format!("{}/.config/yarn/global/node_modules/.bin/claude", home), "yarn-global".to_string(), ), - // Check asdf shims directory - (format!("{}/.asdf/shims/claude", home), "asdf".to_string()), ]); } @@ -802,7 +814,6 @@ pub fn create_command_with_env(program: &str) -> Command { // Add asdf support if the program is in an asdf shims directory // Also check if the program is a symlink pointing to asdf shims let is_asdf_program = program.contains("/.asdf/shims/") - || program.contains("/asdf/shims/") || std::fs::read_link(program) .map(|target| target.to_string_lossy().contains("/.asdf/shims/")) .unwrap_or(false);