1use std::env;
8use std::sync::LazyLock;
9
10use crate::config::{CliRunnerType, RunnerConfig};
11use crate::discovery::resolve_binary;
12use crate::types::{LlmProvider, RunnerError};
13use crate::{
14 ClaudeCodeRunner, ClineCliRunner, CodexCliRunner, ContinueCliRunner, CopilotRunner,
15 CursorAgentRunner, GeminiCliRunner, GooseCliRunner, KiloCliRunner, KiroCliRunner,
16 OpenCodeRunner, WarpCliRunner,
17};
18
19pub async fn create_runner(
28 runner_type: CliRunnerType,
29) -> Result<Box<dyn LlmProvider>, RunnerError> {
30 #[cfg(feature = "copilot-headless")]
32 if runner_type == CliRunnerType::CopilotHeadless {
33 return Ok(Box::new(crate::CopilotHeadlessRunner::from_env()));
34 }
35 #[cfg(feature = "copilot-sdk")]
37 if runner_type == CliRunnerType::CopilotSdk {
38 return Ok(Box::new(crate::CopilotSdkRunner::from_env()));
39 }
40
41 #[cfg(feature = "web-ui")]
43 if runner_type == CliRunnerType::ClaudeWeb {
44 return Ok(Box::new(crate::WebUiRunner::from_env()?));
45 }
46
47 let binary_name = runner_type.binary_name();
48 let env_key = runner_type.env_override_key();
49 let env_override = env::var(env_key).ok();
50
51 let binary_path = resolve_binary(binary_name, env_override.as_deref())?;
52 let config = RunnerConfig::new(binary_path);
53
54 let runner: Box<dyn LlmProvider> = match runner_type {
55 CliRunnerType::ClaudeCode => Box::new(ClaudeCodeRunner::new(config)),
56 CliRunnerType::Copilot => Box::new(CopilotRunner::new(config)),
57 CliRunnerType::CursorAgent => Box::new(CursorAgentRunner::new(config)),
58 CliRunnerType::OpenCode => Box::new(OpenCodeRunner::new(config)),
59 CliRunnerType::GeminiCli => Box::new(GeminiCliRunner::new(config)),
60 CliRunnerType::CodexCli => Box::new(CodexCliRunner::new(config)),
61 CliRunnerType::GooseCli => Box::new(GooseCliRunner::new(config)),
62 CliRunnerType::ClineCli => Box::new(ClineCliRunner::new(config)),
63 CliRunnerType::ContinueCli => Box::new(ContinueCliRunner::new(config)),
64 CliRunnerType::WarpCli => Box::new(WarpCliRunner::new(config)),
65 CliRunnerType::KiroCli => Box::new(KiroCliRunner::new(config)),
66 CliRunnerType::KiloCli => Box::new(KiloCliRunner::new(config)),
67 #[cfg(feature = "copilot-headless")]
68 CliRunnerType::CopilotHeadless => unreachable!("handled above"),
69 #[cfg(feature = "copilot-sdk")]
70 CliRunnerType::CopilotSdk => unreachable!("handled above"),
71 #[cfg(feature = "web-ui")]
72 CliRunnerType::ClaudeWeb => unreachable!("handled above"),
73 };
74
75 Ok(runner)
76}
77
78pub async fn create_runner_with_config(
89 runner_type: CliRunnerType,
90 config: RunnerConfig,
91) -> Result<Box<dyn LlmProvider>, RunnerError> {
92 Ok(match runner_type {
93 CliRunnerType::ClaudeCode => Box::new(ClaudeCodeRunner::new(config)),
94 CliRunnerType::Copilot => Box::new(CopilotRunner::new(config)),
95 CliRunnerType::CursorAgent => Box::new(CursorAgentRunner::new(config)),
96 CliRunnerType::OpenCode => Box::new(OpenCodeRunner::new(config)),
97 CliRunnerType::GeminiCli => Box::new(GeminiCliRunner::new(config)),
98 CliRunnerType::CodexCli => Box::new(CodexCliRunner::new(config)),
99 CliRunnerType::GooseCli => Box::new(GooseCliRunner::new(config)),
100 CliRunnerType::ClineCli => Box::new(ClineCliRunner::new(config)),
101 CliRunnerType::ContinueCli => Box::new(ContinueCliRunner::new(config)),
102 CliRunnerType::WarpCli => Box::new(WarpCliRunner::new(config)),
103 CliRunnerType::KiroCli => Box::new(KiroCliRunner::new(config)),
104 CliRunnerType::KiloCli => Box::new(KiloCliRunner::new(config)),
105 #[cfg(feature = "copilot-headless")]
107 CliRunnerType::CopilotHeadless => Box::new(crate::CopilotHeadlessRunner::from_env()),
108 #[cfg(feature = "copilot-sdk")]
110 CliRunnerType::CopilotSdk => Box::new(crate::CopilotSdkRunner::from_env()),
111 #[cfg(feature = "web-ui")]
113 CliRunnerType::ClaudeWeb => {
114 let provider = crate::WebProviderConfig::claude_web_default()?;
115 Box::new(crate::WebUiRunner::new(
116 crate::WebUiConfig::from_env(),
117 provider,
118 ))
119 }
120 })
121}
122
123pub static ALL_PROVIDERS: LazyLock<Vec<CliRunnerType>> = LazyLock::new(|| {
129 let mut providers = vec![CliRunnerType::ClaudeCode, CliRunnerType::Copilot];
130 #[cfg(feature = "copilot-headless")]
131 providers.push(CliRunnerType::CopilotHeadless);
132 #[cfg(feature = "copilot-sdk")]
133 providers.push(CliRunnerType::CopilotSdk);
134 providers.extend([
135 CliRunnerType::CursorAgent,
136 CliRunnerType::OpenCode,
137 CliRunnerType::GeminiCli,
138 CliRunnerType::CodexCli,
139 CliRunnerType::GooseCli,
140 CliRunnerType::ClineCli,
141 CliRunnerType::ContinueCli,
142 CliRunnerType::WarpCli,
143 CliRunnerType::KiroCli,
144 CliRunnerType::KiloCli,
145 ]);
146 providers
147});
148
149pub fn parse_runner_type(s: &str) -> Option<CliRunnerType> {
154 match s.to_lowercase().as_str() {
155 "claude_code" | "claude" | "claudecode" => Some(CliRunnerType::ClaudeCode),
156 "copilot" => Some(CliRunnerType::Copilot),
157 "cursor_agent" | "cursoragent" | "cursor-agent" => Some(CliRunnerType::CursorAgent),
158 "opencode" | "open_code" => Some(CliRunnerType::OpenCode),
159 "gemini" | "gemini_cli" | "geminicli" | "gemini-cli" => Some(CliRunnerType::GeminiCli),
160 "codex" | "codex_cli" | "codexcli" | "codex-cli" => Some(CliRunnerType::CodexCli),
161 "goose" | "goose_cli" | "goosecli" | "goose-cli" => Some(CliRunnerType::GooseCli),
162 "cline" | "cline_cli" | "clinecli" | "cline-cli" => Some(CliRunnerType::ClineCli),
163 "continue" | "continue_cli" | "continuecli" | "continue-cli" | "cn" => {
164 Some(CliRunnerType::ContinueCli)
165 }
166 "warp" | "warp_cli" | "warpcli" | "warp-cli" | "oz" => Some(CliRunnerType::WarpCli),
167 "kiro" | "kiro_cli" | "kirocli" | "kiro-cli" => Some(CliRunnerType::KiroCli),
168 "kilo" | "kilo_cli" | "kilocli" | "kilo-cli" | "kilocode" => Some(CliRunnerType::KiloCli),
169 #[cfg(feature = "copilot-headless")]
170 "copilot_headless" | "copilot-headless" | "copilotheadless" | "headless" => {
171 Some(CliRunnerType::CopilotHeadless)
172 }
173 #[cfg(feature = "copilot-sdk")]
174 "copilot_sdk" | "copilot-sdk" | "copilotsdk" => Some(CliRunnerType::CopilotSdk),
175 #[cfg(feature = "web-ui")]
176 "claude_web" | "claude-web" | "claudeweb" | "web" => Some(CliRunnerType::ClaudeWeb),
177 _ => None,
178 }
179}
180
181#[must_use]
184pub fn valid_provider_names() -> String {
185 ALL_PROVIDERS
186 .iter()
187 .map(ToString::to_string)
188 .collect::<Vec<_>>()
189 .join(", ")
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn parse_snake_case_variants() {
198 assert_eq!(
199 parse_runner_type("claude_code"),
200 Some(CliRunnerType::ClaudeCode)
201 );
202 assert_eq!(parse_runner_type("copilot"), Some(CliRunnerType::Copilot));
203 assert_eq!(
204 parse_runner_type("cursor_agent"),
205 Some(CliRunnerType::CursorAgent)
206 );
207 assert_eq!(parse_runner_type("opencode"), Some(CliRunnerType::OpenCode));
208 assert_eq!(
209 parse_runner_type("gemini_cli"),
210 Some(CliRunnerType::GeminiCli)
211 );
212 assert_eq!(
213 parse_runner_type("codex_cli"),
214 Some(CliRunnerType::CodexCli)
215 );
216 assert_eq!(
217 parse_runner_type("goose_cli"),
218 Some(CliRunnerType::GooseCli)
219 );
220 assert_eq!(
221 parse_runner_type("cline_cli"),
222 Some(CliRunnerType::ClineCli)
223 );
224 assert_eq!(
225 parse_runner_type("continue_cli"),
226 Some(CliRunnerType::ContinueCli)
227 );
228 assert_eq!(parse_runner_type("warp_cli"), Some(CliRunnerType::WarpCli));
229 assert_eq!(parse_runner_type("kiro_cli"), Some(CliRunnerType::KiroCli));
230 assert_eq!(parse_runner_type("kilo_cli"), Some(CliRunnerType::KiloCli));
231 }
232
233 #[test]
234 fn parse_short_forms() {
235 assert_eq!(parse_runner_type("claude"), Some(CliRunnerType::ClaudeCode));
236 assert_eq!(
237 parse_runner_type("cursor-agent"),
238 Some(CliRunnerType::CursorAgent)
239 );
240 assert_eq!(parse_runner_type("gemini"), Some(CliRunnerType::GeminiCli));
241 assert_eq!(parse_runner_type("codex"), Some(CliRunnerType::CodexCli));
242 assert_eq!(parse_runner_type("goose"), Some(CliRunnerType::GooseCli));
243 assert_eq!(parse_runner_type("cline"), Some(CliRunnerType::ClineCli));
244 assert_eq!(
245 parse_runner_type("continue"),
246 Some(CliRunnerType::ContinueCli)
247 );
248 assert_eq!(parse_runner_type("cn"), Some(CliRunnerType::ContinueCli));
249 assert_eq!(parse_runner_type("warp"), Some(CliRunnerType::WarpCli));
250 assert_eq!(parse_runner_type("oz"), Some(CliRunnerType::WarpCli));
251 assert_eq!(parse_runner_type("kiro"), Some(CliRunnerType::KiroCli));
252 assert_eq!(parse_runner_type("kiro-cli"), Some(CliRunnerType::KiroCli));
253 assert_eq!(parse_runner_type("kilo"), Some(CliRunnerType::KiloCli));
254 assert_eq!(parse_runner_type("kilo-cli"), Some(CliRunnerType::KiloCli));
255 assert_eq!(parse_runner_type("kilocode"), Some(CliRunnerType::KiloCli));
256 }
257
258 #[test]
259 fn parse_case_insensitive() {
260 assert_eq!(parse_runner_type("COPILOT"), Some(CliRunnerType::Copilot));
261 assert_eq!(
262 parse_runner_type("Claude_Code"),
263 Some(CliRunnerType::ClaudeCode)
264 );
265 }
266
267 #[test]
268 fn parse_unknown_returns_none() {
269 assert_eq!(parse_runner_type("gpt4"), None);
270 assert_eq!(parse_runner_type(""), None);
271 }
272
273 #[test]
274 fn all_providers_count() {
275 let expected = 12
276 + usize::from(cfg!(feature = "copilot-headless"))
277 + usize::from(cfg!(feature = "copilot-sdk"));
278 assert_eq!(ALL_PROVIDERS.len(), expected);
279 }
280
281 #[test]
282 fn valid_provider_names_lists_every_provider_once() {
283 let names = valid_provider_names();
284 let listed: Vec<&str> = names.split(", ").collect();
285 for provider in ALL_PROVIDERS.iter() {
286 let name = provider.to_string();
287 assert_eq!(
288 listed.iter().filter(|n| **n == name).count(),
289 1,
290 "{provider} appears once in {names}"
291 );
292 }
293 assert!(names.starts_with("claude_code, copilot"));
294 }
295}