Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use ai::skills::{
|
||||
home_skills_path, read_skills, ParsedSkill, SkillProvider, SKILL_PROVIDER_DEFINITIONS,
|
||||
};
|
||||
use anyhow::Error;
|
||||
use regex::Regex;
|
||||
use repo_metadata::{local_model::GetContentsArgs, RepoContent, RepoMetadataModel};
|
||||
use warpui::AppContext;
|
||||
|
||||
use crate::warp_managed_paths_watcher::warp_managed_skill_dirs;
|
||||
|
||||
/// Finds all skill directories in a repository by querying the RepoMetadataModel tree.
|
||||
///
|
||||
/// Returns a list of paths to skill directories (e.g., `/repo/.agents/skills/`, `/repo/sub/.claude/skills/`).
|
||||
pub fn find_skill_directories_in_tree(
|
||||
repo_path: &Path,
|
||||
repo_metadata: &RepoMetadataModel,
|
||||
ctx: &AppContext,
|
||||
) -> Vec<PathBuf> {
|
||||
// Collect provider skills paths (e.g., ".agents/skills", ".claude/skills")
|
||||
let skill_path_suffixes: Vec<&Path> = SKILL_PROVIDER_DEFINITIONS
|
||||
.iter()
|
||||
.map(|p| p.skills_path.as_path())
|
||||
.collect();
|
||||
|
||||
// Filter during traversal: only collect directories that end with a skill provider path.
|
||||
// The filter rejects files and non-matching directories, avoiding intermediate allocations.
|
||||
let args = GetContentsArgs::default().with_filter(move |content| {
|
||||
let RepoContent::Directory(dir) = content else {
|
||||
return false;
|
||||
};
|
||||
skill_path_suffixes
|
||||
.iter()
|
||||
.any(|suffix| dir.path.ends_with(&suffix.to_string_lossy()))
|
||||
});
|
||||
|
||||
let Some(id) = repo_metadata::RepositoryIdentifier::try_local(repo_path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
repo_metadata
|
||||
.get_repo_contents(&id, args, ctx)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
// Only directories should reach this iterator due to the GetContentsArgs::filter.
|
||||
// Keep the File arm for exhaustive matching in case RepoContent grows new variants.
|
||||
.map(|content| match content {
|
||||
RepoContent::Directory(dir) => dir.path.to_local_path_lossy(),
|
||||
RepoContent::File(f) => f.path.to_local_path_lossy(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
pub fn is_skill_file(path: &Path) -> bool {
|
||||
extract_skill_parent_directory(path).is_ok()
|
||||
}
|
||||
|
||||
static SKILL_PROVIDER_PATHS: LazyLock<HashSet<String>> = LazyLock::new(|| {
|
||||
// Collect the skill provider paths from the definitions
|
||||
SKILL_PROVIDER_DEFINITIONS
|
||||
.iter()
|
||||
.map(|p| p.skills_path.to_string_lossy().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
// Pattern: {prefix}/{provider_path}/{skill-name}/SKILL.md
|
||||
// where provider_path is 2 parts (e.g., ".agents/skills") and skill-name is 1 part
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
static SKILL_FILE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(.+)/([^/]+/[^/]+)/[^/]+/SKILL\.md$")
|
||||
.expect("Failed to compile skill file pattern")
|
||||
});
|
||||
|
||||
// On windows, the path separator is \
|
||||
#[cfg(target_os = "windows")]
|
||||
static SKILL_FILE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"(.+)\\([^\\]+\\[^\\]+)\\[^\\]+\\SKILL\.md$")
|
||||
.expect("Failed to compile skill file pattern")
|
||||
});
|
||||
|
||||
pub fn extract_skill_parent_directory(path: &Path) -> Result<PathBuf, Error> {
|
||||
let is_warp_home_skill = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| name == "SKILL.md")
|
||||
&& path
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.is_some_and(|parent| warp_managed_skill_dirs().iter().any(|dir| parent == dir));
|
||||
if is_warp_home_skill {
|
||||
return dirs::home_dir()
|
||||
.ok_or_else(|| anyhow::anyhow!("Home directory not available for {}", path.display()));
|
||||
}
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
if let Some(captures) = SKILL_FILE_PATTERN.captures(&path_str) {
|
||||
if let Some(provider_path) = captures.get(2) {
|
||||
if SKILL_PROVIDER_PATHS.contains(provider_path.as_str()) {
|
||||
if let Some(parent_directory) = captures.get(1) {
|
||||
return Ok(PathBuf::from(parent_directory.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Not a skill path: {}", path.display()))
|
||||
}
|
||||
|
||||
/// 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 warp_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;
|
||||
Reference in New Issue
Block a user