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