mars-agents 0.6.6-rc.1

Agent package manager for .agents/ directories
Documentation
use std::collections::BTreeSet;
use std::collections::HashSet;

use crate::harness::registry::HarnessId;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedLink {
    pub raw: String,
    pub target: String,
    pub harness: Option<HarnessId>,
    pub kind: LinkKind,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkKind {
    KnownHarness,
    GenericTarget,
    PathLike,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkSource {
    Targets,
    ManagedRoot,
    None,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectiveLinks {
    pub links: Vec<NormalizedLink>,
    pub source: LinkSource,
}

impl EffectiveLinks {
    pub fn managed_targets(&self) -> Vec<String> {
        let mut seen = BTreeSet::new();
        let mut targets = Vec::new();
        for link in &self.links {
            if seen.insert(link.target.clone()) {
                targets.push(link.target.clone());
            }
        }
        targets
    }

    pub fn linked_harnesses(&self) -> Vec<HarnessId> {
        let mut seen = HashSet::new();
        let mut harnesses = Vec::new();
        for harness in self.links.iter().filter_map(|link| link.harness) {
            if seen.insert(harness) {
                harnesses.push(harness);
            }
        }
        harnesses
    }

    pub fn linked_harnesses_set(&self) -> BTreeSet<HarnessId> {
        self.linked_harnesses().into_iter().collect()
    }
}

pub fn normalize_link(raw: &str) -> NormalizedLink {
    let trimmed = raw.trim().trim_end_matches('/').trim_end_matches('\\');

    if trimmed.contains('/') || trimmed.contains('\\') {
        return NormalizedLink {
            raw: raw.to_string(),
            target: trimmed.to_string(),
            harness: None,
            kind: LinkKind::PathLike,
        };
    }

    let bare = trimmed.strip_prefix('.').unwrap_or(trimmed);
    if let Some(harness) = crate::harness::registry::parse(bare) {
        return NormalizedLink {
            raw: raw.to_string(),
            target: harness.default_target().to_string(),
            harness: Some(harness),
            kind: LinkKind::KnownHarness,
        };
    }

    if bare.is_empty() {
        return NormalizedLink {
            raw: raw.to_string(),
            target: trimmed.to_string(),
            harness: None,
            kind: LinkKind::GenericTarget,
        };
    }

    NormalizedLink {
        raw: raw.to_string(),
        target: format!(".{bare}"),
        harness: None,
        kind: LinkKind::GenericTarget,
    }
}

pub fn normalized_targets<'a>(links: impl IntoIterator<Item = &'a str>) -> Vec<String> {
    let mut seen = BTreeSet::new();
    let mut targets = Vec::new();
    for link in links {
        let target = normalize_link(link).target;
        if seen.insert(target.clone()) {
            targets.push(target);
        }
    }
    targets
}

pub fn linked_harnesses<'a>(links: impl IntoIterator<Item = &'a str>) -> Vec<String> {
    let mut seen = HashSet::new();
    let mut harnesses = Vec::new();

    for link in links {
        if let Some(harness) = normalize_link(link).harness {
            let name = harness.as_str().to_string();
            if seen.insert(name.clone()) {
                harnesses.push(name);
            }
        }
    }

    harnesses
}

pub fn effective_links(
    targets: Option<&[String]>,
    managed_root: Option<&String>,
) -> EffectiveLinks {
    if let Some(targets) = targets {
        return EffectiveLinks {
            links: targets
                .iter()
                .map(|target| normalize_link(target))
                .collect(),
            source: LinkSource::Targets,
        };
    }

    if let Some(managed_root) = managed_root {
        return EffectiveLinks {
            links: vec![normalize_link(managed_root)],
            source: LinkSource::ManagedRoot,
        };
    }

    EffectiveLinks {
        links: Vec::new(),
        source: LinkSource::None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn normalizes_harness_name_and_legacy_path_form() {
        assert_eq!(
            normalize_link("codex"),
            NormalizedLink {
                raw: "codex".to_string(),
                target: ".codex".to_string(),
                harness: Some(HarnessId::Codex),
                kind: LinkKind::KnownHarness,
            }
        );
        assert_eq!(
            normalize_link(".codex"),
            NormalizedLink {
                raw: ".codex".to_string(),
                target: ".codex".to_string(),
                harness: Some(HarnessId::Codex),
                kind: LinkKind::KnownHarness,
            }
        );
    }

    #[test]
    fn normalizes_agents_as_generic_target() {
        assert_eq!(
            normalize_link("agents"),
            NormalizedLink {
                raw: "agents".to_string(),
                target: ".agents".to_string(),
                harness: None,
                kind: LinkKind::GenericTarget,
            }
        );
    }

    #[test]
    fn extracts_known_harnesses_only() {
        let links = [".codex", ".claude", ".agents", "foo/bar"];
        assert_eq!(
            linked_harnesses(links.iter().copied()),
            vec!["codex".to_string(), "claude".to_string()]
        );
    }
}