mars_agents/
local_source.rs1use std::path::{Path, PathBuf};
8
9use crate::discover::{self, DiscoveredItem};
10use crate::error::MarsError;
11
12pub const LOCAL_SOURCE_DIR: &str = ".mars-src";
13
14#[derive(Debug, Clone)]
15pub struct LocalDiscoveredItem {
16 pub discovered: DiscoveredItem,
17 pub root: PathBuf,
18}
19
20impl LocalDiscoveredItem {
21 pub fn disk_path(&self) -> PathBuf {
22 self.root.join(&self.discovered.source_path)
23 }
24}
25
26pub fn preferred_local_source_root(project_root: &Path) -> PathBuf {
27 project_root.join(LOCAL_SOURCE_DIR)
28}
29
30pub fn local_discovery_roots(project_root: &Path) -> Vec<PathBuf> {
31 vec![preferred_local_source_root(project_root)]
32}
33
34pub fn discover_local_items(
35 project_root: &Path,
36 source_name: Option<&str>,
37) -> Result<Vec<LocalDiscoveredItem>, MarsError> {
38 let mut merged = Vec::new();
39
40 for root in local_discovery_roots(project_root) {
41 let discovered = discover::discover_source(&root, source_name)?;
42 for item in discovered {
43 merged.push(LocalDiscoveredItem {
44 discovered: item,
45 root: root.clone(),
46 });
47 }
48 }
49
50 Ok(merged)
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::types::ItemKind;
57 use tempfile::TempDir;
58
59 #[test]
60 fn discovers_nested_items_under_mars_src() {
61 let dir = TempDir::new().unwrap();
62 let project_root = dir.path();
63 let agent_dir = preferred_local_source_root(project_root).join("nested/agents");
64 let skill_dir = preferred_local_source_root(project_root).join("nested/skills/review");
65 std::fs::create_dir_all(&agent_dir).unwrap();
66 std::fs::create_dir_all(&skill_dir).unwrap();
67 std::fs::write(agent_dir.join("local.md"), "# local").unwrap();
68 std::fs::write(skill_dir.join("SKILL.md"), "# review").unwrap();
69
70 let items = discover_local_items(project_root, Some("_self")).unwrap();
71
72 assert_eq!(items.len(), 2);
73 assert!(items.iter().any(|item| {
74 item.discovered.id.kind == ItemKind::Agent
75 && item.discovered.source_path == Path::new("nested/agents/local.md")
76 }));
77 assert!(items.iter().any(|item| {
78 item.discovered.id.kind == ItemKind::Skill
79 && item.discovered.source_path == Path::new("nested/skills/review")
80 }));
81 }
82
83 #[test]
84 fn discovers_mars_src_not_repo_root() {
85 let dir = TempDir::new().unwrap();
86 let project_root = dir.path();
87
88 std::fs::create_dir_all(project_root.join("skills").join("planning")).unwrap();
89 std::fs::write(
90 project_root
91 .join("skills")
92 .join("planning")
93 .join("SKILL.md"),
94 "# Legacy",
95 )
96 .unwrap();
97
98 let preferred = preferred_local_source_root(project_root)
99 .join("skills")
100 .join("planning");
101 std::fs::create_dir_all(&preferred).unwrap();
102 std::fs::write(preferred.join("SKILL.md"), "# Preferred").unwrap();
103
104 let items = discover_local_items(project_root, Some("_self")).unwrap();
105
106 assert_eq!(items.len(), 1);
107 assert_eq!(items[0].discovered.id.kind, ItemKind::Skill);
108 assert_eq!(items[0].discovered.id.name.as_str(), "planning");
109 assert_eq!(items[0].root, preferred_local_source_root(project_root));
110 }
111}