1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum HarnessId {
10 Claude,
11 Codex,
12 Pi,
13 #[serde(rename = "opencode")]
14 OpenCode,
15 Cursor,
16}
17
18impl HarnessId {
19 pub const fn as_str(self) -> &'static str {
20 match self {
21 Self::Claude => "claude",
22 Self::Codex => "codex",
23 Self::Pi => "pi",
24 Self::OpenCode => "opencode",
25 Self::Cursor => "cursor",
26 }
27 }
28}
29
30impl fmt::Display for HarnessId {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str(self.as_str())
33 }
34}
35
36pub const ALL: &[HarnessId] = &[
37 HarnessId::Claude,
38 HarnessId::Codex,
39 HarnessId::Pi,
40 HarnessId::OpenCode,
41 HarnessId::Cursor,
42];
43
44pub fn names() -> String {
46 ALL.iter()
47 .map(|harness| harness.as_str())
48 .collect::<Vec<_>>()
49 .join(", ")
50}
51
52pub(crate) const RESERVED_CONSUMER_NAMESPACES: &[&str] = &["meridian-flow", "meridian-cli"];
56
57pub(crate) const RENAMED_CONSUMER_NAMESPACES: &[(&str, &str)] =
60 &[("flow", "meridian-flow"), ("meridian", "meridian-cli")];
61
62pub fn parse(name: &str) -> Option<HarnessId> {
63 let normalized = name.trim().to_ascii_lowercase();
64 ALL.iter()
65 .copied()
66 .find(|harness| harness.as_str() == normalized)
67}
68
69pub fn normalize_name(name: &str) -> Option<String> {
70 parse(name).map(|id| id.as_str().to_owned())
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn parse_and_normalize_are_case_insensitive() {
79 assert_eq!(parse("OpenCode"), Some(HarnessId::OpenCode));
80 assert_eq!(normalize_name(" OpenCode "), Some("opencode".to_string()));
81 assert_eq!(parse("gemini"), None);
82 }
83
84 #[test]
85 fn serde_names_round_trip_registry_names() {
86 for &harness in ALL {
87 let json = serde_json::to_string(&harness).unwrap();
88 assert_eq!(json, format!("\"{}\"", harness.as_str()));
89 assert_eq!(serde_json::from_str::<HarnessId>(&json).unwrap(), harness);
90 }
91 assert!(serde_json::from_str::<HarnessId>("\"open_code\"").is_err());
92 }
93}