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 tui_ready_marker: Option<&'static str>,
35 pub inject_term_xterm: bool,
38}
39
40pub const CLI_SPECS: &[CliSpec] = &[
42 CliSpec {
43 cli_id: "claude-code",
44 label: "Claude",
45 bin_candidates: &["claude"],
46 default_cli_args: &[
47 "--dangerously-skip-permissions",
48 "--settings",
49 r#"{"skipDangerousModePermissionPrompt":true,"permissions":{"defaultMode":"bypassPermissions"}}"#,
50 "--disallowed-tools",
51 "EnterPlanMode,ExitPlanMode",
52 ],
53 adopt_command_patterns: &["claude"],
54 supports_resume: true,
55 passes_initial_prompt_via_args: false,
56 tui_ready_marker: Some("Welcome"),
57 inject_term_xterm: false,
58 },
59 CliSpec {
60 cli_id: "codex",
61 label: "Codex",
62 bin_candidates: &["codex"],
63 default_cli_args: &[
64 "--dangerously-bypass-approvals-and-sandbox",
65 "--no-alt-screen",
66 ],
67 adopt_command_patterns: &["codex"],
68 supports_resume: true,
69 passes_initial_prompt_via_args: false,
70 tui_ready_marker: Some("›"),
71 inject_term_xterm: true,
72 },
73 CliSpec {
74 cli_id: "traex",
75 label: "Traex",
76 bin_candidates: &["traex"],
77 default_cli_args: &["-y"],
78 adopt_command_patterns: &["traex"],
79 supports_resume: true,
80 passes_initial_prompt_via_args: false,
81 tui_ready_marker: Some("›"),
82 inject_term_xterm: true,
83 },
84 CliSpec {
85 cli_id: "coco",
86 label: "CoCo",
87 bin_candidates: &["coco"],
88 default_cli_args: &[
89 "--yolo",
90 "--disallowed-tool",
91 "EnterPlanMode",
92 "--disallowed-tool",
93 "ExitPlanMode",
94 ],
95 adopt_command_patterns: &[],
96 supports_resume: true,
97 passes_initial_prompt_via_args: false,
98 tui_ready_marker: Some("Welcome"),
99 inject_term_xterm: false,
100 },
101 CliSpec {
102 cli_id: "gemini",
103 label: "Gemini",
104 bin_candidates: &["gemini"],
105 default_cli_args: &["--yolo"],
106 adopt_command_patterns: &["gemini"],
107 supports_resume: false,
108 passes_initial_prompt_via_args: true,
109 tui_ready_marker: None,
110 inject_term_xterm: false,
111 },
112 CliSpec {
113 cli_id: "opencode",
114 label: "OpenCode",
115 bin_candidates: &["opencode-cli", "opencode"],
116 default_cli_args: &[],
117 adopt_command_patterns: &["opencode"],
118 supports_resume: false,
119 passes_initial_prompt_via_args: true,
120 tui_ready_marker: None,
121 inject_term_xterm: false,
122 },
123 CliSpec {
124 cli_id: "hermes",
125 label: "Hermes",
126 bin_candidates: &["hermes"],
127 default_cli_args: &["--yolo", "--accept-hooks", "--pass-session-id"],
128 adopt_command_patterns: &["hermes"],
129 supports_resume: true,
130 passes_initial_prompt_via_args: false,
131 tui_ready_marker: Some("Welcome"),
132 inject_term_xterm: false,
133 },
134 CliSpec {
135 cli_id: "antigravity",
136 label: "Antigravity",
137 bin_candidates: &["agy"],
138 default_cli_args: &["--dangerously-skip-permissions"],
139 adopt_command_patterns: &[],
140 supports_resume: true,
141 passes_initial_prompt_via_args: false,
142 tui_ready_marker: Some("Welcome"),
143 inject_term_xterm: false,
144 },
145 CliSpec {
146 cli_id: "kimi",
147 label: "Kimi",
148 bin_candidates: &["kimi"],
149 default_cli_args: &["--yolo"],
150 adopt_command_patterns: &["kimi"],
151 supports_resume: true,
152 passes_initial_prompt_via_args: false,
153 tui_ready_marker: Some("Welcome to Kimi Code"),
154 inject_term_xterm: false,
155 },
156 CliSpec {
157 cli_id: "grok",
158 label: "Grok Build",
159 bin_candidates: &["grok"],
160 default_cli_args: &["--always-approve", "--no-alt-screen"],
161 adopt_command_patterns: &["grok"],
162 supports_resume: true,
163 passes_initial_prompt_via_args: false,
164 tui_ready_marker: Some("Grok"),
165 inject_term_xterm: false,
166 },
167];
168
169pub fn cli_spec(cli_id: &str) -> Option<&'static CliSpec> {
171 CLI_SPECS.iter().find(|spec| spec.cli_id == cli_id)
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn every_spec_has_id_label_and_bins() {
180 for spec in CLI_SPECS {
181 assert!(!spec.cli_id.is_empty(), "empty cli_id");
182 assert!(!spec.label.is_empty(), "{}: empty label", spec.cli_id);
183 assert!(
184 !spec.bin_candidates.is_empty(),
185 "{}: empty bin_candidates",
186 spec.cli_id
187 );
188 }
189 }
190
191 #[test]
192 fn cli_spec_lookup() {
193 assert_eq!(cli_spec("kimi").map(|s| s.label), Some("Kimi"));
194 assert!(cli_spec("no-such-cli").is_none());
195 }
196
197 #[test]
198 fn cli_ids_are_unique() {
199 let mut ids: Vec<_> = CLI_SPECS.iter().map(|s| s.cli_id).collect();
200 ids.sort_unstable();
201 ids.dedup();
202 assert_eq!(ids.len(), CLI_SPECS.len(), "duplicate cli_id in CLI_SPECS");
203 }
204
205 #[test]
210 fn resume_support_matches_legacy_allow_list() {
211 let resume: Vec<_> = CLI_SPECS
212 .iter()
213 .filter(|s| s.supports_resume)
214 .map(|s| s.cli_id)
215 .collect();
216 assert_eq!(
217 resume,
218 vec![
219 "claude-code",
220 "codex",
221 "traex",
222 "coco",
223 "hermes",
224 "antigravity",
225 "kimi",
226 "grok"
227 ]
228 );
229 }
230
231 #[test]
232 fn initial_prompt_via_args_matches_legacy_list() {
233 let pass: Vec<_> = CLI_SPECS
234 .iter()
235 .filter(|s| s.passes_initial_prompt_via_args)
236 .map(|s| s.cli_id)
237 .collect();
238 assert_eq!(pass, vec!["gemini", "opencode"]);
239 }
240
241 #[test]
242 fn tui_ready_markers_match_expected_list() {
243 let gated: Vec<_> = CLI_SPECS
244 .iter()
245 .filter_map(|s| s.tui_ready_marker.map(|marker| (s.cli_id, marker)))
246 .collect();
247 assert_eq!(
248 gated,
249 vec![
250 ("claude-code", "Welcome"),
251 ("codex", "›"),
252 ("traex", "›"),
253 ("coco", "Welcome"),
254 ("hermes", "Welcome"),
255 ("antigravity", "Welcome"),
256 ("kimi", "Welcome to Kimi Code"),
257 ("grok", "Grok"),
258 ]
259 );
260 }
261
262 #[test]
263 fn term_injection_matches_legacy_list() {
264 let term: Vec<_> = CLI_SPECS
265 .iter()
266 .filter(|s| s.inject_term_xterm)
267 .map(|s| s.cli_id)
268 .collect();
269 assert_eq!(term, vec!["codex", "traex"]);
270 }
271
272 #[test]
273 fn default_args_match_legacy_values() {
274 assert_eq!(
275 cli_spec("claude-code").unwrap().default_cli_args,
276 &[
277 "--dangerously-skip-permissions",
278 "--settings",
279 r#"{"skipDangerousModePermissionPrompt":true,"permissions":{"defaultMode":"bypassPermissions"}}"#,
280 "--disallowed-tools",
281 "EnterPlanMode,ExitPlanMode",
282 ]
283 );
284 assert_eq!(
285 cli_spec("codex").unwrap().default_cli_args,
286 &[
287 "--dangerously-bypass-approvals-and-sandbox",
288 "--no-alt-screen"
289 ]
290 );
291 assert_eq!(cli_spec("traex").unwrap().default_cli_args, &["-y"]);
292 assert_eq!(
293 cli_spec("coco").unwrap().default_cli_args,
294 &[
295 "--yolo",
296 "--disallowed-tool",
297 "EnterPlanMode",
298 "--disallowed-tool",
299 "ExitPlanMode",
300 ]
301 );
302 assert_eq!(cli_spec("gemini").unwrap().default_cli_args, &["--yolo"]);
303 assert!(cli_spec("opencode").unwrap().default_cli_args.is_empty());
304 assert_eq!(
305 cli_spec("hermes").unwrap().default_cli_args,
306 &["--yolo", "--accept-hooks", "--pass-session-id"]
307 );
308 assert_eq!(
309 cli_spec("antigravity").unwrap().default_cli_args,
310 &["--dangerously-skip-permissions"]
311 );
312 assert_eq!(cli_spec("kimi").unwrap().default_cli_args, &["--yolo"]);
313 assert_eq!(
314 cli_spec("grok").unwrap().default_cli_args,
315 &["--always-approve", "--no-alt-screen"]
316 );
317 }
318
319 #[test]
320 fn adopt_patterns_match_legacy_recognition() {
321 let recognized: Vec<_> = CLI_SPECS
322 .iter()
323 .filter(|s| !s.adopt_command_patterns.is_empty())
324 .map(|s| s.cli_id)
325 .collect();
326 assert_eq!(
327 recognized,
328 vec![
329 "claude-code",
330 "codex",
331 "traex",
332 "gemini",
333 "opencode",
334 "hermes",
335 "kimi",
336 "grok"
337 ]
338 );
339 }
340
341 #[test]
342 fn adopt_patterns_do_not_overlap_across_clis() {
343 for (i, a) in CLI_SPECS.iter().enumerate() {
346 for b in CLI_SPECS.iter().skip(i + 1) {
347 for pa in a.adopt_command_patterns {
348 for pb in b.adopt_command_patterns {
349 assert!(
350 !pa.contains(pb) && !pb.contains(pa),
351 "adopt patterns overlap: {} ({}) vs {} ({})",
352 a.cli_id,
353 pa,
354 b.cli_id,
355 pb
356 );
357 }
358 }
359 }
360 }
361 }
362}