pub mod cli;
pub mod format;
pub mod root;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("SKILL.md has no YAML frontmatter block")]
MissingFrontmatter,
#[error("frontmatter parse error: {0}")]
Frontmatter(String),
#[error(
"invalid skill name {0:?}: use 1-64 lowercase ASCII letters, digits, or single hyphens; hyphens may not lead, trail, or repeat"
)]
InvalidSkillName(String),
#[error("io error: {0}")]
Io(String),
}
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) fn is_valid_skill_name(name: &str) -> bool {
(1..=64).contains(&name.len())
&& name
.bytes()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-')
&& !name.starts_with('-')
&& !name.ends_with('-')
&& !name.contains("--")
}
pub(crate) fn is_valid_skill_description(description: &str) -> bool {
(1..=1024).contains(&description.chars().count())
}
pub fn validate_skill_name(name: &str) -> Result<()> {
if !is_valid_skill_name(name) {
return Err(Error::InvalidSkillName(name.to_string()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_ordinary_names() {
for name in ["alpha", "my-skill", "a1"] {
assert!(validate_skill_name(name).is_ok(), "{name} should be valid");
}
}
#[test]
fn rejects_names_outside_the_agent_skills_grammar() {
for name in [
"",
".hidden",
"UPPER",
"my_skill",
"v1.2",
"a/b",
"a\\b",
"a--b",
"-leading",
"trailing-",
"sp ace",
"é",
] {
assert!(
matches!(validate_skill_name(name), Err(Error::InvalidSkillName(_))),
"{name:?} should be rejected"
);
}
assert!(matches!(
validate_skill_name(&"a".repeat(65)),
Err(Error::InvalidSkillName(_))
));
}
}