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}
100pub fn effective_links(
101    targets: Option<&[String]>,
102    managed_root: Option<&String>,
103) -> EffectiveLinks {
104    if let Some(targets) = targets {
105        return EffectiveLinks {
106            links: targets
107                .iter()
108                .map(|target| normalize_link(target))
109                .collect(),
110            source: LinkSource::Targets,
111        };
112    }
113
114    if let Some(managed_root) = managed_root {
115        return EffectiveLinks {
116            links: vec![normalize_link(managed_root)],
117            source: LinkSource::ManagedRoot,
118        };
119    }
120
121    EffectiveLinks {
122        links: Vec::new(),
123        source: LinkSource::None,
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn normalizes_harness_name_and_legacy_path_form() {
133        assert_eq!(
134            normalize_link("codex"),
135            NormalizedLink {
136                raw: "codex".to_string(),
137                target: ".codex".to_string(),
138                harness: Some(HarnessId::Codex),
139                kind: LinkKind::KnownHarness,
140            }
141        );
142        assert_eq!(
143            normalize_link(".codex"),
144            NormalizedLink {
145                raw: ".codex".to_string(),
146                target: ".codex".to_string(),
147                harness: Some(HarnessId::Codex),
148                kind: LinkKind::KnownHarness,
149            }
150        );
151    }
152
153    #[test]
154    fn normalizes_agents_as_generic_target() {
155        assert_eq!(
156            normalize_link("agents"),
157            NormalizedLink {
158                raw: "agents".to_string(),
159                target: ".agents".to_string(),
160                harness: None,
161                kind: LinkKind::GenericTarget,
162            }
163        );
164    }
165}