use crate::errors::AppError;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
pub trait LlmSpawnBackend: Send + Sync {
fn name(&self) -> &'static str;
fn default_binary_name(&self) -> &'static str;
fn resolve_binary(&self, override_path: Option<&Path>) -> Result<PathBuf, AppError> {
let candidate = match override_path {
Some(p) => p.to_path_buf(),
None => PathBuf::from(self.default_binary_name()),
};
which::which(&candidate).map_err(|_| AppError::BinaryNotFound {
name: candidate.display().to_string(),
})
}
fn spawn_child(&self, cmd: &mut Command) -> std::io::Result<Child> {
cmd.spawn()
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ClaudeSpawnBackend;
impl LlmSpawnBackend for ClaudeSpawnBackend {
fn name(&self) -> &'static str {
"claude"
}
fn default_binary_name(&self) -> &'static str {
"claude"
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CodexSpawnBackend;
impl LlmSpawnBackend for CodexSpawnBackend {
fn name(&self) -> &'static str {
"codex"
}
fn default_binary_name(&self) -> &'static str {
"codex"
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct OpencodeSpawnBackend;
impl LlmSpawnBackend for OpencodeSpawnBackend {
fn name(&self) -> &'static str {
"opencode"
}
fn default_binary_name(&self) -> &'static str {
"opencode"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn claude_backend_names() {
let b = ClaudeSpawnBackend;
assert_eq!(b.name(), "claude");
assert_eq!(b.default_binary_name(), "claude");
}
#[test]
fn codex_backend_names() {
let b = CodexSpawnBackend;
assert_eq!(b.name(), "codex");
assert_eq!(b.default_binary_name(), "codex");
}
#[test]
fn opencode_backend_names() {
let b = OpencodeSpawnBackend;
assert_eq!(b.name(), "opencode");
assert_eq!(b.default_binary_name(), "opencode");
}
#[test]
fn resolve_binary_missing_returns_binary_not_found() {
let b = ClaudeSpawnBackend;
let err = b
.resolve_binary(Some(Path::new(
"/nonexistent/sqlite-graphrag-no-such-llm-binary",
)))
.unwrap_err();
match err {
AppError::BinaryNotFound { name } => {
assert!(name.contains("sqlite-graphrag-no-such-llm-binary"));
}
other => panic!("expected BinaryNotFound, got {other:?}"),
}
}
}