pub mod discovery;
pub mod frontmatter;
pub mod runtime;
pub mod source;
pub mod substitute;
use std::path::{Path, PathBuf};
pub use discovery::{DiscoveryOptions, DiscoveryReport, LayeredDiscovery, Shadowed};
pub use frontmatter::{parse_frontmatter, split_frontmatter, ParsedFrontmatter, SkillManifest};
pub use runtime::{
clear_current_skill_registry, current_skill_registry, install_current_skill_registry,
load_bound_skill_by_name, load_bound_skill_by_name_with_options, load_skill_from_registry,
resolve_skill_entry, skill_entry_id, tool_rejected_error, vm_error as skill_vm_error,
BoundSkillRegistry, LoadSkillOptions, LoadedSkill, SkillFetcher,
};
pub use source::{
skill_entry_to_vm, skill_manifest_ref_to_vm, FsSkillSource, HostSkillSource, Layer, Skill,
SkillManifestRef, SkillSource,
};
pub use substitute::{substitute_skill_body, SubstitutionContext};
#[derive(Debug, Clone, Default)]
pub struct FsLayerConfig {
pub cli_dirs: Vec<PathBuf>,
pub env_dirs: Vec<PathBuf>,
pub project_root: Option<PathBuf>,
pub manifest_paths: Vec<PathBuf>,
pub manifest_sources: Vec<ManifestSource>,
pub user_dir: Option<PathBuf>,
pub packages_dir: Option<PathBuf>,
pub system_dirs: Vec<PathBuf>,
}
#[derive(Debug, Clone)]
pub enum ManifestSource {
Fs {
path: PathBuf,
namespace: Option<String>,
},
Git {
path: PathBuf,
namespace: Option<String>,
},
}
impl ManifestSource {
pub fn path(&self) -> &Path {
match self {
ManifestSource::Fs { path, .. } | ManifestSource::Git { path, .. } => path,
}
}
pub fn namespace(&self) -> Option<&str> {
match self {
ManifestSource::Fs { namespace, .. } | ManifestSource::Git { namespace, .. } => {
namespace.as_deref()
}
}
}
}
pub fn build_fs_discovery(cfg: &FsLayerConfig, options: DiscoveryOptions) -> LayeredDiscovery {
let mut discovery = LayeredDiscovery::new().with_options(options);
for path in &cfg.cli_dirs {
discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::Cli));
}
for path in &cfg.env_dirs {
discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::Env));
}
if let Some(root) = &cfg.project_root {
let proj_skills = root.join(".harn").join("skills");
if proj_skills.exists() {
discovery = discovery.push(FsSkillSource::new(proj_skills, Layer::Project));
}
}
for path in &cfg.manifest_paths {
discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::Manifest));
}
for entry in &cfg.manifest_sources {
let source = FsSkillSource::new(entry.path().to_path_buf(), Layer::Manifest);
let source = if let Some(ns) = entry.namespace() {
source.with_namespace(ns)
} else {
source
};
discovery = discovery.push(source);
}
if let Some(path) = &cfg.user_dir {
if path.exists() {
discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::User));
}
}
if let Some(root) = &cfg.packages_dir {
for skills_root in walk_packages_skills(root) {
discovery = discovery.push(FsSkillSource::new(skills_root, Layer::Package));
}
}
for path in &cfg.system_dirs {
if path.exists() {
discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::System));
}
}
discovery
}
fn walk_packages_skills(packages_dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(packages_dir) else {
return out;
};
for entry in entries.flatten() {
let pkg_skills = entry.path().join("skills");
if pkg_skills.is_dir() {
out.push(pkg_skills);
}
}
out.sort();
out
}
pub fn parse_env_skills_path(raw: &str) -> Vec<PathBuf> {
#[cfg(unix)]
let sep = ':';
#[cfg(not(unix))]
let sep = ';';
raw.split(sep)
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.collect()
}
pub fn default_system_dirs() -> Vec<PathBuf> {
let mut out = Vec::new();
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
if !xdg.is_empty() {
out.push(PathBuf::from(xdg).join("harn").join("skills"));
}
} else if let Some(home) = dirs_home() {
out.push(home.join(".config").join("harn").join("skills"));
}
#[cfg(unix)]
{
out.push(PathBuf::from("/etc/harn/skills"));
}
out
}
pub fn default_user_dir() -> Option<PathBuf> {
dirs_home().map(|h| h.join(".harn").join("skills"))
}
fn dirs_home() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from).or_else(|| {
std::env::var_os("USERPROFILE").map(PathBuf::from)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn env_skills_path_parses_and_skips_empties() {
let raw = if cfg!(unix) {
"/a/b::/c/d"
} else {
"C:\\a\\b;;C:\\c\\d"
};
let parsed = parse_env_skills_path(raw);
assert_eq!(parsed.len(), 2);
}
#[test]
fn default_system_dirs_respects_xdg() {
let tmp = tempfile::tempdir().unwrap();
let xdg = tmp.path().to_path_buf();
std::env::set_var("XDG_CONFIG_HOME", &xdg);
let dirs = default_system_dirs();
assert!(dirs.iter().any(|p| p.starts_with(&xdg)));
std::env::remove_var("XDG_CONFIG_HOME");
}
#[test]
fn walks_packages_skills_one_level_deep() {
let tmp = tempfile::tempdir().unwrap();
fs::create_dir_all(tmp.path().join("pkg-a").join("skills")).unwrap();
fs::create_dir_all(tmp.path().join("pkg-b").join("skills")).unwrap();
fs::create_dir_all(tmp.path().join("pkg-c")).unwrap(); let skills = walk_packages_skills(tmp.path());
assert_eq!(skills.len(), 2);
}
}