@@ -74,6 +74,10 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
74
74
75
75
if installations. is_empty ( ) {
76
76
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" ) ) ]
77
81
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 ( ) ) ;
78
82
}
79
83
@@ -164,55 +168,106 @@ fn discover_system_installations() -> Vec<ClaudeInstallation> {
164
168
installations
165
169
}
166
170
167
- /// Try using the 'which' command to find Claude
171
+ /// Try using the 'which' command (Unix) or 'where' command (Windows) to find Claude
168
172
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
+ }
170
184
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 ( ) ;
174
187
175
- if output_str. is_empty ( ) {
176
- return None ;
177
- }
188
+ debug ! ( "'where' found claude at: {}" , path) ;
178
189
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
+ }
188
195
189
- debug ! ( "'which' found claude at: {}" , path) ;
196
+ // Get version
197
+ let version = get_claude_version ( & path) . ok ( ) . flatten ( ) ;
190
198
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
+ } )
195
205
}
206
+ _ => None ,
207
+ }
208
+ }
209
+
210
+ #[ cfg( not( target_os = "windows" ) ) ]
211
+ {
212
+ debug ! ( "Trying 'which claude' to find binary..." ) ;
196
213
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 ( ) ;
199
217
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 ,
206
251
}
207
- _ => None ,
208
252
}
209
253
}
210
254
211
255
/// Find Claude installations in NVM directories
212
256
fn find_nvm_installations ( ) -> Vec < ClaudeInstallation > {
213
257
let mut installations = Vec :: new ( ) ;
214
258
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 {
216
271
let nvm_dir = PathBuf :: from ( & home)
217
272
. join ( ".nvm" )
218
273
. join ( "versions" )
@@ -254,45 +309,134 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
254
309
let mut installations = Vec :: new ( ) ;
255
310
256
311
// 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
+ {
269
317
paths_to_check. extend ( vec ! [
318
+ ( "/usr/local/bin/claude" . to_string( ) , "system" . to_string( ) ) ,
270
319
(
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( ) ,
293
322
) ,
323
+ ( "/usr/bin/claude" . to_string( ) , "system" . to_string( ) ) ,
324
+ ( "/bin/claude" . to_string( ) , "system" . to_string( ) ) ,
294
325
] ) ;
295
326
}
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
+ }
296
440
297
441
// Check each path
298
442
for ( path, source) in paths_to_check {
@@ -313,17 +457,42 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
313
457
}
314
458
315
459
// 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
+ }
327
496
}
328
497
}
329
498
0 commit comments