diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index e53f834..1e5e2fc 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -49,6 +49,59 @@ fn wsl_path_to_windows(wsl_path: &str) -> Option { } } +/// Create a Command instance for executing Claude CLI commands +/// On Windows, this will use WSL to execute the command +/// On other platforms, it executes directly +fn create_claude_command() -> std::process::Command { + #[cfg(target_os = "windows")] + { + // 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"))] + { + // 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") + } + } + } +} + #[tauri::command] pub async fn start_claude( bridge_manager: State<'_, SharedBridgeManager>, @@ -1233,7 +1286,7 @@ pub async fn list_memory_files() -> Result { pub async fn get_claude_version() -> Result { tracing::debug!("Getting Claude CLI version"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("--version") .output(); @@ -1323,7 +1376,7 @@ fn parse_plugin_list(stdout: &str) -> Vec { pub async fn list_plugins() -> Result, String> { tracing::debug!("Listing Claude Code plugins"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("list") .output(); @@ -1352,7 +1405,7 @@ pub async fn list_plugins() -> Result, String> { pub async fn install_plugin(plugin_name: String) -> Result { tracing::debug!("Installing plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("install") .arg(&plugin_name) @@ -1381,7 +1434,7 @@ pub async fn install_plugin(plugin_name: String) -> Result { pub async fn uninstall_plugin(plugin_name: String) -> Result { tracing::debug!("Uninstalling plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("uninstall") .arg(&plugin_name) @@ -1410,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) @@ -1439,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) @@ -1468,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) @@ -1540,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") @@ -1573,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") @@ -1606,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") @@ -1746,7 +1799,7 @@ fn parse_mcp_server_list(stdout: &str) -> Vec { pub async fn list_mcp_servers() -> Result, String> { tracing::debug!("Listing MCP servers"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("mcp") .arg("list") .output(); @@ -1788,7 +1841,7 @@ pub async fn get_mcp_server(name: String) -> Result { pub async fn remove_mcp_server(name: String) -> Result { tracing::debug!("Removing MCP server: {}", name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("mcp") .arg("remove") .arg(&name) @@ -1823,7 +1876,7 @@ pub async fn add_mcp_server( ) -> Result { tracing::debug!("Adding MCP server: {} with transport {}", name, transport); - let mut cmd = std::process::Command::new("claude"); + let mut cmd = create_claude_command(); cmd.arg("mcp").arg("add"); // Add transport flag @@ -1871,7 +1924,7 @@ pub async fn add_mcp_server( pub async fn get_mcp_server_details(name: String) -> Result { tracing::debug!("Getting detailed info for MCP server: {}", name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("mcp") .arg("get") .arg(&name) @@ -1908,6 +1961,49 @@ mod tests { tokio::runtime::Runtime::new().unwrap().block_on(f) } + // ==================== create_claude_command tests ==================== + + #[test] + #[cfg(target_os = "windows")] + fn test_create_claude_command_windows() { + // 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 the full path to claude + // (resolved via `which` command) + let cmd = create_claude_command(); + let program = cmd.get_program(); + + // 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 ==================== #[test]