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}
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}