#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
use crate::skills::loader::strip_frontmatter_body;
use crate::skills::types::Skill;
pub fn expand_skill_command(text: &str, skills: &[Skill]) -> String {
let Some(rest) = text.strip_prefix("/skill:") else {
return text.to_string();
};
let (name, args) = match rest.find(char::is_whitespace) {
Some(i) => (&rest[..i], rest[i + 1..].trim()),
None => (rest, ""),
};
let Some(skill) = skills.iter().find(|s| s.name == name) else {
return text.to_string();
};
let raw = match std::fs::read_to_string(&skill.file_path) {
Ok(s) => s,
Err(_) => return text.to_string(),
};
let body = strip_frontmatter_body(&raw).trim().to_string();
let block = format!(
"<skill name=\"{}\" location=\"{}\">\nReferences are relative to {}.\n\n{}\n</skill>",
skill.name,
skill.file_path.display(),
skill.base_dir.display(),
body,
);
if args.is_empty() {
block
} else {
format!("{block}\n\n{args}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skills::types::SkillSource;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
fn mk_skill(dir: &std::path::Path, name: &str, body: &str) -> Skill {
let file = dir.join(format!("{name}.md"));
fs::write(&file, format!("---\ndescription: d\n---\n{body}")).unwrap();
Skill {
name: name.into(),
description: "d".into(),
file_path: file,
base_dir: dir.to_path_buf(),
disable_model_invocation: false,
source: SkillSource::Global,
}
}
#[test]
fn non_skill_text_unchanged() {
let skills: Vec<Skill> = vec![];
assert_eq!(expand_skill_command("hello world", &skills), "hello world");
}
#[test]
fn unknown_skill_unchanged() {
let skills: Vec<Skill> = vec![];
assert_eq!(
expand_skill_command("/skill:missing", &skills),
"/skill:missing"
);
}
#[test]
fn expands_to_skill_block_no_args() {
let tmp = tempdir().unwrap();
let s = mk_skill(tmp.path(), "foo", "the body\n");
let out = expand_skill_command("/skill:foo", std::slice::from_ref(&s));
assert!(out.contains("<skill name=\"foo\""), "{out}");
assert!(out.contains(&format!("location=\"{}\"", s.file_path.display())));
assert!(out.contains("the body"));
assert!(!out.ends_with("\n\n")); }
#[test]
fn appends_args_after_block() {
let tmp = tempdir().unwrap();
let s = mk_skill(tmp.path(), "foo", "the body\n");
let out = expand_skill_command("/skill:foo extra context", &[s]);
assert!(out.contains("<skill name=\"foo\""));
assert!(out.ends_with("extra context"));
}
#[test]
fn unreadable_file_unchanged() {
let s = Skill {
name: "ghost".into(),
description: "d".into(),
file_path: PathBuf::from("/no/such/file-for-test"),
base_dir: PathBuf::from("/tmp"),
disable_model_invocation: false,
source: SkillSource::Global,
};
assert_eq!(expand_skill_command("/skill:ghost", &[s]), "/skill:ghost");
}
}