From e3039c0d0a474acbde3e5205df3d3928c2d41580 Mon Sep 17 00:00:00 2001 From: krypticmouse Date: Sat, 28 Mar 2026 23:26:02 +0000 Subject: [PATCH] fix: Windows git detection in desktop app resolve_bin() The Tauri desktop app's resolve_bin("git") function checked incorrect paths for Git on Windows (e.g. {ProgramFiles}\git\git.exe) and had no PATH fallback, causing "git not found" even when Git is installed. Fixes: - Add correct Git for Windows paths: {ProgramFiles}\Git\cmd\git.exe, {ProgramFiles(x86)}\Git\cmd\git.exe, {LOCALAPPDATA}\Programs\Git\cmd\ - Add Scoop package manager path: {home}\scoop\shims\git.exe - Add PATH fallback using `where.exe` on Windows and `which` on Unix, so any binary on PATH is found even if not at a hardcoded location Closes #128 Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src-tauri/src/lib.rs | 238 ++++++++++++++++++++++++++--------- 1 file changed, 178 insertions(+), 60 deletions(-) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 84e2a1e1..0590049a 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use std::time::Duration; -use tauri::Manager; use tauri::menu::{MenuBuilder, MenuItemBuilder}; use tauri::tray::TrayIconBuilder; +use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; use tokio::sync::Mutex; @@ -127,9 +127,18 @@ fn resolve_bin(name: &str) -> String { let candidates = { let localappdata = std::env::var("LOCALAPPDATA").unwrap_or_default(); let programfiles = std::env::var("ProgramFiles").unwrap_or_default(); + let programfiles_x86 = std::env::var("ProgramFiles(x86)").unwrap_or_default(); vec![ + // Git for Windows — standard install paths + format!("{programfiles}\\Git\\cmd\\{name}.exe"), + format!("{programfiles_x86}\\Git\\cmd\\{name}.exe"), + format!("{localappdata}\\Programs\\Git\\cmd\\{name}.exe"), + // Scoop package manager + format!("{home}\\scoop\\shims\\{name}.exe"), + // Cargo, local bin format!("{home}\\.cargo\\bin\\{name}.exe"), format!("{home}\\.local\\bin\\{name}.exe"), + // Generic program locations format!("{localappdata}\\Programs\\{name}\\{name}.exe"), format!("{programfiles}\\{name}\\{name}.exe"), // Ollama installs to LOCALAPPDATA on Windows @@ -144,6 +153,41 @@ fn resolve_bin(name: &str) -> String { return path.clone(); } } + + // Fallback: ask the OS to find it on PATH. + // On Windows this uses `where.exe`, on Unix `which`. + #[cfg(target_os = "windows")] + { + if let Ok(output) = std::process::Command::new("where") + .arg(format!("{name}.exe")) + .output() + { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + if let Some(first_line) = stdout.lines().next() { + let p = first_line.trim(); + if !p.is_empty() && std::path::Path::new(p).exists() { + return p.to_string(); + } + } + } + } + } + #[cfg(not(target_os = "windows"))] + { + if let Ok(output) = std::process::Command::new("which").arg(name).output() { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + if let Some(first_line) = stdout.lines().next() { + let p = first_line.trim(); + if !p.is_empty() && std::path::Path::new(p).exists() { + return p.to_string(); + } + } + } + } + } + name.to_string() } @@ -623,14 +667,19 @@ async fn boot_backend(backend: SharedBackend, status: SharedStatus) { let mut cmd = tokio::process::Command::new(&uv_bin); cmd.args([ - "run", "jarvis", "serve", - "--port", &JARVIS_PORT.to_string(), - "--model", startup_model, - "--agent", "simple", - ]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::piped()) - .current_dir(root); + "run", + "jarvis", + "serve", + "--port", + &JARVIS_PORT.to_string(), + "--model", + startup_model, + "--agent", + "simple", + ]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .current_dir(root); // Inject cloud API keys from ~/.openjarvis/cloud-keys.env for (key, value) in read_cloud_keys() { @@ -720,9 +769,7 @@ fn api_base() -> String { } #[tauri::command] -async fn get_setup_status( - state: tauri::State<'_, SharedStatus>, -) -> Result { +async fn get_setup_status(state: tauri::State<'_, SharedStatus>) -> Result { Ok(state.lock().await.clone()) } @@ -743,83 +790,132 @@ async fn start_backend( } #[tauri::command] -async fn stop_backend( - backend: tauri::State<'_, SharedBackend>, -) -> Result<(), String> { +async fn stop_backend(backend: tauri::State<'_, SharedBackend>) -> Result<(), String> { backend.lock().await.stop_all().await; Ok(()) } #[tauri::command] async fn check_health(api_url: String) -> Result { - let url = format!("{}/health", if api_url.is_empty() { api_base() } else { api_url }); + let url = format!( + "{}/health", + if api_url.is_empty() { + api_base() + } else { + api_url + } + ); let resp = reqwest::get(&url) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_energy(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/telemetry/energy", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_telemetry(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/telemetry/stats", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_traces(api_url: String, limit: u32) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/traces?limit={}", base, limit)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_trace(api_url: String, trace_id: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/traces/{}", base, trace_id)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_learning_stats(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/learning/stats", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_learning_policy(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/learning/policy", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_memory_stats(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/memory/stats", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] @@ -828,7 +924,11 @@ async fn search_memory( query: String, top_k: u32, ) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let client = reqwest::Client::new(); let resp = client .post(format!("{}/v1/memory/search", base)) @@ -836,25 +936,39 @@ async fn search_memory( .send() .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_agents(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/agents", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] async fn fetch_models(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/models", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } #[tauri::command] @@ -877,11 +991,17 @@ async fn run_jarvis_command(args: Vec) -> Result { #[tauri::command] async fn fetch_savings(api_url: String) -> Result { - let base = if api_url.is_empty() { api_base() } else { api_url }; + let base = if api_url.is_empty() { + api_base() + } else { + api_url + }; let resp = reqwest::get(format!("{}/v1/savings", base)) .await .map_err(|e| format!("Connection failed: {}", e))?; - resp.json().await.map_err(|e| format!("Invalid response: {}", e)) + resp.json() + .await + .map_err(|e| format!("Invalid response: {}", e)) } /// Transcribe audio via the speech API endpoint. @@ -926,7 +1046,10 @@ async fn submit_savings( } let client = reqwest::Client::new(); let resp = client - .post(format!("{}/rest/v1/savings_entries?on_conflict=anon_id", supabase_url)) + .post(format!( + "{}/rest/v1/savings_entries?on_conflict=anon_id", + supabase_url + )) .header("Content-Type", "application/json") .header("apikey", &supabase_key) .header("Authorization", format!("Bearer {}", supabase_key)) @@ -992,8 +1115,7 @@ async fn save_cloud_key(key_name: String, key_value: String) -> Result<(), Strin .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("\n"); - std::fs::write(&path, content + "\n") - .map_err(|e| format!("Failed to save key: {}", e))?; + std::fs::write(&path, content + "\n").map_err(|e| format!("Failed to save key: {}", e))?; // Set permissions to owner-only (chmod 600) #[cfg(unix)] @@ -1090,13 +1212,11 @@ pub fn run() { })) .setup(move |app| { // System tray - let show = MenuItemBuilder::with_id("show", "Show / Hide") - .build(app)?; + let show = MenuItemBuilder::with_id("show", "Show / Hide").build(app)?; let health = MenuItemBuilder::with_id("health", "Health: starting...") .enabled(false) .build(app)?; - let quit = MenuItemBuilder::with_id("quit", "Quit OpenJarvis") - .build(app)?; + let quit = MenuItemBuilder::with_id("quit", "Quit OpenJarvis").build(app)?; let menu = MenuBuilder::new(app) .item(&show) @@ -1110,23 +1230,21 @@ pub fn run() { .icon(app.default_window_icon().unwrap().clone()) .tooltip("OpenJarvis") .menu(&menu) - .on_menu_event(move |app, event| { - match event.id().as_ref() { - "show" => { - if let Some(window) = app.get_webview_window("main") { - if window.is_visible().unwrap_or(false) { - let _ = window.hide(); - } else { - let _ = window.show(); - let _ = window.set_focus(); - } + .on_menu_event(move |app, event| match event.id().as_ref() { + "show" => { + if let Some(window) = app.get_webview_window("main") { + if window.is_visible().unwrap_or(false) { + let _ = window.hide(); + } else { + let _ = window.show(); + let _ = window.set_focus(); } } - "quit" => { - app.exit(0); - } - _ => {} } + "quit" => { + app.exit(0); + } + _ => {} }) .build(app)?;