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 adopt_command_patterns: &["claude"],
48 supports_resume: true,
49 passes_initial_prompt_via_args: false,
50 tui_ready_marker: Some("Welcome"),
51 inject_term_xterm: false,
52 },
53 CliSpec {
54 cli_id: "codex",
55 label: "Codex",
56 bin_candidates: &["codex"],
57 default_cli_args: &["--dangerously-bypass-approvals-and-sandbox", "--no-alt-screen"],
58 adopt_command_patterns: &["codex"],
59 supports_resume: true,
60 passes_initial_prompt_via_args: false,
61 tui_ready_marker: None,
62 inject_term_xterm: true,
63 },
64 CliSpec {
65 cli_id: "traex",
66 label: "Traex",
67 bin_candidates: &["traex"],
68 default_cli_args: &["-y"],
69 adopt_command_patterns: &["traex"],
70 supports_resume: true,
71 passes_initial_prompt_via_args: false,
72 tui_ready_marker: None,
73 inject_term_xterm: true,
74 },
75 CliSpec {
76 cli_id: "coco",
77 label: "CoCo",
78 bin_candidates: &["coco"],
79 default_cli_args: &[],
80 adopt_command_patterns: &[],
81 supports_resume: true,
82 passes_initial_prompt_via_args: false,
83 tui_ready_marker: Some("Welcome"),
84 inject_term_xterm: false,
85 },
86 CliSpec {
87 cli_id: "gemini",
88 label: "Gemini",
89 bin_candidates: &["gemini"],
90 default_cli_args: &[],
91 adopt_command_patterns: &["gemini"],
92 supports_resume: false,
93 passes_initial_prompt_via_args: true,
94 tui_ready_marker: None,
95 inject_term_xterm: false,
96 },
97 CliSpec {
98 cli_id: "opencode",
99 label: "OpenCode",
100 bin_candidates: &["opencode-cli", "opencode"],
101 default_cli_args: &[],
102 adopt_command_patterns: &["opencode"],
103 supports_resume: false,
104 passes_initial_prompt_via_args: true,
105 tui_ready_marker: None,
106 inject_term_xterm: false,
107 },
108 CliSpec {
109 cli_id: "hermes",
110 label: "Hermes",
111 bin_candidates: &["hermes"],
112 default_cli_args: &[],
113 adopt_command_patterns: &["hermes"],
114 supports_resume: true,
115 passes_initial_prompt_via_args: false,
116 tui_ready_marker: Some("Welcome"),
117 inject_term_xterm: false,
118 },
119 CliSpec {
120 cli_id: "antigravity",
121 label: "Antigravity",
122 bin_candidates: &["agy"],
123 default_cli_args: &[],
124 adopt_command_patterns: &[],
125 supports_resume: true,
126 passes_initial_prompt_via_args: false,
127 tui_ready_marker: Some("Welcome"),
128 inject_term_xterm: false,
129 },
130 CliSpec {
131 cli_id: "kimi",
132 label: "Kimi",
133 bin_candidates: &["kimi"],
134 default_cli_args: &[],
135 adopt_command_patterns: &["kimi"],
136 supports_resume: true,
137 passes_initial_prompt_via_args: false,
138 tui_ready_marker: Some("Welcome to Kimi Code"),
139 inject_term_xterm: false,
140 },
141];
142
143pub fn cli_spec(cli_id: &str) -> Option<&'static CliSpec> {
145 CLI_SPECS.iter().find(|spec| spec.cli_id == cli_id)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn every_spec_has_id_label_and_bins() {
154 for spec in CLI_SPECS {
155 assert!(!spec.cli_id.is_empty(), "empty cli_id");
156 assert!(!spec.label.is_empty(), "{}: empty label", spec.cli_id);
157 assert!(
158 !spec.bin_candidates.is_empty(),
159 "{}: empty bin_candidates",
160 spec.cli_id
161 );
162 }
163 }
164
165 #[test]
166 fn cli_spec_lookup() {
167 assert_eq!(cli_spec("kimi").map(|s| s.label), Some("Kimi"));
168 assert!(cli_spec("no-such-cli").is_none());
169 }
170
171 #[test]
172 fn cli_ids_are_unique() {
173 let mut ids: Vec<_> = CLI_SPECS.iter().map(|s| s.cli_id).collect();
174 ids.sort_unstable();
175 ids.dedup();
176 assert_eq!(ids.len(), CLI_SPECS.len(), "duplicate cli_id in CLI_SPECS");
177 }
178
179 #[test]
184 fn resume_support_matches_legacy_allow_list() {
185 let resume: Vec<_> = CLI_SPECS
186 .iter()
187 .filter(|s| s.supports_resume)
188 .map(|s| s.cli_id)
189 .collect();
190 assert_eq!(
191 resume,
192 vec![
193 "claude-code",
194 "codex",
195 "traex",
196 "coco",
197 "hermes",
198 "antigravity",
199 "kimi"
200 ]
201 );
202 }
203
204 #[test]
205 fn initial_prompt_via_args_matches_legacy_list() {
206 let pass: Vec<_> = CLI_SPECS
207 .iter()
208 .filter(|s| s.passes_initial_prompt_via_args)
209 .map(|s| s.cli_id)
210 .collect();
211 assert_eq!(pass, vec!["gemini", "opencode"]);
212 }
213
214 #[test]
215 fn tui_ready_markers_match_expected_list() {
216 let gated: Vec<_> = CLI_SPECS
217 .iter()
218 .filter_map(|s| s.tui_ready_marker.map(|marker| (s.cli_id, marker)))
219 .collect();
220 assert_eq!(
221 gated,
222 vec![
223 ("claude-code", "Welcome"),
224 ("coco", "Welcome"),
225 ("hermes", "Welcome"),
226 ("antigravity", "Welcome"),
227 ("kimi", "Welcome to Kimi Code"),
228 ]
229 );
230 }
231
232 #[test]
233 fn term_injection_matches_legacy_list() {
234 let term: Vec<_> = CLI_SPECS
235 .iter()
236 .filter(|s| s.inject_term_xterm)
237 .map(|s| s.cli_id)
238 .collect();
239 assert_eq!(term, vec!["codex", "traex"]);
240 }
241
242 #[test]
243 fn default_args_match_legacy_values() {
244 assert_eq!(
245 cli_spec("codex").unwrap().default_cli_args,
246 &["--dangerously-bypass-approvals-and-sandbox", "--no-alt-screen"]
247 );
248 assert_eq!(cli_spec("traex").unwrap().default_cli_args, &["-y"]);
249 for spec in CLI_SPECS {
250 if spec.cli_id != "codex" && spec.cli_id != "traex" {
251 assert!(
252 spec.default_cli_args.is_empty(),
253 "{}: unexpected default args",
254 spec.cli_id
255 );
256 }
257 }
258 }
259
260 #[test]
261 fn adopt_patterns_match_legacy_recognition() {
262 let recognized: Vec<_> = CLI_SPECS
263 .iter()
264 .filter(|s| !s.adopt_command_patterns.is_empty())
265 .map(|s| s.cli_id)
266 .collect();
267 assert_eq!(
268 recognized,
269 vec![
270 "claude-code",
271 "codex",
272 "traex",
273 "gemini",
274 "opencode",
275 "hermes",
276 "kimi"
277 ]
278 );
279 }
280
281 #[test]
282 fn adopt_patterns_do_not_overlap_across_clis() {
283 for (i, a) in CLI_SPECS.iter().enumerate() {
286 for b in CLI_SPECS.iter().skip(i + 1) {
287 for pa in a.adopt_command_patterns {
288 for pb in b.adopt_command_patterns {
289 assert!(
290 !pa.contains(pb) && !pb.contains(pa),
291 "adopt patterns overlap: {} ({}) vs {} ({})",
292 a.cli_id,
293 pa,
294 b.cli_id,
295 pb
296 );
297 }
298 }
299 }
300 }
301 }
302}