1use std::collections::HashSet;
8use std::path::Path;
9
10use crate::config::FilterMode;
11use crate::discover;
12use crate::error::MarsError;
13use crate::lock::ItemKind;
14use crate::types::ItemName;
15use crate::validate;
16
17pub(crate) fn apply_filter(
22 discovered: &[discover::DiscoveredItem],
23 filter: &FilterMode,
24 package_root: &Path,
25) -> Result<Vec<discover::DiscoveredItem>, MarsError> {
26 match filter {
27 FilterMode::All => Ok(discovered.to_vec()),
28
29 FilterMode::Exclude(excluded) => Ok(discovered
30 .iter()
31 .filter(|item| {
32 let path_str = item.source_path.to_string_lossy();
33 !excluded.iter().any(|e| {
34 crate::target::paths_equivalent(&path_str, e.as_ref()) || item.id.name == *e
36 })
37 })
38 .cloned()
39 .collect()),
40
41 FilterMode::Include { agents, skills } => {
42 let mut include_set: HashSet<ItemName> = HashSet::new();
44
45 for a in agents {
47 include_set.insert(a.clone());
48 }
49 for s in skills {
50 include_set.insert(s.clone());
51 }
52
53 resolve_agent_skill_deps(discovered, agents, package_root, &mut include_set);
55
56 Ok(discovered
57 .iter()
58 .filter(|item| include_set.contains(&item.id.name))
59 .cloned()
60 .collect())
61 }
62
63 FilterMode::OnlySkills => Ok(discovered
64 .iter()
65 .filter(|item| item.id.kind == ItemKind::Skill)
66 .cloned()
67 .collect()),
68
69 FilterMode::OnlyAgents => {
70 let agents: Vec<_> = discovered
72 .iter()
73 .filter(|item| item.id.kind == ItemKind::Agent)
74 .cloned()
75 .collect();
76
77 let agent_names: Vec<ItemName> = agents.iter().map(|a| a.id.name.clone()).collect();
79 let mut skill_deps: HashSet<ItemName> = HashSet::new();
80 resolve_agent_skill_deps(discovered, &agent_names, package_root, &mut skill_deps);
81
82 let skills: Vec<_> = discovered
84 .iter()
85 .filter(|item| {
86 item.id.kind == ItemKind::Skill && skill_deps.contains(&item.id.name)
87 })
88 .cloned()
89 .collect();
90
91 let mut result = agents;
92 result.extend(skills);
93 Ok(result)
94 }
95 }
96}
97
98fn resolve_agent_skill_deps(
103 discovered: &[discover::DiscoveredItem],
104 agent_names: &[ItemName],
105 package_root: &Path,
106 skill_deps: &mut HashSet<ItemName>,
107) {
108 for agent_name in agent_names {
109 if let Some(agent_item) = discovered
110 .iter()
111 .find(|i| i.id.kind == ItemKind::Agent && i.id.name == *agent_name)
112 {
113 let agent_path = package_root.join(&agent_item.source_path);
114 let deps = validate::parse_item_skill_deps(&agent_path).unwrap_or_default();
115 for skill in deps {
116 skill_deps.insert(ItemName::from(skill));
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::discover;
126 use std::fs;
127 use tempfile::TempDir;
128
129 fn make_source_tree(agents: &[(&str, &str)], skills: &[(&str, &str)]) -> TempDir {
131 let dir = TempDir::new().unwrap();
132 if !agents.is_empty() {
133 let agents_dir = dir.path().join("agents");
134 fs::create_dir_all(&agents_dir).unwrap();
135 for (name, content) in agents {
136 fs::write(agents_dir.join(name), content).unwrap();
137 }
138 }
139 if !skills.is_empty() {
140 let skills_dir = dir.path().join("skills");
141 fs::create_dir_all(&skills_dir).unwrap();
142 for (name, content) in skills {
143 let skill_dir = skills_dir.join(name);
144 fs::create_dir_all(&skill_dir).unwrap();
145 fs::write(skill_dir.join("SKILL.md"), content).unwrap();
146 }
147 }
148 dir
149 }
150
151 #[test]
152 fn filter_all_returns_everything() {
153 let tree = make_source_tree(
154 &[("coder.md", "# coder"), ("reviewer.md", "# reviewer")],
155 &[("planning", "# planning")],
156 );
157 let discovered = discover::discover_source(tree.path(), None).unwrap();
158 let filtered = apply_filter(&discovered, &FilterMode::All, tree.path()).unwrap();
159 assert_eq!(filtered.len(), 3);
160 }
161
162 #[test]
163 fn filter_exclude_removes_items() {
164 let tree = make_source_tree(
165 &[("coder.md", "# coder"), ("reviewer.md", "# reviewer")],
166 &[],
167 );
168 let discovered = discover::discover_source(tree.path(), None).unwrap();
169 let filtered = apply_filter(
170 &discovered,
171 &FilterMode::Exclude(vec!["reviewer".into()]),
172 tree.path(),
173 )
174 .unwrap();
175 assert_eq!(filtered.len(), 1);
176 assert_eq!(filtered[0].id.name, "coder");
177 }
178
179 #[cfg(windows)]
180 #[test]
181 fn filter_exclude_path_matches_mixed_separators_on_windows() {
182 let tree = make_source_tree(&[("coder.md", "# coder")], &[]);
183 let discovered = discover::discover_source(tree.path(), None).unwrap();
184 let filtered = apply_filter(
185 &discovered,
186 &FilterMode::Exclude(vec![r"agents\coder.md".into()]),
187 tree.path(),
188 )
189 .unwrap();
190
191 assert!(filtered.is_empty());
192 }
193
194 #[cfg(not(windows))]
195 #[test]
196 fn filter_exclude_path_preserves_backslash_on_posix() {
197 let tree = make_source_tree(&[("coder.md", "# coder")], &[]);
198 let discovered = discover::discover_source(tree.path(), None).unwrap();
199 let filtered = apply_filter(
200 &discovered,
201 &FilterMode::Exclude(vec![r"agents\coder.md".into()]),
202 tree.path(),
203 )
204 .unwrap();
205
206 assert_eq!(filtered.len(), 1);
207 assert_eq!(filtered[0].id.name, "coder");
208 }
209
210 #[test]
211 fn filter_include_agents_only() {
212 let tree = make_source_tree(
213 &[("coder.md", "# coder"), ("reviewer.md", "# reviewer")],
214 &[("planning", "# planning")],
215 );
216 let discovered = discover::discover_source(tree.path(), None).unwrap();
217 let filtered = apply_filter(
218 &discovered,
219 &FilterMode::Include {
220 agents: vec!["coder".into()],
221 skills: vec![],
222 },
223 tree.path(),
224 )
225 .unwrap();
226 assert_eq!(filtered.len(), 1);
227 assert_eq!(filtered[0].id.name, "coder");
228 }
229
230 #[test]
231 fn filter_include_with_transitive_skill_deps() {
232 let tree = make_source_tree(
233 &[(
234 "coder.md",
235 "---\nskills:\n - planning\n---\n# Coder agent\n",
236 )],
237 &[
238 ("planning", "# Planning skill"),
239 ("review", "# Review skill"),
240 ],
241 );
242 let discovered = discover::discover_source(tree.path(), None).unwrap();
243 let filtered = apply_filter(
244 &discovered,
245 &FilterMode::Include {
246 agents: vec!["coder".into()],
247 skills: vec![],
248 },
249 tree.path(),
250 )
251 .unwrap();
252 assert_eq!(filtered.len(), 2);
254 let names: Vec<&str> = filtered.iter().map(|i| i.id.name.as_str()).collect();
255 assert!(names.contains(&"coder"));
256 assert!(names.contains(&"planning"));
257 }
258}