use std::path::{Path, PathBuf};
use serde::Deserialize;
#[derive(Debug, Clone)]
pub struct Skill {
pub name: String,
pub description: String,
pub internal: bool,
pub dir: PathBuf,
}
#[derive(Debug, Deserialize, Default)]
struct Frontmatter {
#[serde(default)]
name: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
metadata: Option<noyalib::Value>,
}
fn split_frontmatter(raw: &str) -> Option<&str> {
let rest = raw
.strip_prefix("---\r\n")
.or_else(|| raw.strip_prefix("---\n"))?;
let end = rest.find("\n---")?;
Some(&rest[..end])
}
fn validated_frontmatter(dir: &Path) -> Option<(String, String, bool)> {
let content = std::fs::read_to_string(dir.join("SKILL.md")).ok()?;
let content = content.strip_prefix('\u{feff}').unwrap_or(&content);
let data = split_frontmatter(content)?;
let fm = noyalib::from_str::<Frontmatter>(data).ok()?;
let name = fm.name?.trim().to_string();
if name.is_empty() {
return None;
}
let description = fm.description.unwrap_or_default();
let internal = fm
.metadata
.as_ref()
.and_then(|m| m.get("internal"))
.and_then(|m| m.as_bool())
.unwrap_or(false);
Some((name, description, internal))
}
pub fn read_skill(dir: &Path) -> Option<Skill> {
let (name, description, internal) = validated_frontmatter(dir)?;
Some(Skill {
name,
description,
internal,
dir: dir.to_path_buf(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::test_utils::{skill_frontmatter, write_skill_md};
#[test]
fn name_and_description_come_from_the_frontmatter() {
let tmp = tempfile::TempDir::new().unwrap();
write_skill_md(tmp.path(), "skills/acrobat", "pdf");
let dir = tmp.path().join("skills/acrobat");
let skill = read_skill(&dir).unwrap();
assert_eq!(skill.name, "pdf");
assert_eq!(skill.description, "does pdf");
assert_eq!(skill.dir, dir);
}
#[test]
fn a_directory_is_only_a_skill_with_a_frontmatter_name() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("no-name");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("SKILL.md"), "---\ndescription: d\n---\nbody").unwrap();
assert!(read_skill(&dir).is_none());
let dir = tmp.path().join("blank-name");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("SKILL.md"),
"---\nname: \" \"\ndescription: d\n---\nbody",
)
.unwrap();
assert!(read_skill(&dir).is_none());
let dir = tmp.path().join("no-fm");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("SKILL.md"), "# just a heading\n").unwrap();
assert!(read_skill(&dir).is_none());
let dir = tmp.path().join("bad-yaml");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("SKILL.md"), "---\nname: [unclosed\n---\nbody").unwrap();
assert!(read_skill(&dir).is_none());
let dir = tmp.path().join("no-md");
std::fs::create_dir_all(&dir).unwrap();
assert!(read_skill(&dir).is_none());
let dir = tmp.path().join("no-desc");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("SKILL.md"), "---\nname: x\n---\nbody").unwrap();
let skill = read_skill(&dir).unwrap();
assert_eq!(skill.name, "x");
assert_eq!(skill.description, "");
let dir = tmp.path().join("blank-desc");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("SKILL.md"),
"---\nname: x\ndescription: \" \"\n---\nbody",
)
.unwrap();
assert_eq!(read_skill(&dir).unwrap().description, " ");
}
#[test]
fn name_is_trimmed_and_block_description_is_kept() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("pdf2");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("SKILL.md"),
"---\nname: \" pdf \"\ndescription: |\n Multi line\n description here\n---\nbody",
)
.unwrap();
let skill = read_skill(&dir).unwrap();
assert_eq!(skill.name, "pdf");
assert!(skill.description.contains("Multi line"));
}
#[test]
fn internal_flag_is_reported_not_filtered() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("secret");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("SKILL.md"),
"---\nname: secret\ndescription: internal\nmetadata:\n internal: true\n---\nbody",
)
.unwrap();
assert!(read_skill(&dir).unwrap().internal);
let dir = tmp.path().join("plain");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("SKILL.md"),
"---\nname: plain\ndescription: d\n---\nb",
)
.unwrap();
assert!(!read_skill(&dir).unwrap().internal);
}
#[test]
fn a_utf8_bom_does_not_hide_the_frontmatter() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("bom");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("SKILL.md"),
format!("\u{feff}{}", skill_frontmatter("bom-skill")),
)
.unwrap();
assert_eq!(read_skill(&dir).unwrap().name, "bom-skill");
}
}