agents-skills 0.28.1

A minimal, stable, easy-to-understand skill installer and manager for AI agents
Documentation
//! Pure path and classification helpers for agent linking.
//!
//! `classify` decides which entries of an agent skills dir are skills vs. other
//! files (the rules linking and `agent --status` share); the rest are
//! symlink / path comparison utilities. No filesystem mutation lives here.

use std::fs;
use std::path::Path;

use crate::core::path_util::{path_contains, same_path};

pub(crate) fn entry_name(entry: &fs::DirEntry) -> String {
    entry.file_name().to_string_lossy().into_owned()
}

/// Whether the agent's root dir exists — the parent of its skills dir, e.g.
/// `~/.claude` for `~/.claude/skills`.
pub(crate) fn agent_root_exists(agent_dir: &Path) -> bool {
    agent_dir.parent().map(|p| p.exists()).unwrap_or(false)
}

/// Whether `link` (a symlink) resolves to `target`.
pub(crate) fn points_to(link: &Path, target: &Path) -> bool {
    match fs::read_link(link) {
        Ok(raw) => {
            let resolved = if raw.is_absolute() {
                raw
            } else {
                link.parent().unwrap_or(Path::new(".")).join(raw)
            };
            same_path(&resolved, target)
        }
        Err(_) => false,
    }
}

/// Whether a dir entry is an old-model per-skill symlink into the canonical dir.
///
/// Such entries are dropped on link instead of being moved: their content already
/// lives in the canonical dir, and moving the link in would make it point at itself.
pub(crate) fn is_legacy_link(entry: &fs::DirEntry, canonical: &Path) -> bool {
    let path = entry.path();
    let is_symlink = fs::symlink_metadata(&path)
        .map(|m| m.file_type().is_symlink())
        .unwrap_or(false);
    if !is_symlink {
        return false;
    }
    match fs::read_link(&path) {
        Ok(raw) => {
            let resolved = if raw.is_absolute() {
                raw
            } else {
                path.parent().unwrap_or(Path::new(".")).join(raw)
            };
            path_contains(canonical, &resolved)
        }
        Err(_) => false,
    }
}

/// Split dir entries into `(skills, others)`: skills are real subdirs and
/// symlinks whose target is a directory (e.g. links into a skills hub); others
/// are files and symlinks to non-directories. Old-model per-skill links into
/// the canonical dir are excluded from both (their content already lives there).
pub(crate) fn classify(entries: &[fs::DirEntry], canonical: &Path) -> (Vec<String>, Vec<String>) {
    let mut skills = Vec::new();
    let mut others = Vec::new();
    for entry in entries {
        if is_legacy_link(entry, canonical) {
            continue;
        }
        let name = entry_name(entry);
        if fs::metadata(entry.path())
            .map(|m| m.is_dir())
            .unwrap_or(false)
        {
            skills.push(name);
        } else {
            others.push(name);
        }
    }
    (skills, others)
}