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().await));
33 }
34
35 let binary_name = runner_type.binary_name();
36 let env_key = runner_type.env_override_key();
37 let env_override = env::var(env_key).ok();
38
39 let binary_path = resolve_binary(binary_name, env_override.as_deref())?;
40 let config = RunnerConfig::new(binary_path);
41
42 let runner: Box<dyn LlmProvider> = match runner_type {
43 CliRunnerType::ClaudeCode => Box::new(ClaudeCodeRunner::new(config)),
44 CliRunnerType::Copilot => Box::new(CopilotRunner::new(config).await),
45 CliRunnerType::CursorAgent => Box::new(CursorAgentRunner::new(config)),
46 CliRunnerType::OpenCode => Box::new(OpenCodeRunner::new(config)),
47 CliRunnerType::GeminiCli => Box::new(GeminiCliRunner::new(config)),
48 CliRunnerType::CodexCli => Box::new(CodexCliRunner::new(config)),
49 CliRunnerType::GooseCli => Box::new(GooseCliRunner::new(config)),
50 CliRunnerType::ClineCli => Box::new(ClineCliRunner::new(config)),
51 CliRunnerType::ContinueCli => Box::new(ContinueCliRunner::new(config)),
52 CliRunnerType::WarpCli => Box::new(WarpCliRunner::new(config)),
53 CliRunnerType::KiroCli => Box::new(KiroCliRunner::new(config)),
54 CliRunnerType::KiloCli => Box::new(KiloCliRunner::new(config)),
55 #[cfg(feature = "copilot-headless")]
56 CliRunnerType::CopilotHeadless => unreachable!("handled above"),
57 };
58
59 Ok(runner)
60}
61
62pub async fn create_runner_with_config(
67 runner_type: CliRunnerType,
68 config: RunnerConfig,
69) -> Box<dyn LlmProvider> {
70 match runner_type {
71 CliRunnerType::ClaudeCode => Box::new(ClaudeCodeRunner::new(config)),
72 CliRunnerType::Copilot => Box::new(CopilotRunner::new(config).await),
73 CliRunnerType::CursorAgent => Box::new(CursorAgentRunner::new(config)),
74 CliRunnerType::OpenCode => Box::new(OpenCodeRunner::new(config)),
75 CliRunnerType::GeminiCli => Box::new(GeminiCliRunner::new(config)),
76 CliRunnerType::CodexCli => Box::new(CodexCliRunner::new(config)),
77 CliRunnerType::GooseCli => Box::new(GooseCliRunner::new(config)),
78 CliRunnerType::ClineCli => Box::new(ClineCliRunner::new(config)),
79 CliRunnerType::ContinueCli => Box::new(ContinueCliRunner::new(config)),
80 CliRunnerType::WarpCli => Box::new(WarpCliRunner::new(config)),
81 CliRunnerType::KiroCli => Box::new(KiroCliRunner::new(config)),
82 CliRunnerType::KiloCli => Box::new(KiloCliRunner::new(config)),
83 #[cfg(feature = "copilot-headless")]
85 CliRunnerType::CopilotHeadless => Box::new(crate::CopilotHeadlessRunner::from_env().await),
86 }
87}
88
89#[cfg(not(feature = "copilot-headless"))]
91pub const ALL_PROVIDERS: &[CliRunnerType] = &[
92 CliRunnerType::ClaudeCode,
93 CliRunnerType::Copilot,
94 CliRunnerType::CursorAgent,
95 CliRunnerType::OpenCode,
96 CliRunnerType::GeminiCli,
97 CliRunnerType::CodexCli,
98 CliRunnerType::GooseCli,
99 CliRunnerType::ClineCli,
100 CliRunnerType::ContinueCli,
101 CliRunnerType::WarpCli,
102 CliRunnerType::KiroCli,
103 CliRunnerType::KiloCli,
104];
105
106#[cfg(feature = "copilot-headless")]
108pub const ALL_PROVIDERS: &[CliRunnerType] = &[
109 CliRunnerType::ClaudeCode,
110 CliRunnerType::Copilot,
111 CliRunnerType::CopilotHeadless,
112 CliRunnerType::CursorAgent,
113 CliRunnerType::OpenCode,
114 CliRunnerType::GeminiCli,
115 CliRunnerType::CodexCli,
116 CliRunnerType::GooseCli,
117 CliRunnerType::ClineCli,
118 CliRunnerType::ContinueCli,
119 CliRunnerType::WarpCli,
120 CliRunnerType::KiroCli,
121 CliRunnerType::KiloCli,
122];
123
124pub fn parse_runner_type(s: &str) -> Option<CliRunnerType> {
129 match s.to_lowercase().as_str() {
130 "claude_code" | "claude" | "claudecode" => Some(CliRunnerType::ClaudeCode),
131 "copilot" => Some(CliRunnerType::Copilot),
132 "cursor_agent" | "cursoragent" | "cursor-agent" => Some(CliRunnerType::CursorAgent),
133 "opencode" | "open_code" => Some(CliRunnerType::OpenCode),
134 "gemini" | "gemini_cli" | "geminicli" | "gemini-cli" => Some(CliRunnerType::GeminiCli),
135 "codex" | "codex_cli" | "codexcli" | "codex-cli" => Some(CliRunnerType::CodexCli),
136 "goose" | "goose_cli" | "goosecli" | "goose-cli" => Some(CliRunnerType::GooseCli),
137 "cline" | "cline_cli" | "clinecli" | "cline-cli" => Some(CliRunnerType::ClineCli),
138 "continue" | "continue_cli" | "continuecli" | "continue-cli" | "cn" => {
139 Some(CliRunnerType::ContinueCli)
140 }
141 "warp" | "warp_cli" | "warpcli" | "warp-cli" | "oz" => Some(CliRunnerType::WarpCli),
142 "kiro" | "kiro_cli" | "kirocli" | "kiro-cli" => Some(CliRunnerType::KiroCli),
143 "kilo" | "kilo_cli" | "kilocli" | "kilo-cli" | "kilocode" => Some(CliRunnerType::KiloCli),
144 #[cfg(feature = "copilot-headless")]
145 "copilot_headless" | "copilot-headless" | "copilotheadless" | "headless" => {
146 Some(CliRunnerType::CopilotHeadless)
147 }
148 _ => None,
149 }
150}
151
152pub const fn valid_provider_names() -> &'static str {
154 if cfg!(feature = "copilot-headless") {
155 "claude_code, copilot, copilot_headless, cursor_agent, opencode, gemini_cli, codex_cli, goose_cli, cline_cli, continue_cli, warp_cli, kiro_cli, kilo_cli"
156 } else {
157 "claude_code, copilot, cursor_agent, opencode, gemini_cli, codex_cli, goose_cli, cline_cli, continue_cli, warp_cli, kiro_cli, kilo_cli"
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn parse_snake_case_variants() {
167 assert_eq!(
168 parse_runner_type("claude_code"),
169 Some(CliRunnerType::ClaudeCode)
170 );
171 assert_eq!(parse_runner_type("copilot"), Some(CliRunnerType::Copilot));
172 assert_eq!(
173 parse_runner_type("cursor_agent"),
174 Some(CliRunnerType::CursorAgent)
175 );
176 assert_eq!(parse_runner_type("opencode"), Some(CliRunnerType::OpenCode));
177 assert_eq!(
178 parse_runner_type("gemini_cli"),
179 Some(CliRunnerType::GeminiCli)
180 );
181 assert_eq!(
182 parse_runner_type("codex_cli"),
183 Some(CliRunnerType::CodexCli)
184 );
185 assert_eq!(
186 parse_runner_type("goose_cli"),
187 Some(CliRunnerType::GooseCli)
188 );
189 assert_eq!(
190 parse_runner_type("cline_cli"),
191 Some(CliRunnerType::ClineCli)
192 );
193 assert_eq!(
194 parse_runner_type("continue_cli"),
195 Some(CliRunnerType::ContinueCli)
196 );
197 assert_eq!(parse_runner_type("warp_cli"), Some(CliRunnerType::WarpCli));
198 assert_eq!(parse_runner_type("kiro_cli"), Some(CliRunnerType::KiroCli));
199 assert_eq!(parse_runner_type("kilo_cli"), Some(CliRunnerType::KiloCli));
200 }
201
202 #[test]
203 fn parse_short_forms() {
204 assert_eq!(parse_runner_type("claude"), Some(CliRunnerType::ClaudeCode));
205 assert_eq!(
206 parse_runner_type("cursor-agent"),
207 Some(CliRunnerType::CursorAgent)
208 );
209 assert_eq!(parse_runner_type("gemini"), Some(CliRunnerType::GeminiCli));
210 assert_eq!(parse_runner_type("codex"), Some(CliRunnerType::CodexCli));
211 assert_eq!(parse_runner_type("goose"), Some(CliRunnerType::GooseCli));
212 assert_eq!(parse_runner_type("cline"), Some(CliRunnerType::ClineCli));
213 assert_eq!(
214 parse_runner_type("continue"),
215 Some(CliRunnerType::ContinueCli)
216 );
217 assert_eq!(parse_runner_type("cn"), Some(CliRunnerType::ContinueCli));
218 assert_eq!(parse_runner_type("warp"), Some(CliRunnerType::WarpCli));
219 assert_eq!(parse_runner_type("oz"), Some(CliRunnerType::WarpCli));
220 assert_eq!(parse_runner_type("kiro"), Some(CliRunnerType::KiroCli));
221 assert_eq!(parse_runner_type("kiro-cli"), Some(CliRunnerType::KiroCli));
222 assert_eq!(parse_runner_type("kilo"), Some(CliRunnerType::KiloCli));
223 assert_eq!(parse_runner_type("kilo-cli"), Some(CliRunnerType::KiloCli));
224 assert_eq!(parse_runner_type("kilocode"), Some(CliRunnerType::KiloCli));
225 }
226
227 #[test]
228 fn parse_case_insensitive() {
229 assert_eq!(parse_runner_type("COPILOT"), Some(CliRunnerType::Copilot));
230 assert_eq!(
231 parse_runner_type("Claude_Code"),
232 Some(CliRunnerType::ClaudeCode)
233 );
234 }
235
236 #[test]
237 fn parse_unknown_returns_none() {
238 assert_eq!(parse_runner_type("gpt4"), None);
239 assert_eq!(parse_runner_type(""), None);
240 }
241
242 #[test]
243 fn all_providers_count() {
244 if cfg!(feature = "copilot-headless") {
245 assert_eq!(ALL_PROVIDERS.len(), 13);
246 } else {
247 assert_eq!(ALL_PROVIDERS.len(), 12);
248 }
249 }
250}