Skip to main content

embacle/
factory.rs

1// ABOUTME: Factory for creating LlmProvider instances from runner type identifiers
2// ABOUTME: Centralizes binary resolution and runner construction for all CLI runners
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use 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
18/// Create an `LlmProvider` instance for the given runner type
19///
20/// Resolves the CLI binary via environment variable override or PATH lookup,
21/// then constructs the appropriate runner with default configuration.
22///
23/// # Errors
24///
25/// Returns [`RunnerError`] if the CLI binary cannot be found.
26pub async fn create_runner(
27    runner_type: CliRunnerType,
28) -> Result<Box<dyn LlmProvider>, RunnerError> {
29    // CopilotHeadless uses its own config (env-based), not RunnerConfig
30    #[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
62/// Create an `LlmProvider` instance for the given runner type with a pre-built config.
63///
64/// Unlike [`create_runner()`], this function does not perform binary discovery;
65/// it uses the provided `RunnerConfig` directly.
66pub 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        // CopilotHeadless ignores RunnerConfig — uses env-based config
84        #[cfg(feature = "copilot-headless")]
85        CliRunnerType::CopilotHeadless => Box::new(crate::CopilotHeadlessRunner::from_env().await),
86    }
87}
88
89/// All provider types supported by embacle, in discovery priority order
90#[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/// All provider types supported by embacle, in discovery priority order
107#[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
124/// Parse a provider name string into a `CliRunnerType`
125///
126/// Accepts multiple naming conventions: `snake_case`, kebab-case, and
127/// short forms for flexible input.
128pub 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
152/// Format the list of valid provider names for error messages
153pub 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}