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, OpenCodeRunner, WarpCliRunner,
15};
16
17/// Create an `LlmProvider` instance for the given runner type
18///
19/// Resolves the CLI binary via environment variable override or PATH lookup,
20/// then constructs the appropriate runner with default configuration.
21///
22/// # Errors
23///
24/// Returns [`RunnerError`] if the CLI binary cannot be found.
25pub 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
51/// All provider types supported by embacle, in discovery priority order
52pub 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
65/// Parse a provider name string into a `CliRunnerType`
66///
67/// Accepts multiple naming conventions: `snake_case`, kebab-case, and
68/// short forms for flexible input.
69pub 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
87/// Format the list of valid provider names for error messages
88pub 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}