mars_agents/config/
targets.rs1use 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}