1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct CliSpec {
12 pub cli_id: &'static str,
14 pub label: &'static str,
16 pub bin_candidates: &'static [&'static str],
18 pub default_cli_args: &'static [&'static str],
20 pub adopt_command_patterns: &'static [&'static str],
24 pub supports_resume: bool,
26 pub passes_initial_prompt_via_args: bool,
29 pub inject_term_xterm: bool,
32}
33
34pub const CLI_SPECS: &[CliSpec] = &[
36 CliSpec {
37 cli_id: "claude-code",
38 label: "Claude",
39 bin_candidates: &["claude"],
40 default_cli_args: &[],
41 adopt_command_patterns: &["claude"],
42 supports_resume: true,
43 passes_initial_prompt_via_args: false,
44 inject_term_xterm: false,
45 },
46 CliSpec {
47 cli_id: "codex",
48 label: "Codex",
49 bin_candidates: &["codex"],
50 default_cli_args: &["--dangerously-bypass-approvals-and-sandbox", "--no-alt-screen"],
51 adopt_command_patterns: &["codex"],
52 supports_resume: true,
53 passes_initial_prompt_via_args: false,
54 inject_term_xterm: true,
55 },
56 CliSpec {
57 cli_id: "traex",
58 label: "Traex",
59 bin_candidates: &["traex"],
60 default_cli_args: &["-y"],
61 adopt_command_patterns: &["traex"],
62 supports_resume: true,
63 passes_initial_prompt_via_args: false,
64 inject_term_xterm: true,
65 },
66 CliSpec {
67 cli_id: "coco",
68 label: "CoCo",
69 bin_candidates: &["coco"],
70 default_cli_args: &[],
71 adopt_command_patterns: &[],
72 supports_resume: true,
73 passes_initial_prompt_via_args: false,
74 inject_term_xterm: false,
75 },
76 CliSpec {
77 cli_id: "gemini",
78 label: "Gemini",
79 bin_candidates: &["gemini"],
80 default_cli_args: &[],
81 adopt_command_patterns: &["gemini"],
82 supports_resume: false,
83 passes_initial_prompt_via_args: true,
84 inject_term_xterm: false,
85 },
86 CliSpec {
87 cli_id: "opencode",
88 label: "OpenCode",
89 bin_candidates: &["opencode-cli", "opencode"],
90 default_cli_args: &[],
91 adopt_command_patterns: &["opencode"],
92 supports_resume: false,
93 passes_initial_prompt_via_args: true,
94 inject_term_xterm: false,
95 },
96 CliSpec {
97 cli_id: "hermes",
98 label: "Hermes",
99 bin_candidates: &["hermes"],
100 default_cli_args: &[],
101 adopt_command_patterns: &["hermes"],
102 supports_resume: true,
103 passes_initial_prompt_via_args: false,
104 inject_term_xterm: false,
105 },
106 CliSpec {
107 cli_id: "antigravity",
108 label: "Antigravity",
109 bin_candidates: &["agy"],
110 default_cli_args: &[],
111 adopt_command_patterns: &[],
112 supports_resume: true,
113 passes_initial_prompt_via_args: false,
114 inject_term_xterm: false,
115 },
116 CliSpec {
117 cli_id: "kimi",
118 label: "Kimi",
119 bin_candidates: &["kimi"],
120 default_cli_args: &[],
121 adopt_command_patterns: &["kimi"],
122 supports_resume: true,
123 passes_initial_prompt_via_args: false,
124 inject_term_xterm: false,
125 },
126];
127
128pub fn cli_spec(cli_id: &str) -> Option<&'static CliSpec> {
130 CLI_SPECS.iter().find(|spec| spec.cli_id == cli_id)
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn every_spec_has_id_label_and_bins() {
139 for spec in CLI_SPECS {
140 assert!(!spec.cli_id.is_empty(), "empty cli_id");
141 assert!(!spec.label.is_empty(), "{}: empty label", spec.cli_id);
142 assert!(
143 !spec.bin_candidates.is_empty(),
144 "{}: empty bin_candidates",
145 spec.cli_id
146 );
147 }
148 }
149
150 #[test]
151 fn cli_spec_lookup() {
152 assert_eq!(cli_spec("kimi").map(|s| s.label), Some("Kimi"));
153 assert!(cli_spec("no-such-cli").is_none());
154 }
155
156 #[test]
157 fn cli_ids_are_unique() {
158 let mut ids: Vec<_> = CLI_SPECS.iter().map(|s| s.cli_id).collect();
159 ids.sort_unstable();
160 ids.dedup();
161 assert_eq!(ids.len(), CLI_SPECS.len(), "duplicate cli_id in CLI_SPECS");
162 }
163
164 #[test]
169 fn resume_support_matches_legacy_allow_list() {
170 let resume: Vec<_> = CLI_SPECS
171 .iter()
172 .filter(|s| s.supports_resume)
173 .map(|s| s.cli_id)
174 .collect();
175 assert_eq!(
176 resume,
177 vec![
178 "claude-code",
179 "codex",
180 "traex",
181 "coco",
182 "hermes",
183 "antigravity",
184 "kimi"
185 ]
186 );
187 }
188
189 #[test]
190 fn initial_prompt_via_args_matches_legacy_list() {
191 let pass: Vec<_> = CLI_SPECS
192 .iter()
193 .filter(|s| s.passes_initial_prompt_via_args)
194 .map(|s| s.cli_id)
195 .collect();
196 assert_eq!(pass, vec!["gemini", "opencode"]);
197 }
198
199 #[test]
200 fn term_injection_matches_legacy_list() {
201 let term: Vec<_> = CLI_SPECS
202 .iter()
203 .filter(|s| s.inject_term_xterm)
204 .map(|s| s.cli_id)
205 .collect();
206 assert_eq!(term, vec!["codex", "traex"]);
207 }
208
209 #[test]
210 fn default_args_match_legacy_values() {
211 assert_eq!(
212 cli_spec("codex").unwrap().default_cli_args,
213 &["--dangerously-bypass-approvals-and-sandbox", "--no-alt-screen"]
214 );
215 assert_eq!(cli_spec("traex").unwrap().default_cli_args, &["-y"]);
216 for spec in CLI_SPECS {
217 if spec.cli_id != "codex" && spec.cli_id != "traex" {
218 assert!(
219 spec.default_cli_args.is_empty(),
220 "{}: unexpected default args",
221 spec.cli_id
222 );
223 }
224 }
225 }
226
227 #[test]
228 fn adopt_patterns_match_legacy_recognition() {
229 let recognized: Vec<_> = CLI_SPECS
230 .iter()
231 .filter(|s| !s.adopt_command_patterns.is_empty())
232 .map(|s| s.cli_id)
233 .collect();
234 assert_eq!(
235 recognized,
236 vec![
237 "claude-code",
238 "codex",
239 "traex",
240 "gemini",
241 "opencode",
242 "hermes",
243 "kimi"
244 ]
245 );
246 }
247
248 #[test]
249 fn adopt_patterns_do_not_overlap_across_clis() {
250 for (i, a) in CLI_SPECS.iter().enumerate() {
253 for b in CLI_SPECS.iter().skip(i + 1) {
254 for pa in a.adopt_command_patterns {
255 for pb in b.adopt_command_patterns {
256 assert!(
257 !pa.contains(pb) && !pb.contains(pa),
258 "adopt patterns overlap: {} ({}) vs {} ({})",
259 a.cli_id,
260 pa,
261 b.cli_id,
262 pb
263 );
264 }
265 }
266 }
267 }
268 }
269}