use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub(crate) const DEFAULT_GEMINI_HEADER: &str =
"Gemini-specific refresh behavior belongs in tooling, not in a second policy file.";
pub(crate) const SKILL_CANONICAL_DIR: &str = "skills/canonical";
#[derive(Clone, Copy)]
pub(crate) struct HostSkillHeaders<'a> {
pub(crate) claude: &'a str,
pub(crate) gemini: &'a str,
}
pub(crate) struct EmbeddedSkillFile {
pub(crate) skill: &'static str,
pub(crate) relative_path: &'static str,
pub(crate) contents: &'static str,
}
pub(crate) struct RepoCanonicalSkillFile {
pub(crate) relative_path: PathBuf,
pub(crate) contents: String,
}
include!(concat!(env!("OUT_DIR"), "/embedded_skills.rs"));
pub(crate) fn embedded_skill_files() -> &'static [EmbeddedSkillFile] {
EMBEDDED_SKILL_FILES
}
pub(crate) fn embedded_skill_names() -> Vec<&'static str> {
let mut names: Vec<&str> = EMBEDDED_SKILL_FILES.iter().map(|f| f.skill).collect();
names.sort_unstable();
names.dedup();
names
}
pub(crate) fn load_repo_canonical_skill_files(
repo_root: &Path,
) -> Result<Vec<RepoCanonicalSkillFile>> {
let canonical_root = repo_root.join(SKILL_CANONICAL_DIR);
if !canonical_root.is_dir() {
return Ok(Vec::new());
}
let relative_paths = collect_relative_files(&canonical_root, &canonical_root)?;
let mut files = Vec::with_capacity(relative_paths.len());
for relative_path in relative_paths {
let source = canonical_root.join(&relative_path);
let contents = fs::read_to_string(&source)
.with_context(|| format!("failed to read {}", source.display()))?;
files.push(RepoCanonicalSkillFile {
relative_path,
contents,
});
}
Ok(files)
}
pub(crate) fn render_skill_for_runtime(
relative_path: &Path,
contents: &str,
runtime_name: &str,
headers: HostSkillHeaders<'_>,
) -> String {
if !should_render_runtime_header(relative_path) {
return contents.to_owned();
}
match runtime_name {
"gemini" if !headers.gemini.is_empty() => {
format!("> {}\n\n{}", headers.gemini, contents)
}
"claude" if !headers.claude.is_empty() => {
format!("> {}\n\n{}", headers.claude, contents)
}
_ => contents.to_owned(),
}
}
fn should_render_runtime_header(relative_path: &Path) -> bool {
relative_path.file_name().and_then(|name| name.to_str()) == Some("SKILL.md")
}
pub(crate) fn collect_relative_files(root: &Path, dir: &Path) -> Result<Vec<PathBuf>> {
let mut entries: Vec<_> = fs::read_dir(dir)
.with_context(|| format!("failed to read directory {}", dir.display()))?
.collect::<std::result::Result<Vec<_>, _>>()
.with_context(|| format!("failed to list directory {}", dir.display()))?;
entries.sort_by_key(|entry| entry.file_name());
let mut files = Vec::new();
for entry in entries {
let path = entry.path();
let file_type = entry
.file_type()
.with_context(|| format!("failed to inspect {}", path.display()))?;
if file_type.is_dir() {
files.extend(collect_relative_files(root, &path)?);
continue;
}
if file_type.is_file() {
let relative = path
.strip_prefix(root)
.with_context(|| format!("failed to relativize {}", path.display()))?;
files.push(relative.to_path_buf());
}
}
Ok(files)
}