1pub mod discovery;
19pub mod frontmatter;
20pub mod runtime;
21pub mod source;
22pub mod substitute;
23
24use std::path::{Path, PathBuf};
25
26pub use discovery::{DiscoveryOptions, DiscoveryReport, LayeredDiscovery, Shadowed};
27pub use frontmatter::{parse_frontmatter, split_frontmatter, ParsedFrontmatter, SkillManifest};
28pub use runtime::{
29 clear_current_skill_registry, current_skill_registry, install_current_skill_registry,
30 load_bound_skill_by_name, load_bound_skill_by_name_with_options, load_skill_from_registry,
31 resolve_skill_entry, skill_entry_id, tool_rejected_error, vm_error as skill_vm_error,
32 BoundSkillRegistry, LoadSkillOptions, LoadedSkill, SkillFetcher,
33};
34pub use source::{
35 skill_entry_to_vm, skill_manifest_ref_to_vm, FsSkillSource, HostSkillSource, Layer, Skill,
36 SkillManifestRef, SkillSource,
37};
38pub use substitute::{substitute_skill_body, SubstitutionContext};
39
40#[derive(Debug, Clone, Default)]
42pub struct FsLayerConfig {
43 pub cli_dirs: Vec<PathBuf>,
47 pub env_dirs: Vec<PathBuf>,
49 pub project_root: Option<PathBuf>,
52 pub manifest_paths: Vec<PathBuf>,
55 pub manifest_sources: Vec<ManifestSource>,
57 pub user_dir: Option<PathBuf>,
59 pub packages_dir: Option<PathBuf>,
61 pub system_dirs: Vec<PathBuf>,
63}
64
65#[derive(Debug, Clone)]
69pub enum ManifestSource {
70 Fs {
71 path: PathBuf,
72 namespace: Option<String>,
73 },
74 Git {
75 path: PathBuf,
76 namespace: Option<String>,
77 },
78}
79
80impl ManifestSource {
81 pub fn path(&self) -> &Path {
82 match self {
83 ManifestSource::Fs { path, .. } | ManifestSource::Git { path, .. } => path,
84 }
85 }
86 pub fn namespace(&self) -> Option<&str> {
87 match self {
88 ManifestSource::Fs { namespace, .. } | ManifestSource::Git { namespace, .. } => {
89 namespace.as_deref()
90 }
91 }
92 }
93}
94
95pub fn build_fs_discovery(cfg: &FsLayerConfig, options: DiscoveryOptions) -> LayeredDiscovery {
99 let mut discovery = LayeredDiscovery::new().with_options(options);
100
101 for path in &cfg.cli_dirs {
102 discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::Cli));
103 }
104 for path in &cfg.env_dirs {
105 discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::Env));
106 }
107 if let Some(root) = &cfg.project_root {
108 let proj_skills = root.join(".harn").join("skills");
109 if proj_skills.exists() {
110 discovery = discovery.push(FsSkillSource::new(proj_skills, Layer::Project));
111 }
112 }
113 for path in &cfg.manifest_paths {
114 discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::Manifest));
115 }
116 for entry in &cfg.manifest_sources {
117 let source = FsSkillSource::new(entry.path().to_path_buf(), Layer::Manifest);
118 let source = if let Some(ns) = entry.namespace() {
119 source.with_namespace(ns)
120 } else {
121 source
122 };
123 discovery = discovery.push(source);
124 }
125 if let Some(path) = &cfg.user_dir {
126 if path.exists() {
127 discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::User));
128 }
129 }
130 if let Some(root) = &cfg.packages_dir {
131 for skills_root in walk_packages_skills(root) {
132 discovery = discovery.push(FsSkillSource::new(skills_root, Layer::Package));
133 }
134 }
135 for path in &cfg.system_dirs {
136 if path.exists() {
137 discovery = discovery.push(FsSkillSource::new(path.clone(), Layer::System));
138 }
139 }
140
141 discovery
142}
143
144fn walk_packages_skills(packages_dir: &Path) -> Vec<PathBuf> {
148 let mut out = Vec::new();
149 let Ok(entries) = std::fs::read_dir(packages_dir) else {
150 return out;
151 };
152 for entry in entries.flatten() {
153 let pkg_skills = entry.path().join("skills");
154 if pkg_skills.is_dir() {
155 out.push(pkg_skills);
156 }
157 }
158 out.sort();
159 out
160}
161
162pub fn parse_env_skills_path(raw: &str) -> Vec<PathBuf> {
165 #[cfg(unix)]
166 let sep = ':';
167 #[cfg(not(unix))]
168 let sep = ';';
169 raw.split(sep)
170 .filter(|s| !s.is_empty())
171 .map(PathBuf::from)
172 .collect()
173}
174
175pub fn default_system_dirs() -> Vec<PathBuf> {
179 let mut out = Vec::new();
180 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
181 if !xdg.is_empty() {
182 out.push(PathBuf::from(xdg).join("harn").join("skills"));
183 }
184 } else if let Some(home) = dirs_home() {
185 out.push(home.join(".config").join("harn").join("skills"));
186 }
187 #[cfg(unix)]
188 {
189 out.push(PathBuf::from("/etc/harn/skills"));
190 }
191 out
192}
193
194pub fn default_user_dir() -> Option<PathBuf> {
196 dirs_home().map(|h| h.join(".harn").join("skills"))
197}
198
199fn dirs_home() -> Option<PathBuf> {
200 std::env::var_os("HOME").map(PathBuf::from).or_else(|| {
201 std::env::var_os("USERPROFILE").map(PathBuf::from)
203 })
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use std::fs;
210
211 #[test]
212 fn env_skills_path_parses_and_skips_empties() {
213 let raw = if cfg!(unix) {
214 "/a/b::/c/d"
215 } else {
216 "C:\\a\\b;;C:\\c\\d"
217 };
218 let parsed = parse_env_skills_path(raw);
219 assert_eq!(parsed.len(), 2);
220 }
221
222 #[test]
223 fn default_system_dirs_respects_xdg() {
224 let tmp = tempfile::tempdir().unwrap();
225 let xdg = tmp.path().to_path_buf();
226 std::env::set_var("XDG_CONFIG_HOME", &xdg);
228 let dirs = default_system_dirs();
229 assert!(dirs.iter().any(|p| p.starts_with(&xdg)));
230 std::env::remove_var("XDG_CONFIG_HOME");
231 }
232
233 #[test]
234 fn walks_packages_skills_one_level_deep() {
235 let tmp = tempfile::tempdir().unwrap();
236 fs::create_dir_all(tmp.path().join("pkg-a").join("skills")).unwrap();
237 fs::create_dir_all(tmp.path().join("pkg-b").join("skills")).unwrap();
238 fs::create_dir_all(tmp.path().join("pkg-c")).unwrap(); let skills = walk_packages_skills(tmp.path());
240 assert_eq!(skills.len(), 2);
241 }
242}