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, OpenCodeRunner, WarpCliRunner,
15};
16
17pub async fn create_runner(
26 runner_type: CliRunnerType,
27) -> Result<Box<dyn LlmProvider>, RunnerError> {
28 let binary_name = runner_type.binary_name();
29 let env_key = runner_type.env_override_key();
30 let env_override = env::var(env_key).ok();
31
32 let binary_path = resolve_binary(binary_name, env_override.as_deref())?;
33 let config = RunnerConfig::new(binary_path);
34
35 let runner: Box<dyn LlmProvider> = match runner_type {
36 CliRunnerType::ClaudeCode => Box::new(ClaudeCodeRunner::new(config)),
37 CliRunnerType::Copilot => Box::new(CopilotRunner::new(config).await),
38 CliRunnerType::CursorAgent => Box::new(CursorAgentRunner::new(config)),
39 CliRunnerType::OpenCode => Box::new(OpenCodeRunner::new(config)),
40 CliRunnerType::GeminiCli => Box::new(GeminiCliRunner::new(config)),
41 CliRunnerType::CodexCli => Box::new(CodexCliRunner::new(config)),
42 CliRunnerType::GooseCli => Box::new(GooseCliRunner::new(config)),
43 CliRunnerType::ClineCli => Box::new(ClineCliRunner::new(config)),
44 CliRunnerType::ContinueCli => Box::new(ContinueCliRunner::new(config)),
45 CliRunnerType::WarpCli => Box::new(WarpCliRunner::new(config)),
46 };
47
48 Ok(runner)
49}
50
51pub const ALL_PROVIDERS: &[CliRunnerType] = &[
53 CliRunnerType::ClaudeCode,
54 CliRunnerType::Copilot,
55 CliRunnerType::CursorAgent,
56 CliRunnerType::OpenCode,
57 CliRunnerType::GeminiCli,
58 CliRunnerType::CodexCli,
59 CliRunnerType::GooseCli,
60 CliRunnerType::ClineCli,
61 CliRunnerType::ContinueCli,
62 CliRunnerType::WarpCli,
63];
64
65pub fn parse_runner_type(s: &str) -> Option<CliRunnerType> {
70 match s.to_lowercase().as_str() {
71 "claude_code" | "claude" | "claudecode" => Some(CliRunnerType::ClaudeCode),
72 "copilot" => Some(CliRunnerType::Copilot),
73 "cursor_agent" | "cursoragent" | "cursor-agent" => Some(CliRunnerType::CursorAgent),
74 "opencode" | "open_code" => Some(CliRunnerType::OpenCode),
75 "gemini" | "gemini_cli" | "geminicli" | "gemini-cli" => Some(CliRunnerType::GeminiCli),
76 "codex" | "codex_cli" | "codexcli" | "codex-cli" => Some(CliRunnerType::CodexCli),
77 "goose" | "goose_cli" | "goosecli" | "goose-cli" => Some(CliRunnerType::GooseCli),
78 "cline" | "cline_cli" | "clinecli" | "cline-cli" => Some(CliRunnerType::ClineCli),
79 "continue" | "continue_cli" | "continuecli" | "continue-cli" | "cn" => {
80 Some(CliRunnerType::ContinueCli)
81 }
82 "warp" | "warp_cli" | "warpcli" | "warp-cli" | "oz" => Some(CliRunnerType::WarpCli),
83 _ => None,
84 }
85}
86
87pub const fn valid_provider_names() -> &'static str {
89 "claude_code, copilot, cursor_agent, opencode, gemini_cli, codex_cli, goose_cli, cline_cli, continue_cli, warp_cli"
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn parse_snake_case_variants() {
98 assert_eq!(
99 parse_runner_type("claude_code"),
100 Some(CliRunnerType::ClaudeCode)
101 );
102 assert_eq!(parse_runner_type("copilot"), Some(CliRunnerType::Copilot));
103 assert_eq!(
104 parse_runner_type("cursor_agent"),
105 Some(CliRunnerType::CursorAgent)
106 );
107 assert_eq!(parse_runner_type("opencode"), Some(CliRunnerType::OpenCode));
108 assert_eq!(
109 parse_runner_type("gemini_cli"),
110 Some(CliRunnerType::GeminiCli)
111 );
112 assert_eq!(
113 parse_runner_type("codex_cli"),
114 Some(CliRunnerType::CodexCli)
115 );
116 assert_eq!(
117 parse_runner_type("goose_cli"),
118 Some(CliRunnerType::GooseCli)
119 );
120 assert_eq!(
121 parse_runner_type("cline_cli"),
122 Some(CliRunnerType::ClineCli)
123 );
124 assert_eq!(
125 parse_runner_type("continue_cli"),
126 Some(CliRunnerType::ContinueCli)
127 );
128 assert_eq!(parse_runner_type("warp_cli"), Some(CliRunnerType::WarpCli));
129 }
130
131 #[test]
132 fn parse_short_forms() {
133 assert_eq!(parse_runner_type("claude"), Some(CliRunnerType::ClaudeCode));
134 assert_eq!(
135 parse_runner_type("cursor-agent"),
136 Some(CliRunnerType::CursorAgent)
137 );
138 assert_eq!(parse_runner_type("gemini"), Some(CliRunnerType::GeminiCli));
139 assert_eq!(parse_runner_type("codex"), Some(CliRunnerType::CodexCli));
140 assert_eq!(parse_runner_type("goose"), Some(CliRunnerType::GooseCli));
141 assert_eq!(parse_runner_type("cline"), Some(CliRunnerType::ClineCli));
142 assert_eq!(
143 parse_runner_type("continue"),
144 Some(CliRunnerType::ContinueCli)
145 );
146 assert_eq!(parse_runner_type("cn"), Some(CliRunnerType::ContinueCli));
147 assert_eq!(parse_runner_type("warp"), Some(CliRunnerType::WarpCli));
148 assert_eq!(parse_runner_type("oz"), Some(CliRunnerType::WarpCli));
149 }
150
151 #[test]
152 fn parse_case_insensitive() {
153 assert_eq!(parse_runner_type("COPILOT"), Some(CliRunnerType::Copilot));
154 assert_eq!(
155 parse_runner_type("Claude_Code"),
156 Some(CliRunnerType::ClaudeCode)
157 );
158 }
159
160 #[test]
161 fn parse_unknown_returns_none() {
162 assert_eq!(parse_runner_type("gpt4"), None);
163 assert_eq!(parse_runner_type(""), None);
164 }
165
166 #[test]
167 fn all_providers_has_ten_entries() {
168 assert_eq!(ALL_PROVIDERS.len(), 10);
169 }
170}