Skip to main content

mars_agents/config/
targets.rs

1use std::collections::BTreeSet;
2use std::collections::HashSet;
3
4use crate::harness::registry::HarnessId;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct NormalizedLink {
8    pub raw: String,
9    pub target: String,
10    pub harness: Option<HarnessId>,
11    pub kind: LinkKind,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum LinkKind {
16    KnownHarness,
17    GenericTarget,
18    PathLike,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LinkSource {
23    Targets,
24    ManagedRoot,
25    None,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct EffectiveLinks {
30    pub links: Vec<NormalizedLink>,
31    pub source: LinkSource,
32}
33
34impl EffectiveLinks {
35    pub fn managed_targets(&self) -> Vec<String> {
36        let mut seen = BTreeSet::new();
37        let mut targets = Vec::new();
38        for link in &self.links {
39            if seen.insert(link.target.clone()) {
40                targets.push(link.target.clone());
41            }
42        }
43        targets
44    }
45
46    pub fn linked_harnesses(&self) -> Vec<HarnessId> {
47        let mut seen = HashSet::new();
48        let mut harnesses = Vec::new();
49        for harness in self.links.iter().filter_map(|link| link.harness) {
50            if seen.insert(harness) {
51                harnesses.push(harness);
52            }
53        }
54        harnesses
55    }
56
57    pub fn linked_harnesses_set(&self) -> BTreeSet<HarnessId> {
58        self.linked_harnesses().into_iter().collect()
59    }
60}
61
62pub fn normalize_link(raw: &str) -> NormalizedLink {
63    let trimmed = raw.trim().trim_end_matches('/').trim_end_matches('\\');
64
65    if trimmed.contains('/') || trimmed.contains('\\') {
66        return NormalizedLink {
67            raw: raw.to_string(),
68            target: trimmed.to_string(),
69            harness: None,
70            kind: LinkKind::PathLike,
71        };
72    }
73
74    let bare = trimmed.strip_prefix('.').unwrap_or(trimmed);
75    if let Some(harness) = crate::harness::registry::parse(bare) {
76        return NormalizedLink {
77            raw: raw.to_string(),
78            target: harness.default_target().to_string(),
79            harness: Some(harness),
80            kind: LinkKind::KnownHarness,
81        };
82    }
83
84    if bare.is_empty() {
85        return NormalizedLink {
86            raw: raw.to_string(),
87            target: trimmed.to_string(),
88            harness: None,
89            kind: LinkKind::GenericTarget,
90        };
91    }
92
93    NormalizedLink {
94        raw: raw.to_string(),
95        target: format!(".{bare}"),
96        harness: None,
97        kind: LinkKind::GenericTarget,
98    }
99}
100
101pub fn normalized_targets<'a>(links: impl IntoIterator<Item = &'a str>) -> Vec<String> {
102    let mut seen = BTreeSet::new();
103    let mut targets = Vec::new();
104    for link in links {
105        let target = normalize_link(link).target;
106        if seen.insert(target.clone()) {
107            targets.push(target);
108        }
109    }
110    targets
111}
112
113pub fn linked_harnesses<'a>(links: impl IntoIterator<Item = &'a str>) -> Vec<String> {
114    let mut seen = HashSet::new();
115    let mut harnesses = Vec::new();
116
117    for link in links {
118        if let Some(harness) = normalize_link(link).harness {
119            let name = harness.as_str().to_string();
120            if seen.insert(name.clone()) {
121                harnesses.push(name);
122            }
123        }
124    }
125
126    harnesses
127}
128
129pub fn effective_links(
130    targets: Option<&[String]>,
131    managed_root: Option<&String>,
132) -> EffectiveLinks {
133    if let Some(targets) = targets {
134        return EffectiveLinks {
135            links: targets
136                .iter()
137                .map(|target| normalize_link(target))
138                .collect(),
139            source: LinkSource::Targets,
140        };
141    }
142
143    if let Some(managed_root) = managed_root {
144        return EffectiveLinks {
145            links: vec![normalize_link(managed_root)],
146            source: LinkSource::ManagedRoot,
147        };
148    }
149
150    EffectiveLinks {
151        links: Vec::new(),
152        source: LinkSource::None,
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn normalizes_harness_name_and_legacy_path_form() {
162        assert_eq!(
163            normalize_link("codex"),
164            NormalizedLink {
165                raw: "codex".to_string(),
166                target: ".codex".to_string(),
167                harness: Some(HarnessId::Codex),
168                kind: LinkKind::KnownHarness,
169            }
170        );
171        assert_eq!(
172            normalize_link(".codex"),
173            NormalizedLink {
174                raw: ".codex".to_string(),
175                target: ".codex".to_string(),
176                harness: Some(HarnessId::Codex),
177                kind: LinkKind::KnownHarness,
178            }
179        );
180    }
181
182    #[test]
183    fn normalizes_agents_as_generic_target() {
184        assert_eq!(
185            normalize_link("agents"),
186            NormalizedLink {
187                raw: "agents".to_string(),
188                target: ".agents".to_string(),
189                harness: None,
190                kind: LinkKind::GenericTarget,
191            }
192        );
193    }
194
195    #[test]
196    fn extracts_known_harnesses_only() {
197        let links = [".codex", ".claude", ".agents", "foo/bar"];
198        assert_eq!(
199            linked_harnesses(links.iter().copied()),
200            vec!["codex".to_string(), "claude".to_string()]
201        );
202    }
203}