mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-31 03:12:16 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
eea5f65ea0
commit
e3039c0d0a
+178
-60
@@ -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<SetupStatus, String> {
|
||||
async fn get_setup_status(state: tauri::State<'_, SharedStatus>) -> Result<SetupStatus, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<serde_json::Value, String> {
|
||||
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<String>) -> Result<String, String> {
|
||||
|
||||
#[tauri::command]
|
||||
async fn fetch_savings(api_url: String) -> Result<serde_json::Value, String> {
|
||||
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::<Vec<_>>()
|
||||
.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)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user