1use std::env;
8
9use crate::config::{CliRunnerType, RunnerConfig};
10use crate::discovery::resolve_binary;
11use crate::types::{LlmProvider, RunnerError};
12use crate::{
13 ClaudeCodeRunner, ClineCliRunner, CodexCliRunner, ContinueCliRunner, CopilotRunner,
14 CursorAgentRunner, GeminiCliRunner, GooseCliRunner, KiloCliRunner, KiroCliRunner,
15 OpenCodeRunner, WarpCliRunner,
16};
17
18pub async fn create_runner(
27 runner_type: CliRunnerType,
28) -> Result<Box<dyn LlmProvider>, RunnerError> {
29 #[cfg(feature = "copilot-headless")]
31 if runner_type == CliRunnerType::CopilotHeadless {
32 return Ok(Box::new(crate::CopilotHeadlessRunner::from_env()));
33 }
34
35 #[cfg(feature = "web-ui")]
37 if runner_type == CliRunnerType::ClaudeWeb {
38 return Ok(Box::new(crate::WebUiRunner::from_env()?));
39 }
40
41 let binary_name = runner_type.binary_name();
42 let env_key = runner_type.env_override_key();
43 let env_override = env::var(env_key).ok();
44
45 let binary_path = resolve_binary(binary_name, env_override.as_deref())?;
46 let config = RunnerConfig::new(binary_path);
47
48 let runner: Box<dyn LlmProvider> = match runner_type {
49 CliRunnerType::ClaudeCode => Box::new(ClaudeCodeRunner::new(config)),
50 CliRunnerType::Copilot => Box::new(CopilotRunner::new(config)),
51 CliRunnerType::CursorAgent => Box::new(CursorAgentRunner::new(config)),
52 CliRunnerType::OpenCode => Box::new(OpenCodeRunner::new(config)),
53 CliRunnerType::GeminiCli => Box::new(GeminiCliRunner::new(config)),
54 CliRunnerType::CodexCli => Box::new(CodexCliRunner::new(config)),
55 CliRunnerType::GooseCli => Box::new(GooseCliRunner::new(config)),
56 CliRunnerType::ClineCli => Box::new(ClineCliRunner::new(config)),
57 CliRunnerType::ContinueCli => Box::new(ContinueCliRunner::new(config)),
58 CliRunnerType::WarpCli => Box::new(WarpCliRunner::new(config)),
59 CliRunnerType::KiroCli => Box::new(KiroCliRunner::new(config)),
60 CliRunnerType::KiloCli => Box::new(KiloCliRunner::new(config)),
61 #[cfg(feature = "copilot-headless")]
62 CliRunnerType::CopilotHeadless => unreachable!("handled above"),
63 #[cfg(feature = "web-ui")]
64 CliRunnerType::ClaudeWeb => unreachable!("handled above"),
65 };
66
67 Ok(runner)
68}
69
70pub async fn create_runner_with_config(
81 runner_type: CliRunnerType,
82 config: RunnerConfig,
83) -> Result<Box<dyn LlmProvider>, RunnerError> {
84 Ok(match runner_type {
85 CliRunnerType::ClaudeCode => Box::new(ClaudeCodeRunner::new(config)),
86 CliRunnerType::Copilot => Box::new(CopilotRunner::new(config)),
87 CliRunnerType::CursorAgent => Box::new(CursorAgentRunner::new(config)),
88 CliRunnerType::OpenCode => Box::new(OpenCodeRunner::new(config)),
89 CliRunnerType::GeminiCli => Box::new(GeminiCliRunner::new(config)),
90 CliRunnerType::CodexCli => Box::new(CodexCliRunner::new(config)),
91 CliRunnerType::GooseCli => Box::new(GooseCliRunner::new(config)),
92 CliRunnerType::ClineCli => Box::new(ClineCliRunner::new(config)),
93 CliRunnerType::ContinueCli => Box::new(ContinueCliRunner::new(config)),
94 CliRunnerType::WarpCli => Box::new(WarpCliRunner::new(config)),
95 CliRunnerType::KiroCli => Box::new(KiroCliRunner::new(config)),
96 CliRunnerType::KiloCli => Box::new(KiloCliRunner::new(config)),
97 #[cfg(feature = "copilot-headless")]
99 CliRunnerType::CopilotHeadless => Box::new(crate::CopilotHeadlessRunner::from_env()),
100 #[cfg(feature = "web-ui")]
102 CliRunnerType::ClaudeWeb => {
103 let provider = crate::WebProviderConfig::claude_web_default()?;
104 Box::new(crate::WebUiRunner::new(
105 crate::WebUiConfig::from_env(),
106 provider,
107 ))
108 }
109 })
110}
111
112#[cfg(not(feature = "copilot-headless"))]
114pub const ALL_PROVIDERS: &[CliRunnerType] = &[
115 CliRunnerType::ClaudeCode,
116 CliRunnerType::Copilot,
117 CliRunnerType::CursorAgent,
118 CliRunnerType::OpenCode,
119 CliRunnerType::GeminiCli,
120 CliRunnerType::CodexCli,
121 CliRunnerType::GooseCli,
122 CliRunnerType::ClineCli,
123 CliRunnerType::ContinueCli,
124 CliRunnerType::WarpCli,
125 CliRunnerType::KiroCli,
126 CliRunnerType::KiloCli,
127];
128
129#[cfg(feature = "copilot-headless")]
131pub const ALL_PROVIDERS: &[CliRunnerType] = &[
132 CliRunnerType::ClaudeCode,
133 CliRunnerType::Copilot,
134 CliRunnerType::CopilotHeadless,
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
147pub fn parse_runner_type(s: &str) -> Option<CliRunnerType> {
152 match s.to_lowercase().as_str() {
153 "claude_code" | "claude" | "claudecode" => Some(CliRunnerType::ClaudeCode),
154 "copilot" => Some(CliRunnerType::Copilot),
155 "cursor_agent" | "cursoragent" | "cursor-agent" => Some(CliRunnerType::CursorAgent),
156 "opencode" | "open_code" => Some(CliRunnerType::OpenCode),
157 "gemini" | "gemini_cli" | "geminicli" | "gemini-cli" => Some(CliRunnerType::GeminiCli),
158 "codex" | "codex_cli" | "codexcli" | "codex-cli" => Some(CliRunnerType::CodexCli),
159 "goose" | "goose_cli" | "goosecli" | "goose-cli" => Some(CliRunnerType::GooseCli),
160 "cline" | "cline_cli" | "clinecli" | "cline-cli" => Some(CliRunnerType::ClineCli),
161 "continue" | "continue_cli" | "continuecli" | "continue-cli" | "cn" => {
162 Some(CliRunnerType::ContinueCli)
163 }
164 "warp" | "warp_cli" | "warpcli" | "warp-cli" | "oz" => Some(CliRunnerType::WarpCli),
165 "kiro" | "kiro_cli" | "kirocli" | "kiro-cli" => Some(CliRunnerType::KiroCli),
166 "kilo" | "kilo_cli" | "kilocli" | "kilo-cli" | "kilocode" => Some(CliRunnerType::KiloCli),
167 #[cfg(feature = "copilot-headless")]
168 "copilot_headless" | "copilot-headless" | "copilotheadless" | "headless" => {
169 Some(CliRunnerType::CopilotHeadless)
170 }
171 #[cfg(feature = "web-ui")]
172 "claude_web" | "claude-web" | "claudeweb" | "web" => Some(CliRunnerType::ClaudeWeb),
173 _ => None,
174 }
175}
176
177pub const fn valid_provider_names() -> &'static str {
179 if cfg!(feature = "copilot-headless") {
180 "claude_code, copilot, copilot_headless, cursor_agent, opencode, gemini_cli, codex_cli, goose_cli, cline_cli, continue_cli, warp_cli, kiro_cli, kilo_cli"
181 } else {
182 "claude_code, copilot, cursor_agent, opencode, gemini_cli, codex_cli, goose_cli, cline_cli, continue_cli, warp_cli, kiro_cli, kilo_cli"
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn parse_snake_case_variants() {
192 assert_eq!(
193 parse_runner_type("claude_code"),
194 Some(CliRunnerType::ClaudeCode)
195 );
196 assert_eq!(parse_runner_type("copilot"), Some(CliRunnerType::Copilot));
197 assert_eq!(
198 parse_runner_type("cursor_agent"),
199 Some(CliRunnerType::CursorAgent)
200 );
201 assert_eq!(parse_runner_type("opencode"), Some(CliRunnerType::OpenCode));
202 assert_eq!(
203 parse_runner_type("gemini_cli"),
204 Some(CliRunnerType::GeminiCli)
205 );
206 assert_eq!(
207 parse_runner_type("codex_cli"),
208 Some(CliRunnerType::CodexCli)
209 );
210 assert_eq!(
211 parse_runner_type("goose_cli"),
212 Some(CliRunnerType::GooseCli)
213 );
214 assert_eq!(
215 parse_runner_type("cline_cli"),
216 Some(CliRunnerType::ClineCli)
217 );
218 assert_eq!(
219 parse_runner_type("continue_cli"),
220 Some(CliRunnerType::ContinueCli)
221 );
222 assert_eq!(parse_runner_type("warp_cli"), Some(CliRunnerType::WarpCli));
223 assert_eq!(parse_runner_type("kiro_cli"), Some(CliRunnerType::KiroCli));
224 assert_eq!(parse_runner_type("kilo_cli"), Some(CliRunnerType::KiloCli));
225 }
226
227 #[test]
228 fn parse_short_forms() {
229 assert_eq!(parse_runner_type("claude"), Some(CliRunnerType::ClaudeCode));
230 assert_eq!(
231 parse_runner_type("cursor-agent"),
232 Some(CliRunnerType::CursorAgent)
233 );
234 assert_eq!(parse_runner_type("gemini"), Some(CliRunnerType::GeminiCli));
235 assert_eq!(parse_runner_type("codex"), Some(CliRunnerType::CodexCli));
236 assert_eq!(parse_runner_type("goose"), Some(CliRunnerType::GooseCli));
237 assert_eq!(parse_runner_type("cline"), Some(CliRunnerType::ClineCli));
238 assert_eq!(
239 parse_runner_type("continue"),
240 Some(CliRunnerType::ContinueCli)
241 );
242 assert_eq!(parse_runner_type("cn"), Some(CliRunnerType::ContinueCli));
243 assert_eq!(parse_runner_type("warp"), Some(CliRunnerType::WarpCli));
244 assert_eq!(parse_runner_type("oz"), Some(CliRunnerType::WarpCli));
245 assert_eq!(parse_runner_type("kiro"), Some(CliRunnerType::KiroCli));
246 assert_eq!(parse_runner_type("kiro-cli"), Some(CliRunnerType::KiroCli));
247 assert_eq!(parse_runner_type("kilo"), Some(CliRunnerType::KiloCli));
248 assert_eq!(parse_runner_type("kilo-cli"), Some(CliRunnerType::KiloCli));
249 assert_eq!(parse_runner_type("kilocode"), Some(CliRunnerType::KiloCli));
250 }
251
252 #[test]
253 fn parse_case_insensitive() {
254 assert_eq!(parse_runner_type("COPILOT"), Some(CliRunnerType::Copilot));
255 assert_eq!(
256 parse_runner_type("Claude_Code"),
257 Some(CliRunnerType::ClaudeCode)
258 );
259 }
260
261 #[test]
262 fn parse_unknown_returns_none() {
263 assert_eq!(parse_runner_type("gpt4"), None);
264 assert_eq!(parse_runner_type(""), None);
265 }
266
267 #[test]
268 fn all_providers_count() {
269 if cfg!(feature = "copilot-headless") {
270 assert_eq!(ALL_PROVIDERS.len(), 13);
271 } else {
272 assert_eq!(ALL_PROVIDERS.len(), 12);
273 }
274 }
275}