diff --git a/src-tauri/src/opencode_discovery.rs b/src-tauri/src/opencode_discovery.rs index 7e2c0e77..fb514cf8 100644 --- a/src-tauri/src/opencode_discovery.rs +++ b/src-tauri/src/opencode_discovery.rs @@ -40,13 +40,21 @@ fn version_for_binary(binary: &PathBuf) -> Option { } fn find_binary_on_path() -> Option { - crate::hidden_command("which") + crate::hidden_command(path_lookup_command()) .arg("opencode") .output() .ok() .and_then(|output| path_from_successful_output(&output)) } +fn path_lookup_command() -> &'static str { + if cfg!(windows) { + "where" + } else { + "which" + } +} + fn find_binary_in_user_shell() -> Option { user_shell_candidates() .into_iter() @@ -107,12 +115,26 @@ fn opencode_binary_candidates() -> Vec { fn opencode_binary_candidates_for_home(home: &Path) -> Vec { vec![ home.join(".local/bin/opencode"), + home.join(".local/bin/opencode.exe"), home.join(".opencode/bin/opencode"), + home.join(".opencode/bin/opencode.exe"), home.join(".local/share/mise/shims/opencode"), + home.join(".local/share/mise/shims/opencode.exe"), home.join(".asdf/shims/opencode"), + home.join(".asdf/shims/opencode.exe"), home.join(".npm-global/bin/opencode"), + home.join(".npm-global/bin/opencode.cmd"), + home.join(".npm-global/bin/opencode.exe"), home.join(".npm/bin/opencode"), + home.join(".npm/bin/opencode.cmd"), + home.join(".npm/bin/opencode.exe"), home.join(".bun/bin/opencode"), + home.join(".bun/bin/opencode.exe"), + home.join("AppData/Roaming/npm/opencode.cmd"), + home.join("AppData/Roaming/npm/opencode.exe"), + home.join("AppData/Local/pnpm/opencode.cmd"), + home.join("AppData/Local/pnpm/opencode.exe"), + home.join("scoop/shims/opencode.exe"), PathBuf::from("/usr/local/bin/opencode"), PathBuf::from("/opt/homebrew/bin/opencode"), ] @@ -145,6 +167,38 @@ mod tests { } } + #[test] + fn binary_candidates_include_windows_npm_and_toolchain_shims() { + let home = PathBuf::from(r"C:\Users\alex"); + let candidates = opencode_binary_candidates_for_home(&home); + let expected = [ + home.join(".npm-global/bin/opencode.cmd"), + home.join(".npm-global/bin/opencode.exe"), + home.join(".npm/bin/opencode.cmd"), + home.join(".npm/bin/opencode.exe"), + home.join("AppData/Roaming/npm/opencode.cmd"), + home.join("AppData/Roaming/npm/opencode.exe"), + home.join("AppData/Local/pnpm/opencode.cmd"), + home.join("AppData/Local/pnpm/opencode.exe"), + home.join("scoop/shims/opencode.exe"), + ]; + + for candidate in expected { + assert!( + candidates.contains(&candidate), + "missing {}", + candidate.display() + ); + } + } + + #[test] + fn path_lookup_command_matches_current_platform() { + let expected = if cfg!(windows) { "where" } else { "which" }; + + assert_eq!(path_lookup_command(), expected); + } + #[test] fn first_existing_path_skips_empty_and_missing_lines() { let dir = tempfile::tempdir().unwrap(); diff --git a/src-tauri/src/pi_discovery.rs b/src-tauri/src/pi_discovery.rs index 02d3b6c5..08fb6d15 100644 --- a/src-tauri/src/pi_discovery.rs +++ b/src-tauri/src/pi_discovery.rs @@ -40,13 +40,21 @@ fn version_for_binary(binary: &PathBuf) -> Option { } fn find_binary_on_path() -> Option { - crate::hidden_command("which") + crate::hidden_command(path_lookup_command()) .arg("pi") .output() .ok() .and_then(|output| path_from_successful_output(&output)) } +fn path_lookup_command() -> &'static str { + if cfg!(windows) { + "where" + } else { + "which" + } +} + fn find_binary_in_user_shell() -> Option { user_shell_candidates() .into_iter() @@ -113,12 +121,26 @@ fn pi_binary_candidates_for_home(home: &Path) -> Vec { let mut candidates = pi_nvm_binary_candidates_for_home(home); candidates.extend([ home.join(".local/bin/pi"), + home.join(".local/bin/pi.exe"), home.join(".pi/bin/pi"), + home.join(".pi/bin/pi.exe"), home.join(".local/share/mise/shims/pi"), + home.join(".local/share/mise/shims/pi.exe"), home.join(".asdf/shims/pi"), + home.join(".asdf/shims/pi.exe"), home.join(".npm-global/bin/pi"), + home.join(".npm-global/bin/pi.cmd"), + home.join(".npm-global/bin/pi.exe"), home.join(".npm/bin/pi"), + home.join(".npm/bin/pi.cmd"), + home.join(".npm/bin/pi.exe"), home.join(".bun/bin/pi"), + home.join(".bun/bin/pi.exe"), + home.join("AppData/Roaming/npm/pi.cmd"), + home.join("AppData/Roaming/npm/pi.exe"), + home.join("AppData/Local/pnpm/pi.cmd"), + home.join("AppData/Local/pnpm/pi.exe"), + home.join("scoop/shims/pi.exe"), ]); candidates } @@ -202,6 +224,38 @@ mod tests { } } + #[test] + fn binary_candidates_include_windows_npm_and_toolchain_shims() { + let home = PathBuf::from(r"C:\Users\alex"); + let candidates = pi_binary_candidates_for_home(&home); + let expected = [ + home.join(".npm-global/bin/pi.cmd"), + home.join(".npm-global/bin/pi.exe"), + home.join(".npm/bin/pi.cmd"), + home.join(".npm/bin/pi.exe"), + home.join("AppData/Roaming/npm/pi.cmd"), + home.join("AppData/Roaming/npm/pi.exe"), + home.join("AppData/Local/pnpm/pi.cmd"), + home.join("AppData/Local/pnpm/pi.exe"), + home.join("scoop/shims/pi.exe"), + ]; + + for candidate in expected { + assert!( + candidates.contains(&candidate), + "missing {}", + candidate.display() + ); + } + } + + #[test] + fn path_lookup_command_matches_current_platform() { + let expected = if cfg!(windows) { "where" } else { "which" }; + + assert_eq!(path_lookup_command(), expected); + } + #[test] fn binary_candidates_include_nvm_node_version_installs() { let home = tempfile::tempdir().unwrap();