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