1use std::process::Command;
7use std::sync::OnceLock;
8use tracing::debug;
9
10pub const DEFAULT_PRIORITY: &[&str] = &["claude", "kiro", "gemini", "codex", "amp"];
12
13fn detection_command(backend: &str) -> &str {
18 match backend {
19 "kiro" => "kiro-cli",
20 _ => backend,
21 }
22}
23
24static DETECTED_BACKEND: OnceLock<Option<String>> = OnceLock::new();
26
27#[derive(Debug, Clone)]
29pub struct NoBackendError {
30 pub checked: Vec<String>,
32}
33
34impl std::fmt::Display for NoBackendError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 writeln!(f, "No supported AI backend found in PATH.")?;
37 writeln!(f)?;
38 writeln!(f, "Checked backends: {}", self.checked.join(", "))?;
39 writeln!(f)?;
40 writeln!(f, "Install one of the following:")?;
41 writeln!(f, " • Claude CLI: https://docs.anthropic.com/claude-code")?;
42 writeln!(f, " • Kiro CLI: https://kiro.dev")?;
43 writeln!(f, " • Gemini CLI: https://cloud.google.com/gemini")?;
44 writeln!(f, " • Codex CLI: https://openai.com/codex")?;
45 writeln!(f, " • Amp CLI: https://amp.dev")?;
46 Ok(())
47 }
48}
49
50impl std::error::Error for NoBackendError {}
51
52pub fn is_backend_available(backend: &str) -> bool {
58 let command = detection_command(backend);
59 let result = Command::new(command).arg("--version").output();
60
61 match result {
62 Ok(output) => {
63 let available = output.status.success();
64 debug!(backend = backend, command = command, available = available, "Backend availability check");
65 available
66 }
67 Err(_) => {
68 debug!(backend = backend, command = command, available = false, "Backend not found in PATH");
69 false
70 }
71 }
72}
73
74pub fn detect_backend<F>(priority: &[&str], adapter_enabled: F) -> Result<String, NoBackendError>
84where
85 F: Fn(&str) -> bool,
86{
87 debug!(priority = ?priority, "Starting backend auto-detection");
88
89 if let Some(cached) = DETECTED_BACKEND.get() {
91 if let Some(backend) = cached {
92 debug!(backend = %backend, "Using cached backend detection result");
93 return Ok(backend.clone());
94 }
95 }
96
97 let mut checked = Vec::new();
98
99 for &backend in priority {
100 if !adapter_enabled(backend) {
102 debug!(backend = backend, "Skipping disabled adapter");
103 continue;
104 }
105
106 checked.push(backend.to_string());
107
108 if is_backend_available(backend) {
109 debug!(backend = backend, "Backend detected and selected");
110 let _ = DETECTED_BACKEND.set(Some(backend.to_string()));
112 return Ok(backend.to_string());
113 }
114 }
115
116 debug!(checked = ?checked, "No backends available");
117 let _ = DETECTED_BACKEND.set(None);
119
120 Err(NoBackendError { checked })
121}
122
123pub fn detect_backend_default() -> Result<String, NoBackendError> {
125 detect_backend(DEFAULT_PRIORITY, |_| true)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_is_backend_available_echo() {
134 let result = Command::new("echo").arg("--version").output();
136 assert!(result.is_ok());
138 }
139
140 #[test]
141 fn test_is_backend_available_nonexistent() {
142 assert!(!is_backend_available("definitely_not_a_real_command_xyz123"));
144 }
145
146 #[test]
147 fn test_detect_backend_with_disabled_adapters() {
148 let result = detect_backend(&["claude", "gemini"], |_| false);
150 assert!(result.is_err());
152 if let Err(e) = result {
153 assert!(e.checked.is_empty());
154 }
155 }
156
157 #[test]
158 fn test_no_backend_error_display() {
159 let err = NoBackendError {
160 checked: vec!["claude".to_string(), "gemini".to_string()],
161 };
162 let msg = format!("{}", err);
163 assert!(msg.contains("No supported AI backend found"));
164 assert!(msg.contains("claude, gemini"));
165 }
166
167 #[test]
168 fn test_detection_command_kiro() {
169 assert_eq!(detection_command("kiro"), "kiro-cli");
171 }
172
173 #[test]
174 fn test_detection_command_others() {
175 assert_eq!(detection_command("claude"), "claude");
177 assert_eq!(detection_command("gemini"), "gemini");
178 assert_eq!(detection_command("codex"), "codex");
179 assert_eq!(detection_command("amp"), "amp");
180 }
181
182 #[test]
183 fn test_detect_backend_default_priority_order() {
184 let fake_priority = &["fake_claude", "fake_kiro", "fake_gemini", "fake_codex", "fake_amp"];
187 let result = detect_backend(fake_priority, |_| true);
188
189 assert!(result.is_err());
191 if let Err(e) = result {
192 assert_eq!(e.checked, vec!["fake_claude", "fake_kiro", "fake_gemini", "fake_codex", "fake_amp"]);
194 }
195 }
196
197 #[test]
198 fn test_detect_backend_custom_priority_order() {
199 let custom_priority = &["fake_gemini", "fake_claude", "fake_amp"];
201 let result = detect_backend(custom_priority, |_| true);
202
203 assert!(result.is_err());
205 if let Err(e) = result {
206 assert_eq!(e.checked, vec!["fake_gemini", "fake_claude", "fake_amp"]);
208 }
209 }
210
211 #[test]
212 fn test_detect_backend_skips_disabled_adapters() {
213 let priority = &["fake_claude", "fake_gemini", "fake_kiro", "fake_codex"];
215 let result = detect_backend(priority, |backend| {
216 matches!(backend, "fake_gemini" | "fake_codex")
218 });
219
220 assert!(result.is_err());
222 if let Err(e) = result {
223 assert_eq!(e.checked, vec!["fake_gemini", "fake_codex"]);
225 }
226 }
227
228 #[test]
229 fn test_detect_backend_respects_priority_with_mixed_enabled() {
230 let priority = &["fake_claude", "fake_kiro", "fake_gemini", "fake_codex", "fake_amp"];
232 let result = detect_backend(priority, |backend| {
233 !matches!(backend, "fake_kiro" | "fake_codex")
235 });
236
237 assert!(result.is_err());
239 if let Err(e) = result {
240 assert_eq!(e.checked, vec!["fake_claude", "fake_gemini", "fake_amp"]);
242 }
243 }
244
245 #[test]
246 fn test_detect_backend_empty_priority_list() {
247 let result = detect_backend(&[], |_| true);
249
250 assert!(result.is_err());
252 if let Err(e) = result {
253 assert!(e.checked.is_empty());
254 }
255 }
256
257 #[test]
258 fn test_detect_backend_all_disabled() {
259 let priority = &["claude", "gemini", "kiro"];
261 let result = detect_backend(priority, |_| false);
262
263 assert!(result.is_err());
265 if let Err(e) = result {
266 assert!(e.checked.is_empty());
267 }
268 }
269
270 #[test]
271 fn test_detect_backend_finds_first_available() {
272 let priority = &["fake_nonexistent1", "fake_nonexistent2", "echo", "fake_nonexistent3"];
275 let result = detect_backend(priority, |_| true);
276
277 assert!(result.is_ok());
279 if let Ok(backend) = result {
280 assert_eq!(backend, "echo");
281 }
282 }
283
284 #[test]
285 fn test_detect_backend_skips_to_next_available() {
286 let priority = &["fake_nonexistent1", "fake_nonexistent2", "echo"];
288 let result = detect_backend(priority, |backend| {
289 backend != "fake_nonexistent1"
291 });
292
293 assert!(result.is_ok());
295 if let Ok(backend) = result {
296 assert_eq!(backend, "echo");
297 }
298 }
299}