Skip to main content

ralph/commands/runner/
list.rs

1//! Runner list command.
2//!
3//! Responsibilities:
4//! - List all available runners with brief descriptions.
5
6use anyhow::Result;
7use serde::Serialize;
8
9use crate::cli::runner::RunnerFormat;
10use crate::contracts::Runner;
11use crate::runner::default_model_for_runner;
12
13#[derive(Debug, Clone, Serialize)]
14pub struct RunnerInfo {
15    pub id: String,
16    pub name: String,
17    pub provider: String,
18    pub default_model: String,
19}
20
21fn get_all_runners() -> Vec<RunnerInfo> {
22    vec![
23        RunnerInfo {
24            id: "claude".into(),
25            name: "Anthropic Claude Code".into(),
26            provider: "Anthropic".into(),
27            default_model: default_model_for_runner(&Runner::Claude)
28                .as_str()
29                .to_string(),
30        },
31        RunnerInfo {
32            id: "codex".into(),
33            name: "OpenAI Codex CLI".into(),
34            provider: "OpenAI".into(),
35            default_model: default_model_for_runner(&Runner::Codex)
36                .as_str()
37                .to_string(),
38        },
39        RunnerInfo {
40            id: "opencode".into(),
41            name: "Opencode".into(),
42            provider: "Flexible".into(),
43            default_model: default_model_for_runner(&Runner::Opencode)
44                .as_str()
45                .to_string(),
46        },
47        RunnerInfo {
48            id: "gemini".into(),
49            name: "Google Gemini CLI".into(),
50            provider: "Google".into(),
51            default_model: default_model_for_runner(&Runner::Gemini)
52                .as_str()
53                .to_string(),
54        },
55        RunnerInfo {
56            id: "cursor".into(),
57            name: "Cursor Agent".into(),
58            provider: "Cursor".into(),
59            default_model: default_model_for_runner(&Runner::Cursor)
60                .as_str()
61                .to_string(),
62        },
63        RunnerInfo {
64            id: "kimi".into(),
65            name: "Kimi CLI".into(),
66            provider: "Moonshot AI".into(),
67            default_model: default_model_for_runner(&Runner::Kimi).as_str().to_string(),
68        },
69        RunnerInfo {
70            id: "pi".into(),
71            name: "Pi Coding Agent".into(),
72            provider: "Pi".into(),
73            default_model: default_model_for_runner(&Runner::Pi).as_str().to_string(),
74        },
75    ]
76}
77
78pub fn handle_list(format: RunnerFormat) -> Result<()> {
79    let runners = get_all_runners();
80
81    match format {
82        RunnerFormat::Text => {
83            println!("Available runners:\n");
84            for r in &runners {
85                println!("  {:12} {} (default: {})", r.id, r.name, r.default_model);
86            }
87            println!("\nUse 'ralph runner capabilities <id>' for details.");
88        }
89        RunnerFormat::Json => {
90            println!("{}", serde_json::to_string_pretty(&runners)?);
91        }
92    }
93
94    Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn get_all_runners_returns_all_built_ins() {
103        let runners = get_all_runners();
104        assert_eq!(runners.len(), 7);
105
106        let ids: Vec<_> = runners.iter().map(|r| r.id.as_str()).collect();
107        assert!(ids.contains(&"claude"));
108        assert!(ids.contains(&"codex"));
109        assert!(ids.contains(&"opencode"));
110        assert!(ids.contains(&"gemini"));
111        assert!(ids.contains(&"cursor"));
112        assert!(ids.contains(&"kimi"));
113        assert!(ids.contains(&"pi"));
114    }
115
116    #[test]
117    fn runner_info_has_required_fields() {
118        let runners = get_all_runners();
119        for r in &runners {
120            assert!(!r.id.is_empty(), "runner {} has empty id", r.name);
121            assert!(!r.name.is_empty(), "runner {} has empty name", r.id);
122            assert!(!r.provider.is_empty(), "runner {} has empty provider", r.id);
123            assert!(
124                !r.default_model.is_empty(),
125                "runner {} has empty default_model",
126                r.id
127            );
128        }
129    }
130}