generated from nhcarrigan/template
fix: execute Claude CLI commands through WSL on Windows (#139)
Resolves #137 ## Summary Claude CLI commands (plugin list, MCP list, version check, etc.) were being executed directly in Windows context where the `claude` binary doesn't exist, causing "program not found" errors across the UI. This PR adds a helper function that automatically prefixes commands with `wsl` on Windows builds, ensuring all Claude CLI commands execute in the correct context. ## Changes - **Added `create_claude_command()` helper function** that: - On Windows: Creates command with `wsl claude` prefix - On Linux/Mac: Creates command with `claude` directly - **Updated 8 command functions** to use the helper: - `get_claude_version` - `list_plugins` - `install_plugin` - `uninstall_plugin` - `list_mcp_servers` - `remove_mcp_server` - `add_mcp_server` - `get_mcp_server_details` - **Added comprehensive tests** for both Windows and Linux contexts ## What This Fixes ✅ Memory pane will now display files correctly ✅ CLI version will be detected properly ✅ Plugin pane will work correctly ✅ MCP servers pane will function properly ✅ All Claude CLI commands will execute in the correct context on Windows ## Testing - ✅ All 427 backend tests pass (added 1 new test) - ✅ All 387 frontend tests pass - ✅ All linting and formatting checks pass - ✅ `check-all.sh` reports: "✨ All checks passed!" ✨ This fix was created by Hikari~ 🌸 Reviewed-on: #139 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #139.
This commit is contained in:
+110
-14
@@ -49,6 +49,59 @@ fn wsl_path_to_windows(wsl_path: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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]
|
#[tauri::command]
|
||||||
pub async fn start_claude(
|
pub async fn start_claude(
|
||||||
bridge_manager: State<'_, SharedBridgeManager>,
|
bridge_manager: State<'_, SharedBridgeManager>,
|
||||||
@@ -1233,7 +1286,7 @@ pub async fn list_memory_files() -> Result<MemoryFilesResponse, String> {
|
|||||||
pub async fn get_claude_version() -> Result<String, String> {
|
pub async fn get_claude_version() -> Result<String, String> {
|
||||||
tracing::debug!("Getting Claude CLI version");
|
tracing::debug!("Getting Claude CLI version");
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
@@ -1323,7 +1376,7 @@ fn parse_plugin_list(stdout: &str) -> Vec<PluginInfo> {
|
|||||||
pub async fn list_plugins() -> Result<Vec<PluginInfo>, String> {
|
pub async fn list_plugins() -> Result<Vec<PluginInfo>, String> {
|
||||||
tracing::debug!("Listing Claude Code plugins");
|
tracing::debug!("Listing Claude Code plugins");
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
.output();
|
.output();
|
||||||
@@ -1352,7 +1405,7 @@ pub async fn list_plugins() -> Result<Vec<PluginInfo>, String> {
|
|||||||
pub async fn install_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn install_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Installing plugin: {}", plugin_name);
|
tracing::debug!("Installing plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("install")
|
.arg("install")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1381,7 +1434,7 @@ pub async fn install_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
pub async fn uninstall_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn uninstall_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Uninstalling plugin: {}", plugin_name);
|
tracing::debug!("Uninstalling plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("uninstall")
|
.arg("uninstall")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1410,7 +1463,7 @@ pub async fn uninstall_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Enabling plugin: {}", plugin_name);
|
tracing::debug!("Enabling plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("enable")
|
.arg("enable")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1439,7 +1492,7 @@ pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Disabling plugin: {}", plugin_name);
|
tracing::debug!("Disabling plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("disable")
|
.arg("disable")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1468,7 +1521,7 @@ pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
|||||||
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Updating plugin: {}", plugin_name);
|
tracing::debug!("Updating plugin: {}", plugin_name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("update")
|
.arg("update")
|
||||||
.arg(&plugin_name)
|
.arg(&plugin_name)
|
||||||
@@ -1540,7 +1593,7 @@ fn parse_marketplace_list(stdout: &str) -> Vec<MarketplaceInfo> {
|
|||||||
pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, String> {
|
pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, String> {
|
||||||
tracing::debug!("Listing plugin marketplaces");
|
tracing::debug!("Listing plugin marketplaces");
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("marketplace")
|
.arg("marketplace")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
@@ -1573,7 +1626,7 @@ pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, String> {
|
|||||||
pub async fn add_marketplace(source: String) -> Result<String, String> {
|
pub async fn add_marketplace(source: String) -> Result<String, String> {
|
||||||
tracing::debug!("Adding marketplace: {}", source);
|
tracing::debug!("Adding marketplace: {}", source);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("marketplace")
|
.arg("marketplace")
|
||||||
.arg("add")
|
.arg("add")
|
||||||
@@ -1606,7 +1659,7 @@ pub async fn add_marketplace(source: String) -> Result<String, String> {
|
|||||||
pub async fn remove_marketplace(name: String) -> Result<String, String> {
|
pub async fn remove_marketplace(name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Removing marketplace: {}", name);
|
tracing::debug!("Removing marketplace: {}", name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("plugin")
|
.arg("plugin")
|
||||||
.arg("marketplace")
|
.arg("marketplace")
|
||||||
.arg("remove")
|
.arg("remove")
|
||||||
@@ -1746,7 +1799,7 @@ fn parse_mcp_server_list(stdout: &str) -> Vec<McpServerInfo> {
|
|||||||
pub async fn list_mcp_servers() -> Result<Vec<McpServerInfo>, String> {
|
pub async fn list_mcp_servers() -> Result<Vec<McpServerInfo>, String> {
|
||||||
tracing::debug!("Listing MCP servers");
|
tracing::debug!("Listing MCP servers");
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("mcp")
|
.arg("mcp")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
.output();
|
.output();
|
||||||
@@ -1788,7 +1841,7 @@ pub async fn get_mcp_server(name: String) -> Result<McpServerInfo, String> {
|
|||||||
pub async fn remove_mcp_server(name: String) -> Result<String, String> {
|
pub async fn remove_mcp_server(name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Removing MCP server: {}", name);
|
tracing::debug!("Removing MCP server: {}", name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("mcp")
|
.arg("mcp")
|
||||||
.arg("remove")
|
.arg("remove")
|
||||||
.arg(&name)
|
.arg(&name)
|
||||||
@@ -1823,7 +1876,7 @@ pub async fn add_mcp_server(
|
|||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
tracing::debug!("Adding MCP server: {} with transport {}", name, transport);
|
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");
|
cmd.arg("mcp").arg("add");
|
||||||
|
|
||||||
// Add transport flag
|
// Add transport flag
|
||||||
@@ -1871,7 +1924,7 @@ pub async fn add_mcp_server(
|
|||||||
pub async fn get_mcp_server_details(name: String) -> Result<String, String> {
|
pub async fn get_mcp_server_details(name: String) -> Result<String, String> {
|
||||||
tracing::debug!("Getting detailed info for MCP server: {}", name);
|
tracing::debug!("Getting detailed info for MCP server: {}", name);
|
||||||
|
|
||||||
let output = std::process::Command::new("claude")
|
let output = create_claude_command()
|
||||||
.arg("mcp")
|
.arg("mcp")
|
||||||
.arg("get")
|
.arg("get")
|
||||||
.arg(&name)
|
.arg(&name)
|
||||||
@@ -1908,6 +1961,49 @@ mod tests {
|
|||||||
tokio::runtime::Runtime::new().unwrap().block_on(f)
|
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 ====================
|
// ==================== validate_directory tests ====================
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user