From dfbb6a9b6486a639d9f87aca61b136d526c2509b Mon Sep 17 00:00:00 2001 From: Hikari Date: Sun, 8 Feb 2026 12:33:26 -0800 Subject: [PATCH] fix: dynamically resolve Claude binary path on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously hardcoded `/home/naomi/.local/bin/claude` which would break for other users. Now uses `wsl -e bash -l -c "which claude"` to find the Claude binary dynamically using a login shell that has the full PATH. Also updated 6 plugin/marketplace functions that were still using `std::process::Command::new("claude")` directly instead of the `create_claude_command()` helper: - enable_plugin - disable_plugin - update_plugin - list_marketplaces - add_marketplace - remove_marketplace This ensures all Claude CLI commands work properly on Windows regardless of where Claude is installed, whilst maintaining backwards compatibility. ✨ This fix was created by Hikari~ 🌸 --- src-tauri/src/commands.rs | 83 +++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4fbd7dd..1e5e2fc 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -55,14 +55,50 @@ fn wsl_path_to_windows(wsl_path: &str) -> Option { fn create_claude_command() -> std::process::Command { #[cfg(target_os = "windows")] { - let mut cmd = std::process::Command::new("wsl"); - cmd.arg("claude"); - cmd + // Use `which` inside WSL to find the claude binary dynamically + // Non-login shells launched by `wsl` don't inherit the full user PATH, + // so we need to use a login shell to get the correct PATH + let which_output = std::process::Command::new("wsl") + .args(["-e", "bash", "-l", "-c", "which claude"]) + .output(); + + match which_output { + Ok(output) if output.status.success() => { + let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let mut cmd = std::process::Command::new("wsl"); + cmd.arg(claude_path); + cmd + } + _ => { + // Fallback to just "claude" if which fails + // This maintains backwards compatibility + let mut cmd = std::process::Command::new("wsl"); + cmd.arg("claude"); + cmd + } + } } #[cfg(not(target_os = "windows"))] { - std::process::Command::new("claude") + // Use `which` to find the claude binary dynamically + // This works regardless of how Claude Code was installed (standalone, npm, etc.) + // and avoids hardcoding paths + let which_output = std::process::Command::new("which") + .arg("claude") + .output(); + + match which_output { + Ok(output) if output.status.success() => { + let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + std::process::Command::new(claude_path) + } + _ => { + // Fallback to just "claude" if which fails + // This maintains backwards compatibility + std::process::Command::new("claude") + } + } } } @@ -1427,7 +1463,7 @@ pub async fn uninstall_plugin(plugin_name: String) -> Result { pub async fn enable_plugin(plugin_name: String) -> Result { tracing::debug!("Enabling plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("enable") .arg(&plugin_name) @@ -1456,7 +1492,7 @@ pub async fn enable_plugin(plugin_name: String) -> Result { pub async fn disable_plugin(plugin_name: String) -> Result { tracing::debug!("Disabling plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("disable") .arg(&plugin_name) @@ -1485,7 +1521,7 @@ pub async fn disable_plugin(plugin_name: String) -> Result { pub async fn update_plugin(plugin_name: String) -> Result { tracing::debug!("Updating plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("update") .arg(&plugin_name) @@ -1557,7 +1593,7 @@ fn parse_marketplace_list(stdout: &str) -> Vec { pub async fn list_marketplaces() -> Result, String> { tracing::debug!("Listing plugin marketplaces"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("marketplace") .arg("list") @@ -1590,7 +1626,7 @@ pub async fn list_marketplaces() -> Result, String> { pub async fn add_marketplace(source: String) -> Result { tracing::debug!("Adding marketplace: {}", source); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("marketplace") .arg("add") @@ -1623,7 +1659,7 @@ pub async fn add_marketplace(source: String) -> Result { pub async fn remove_marketplace(name: String) -> Result { tracing::debug!("Removing marketplace: {}", name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("marketplace") .arg("remove") @@ -1930,21 +1966,42 @@ mod tests { #[test] #[cfg(target_os = "windows")] fn test_create_claude_command_windows() { - // On Windows, should create a command that uses wsl as the program with claude as first arg + // On Windows, should create a command that uses wsl with full path to claude + // The path is resolved dynamically via `which` in a login shell let cmd = create_claude_command(); let program = cmd.get_program(); assert_eq!(program, "wsl"); + + // Verify the first argument is a path to claude (full path from `which`) + // or fallback to just "claude" if which fails + let args: Vec<&std::ffi::OsStr> = cmd.get_args().collect(); + assert_eq!(args.len(), 1); + + let arg_str = args[0].to_string_lossy(); + assert!( + arg_str.contains("claude"), + "Expected argument to contain 'claude', got: {}", + arg_str + ); } #[test] #[cfg(not(target_os = "windows"))] fn test_create_claude_command_linux() { - // On Linux/Mac, should create a command that uses claude directly + // On Linux/Mac, should create a command that uses the full path to claude + // (resolved via `which` command) let cmd = create_claude_command(); let program = cmd.get_program(); - assert_eq!(program, "claude"); + // The program should be the full path to claude (from `which`) + // or fallback to "claude" if which fails + let program_str = program.to_string_lossy(); + assert!( + program_str.ends_with("claude"), + "Expected program to end with 'claude', got: {}", + program_str + ); } // ==================== validate_directory tests ====================