#![allow(dead_code)]
use anyhow::{Context, Result};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
pub name: String,
pub description: String,
pub level: Option<u8>,
pub aliases: Vec<String>,
pub triggers: Vec<String>,
pub body: String,
pub source_path: std::path::PathBuf,
}
pub async fn parse_skill(path: &Path) -> Result<Skill> {
let content = tokio::fs::read_to_string(path)
.await
.with_context(|| format!("Failed to read skill file: {}", path.display()))?;
let (frontmatter, body) = extract_frontmatter(&content)?;
let meta: SkillMeta = serde_yaml::from_str(&frontmatter)
.with_context(|| format!("Invalid YAML frontmatter in {}", path.display()))?;
Ok(Skill {
name: meta.name.unwrap_or_else(|| {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string()
}),
description: meta.description.unwrap_or_else(|| {
body.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("No description")
.to_string()
}),
level: meta.level,
aliases: meta.aliases.unwrap_or_default(),
triggers: meta.triggers.unwrap_or_default(),
body: body.to_string(),
source_path: path.to_path_buf(),
})
}
fn extract_frontmatter(content: &str) -> Result<(String, String)> {
let re = Regex::new(r"(?s)^---\s*\n(.*?)\n---\s*\n?(.*)$")?;
if let Some(caps) = re.captures(content) {
let fm = caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string();
let body = caps.get(2).map(|m| m.as_str()).unwrap_or("").to_string();
Ok((fm, body))
} else {
Ok((String::new(), content.to_string()))
}
}
#[derive(Debug, Deserialize)]
struct SkillMeta {
name: Option<String>,
description: Option<String>,
level: Option<u8>,
aliases: Option<Vec<String>>,
triggers: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_frontmatter() {
let input = "---\nname: team\ndescription: N coordinated agents\nlevel: 4\naliases: [\"tm\", \"swarm\"]\ntriggers: [\"team\", \"orchestrate\"]\n---\n\n# Team Mode\n\nUse this when coordinating multiple agents.\n";
let (fm, body) = extract_frontmatter(input).unwrap();
eprintln!("FM: {:?}", fm);
eprintln!("BODY: {:?}", body);
assert!(
fm.contains("name: team"),
"frontmatter missing name: {:?}",
fm
);
assert!(
body.contains("# Team Mode"),
"body missing header: {:?}",
body
);
}
}