generated from nhcarrigan/template
fix: dynamically resolve Claude binary path on Windows
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~ 🌸
This commit is contained in:
+70
-13
@@ -55,14 +55,50 @@ fn wsl_path_to_windows(wsl_path: &str) -> Option<String> {
|
||||
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<String, String> {
|
||||
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
||||
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<MarketplaceInfo> {
|
||||
pub async fn list_marketplaces() -> Result<Vec<MarketplaceInfo>, 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<Vec<MarketplaceInfo>, String> {
|
||||
pub async fn add_marketplace(source: String) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
pub async fn remove_marketplace(name: String) -> Result<String, String> {
|
||||
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 ====================
|
||||
|
||||
Reference in New Issue
Block a user