1use crate::Harness;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum PromptMode {
12 Flag(&'static str),
14 KeystrokeInjection,
17 Unsupported,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ProcessMatcher {
24 Exact(&'static str),
26 Suffix(&'static str),
28 Contains(&'static str),
30}
31
32impl ProcessMatcher {
33 #[must_use]
35 pub fn matches(&self, candidate: &str) -> bool {
36 match self {
37 Self::Exact(expected) => candidate == *expected,
38 Self::Suffix(suffix) => candidate.ends_with(suffix),
39 Self::Contains(needle) => candidate.contains(needle),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct HarnessDefinition {
47 pub id: &'static str,
49 pub aliases: &'static [&'static str],
51 pub display_name: &'static str,
53 pub harness: Harness,
55 pub binary: &'static str,
57 pub default_args: &'static [&'static str],
59 pub model_flag: Option<&'static str>,
61 pub prompt_mode: PromptMode,
63 pub version_args: &'static [&'static str],
65 pub process_matchers: &'static [ProcessMatcher],
67 pub discovery_enabled: bool,
72 pub allow_bare_cmdline_match: bool,
78}
79
80const CLAUDE_MATCHERS: &[ProcessMatcher] = &[
81 ProcessMatcher::Exact("claude"),
82 ProcessMatcher::Suffix("/claude"),
83 ProcessMatcher::Contains("claude/versions/"),
84];
85
86const PI_MATCHERS: &[ProcessMatcher] = &[
87 ProcessMatcher::Suffix("/bin/pi"),
88 ProcessMatcher::Suffix("/pi"),
89 ProcessMatcher::Contains("pi-coding-agent"),
90];
91
92const CODEX_MATCHERS: &[ProcessMatcher] = &[
93 ProcessMatcher::Exact("codex"),
94 ProcessMatcher::Suffix("/codex"),
95 ProcessMatcher::Contains("codex-cli"),
96];
97
98const AMP_MATCHERS: &[ProcessMatcher] =
99 &[ProcessMatcher::Exact("amp"), ProcessMatcher::Suffix("/amp")];
100
101const QWEN_MATCHERS: &[ProcessMatcher] = &[
102 ProcessMatcher::Exact("qwen"),
103 ProcessMatcher::Suffix("/qwen"),
104 ProcessMatcher::Exact("qwen-code"),
105 ProcessMatcher::Suffix("/qwen-code"),
106];
107
108const GEMINI_MATCHERS: &[ProcessMatcher] = &[
109 ProcessMatcher::Exact("gemini"),
110 ProcessMatcher::Suffix("/gemini"),
111 ProcessMatcher::Contains("gemini-cli"),
112];
113
114pub const BUILTIN_HARNESSES: &[HarnessDefinition] = &[
116 HarnessDefinition {
117 id: "claude",
118 aliases: &["claude_code", "claude-code", "cc"],
119 display_name: "Claude Code",
120 harness: Harness::ClaudeCode,
121 binary: "claude",
122 default_args: &[],
123 model_flag: Some("--model"),
124 prompt_mode: PromptMode::KeystrokeInjection,
125 version_args: &["--version"],
126 process_matchers: CLAUDE_MATCHERS,
127 discovery_enabled: true,
128 allow_bare_cmdline_match: true,
129 },
130 HarnessDefinition {
131 id: "pi",
132 aliases: &[],
133 display_name: "pi",
134 harness: Harness::Pi,
135 binary: "pi",
136 default_args: &[],
137 model_flag: Some("--model"),
138 prompt_mode: PromptMode::KeystrokeInjection,
139 version_args: &["--version"],
140 process_matchers: PI_MATCHERS,
141 discovery_enabled: true,
142 allow_bare_cmdline_match: false,
143 },
144 HarnessDefinition {
145 id: "codex",
146 aliases: &["codex-cli"],
147 display_name: "Codex CLI",
148 harness: Harness::Codex,
149 binary: "codex",
150 default_args: &[],
151 model_flag: None,
152 prompt_mode: PromptMode::KeystrokeInjection,
153 version_args: &["--version"],
154 process_matchers: CODEX_MATCHERS,
155 discovery_enabled: false,
156 allow_bare_cmdline_match: true,
157 },
158 HarnessDefinition {
159 id: "amp",
160 aliases: &[],
161 display_name: "Amp",
162 harness: Harness::Amp,
163 binary: "amp",
164 default_args: &[],
165 model_flag: None,
166 prompt_mode: PromptMode::KeystrokeInjection,
167 version_args: &["--version"],
168 process_matchers: AMP_MATCHERS,
169 discovery_enabled: false,
170 allow_bare_cmdline_match: true,
171 },
172 HarnessDefinition {
173 id: "qwen",
174 aliases: &["qwen-code"],
175 display_name: "Qwen Code",
176 harness: Harness::Qwen,
177 binary: "qwen",
178 default_args: &[],
179 model_flag: None,
180 prompt_mode: PromptMode::KeystrokeInjection,
181 version_args: &["--version"],
182 process_matchers: QWEN_MATCHERS,
183 discovery_enabled: false,
184 allow_bare_cmdline_match: true,
185 },
186 HarnessDefinition {
187 id: "gemini",
188 aliases: &["gemini-cli"],
189 display_name: "Gemini CLI",
190 harness: Harness::Gemini,
191 binary: "gemini",
192 default_args: &[],
193 model_flag: None,
194 prompt_mode: PromptMode::KeystrokeInjection,
195 version_args: &["--version"],
196 process_matchers: GEMINI_MATCHERS,
197 discovery_enabled: false,
198 allow_bare_cmdline_match: true,
199 },
200];
201
202#[must_use]
204pub fn default_harness_definition() -> &'static HarnessDefinition {
205 if let Some(definition) = find_harness_definition("claude") {
207 definition
208 } else {
209 BUILTIN_HARNESSES
212 .iter()
213 .next()
214 .unwrap_or(&FALLBACK_HARNESS_DEFINITION)
215 }
216}
217
218const FALLBACK_HARNESS_DEFINITION: HarnessDefinition = HarnessDefinition {
219 id: "unknown",
220 aliases: &[],
221 display_name: "unknown",
222 harness: Harness::Unknown,
223 binary: "claude",
224 default_args: &[],
225 model_flag: None,
226 prompt_mode: PromptMode::Unsupported,
227 version_args: &[],
228 process_matchers: &[],
229 discovery_enabled: false,
230 allow_bare_cmdline_match: false,
231};
232
233#[must_use]
235pub fn find_harness_definition(id: &str) -> Option<&'static HarnessDefinition> {
236 BUILTIN_HARNESSES
237 .iter()
238 .find(|definition| definition.id == id || definition.aliases.contains(&id))
239}
240
241pub fn builtin_harnesses() -> impl Iterator<Item = &'static HarnessDefinition> {
243 BUILTIN_HARNESSES.iter()
244}
245
246#[must_use]
248pub fn builtin_harness_ids_display() -> String {
249 BUILTIN_HARNESSES
250 .iter()
251 .map(|definition| definition.id)
252 .collect::<Vec<_>>()
253 .join(", ")
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn finds_default_claude_harness() {
262 let definition = default_harness_definition();
263 assert_eq!(definition.id, "claude");
264 assert_eq!(definition.harness, Harness::ClaudeCode);
265 }
266
267 #[test]
268 fn aliases_resolve_to_canonical_harnesses() {
269 assert_eq!(
270 find_harness_definition("claude_code").map(|d| d.id),
271 Some("claude")
272 );
273 assert_eq!(
274 find_harness_definition("qwen-code").map(|d| d.id),
275 Some("qwen")
276 );
277 assert_eq!(find_harness_definition("nope").map(|d| d.id), None);
278 }
279
280 #[test]
281 fn process_matchers_cover_expected_paths() {
282 let claude = find_harness_definition("claude").unwrap_or(default_harness_definition());
283 assert!(claude.process_matchers.iter().any(|m| m.matches("claude")));
284 assert!(claude
285 .process_matchers
286 .iter()
287 .any(|m| m.matches("/usr/local/bin/claude")));
288
289 let pi = find_harness_definition("pi").unwrap_or(default_harness_definition());
290 assert!(pi.process_matchers.iter().any(|m| m.matches("/usr/bin/pi")));
291 assert!(pi.discovery_enabled);
292 assert!(!pi.allow_bare_cmdline_match);
293 }
294}