From e4083ded1af521e2c6ea1e6bc6138b4a8c5b16df Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 26 Apr 2026 07:34:27 +0200 Subject: [PATCH] fix: detect claude cli on windows --- docs/ABSTRACTIONS.md | 2 +- docs/ARCHITECTURE.md | 2 +- src-tauri/src/claude_cli.rs | 69 ++++++++++++++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index ac24d174..2ec8af55 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -635,7 +635,7 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte `useAiAgentsOnboarding(enabled)` adds a separate first-launch agent step: - Reads a local dismissal flag for the AI agents prompt (with a legacy fallback to the older Claude-only key) - Only shows after vault onboarding has already resolved to a ready state -- Uses `get_ai_agents_status`, whose backend treats the app process path, login-shell path, and supported local/toolchain/app install locations as valid CLI-agent sources +- Uses `get_ai_agents_status`, whose backend treats the app process path, login-shell path, and supported local/toolchain/app install locations, including Windows `.exe` and npm/pnpm/Scoop shim paths, as valid CLI-agent sources - Persists dismissal locally once the user continues ### Remote Git Operations diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f9540b61..9efe907e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -217,7 +217,7 @@ Full agent mode — spawns the selected local CLI agent as a subprocess with too 3. **Agent adapters** — Claude Code still uses `claude_cli.rs`; Codex runs through `codex exec --json` with the CLI's normal approval / sandbox defaults 4. **MCP Integration** — Claude receives the generated MCP config file path, while Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides -CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, npm-global, Homebrew, and the macOS Codex app resource path so first-run onboarding works on fresh macOS installs. +CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs. #### Agent Event Flow diff --git a/src-tauri/src/claude_cli.rs b/src-tauri/src/claude_cli.rs index a670bf00..a987c0c8 100644 --- a/src-tauri/src/claude_cli.rs +++ b/src-tauri/src/claude_cli.rs @@ -63,7 +63,7 @@ pub struct AgentStreamRequest { // --------------------------------------------------------------------------- pub(crate) fn find_claude_binary() -> Result { - if let Some(binary) = find_claude_binary_on_path()? { + if let Some(binary) = find_claude_binary_on_path() { return Ok(binary); } @@ -78,13 +78,20 @@ pub(crate) fn find_claude_binary() -> Result { Err("Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code".into()) } -fn find_claude_binary_on_path() -> Result, String> { - let output = Command::new("which") +fn find_claude_binary_on_path() -> Option { + Command::new(claude_path_lookup_command()) .arg("claude") .output() - .map_err(|e| format!("Failed to run `which claude`: {e}"))?; + .ok() + .and_then(|output| path_from_successful_output(&output)) +} - Ok(path_from_successful_output(&output)) +fn claude_path_lookup_command() -> &'static str { + if cfg!(windows) { + "where" + } else { + "which" + } } fn find_claude_binary_in_user_shell() -> Option { @@ -143,11 +150,24 @@ fn claude_binary_candidates() -> Vec { fn claude_binary_candidates_for_home(home: &Path) -> Vec { vec![ home.join(".local/bin/claude"), + home.join(".local/bin/claude.exe"), home.join(".claude/local/claude"), + home.join(".claude/local/claude.exe"), home.join(".local/share/mise/shims/claude"), + home.join(".local/share/mise/shims/claude.exe"), home.join(".asdf/shims/claude"), + home.join(".asdf/shims/claude.exe"), home.join(".npm-global/bin/claude"), + home.join(".npm-global/bin/claude.cmd"), + home.join(".npm-global/bin/claude.exe"), home.join(".npm/bin/claude"), + home.join(".npm/bin/claude.cmd"), + home.join(".npm/bin/claude.exe"), + home.join("AppData/Roaming/npm/claude.cmd"), + home.join("AppData/Roaming/npm/claude.exe"), + home.join("AppData/Local/pnpm/claude.cmd"), + home.join("AppData/Local/pnpm/claude.exe"), + home.join("scoop/shims/claude.exe"), PathBuf::from("/opt/homebrew/bin/claude"), PathBuf::from("/usr/local/bin/claude"), ] @@ -1099,6 +1119,45 @@ mod tests { } } + #[test] + fn claude_binary_candidates_include_windows_exe_installs() { + let home = PathBuf::from(r"C:\Users\alex"); + let candidates = claude_binary_candidates_for_home(&home); + let expected = [ + home.join(".local/bin/claude.exe"), + home.join(".claude/local/claude.exe"), + home.join("AppData/Roaming/npm/claude.cmd"), + ]; + + for candidate in expected { + assert!( + candidates.contains(&candidate), + "missing {}", + candidate.display() + ); + } + } + + #[test] + fn claude_path_lookup_command_matches_current_platform() { + let expected = if cfg!(windows) { "where" } else { "which" }; + + assert_eq!(claude_path_lookup_command(), expected); + } + + #[test] + fn find_existing_binary_finds_windows_exe_candidate() { + let dir = tempfile::tempdir().unwrap(); + let claude = dir.path().join(".local/bin/claude.exe"); + std::fs::create_dir_all(claude.parent().unwrap()).unwrap(); + std::fs::write(&claude, "").unwrap(); + + assert_eq!( + find_existing_binary(claude_binary_candidates_for_home(dir.path())), + Some(claude) + ); + } + #[test] fn find_claude_binary_returns_result() { let result = find_claude_binary();