use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SkillSource {
pub(super) host_root: PathBuf,
pub(super) guest_root: PathBuf,
pub(super) precedence: u8,
}
pub(super) fn discover_skill_sources(workspace_dir: Option<&Path>) -> Result<Vec<SkillSource>> {
let mut sources = Vec::new();
if let Some(workspace_dir) = workspace_dir {
let root = find_project_root(workspace_dir).unwrap_or_else(|| workspace_dir.to_path_buf());
sources.push(SkillSource {
host_root: root.join(".sac").join("skills"),
guest_root: PathBuf::from(PROJECT_SAC_SKILLS_GUEST_ROOT),
precedence: 0,
});
sources.push(SkillSource {
host_root: root.join(".agents").join("skills"),
guest_root: PathBuf::from(PROJECT_AGENTS_SKILLS_GUEST_ROOT),
precedence: 1,
});
}
if let Some(sac_home) = sac_home_dir() {
sources.push(SkillSource {
host_root: sac_home.join("skills"),
guest_root: PathBuf::from(USER_SAC_HOME_SKILLS_GUEST_ROOT),
precedence: 2,
});
}
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
sources.push(SkillSource {
host_root: home.join(".agents").join("skills"),
guest_root: PathBuf::from(USER_AGENTS_HOME_SKILLS_GUEST_ROOT),
precedence: 3,
});
}
sources.sort_by_key(|source| source.precedence);
Ok(sources)
}
pub(super) fn visible_root_for_source(
source: &SkillSource,
sandbox: Option<&SandboxSession>,
) -> Option<PathBuf> {
if !source.host_root.exists() {
return None;
}
if let Some(sandbox) = sandbox {
return sandbox
.resolve_path(&source.host_root.display().to_string())
.ok();
}
Some(source.host_root.clone())
}
pub(super) fn discover_skill_dirs(root: &Path) -> Result<Vec<PathBuf>> {
if !root.is_dir() {
return Ok(Vec::new());
}
let mut found = Vec::new();
let mut queue = std::collections::VecDeque::from([(root.to_path_buf(), 0usize)]);
let mut scanned = 0usize;
while let Some((dir, depth)) = queue.pop_front() {
if scanned >= MAX_SCAN_DIRS {
break;
}
scanned += 1;
let skill_md = dir.join(SKILL_FILENAME);
if skill_md.is_file() {
found.push(dir);
continue;
}
if depth >= MAX_SCAN_DEPTH {
continue;
}
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
continue;
};
if matches!(
name,
".git" | "node_modules" | "target" | ".venv" | "__pycache__"
) {
continue;
}
queue.push_back((path, depth + 1));
}
}
found.sort();
Ok(found)
}
pub(super) fn find_project_root(workspace_dir: &Path) -> Option<PathBuf> {
let mut current = Some(workspace_dir);
while let Some(dir) = current {
let git_marker = dir.join(".git");
if git_marker.is_dir() || git_marker.is_file() {
return Some(dir.to_path_buf());
}
current = dir.parent();
}
None
}
pub(super) fn join_path(base: &Path, suffix: &Path) -> PathBuf {
if suffix.as_os_str().is_empty() {
return base.to_path_buf();
}
let mut out = base.to_path_buf();
for component in suffix.components() {
if let std::path::Component::Normal(part) = component {
out.push(part);
}
}
out
}