Skip to main content

mars_agents/
local_source.rs

1//! Project-local source discovery rooted under `.mars-src/`.
2//!
3//! Local items intentionally use the same convention walk as dependency packages
4//! so nested `.mars-src/**/agents` and `.mars-src/**/skills` layouts follow the
5//! same layer-grounding rules as published source trees.
6
7use 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}