1use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{RuntimeError, RuntimeResult};
12
13pub const PACKAGED_SKILLS_ROOT: &str = "/.flue/packaged-skills/";
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Skill {
19 pub name: String,
21 pub description: String,
23 pub body: String,
25 pub source: PathBuf,
27}
28
29impl Skill {
30 pub async fn load(path: impl AsRef<Path>) -> RuntimeResult<Arc<Self>> {
35 let path = path.as_ref();
36 let raw = tokio::fs::read_to_string(path)
37 .await
38 .map_err(RuntimeError::Io)?;
39 let (front, body) = split_frontmatter(&raw);
40 let name = front
41 .iter()
42 .find(|(k, _)| k == "name")
43 .map(|(_, v)| v.clone())
44 .unwrap_or_else(|| {
45 path.file_stem()
46 .and_then(|s| s.to_str())
47 .unwrap_or("skill")
48 .to_string()
49 });
50 let description = front
51 .iter()
52 .find(|(k, _)| k == "description")
53 .map(|(_, v)| v.clone())
54 .unwrap_or_default();
55 Ok(Arc::new(Self {
56 name,
57 description,
58 body: body.to_string(),
59 source: path.to_path_buf(),
60 }))
61 }
62}
63
64pub(crate) fn split_frontmatter(raw: &str) -> (Vec<(String, String)>, &str) {
66 let raw = raw.strip_prefix("---\n").unwrap_or(raw);
67 let Some(end) = raw.find("\n---\n") else {
68 return (Vec::new(), raw);
69 };
70 let front = &raw[..end];
71 let body = &raw[end + "\n---\n".len()..];
72 let pairs = front
73 .lines()
74 .filter_map(|line| line.split_once(':'))
75 .map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
76 .collect();
77 (pairs, body)
78}