use std::path::{Path, PathBuf};
use crate::errors::{Result, SkillError};
use crate::models::SkillProperties;
use crate::yaml::{FmValue, Frontmatter, parse_mapping};
pub fn find_skill_md(skill_dir: &Path) -> Option<PathBuf> {
for name in ["SKILL.md", "skill.md"] {
let path = skill_dir.join(name);
if path.exists() {
return Some(path);
}
}
None
}
pub fn parse_frontmatter(content: &str) -> Result<(Frontmatter, String)> {
if !content.starts_with("---") {
return Err(SkillError::parse(
"SKILL.md must start with YAML frontmatter (---)",
));
}
let parts: Vec<&str> = content.splitn(3, "---").collect();
if parts.len() < 3 {
return Err(SkillError::parse(
"SKILL.md frontmatter not properly closed with ---",
));
}
let frontmatter_str = parts[1];
let body = parts[2].trim().to_string();
match parse_mapping(frontmatter_str) {
Ok(Some(frontmatter)) => Ok((frontmatter, body)),
Ok(None) => Err(SkillError::parse(
"SKILL.md frontmatter must be a YAML mapping",
)),
Err(e) => Err(SkillError::parse(format!(
"Invalid YAML in frontmatter: {e}"
))),
}
}
pub fn read_properties(skill_dir: &Path) -> Result<SkillProperties> {
let skill_md = find_skill_md(skill_dir).ok_or_else(|| {
SkillError::parse(format!("SKILL.md not found in {}", skill_dir.display()))
})?;
let content = std::fs::read_to_string(&skill_md)
.map_err(|e| SkillError::parse(format!("Could not read {}: {e}", skill_md.display())))?;
let (metadata, _body) = parse_frontmatter(&content)?;
if !metadata.contains_key("name") {
return Err(SkillError::validation(
"Missing required field in frontmatter: name",
));
}
if !metadata.contains_key("description") {
return Err(SkillError::validation(
"Missing required field in frontmatter: description",
));
}
let name = match metadata.get("name").and_then(FmValue::as_str) {
Some(s) if !s.trim().is_empty() => s.trim().to_string(),
_ => {
return Err(SkillError::validation(
"Field 'name' must be a non-empty string",
));
}
};
let description = match metadata.get("description").and_then(FmValue::as_str) {
Some(s) if !s.trim().is_empty() => s.trim().to_string(),
_ => {
return Err(SkillError::validation(
"Field 'description' must be a non-empty string",
));
}
};
let license = metadata
.get("license")
.and_then(FmValue::as_str)
.map(str::to_string);
let compatibility = metadata
.get("compatibility")
.and_then(FmValue::as_str)
.map(str::to_string);
let allowed_tools = metadata
.get("allowed-tools")
.and_then(FmValue::as_str)
.map(str::to_string);
let mut props = SkillProperties {
name,
description,
license,
compatibility,
allowed_tools,
metadata: Vec::new(),
};
if let Some(FmValue::Map(entries)) = metadata.get("metadata") {
props.metadata = entries
.iter()
.map(|(k, v)| (k.clone(), v.to_flat_string()))
.collect();
}
Ok(props)
}