192 lines
6.7 KiB
Rust
192 lines
6.7 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use ai::skills::{
|
|
home_skills_path, parse_skill, provider_parent_directory_for_skills_root, read_skills,
|
|
ParsedSkill, SkillProvider, SKILL_PROVIDER_DEFINITIONS,
|
|
};
|
|
use anyhow::Error;
|
|
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
|
|
use galaxy_util::remote_path::RemotePath;
|
|
use galaxy_util::standardized_path::StandardizedPath;
|
|
use galaxyui::AppContext;
|
|
use repo_metadata::{RepoMetadataModel, RepositoryIdentifier};
|
|
use walkdir::{DirEntry, WalkDir};
|
|
|
|
use crate::galaxy_managed_paths_watcher::galaxy_managed_skill_dirs;
|
|
|
|
fn local_or_remote_path_for_repo_path(
|
|
repo_id: &RepositoryIdentifier,
|
|
path: &StandardizedPath,
|
|
) -> LocalOrRemotePath {
|
|
match repo_id {
|
|
RepositoryIdentifier::Local(_) => LocalOrRemotePath::Local(path.to_local_path_lossy()),
|
|
RepositoryIdentifier::Remote(remote) => {
|
|
LocalOrRemotePath::Remote(RemotePath::new(remote.host_id.clone(), path.clone()))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Finds project skill files from stored standing results.
|
|
///
|
|
/// Symlinked project skills are resolved while evaluating standing queries on the process that
|
|
/// owns the repository. This consumer treats those results as authoritative for both local and
|
|
/// remote repositories; direct filesystem discovery remains confined to metadata-failure fallback.
|
|
pub(super) fn find_project_skill_files_in_tree(
|
|
repo_id: &RepositoryIdentifier,
|
|
repo_metadata: &RepoMetadataModel,
|
|
ctx: &AppContext,
|
|
) -> Vec<LocalOrRemotePath> {
|
|
repo_metadata
|
|
.standing_query_results(repo_id, ctx)
|
|
.into_iter()
|
|
.flat_map(|results| results.project_skills())
|
|
.filter(|content| !content.is_directory)
|
|
.map(|content| local_or_remote_path_for_repo_path(repo_id, &content.path))
|
|
.collect()
|
|
}
|
|
|
|
/// Finds local project skill files by discovering provider directories on the filesystem.
|
|
///
|
|
/// This is a local-only fallback for repositories whose repo metadata indexing fails. Successful
|
|
/// local and remote project refreshes should use [`find_project_skill_files_in_tree`] so the
|
|
/// normal metadata-backed path remains shared.
|
|
pub(super) fn find_local_project_skill_files_on_filesystem(
|
|
scan_root: &Path,
|
|
) -> Vec<LocalOrRemotePath> {
|
|
let direct_skill_file = scan_root.join("SKILL.md");
|
|
if is_skill_file(&direct_skill_file) {
|
|
return vec![LocalOrRemotePath::Local(direct_skill_file)];
|
|
}
|
|
|
|
find_local_provider_directories_on_filesystem(scan_root)
|
|
.into_iter()
|
|
.flat_map(|provider_dir| std::fs::read_dir(provider_dir).into_iter().flatten())
|
|
.filter_map(Result::ok)
|
|
.filter_map(|entry| {
|
|
let skill_dir = entry.path();
|
|
if !skill_dir.is_dir() {
|
|
return None;
|
|
}
|
|
let skill_file = skill_dir.join("SKILL.md");
|
|
skill_file
|
|
.exists()
|
|
.then_some(LocalOrRemotePath::Local(skill_file))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn find_local_provider_directories_on_filesystem(scan_root: &Path) -> Vec<PathBuf> {
|
|
let mut provider_dirs = Vec::new();
|
|
let mut entries = WalkDir::new(scan_root).follow_links(false).into_iter();
|
|
while let Some(entry) = entries.next() {
|
|
let Ok(entry) = entry else {
|
|
continue;
|
|
};
|
|
if is_ignored_fallback_scan_entry(&entry) {
|
|
if entry.file_type().is_dir() {
|
|
entries.skip_current_dir();
|
|
}
|
|
continue;
|
|
}
|
|
if entry.file_type().is_dir() && is_project_provider_path(entry.path()) {
|
|
provider_dirs.push(entry.into_path());
|
|
entries.skip_current_dir();
|
|
}
|
|
}
|
|
provider_dirs.sort();
|
|
provider_dirs
|
|
}
|
|
|
|
fn is_ignored_fallback_scan_entry(entry: &DirEntry) -> bool {
|
|
entry.file_name().to_str() == Some(".git")
|
|
}
|
|
|
|
fn is_project_provider_path(path: &Path) -> bool {
|
|
SKILL_PROVIDER_DEFINITIONS
|
|
.iter()
|
|
.any(|provider| path.ends_with(&provider.skills_path))
|
|
}
|
|
/// Reads all skills from the given skill directories.
|
|
pub fn read_skills_from_directories(
|
|
skill_dirs: impl IntoIterator<Item = PathBuf>,
|
|
) -> Vec<ParsedSkill> {
|
|
skill_dirs
|
|
.into_iter()
|
|
.flat_map(|dir| read_skills(&dir))
|
|
.collect()
|
|
}
|
|
/// Reads all skills from the given concrete skill files.
|
|
pub fn read_skills_from_files(skill_files: impl IntoIterator<Item = PathBuf>) -> Vec<ParsedSkill> {
|
|
skill_files
|
|
.into_iter()
|
|
.filter_map(|path| parse_skill(&path).ok())
|
|
.collect()
|
|
}
|
|
|
|
pub fn is_skill_file(path: &Path) -> bool {
|
|
extract_skill_parent_directory(&LocalOrRemotePath::Local(path.to_path_buf())).is_ok()
|
|
}
|
|
|
|
pub fn extract_skill_parent_directory(
|
|
path: &LocalOrRemotePath,
|
|
) -> Result<LocalOrRemotePath, Error> {
|
|
let is_warp_home_skill = path
|
|
.to_local_path()
|
|
.and_then(Path::file_name)
|
|
.and_then(|name| name.to_str())
|
|
.is_some_and(|name| name == "SKILL.md")
|
|
&& path
|
|
.to_local_path()
|
|
.and_then(Path::parent)
|
|
.and_then(Path::parent)
|
|
.is_some_and(|parent| galaxy_managed_skill_dirs().iter().any(|dir| parent == dir));
|
|
if is_warp_home_skill {
|
|
return dirs::home_dir()
|
|
.map(LocalOrRemotePath::Local)
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!("Home directory not available for {}", path.display_path())
|
|
});
|
|
}
|
|
if path.file_name() != Some("SKILL.md") {
|
|
return Err(anyhow::anyhow!("Not a skill path: {}", path.display_path()));
|
|
}
|
|
|
|
let Some(skill_dir) = path.parent() else {
|
|
return Err(anyhow::anyhow!("Not a skill path: {}", path.display_path()));
|
|
};
|
|
let Some(skills_root) = skill_dir.parent() else {
|
|
return Err(anyhow::anyhow!("Not a skill path: {}", path.display_path()));
|
|
};
|
|
|
|
provider_parent_directory_for_skills_root(&skills_root)
|
|
.ok_or_else(|| anyhow::anyhow!("Not a skill path: {}", path.display_path()))
|
|
}
|
|
|
|
/// Check if this path is a skill directory under a home directory provider path
|
|
/// E.g. ~/.agents/skills/skill-name
|
|
pub fn is_home_skill_directory(path: &Path) -> bool {
|
|
let parent_directory = path.parent();
|
|
if let Some(parent_directory) = parent_directory {
|
|
is_home_provider_path(parent_directory)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Check if this path is a home directory provider path
|
|
/// E.g. ~/.agents/skills
|
|
pub fn is_home_provider_path(path: &Path) -> bool {
|
|
SKILL_PROVIDER_DEFINITIONS.iter().any(|provider| {
|
|
if provider.provider == SkillProvider::Warp {
|
|
return galaxy_managed_skill_dirs().iter().any(|dir| path == dir);
|
|
}
|
|
home_skills_path(provider.provider)
|
|
.as_ref()
|
|
.is_some_and(|home_skills_path| path == home_skills_path)
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "utils_tests.rs"]
|
|
mod tests;
|