Files
tolaria/src-tauri/src/opencode_discovery.rs

224 lines
6.9 KiB
Rust
Raw Normal View History

2026-04-28 01:26:21 +02:00
use crate::ai_agents::AiAgentAvailability;
use std::path::{Path, PathBuf};
pub(crate) fn check_cli() -> AiAgentAvailability {
match find_binary() {
Ok(binary) => AiAgentAvailability {
installed: true,
version: crate::cli_agent_runtime::version_for_binary(&binary),
2026-04-28 01:26:21 +02:00
},
Err(_) => AiAgentAvailability {
installed: false,
version: None,
},
}
}
pub(crate) fn find_binary() -> Result<PathBuf, String> {
if let Some(binary) = find_binary_on_path() {
return Ok(binary);
}
if let Some(binary) = find_binary_in_user_shell() {
return Ok(binary);
}
if let Some(binary) = crate::cli_agent_runtime::find_executable_binary_candidate(
opencode_binary_candidates(),
"OpenCode CLI",
)? {
2026-04-28 01:26:21 +02:00
return Ok(binary);
}
Err("OpenCode CLI not found. Install it: https://opencode.ai/docs/".into())
}
fn find_binary_on_path() -> Option<PathBuf> {
2026-04-29 04:01:53 +02:00
crate::hidden_command(path_lookup_command())
2026-04-28 01:26:21 +02:00
.arg("opencode")
.output()
.ok()
.and_then(|output| path_from_successful_output(&output))
}
2026-04-29 04:01:53 +02:00
fn path_lookup_command() -> &'static str {
if cfg!(windows) {
"where"
} else {
"which"
}
}
2026-04-28 01:26:21 +02:00
fn find_binary_in_user_shell() -> Option<PathBuf> {
user_shell_candidates()
.into_iter()
.filter(|shell| shell.exists())
.find_map(|shell| command_path_from_shell(&shell, "opencode"))
}
fn user_shell_candidates() -> Vec<PathBuf> {
let mut shells = Vec::new();
if let Some(shell) = std::env::var_os("SHELL") {
if !shell.is_empty() {
shells.push(PathBuf::from(shell));
}
}
shells.push(PathBuf::from("/bin/zsh"));
shells.push(PathBuf::from("/bin/bash"));
shells
}
fn command_path_from_shell(shell: &Path, command: &str) -> Option<PathBuf> {
crate::hidden_command(shell)
.arg("-lc")
.arg(format!("command -v {command}"))
.output()
.ok()
.and_then(|output| path_from_successful_output(&output))
}
fn path_from_successful_output(output: &std::process::Output) -> Option<PathBuf> {
if output.status.success() {
first_existing_path(&String::from_utf8_lossy(&output.stdout))
} else {
None
}
}
fn first_existing_path(stdout: &str) -> Option<PathBuf> {
stdout.lines().find_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
let candidate = PathBuf::from(trimmed);
candidate.exists().then_some(candidate)
})
}
fn opencode_binary_candidates() -> Vec<PathBuf> {
dirs::home_dir()
.map(|home| opencode_binary_candidates_for_home(&home))
.unwrap_or_default()
}
fn opencode_binary_candidates_for_home(home: &Path) -> Vec<PathBuf> {
vec![
home.join(".local/bin/opencode"),
2026-04-29 04:01:53 +02:00
home.join(".local/bin/opencode.exe"),
2026-04-28 01:26:21 +02:00
home.join(".opencode/bin/opencode"),
2026-04-29 04:01:53 +02:00
home.join(".opencode/bin/opencode.exe"),
2026-04-28 01:26:21 +02:00
home.join(".local/share/mise/shims/opencode"),
2026-04-29 04:01:53 +02:00
home.join(".local/share/mise/shims/opencode.exe"),
2026-04-28 01:26:21 +02:00
home.join(".asdf/shims/opencode"),
2026-04-29 04:01:53 +02:00
home.join(".asdf/shims/opencode.exe"),
2026-04-28 01:26:21 +02:00
home.join(".npm-global/bin/opencode"),
2026-04-29 04:01:53 +02:00
home.join(".npm-global/bin/opencode.cmd"),
home.join(".npm-global/bin/opencode.exe"),
2026-04-28 01:26:21 +02:00
home.join(".npm/bin/opencode"),
2026-04-29 04:01:53 +02:00
home.join(".npm/bin/opencode.cmd"),
home.join(".npm/bin/opencode.exe"),
2026-04-28 01:26:21 +02:00
home.join(".bun/bin/opencode"),
2026-04-29 04:01:53 +02:00
home.join(".bun/bin/opencode.exe"),
home.join(".linuxbrew/bin/opencode"),
2026-04-29 04:01:53 +02:00
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("/home/linuxbrew/.linuxbrew/bin/opencode"),
2026-04-28 01:26:21 +02:00
PathBuf::from("/usr/local/bin/opencode"),
PathBuf::from("/opt/homebrew/bin/opencode"),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_candidates_include_supported_local_installs() {
let home = PathBuf::from("/Users/alex");
let candidates = opencode_binary_candidates_for_home(&home);
let expected = [
home.join(".local/bin/opencode"),
home.join(".opencode/bin/opencode"),
home.join(".local/share/mise/shims/opencode"),
home.join(".asdf/shims/opencode"),
home.join(".npm-global/bin/opencode"),
home.join(".bun/bin/opencode"),
PathBuf::from("/opt/homebrew/bin/opencode"),
];
for candidate in expected {
assert!(
candidates.contains(&candidate),
"missing {}",
candidate.display()
);
}
}
#[test]
fn binary_candidates_include_linuxbrew_installs() {
let home = PathBuf::from("/home/alex");
let candidates = opencode_binary_candidates_for_home(&home);
let expected = [
home.join(".linuxbrew/bin/opencode"),
PathBuf::from("/home/linuxbrew/.linuxbrew/bin/opencode"),
];
for candidate in expected {
assert!(
candidates.contains(&candidate),
"missing {}",
candidate.display()
);
}
}
2026-04-29 04:01:53 +02:00
#[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);
}
2026-04-28 01:26:21 +02:00
#[test]
fn first_existing_path_skips_empty_and_missing_lines() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("missing-opencode");
let opencode = dir.path().join("opencode");
std::fs::write(&opencode, "#!/bin/sh\n").unwrap();
let stdout = format!("\n{}\n{}\n", missing.display(), opencode.display());
assert_eq!(first_existing_path(&stdout), Some(opencode));
}
}