Skip to content

Commit 4bdbae9

Browse files
committed
fix: improve Claude installation detection for Windows
- Add Windows-specific home directory detection using USERPROFILE - Support Windows paths including %USERPROFILE%\.claude - Use 'where' command instead of 'which' on Windows - Add comprehensive Windows installation paths checking - Support .exe, .cmd, and .bat file extensions on Windows - Update error messages with platform-specific paths Fixes issue where Claude installation at C:\Users\<username>\.claude was not detected on Windows systems.
1 parent d9859ac commit 4bdbae9

File tree

1 file changed

+246
-77
lines changed

1 file changed

+246
-77
lines changed

src-tauri/src/claude_binary.rs

Lines changed: 246 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
7474

7575
if installations.is_empty() {
7676
error!("Could not find claude binary in any location");
77+
#[cfg(target_os = "windows")]
78+
return Err("Claude Code not found. Please ensure it's installed in one of these locations: PATH, %USERPROFILE%\\.claude, %LOCALAPPDATA%\\claude, %ProgramFiles%\\claude, or %USERPROFILE%\\scoop\\apps\\claude".to_string());
79+
80+
#[cfg(not(target_os = "windows"))]
7781
return Err("Claude Code not found. Please ensure it's installed in one of these locations: PATH, /usr/local/bin, /opt/homebrew/bin, ~/.nvm/versions/node/*/bin, ~/.claude/local, ~/.local/bin".to_string());
7882
}
7983

@@ -164,55 +168,106 @@ fn discover_system_installations() -> Vec<ClaudeInstallation> {
164168
installations
165169
}
166170

167-
/// Try using the 'which' command to find Claude
171+
/// Try using the 'which' command (Unix) or 'where' command (Windows) to find Claude
168172
fn try_which_command() -> Option<ClaudeInstallation> {
169-
debug!("Trying 'which claude' to find binary...");
173+
#[cfg(target_os = "windows")]
174+
{
175+
debug!("Trying 'where claude' to find binary...");
176+
177+
match Command::new("where").arg("claude").output() {
178+
Ok(output) if output.status.success() => {
179+
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
180+
181+
if output_str.is_empty() {
182+
return None;
183+
}
170184

171-
match Command::new("which").arg("claude").output() {
172-
Ok(output) if output.status.success() => {
173-
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
185+
// 'where' can return multiple paths, one per line - take the first one
186+
let path = output_str.lines().next()?.to_string();
174187

175-
if output_str.is_empty() {
176-
return None;
177-
}
188+
debug!("'where' found claude at: {}", path);
178189

179-
// Parse aliased output: "claude: aliased to /path/to/claude"
180-
let path = if output_str.starts_with("claude:") && output_str.contains("aliased to") {
181-
output_str
182-
.split("aliased to")
183-
.nth(1)
184-
.map(|s| s.trim().to_string())
185-
} else {
186-
Some(output_str)
187-
}?;
190+
// Verify the path exists
191+
if !PathBuf::from(&path).exists() {
192+
warn!("Path from 'where' does not exist: {}", path);
193+
return None;
194+
}
188195

189-
debug!("'which' found claude at: {}", path);
196+
// Get version
197+
let version = get_claude_version(&path).ok().flatten();
190198

191-
// Verify the path exists
192-
if !PathBuf::from(&path).exists() {
193-
warn!("Path from 'which' does not exist: {}", path);
194-
return None;
199+
Some(ClaudeInstallation {
200+
path,
201+
version,
202+
source: "where".to_string(),
203+
installation_type: InstallationType::System,
204+
})
195205
}
206+
_ => None,
207+
}
208+
}
209+
210+
#[cfg(not(target_os = "windows"))]
211+
{
212+
debug!("Trying 'which claude' to find binary...");
196213

197-
// Get version
198-
let version = get_claude_version(&path).ok().flatten();
214+
match Command::new("which").arg("claude").output() {
215+
Ok(output) if output.status.success() => {
216+
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
199217

200-
Some(ClaudeInstallation {
201-
path,
202-
version,
203-
source: "which".to_string(),
204-
installation_type: InstallationType::System,
205-
})
218+
if output_str.is_empty() {
219+
return None;
220+
}
221+
222+
// Parse aliased output: "claude: aliased to /path/to/claude"
223+
let path = if output_str.starts_with("claude:") && output_str.contains("aliased to") {
224+
output_str
225+
.split("aliased to")
226+
.nth(1)
227+
.map(|s| s.trim().to_string())
228+
} else {
229+
Some(output_str)
230+
}?;
231+
232+
debug!("'which' found claude at: {}", path);
233+
234+
// Verify the path exists
235+
if !PathBuf::from(&path).exists() {
236+
warn!("Path from 'which' does not exist: {}", path);
237+
return None;
238+
}
239+
240+
// Get version
241+
let version = get_claude_version(&path).ok().flatten();
242+
243+
Some(ClaudeInstallation {
244+
path,
245+
version,
246+
source: "which".to_string(),
247+
installation_type: InstallationType::System,
248+
})
249+
}
250+
_ => None,
206251
}
207-
_ => None,
208252
}
209253
}
210254

211255
/// Find Claude installations in NVM directories
212256
fn find_nvm_installations() -> Vec<ClaudeInstallation> {
213257
let mut installations = Vec::new();
214258

215-
if let Ok(home) = std::env::var("HOME") {
259+
// Get home directory - works on both Unix and Windows
260+
let home = std::env::var("HOME")
261+
.or_else(|_| std::env::var("USERPROFILE"))
262+
.or_else(|_| {
263+
// Fallback for Windows: combine HOMEDRIVE and HOMEPATH
264+
match (std::env::var("HOMEDRIVE"), std::env::var("HOMEPATH")) {
265+
(Ok(drive), Ok(path)) => Ok(format!("{}{}", drive, path)),
266+
_ => Err(std::env::VarError::NotPresent)
267+
}
268+
});
269+
270+
if let Ok(home) = home {
216271
let nvm_dir = PathBuf::from(&home)
217272
.join(".nvm")
218273
.join("versions")
@@ -254,45 +309,134 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
254309
let mut installations = Vec::new();
255310

256311
// Common installation paths for claude
257-
let mut paths_to_check: Vec<(String, String)> = vec![
258-
("/usr/local/bin/claude".to_string(), "system".to_string()),
259-
(
260-
"/opt/homebrew/bin/claude".to_string(),
261-
"homebrew".to_string(),
262-
),
263-
("/usr/bin/claude".to_string(), "system".to_string()),
264-
("/bin/claude".to_string(), "system".to_string()),
265-
];
266-
267-
// Also check user-specific paths
268-
if let Ok(home) = std::env::var("HOME") {
312+
let mut paths_to_check: Vec<(String, String)> = vec![];
313+
314+
// Unix/Linux/macOS paths
315+
#[cfg(not(target_os = "windows"))]
316+
{
269317
paths_to_check.extend(vec![
318+
("/usr/local/bin/claude".to_string(), "system".to_string()),
270319
(
271-
format!("{}/.claude/local/claude", home),
272-
"claude-local".to_string(),
273-
),
274-
(
275-
format!("{}/.local/bin/claude", home),
276-
"local-bin".to_string(),
277-
),
278-
(
279-
format!("{}/.npm-global/bin/claude", home),
280-
"npm-global".to_string(),
281-
),
282-
(format!("{}/.yarn/bin/claude", home), "yarn".to_string()),
283-
(format!("{}/.bun/bin/claude", home), "bun".to_string()),
284-
(format!("{}/bin/claude", home), "home-bin".to_string()),
285-
// Check common node_modules locations
286-
(
287-
format!("{}/node_modules/.bin/claude", home),
288-
"node-modules".to_string(),
289-
),
290-
(
291-
format!("{}/.config/yarn/global/node_modules/.bin/claude", home),
292-
"yarn-global".to_string(),
320+
"/opt/homebrew/bin/claude".to_string(),
321+
"homebrew".to_string(),
293322
),
323+
("/usr/bin/claude".to_string(), "system".to_string()),
324+
("/bin/claude".to_string(), "system".to_string()),
294325
]);
295326
}
327+
328+
// Windows-specific paths
329+
#[cfg(target_os = "windows")]
330+
{
331+
// Check Program Files locations
332+
if let Ok(program_files) = std::env::var("ProgramFiles") {
333+
paths_to_check.push((
334+
format!("{}\\claude\\claude.exe", program_files),
335+
"program-files".to_string(),
336+
));
337+
}
338+
if let Ok(program_files_x86) = std::env::var("ProgramFiles(x86)") {
339+
paths_to_check.push((
340+
format!("{}\\claude\\claude.exe", program_files_x86),
341+
"program-files-x86".to_string(),
342+
));
343+
}
344+
// Check LocalAppData
345+
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
346+
paths_to_check.push((
347+
format!("{}\\claude\\claude.exe", local_app_data),
348+
"local-app-data".to_string(),
349+
));
350+
}
351+
}
352+
353+
// Get home directory - works on both Unix and Windows
354+
let home = std::env::var("HOME")
355+
.or_else(|_| std::env::var("USERPROFILE"))
356+
.or_else(|_| {
357+
// Fallback for Windows: combine HOMEDRIVE and HOMEPATH
358+
match (std::env::var("HOMEDRIVE"), std::env::var("HOMEPATH")) {
359+
(Ok(drive), Ok(path)) => Ok(format!("{}{}", drive, path)),
360+
_ => Err(std::env::VarError::NotPresent)
361+
}
362+
});
363+
364+
// Also check user-specific paths
365+
if let Ok(home) = home {
366+
// Platform-specific path separator and executable extension
367+
#[cfg(target_os = "windows")]
368+
{
369+
paths_to_check.extend(vec![
370+
(
371+
format!("{}\\.claude\\claude.exe", home),
372+
"claude-home".to_string(),
373+
),
374+
(
375+
format!("{}\\.claude\\bin\\claude.exe", home),
376+
"claude-home-bin".to_string(),
377+
),
378+
(
379+
format!("{}\\.claude\\local\\claude.exe", home),
380+
"claude-local".to_string(),
381+
),
382+
(
383+
format!("{}\\AppData\\Local\\claude\\claude.exe", home),
384+
"app-data-local".to_string(),
385+
),
386+
(
387+
format!("{}\\AppData\\Roaming\\claude\\claude.exe", home),
388+
"app-data-roaming".to_string(),
389+
),
390+
(
391+
format!("{}\\scoop\\apps\\claude\\current\\claude.exe", home),
392+
"scoop".to_string(),
393+
),
394+
// Also check without .exe extension for cross-platform scripts
395+
(
396+
format!("{}\\.claude\\claude", home),
397+
"claude-home".to_string(),
398+
),
399+
(
400+
format!("{}\\.claude\\bin\\claude", home),
401+
"claude-home-bin".to_string(),
402+
),
403+
(
404+
format!("{}\\.claude\\local\\claude", home),
405+
"claude-local".to_string(),
406+
),
407+
]);
408+
}
409+
410+
#[cfg(not(target_os = "windows"))]
411+
{
412+
paths_to_check.extend(vec![
413+
(
414+
format!("{}/.claude/local/claude", home),
415+
"claude-local".to_string(),
416+
),
417+
(
418+
format!("{}/.local/bin/claude", home),
419+
"local-bin".to_string(),
420+
),
421+
(
422+
format!("{}/.npm-global/bin/claude", home),
423+
"npm-global".to_string(),
424+
),
425+
(format!("{}/.yarn/bin/claude", home), "yarn".to_string()),
426+
(format!("{}/.bun/bin/claude", home), "bun".to_string()),
427+
(format!("{}/bin/claude", home), "home-bin".to_string()),
428+
// Check common node_modules locations
429+
(
430+
format!("{}/node_modules/.bin/claude", home),
431+
"node-modules".to_string(),
432+
),
433+
(
434+
format!("{}/.config/yarn/global/node_modules/.bin/claude", home),
435+
"yarn-global".to_string(),
436+
),
437+
]);
438+
}
439+
}
296440

297441
// Check each path
298442
for (path, source) in paths_to_check {
@@ -313,17 +457,42 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
313457
}
314458

315459
// Also check if claude is available in PATH (without full path)
316-
if let Ok(output) = Command::new("claude").arg("--version").output() {
317-
if output.status.success() {
318-
debug!("claude is available in PATH");
319-
let version = extract_version_from_output(&output.stdout);
320-
321-
installations.push(ClaudeInstallation {
322-
path: "claude".to_string(),
323-
version,
324-
source: "PATH".to_string(),
325-
installation_type: InstallationType::System,
326-
});
460+
// On Windows, we might need to try both 'claude' and 'claude.exe'
461+
#[cfg(target_os = "windows")]
462+
{
463+
let commands_to_try = vec!["claude", "claude.exe", "claude.cmd", "claude.bat"];
464+
for cmd in commands_to_try {
465+
if let Ok(output) = Command::new(cmd).arg("--version").output() {
466+
if output.status.success() {
467+
debug!("{} is available in PATH", cmd);
468+
let version = extract_version_from_output(&output.stdout);
469+
470+
installations.push(ClaudeInstallation {
471+
path: cmd.to_string(),
472+
version,
473+
source: "PATH".to_string(),
474+
installation_type: InstallationType::System,
475+
});
476+
break; // Only add once if found
477+
}
478+
}
479+
}
480+
}
481+
482+
#[cfg(not(target_os = "windows"))]
483+
{
484+
if let Ok(output) = Command::new("claude").arg("--version").output() {
485+
if output.status.success() {
486+
debug!("claude is available in PATH");
487+
let version = extract_version_from_output(&output.stdout);
488+
489+
installations.push(ClaudeInstallation {
490+
path: "claude".to_string(),
491+
version,
492+
source: "PATH".to_string(),
493+
installation_type: InstallationType::System,
494+
});
495+
}
327496
}
328497
}
329498

0 commit comments

Comments
 (0)