use std::fs;
use std::path::Path;
use include_dir::{Dir, include_dir};
use tracing::{debug, info, warn};
static BUNDLED_SKILLS_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
#[derive(Debug, Default)]
pub struct ProvisionReport {
pub installed: Vec<String>,
pub updated: Vec<String>,
pub skipped: Vec<String>,
pub failed: Vec<(String, String)>,
}
pub fn provision_bundled_skills(managed_dir: &Path) -> Result<ProvisionReport, std::io::Error> {
fs::create_dir_all(managed_dir)?;
let mut report = ProvisionReport::default();
for entry in BUNDLED_SKILLS_DIR.entries() {
let include_dir::DirEntry::Dir(skill_dir) = entry else {
continue; };
let skill_name = skill_dir.path().to_string_lossy().into_owned();
let skill_md_path = format!("{skill_name}/SKILL.md");
if BUNDLED_SKILLS_DIR.get_file(&skill_md_path).is_none() {
debug!(skill = %skill_name, "skipping embedded entry without SKILL.md");
continue;
}
let embedded_version = extract_embedded_version(skill_dir);
let target_dir = managed_dir.join(&skill_name);
let marker_path = target_dir.join(".bundled");
if !target_dir.exists() {
match write_skill(skill_dir, &target_dir, &marker_path, &embedded_version) {
Ok(()) => {
info!(skill = %skill_name, version = %embedded_version, "installed bundled skill");
report.installed.push(skill_name);
}
Err(e) => {
warn!(skill = %skill_name, error = %e, "failed to install bundled skill");
report.failed.push((skill_name, e.to_string()));
}
}
continue;
}
match read_marker_version(&marker_path) {
MarkerState::NoMarker => {
if is_legacy_bundled(&target_dir, &skill_name) {
match write_skill(skill_dir, &target_dir, &marker_path, &embedded_version) {
Ok(()) => {
info!(
skill = %skill_name,
to = %embedded_version,
"migrated legacy bundled skill (added .bundled marker)"
);
report.updated.push(skill_name);
}
Err(e) => {
warn!(skill = %skill_name, error = %e, "failed to migrate legacy bundled skill");
report.failed.push((skill_name, e.to_string()));
}
}
} else {
debug!(skill = %skill_name, "skipping user-owned skill (no .bundled marker)");
report.skipped.push(skill_name);
}
}
MarkerState::CorruptMarker => {
warn!(
skill = %skill_name,
"corrupt .bundled marker — treating skill as user-owned, skipping"
);
report.skipped.push(skill_name);
}
MarkerState::Version(marker_version) => {
if marker_version != embedded_version {
match write_skill(skill_dir, &target_dir, &marker_path, &embedded_version) {
Ok(()) => {
info!(
skill = %skill_name,
from = %marker_version,
to = %embedded_version,
"updated bundled skill"
);
report.updated.push(skill_name);
}
Err(e) => {
warn!(skill = %skill_name, error = %e, "failed to update bundled skill");
report.failed.push((skill_name, e.to_string()));
}
}
}
}
}
}
if report.installed.is_empty() && report.updated.is_empty() && report.failed.is_empty() {
debug!(
skipped = report.skipped.len(),
"all bundled skills are up to date"
);
}
Ok(report)
}
fn write_skill(
skill_dir: &include_dir::Dir<'_>,
target_dir: &Path,
marker_path: &Path,
version: &str,
) -> Result<(), std::io::Error> {
let parent = target_dir.parent().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "target_dir has no parent")
})?;
let tmp_name = format!(
".zeph-provision-tmp-{}",
target_dir
.file_name()
.map_or("skill", |n| n.to_str().unwrap_or("skill"))
);
let tmp_dir = parent.join(&tmp_name);
if tmp_dir.exists() {
fs::remove_dir_all(&tmp_dir)?;
}
fs::create_dir_all(&tmp_dir)?;
write_dir_contents(skill_dir, &tmp_dir)?;
let tmp_marker = tmp_dir.join(".bundled");
fs::write(&tmp_marker, version)?;
if target_dir.exists() {
fs::remove_dir_all(target_dir)?;
}
fs::rename(&tmp_dir, target_dir)?;
debug_assert_eq!(marker_path, &target_dir.join(".bundled"));
Ok(())
}
fn write_dir_contents(dir: &include_dir::Dir<'_>, dest: &Path) -> Result<(), std::io::Error> {
for file in dir.files() {
let rel = file.path().file_name().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "file has no name")
})?;
fs::write(dest.join(rel), file.contents())?;
}
for subdir in dir.dirs() {
let rel = subdir.path().file_name().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "subdir has no name")
})?;
let sub_dest = dest.join(rel);
fs::create_dir_all(&sub_dest)?;
write_dir_contents(subdir, &sub_dest)?;
}
Ok(())
}
enum MarkerState {
NoMarker,
Version(String),
CorruptMarker,
}
fn read_marker_version(marker_path: &Path) -> MarkerState {
if !marker_path.exists() {
return MarkerState::NoMarker;
}
match fs::read_to_string(marker_path) {
Ok(content) => {
let v = content.trim().to_owned();
if v.is_empty() {
MarkerState::CorruptMarker
} else {
MarkerState::Version(v)
}
}
Err(_) => MarkerState::CorruptMarker,
}
}
fn extract_embedded_version(skill_dir: &include_dir::Dir<'_>) -> String {
let skill_md_path = format!("{}/SKILL.md", skill_dir.path().display());
let Some(skill_file) = BUNDLED_SKILLS_DIR.get_file(&skill_md_path) else {
return "1.0".to_owned();
};
let Ok(content) = std::str::from_utf8(skill_file.contents()) else {
return "1.0".to_owned();
};
parse_frontmatter_version(content).unwrap_or_else(|| "1.0".to_owned())
}
fn is_legacy_bundled(target_dir: &Path, skill_name: &str) -> bool {
let embedded_path = format!("{skill_name}/SKILL.md");
let Some(embedded_file) = BUNDLED_SKILLS_DIR.get_file(&embedded_path) else {
return false;
};
let Ok(embedded_content) = std::str::from_utf8(embedded_file.contents()) else {
return false;
};
match fs::read_to_string(target_dir.join("SKILL.md")) {
Ok(on_disk) => on_disk.trim() == embedded_content.trim(),
Err(_) => false,
}
}
fn parse_frontmatter_version(content: &str) -> Option<String> {
let mut in_frontmatter = false;
let mut in_metadata = false;
for line in content.lines() {
if !in_frontmatter {
if line.trim() == "---" {
in_frontmatter = true;
}
continue;
}
if line.trim() == "---" {
break; }
if line.trim_start().starts_with("metadata:") {
in_metadata = true;
continue;
}
if in_metadata {
if !line.starts_with(' ') && !line.starts_with('\t') {
in_metadata = false;
continue;
}
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("version:") {
let v = rest.trim().trim_matches('"').trim_matches('\'').to_owned();
if !v.is_empty() {
return Some(v);
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn make_skill_md(version: &str) -> String {
format!(
"---\nname: test-skill\ndescription: A test skill\nmetadata:\n version: {version}\n---\n\nSkill body.\n"
)
}
#[test]
fn parse_version_from_frontmatter() {
let content = make_skill_md("2.3");
assert_eq!(parse_frontmatter_version(&content), Some("2.3".to_owned()));
}
#[test]
fn parse_version_missing_returns_none() {
let content = "---\nname: test-skill\ndescription: desc\n---\n\nbody\n";
assert_eq!(parse_frontmatter_version(content), None);
}
#[test]
fn marker_no_file_returns_no_marker() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join(".bundled");
assert!(matches!(read_marker_version(&path), MarkerState::NoMarker));
}
#[test]
fn marker_empty_file_returns_corrupt() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join(".bundled");
fs::write(&path, "").unwrap();
assert!(matches!(
read_marker_version(&path),
MarkerState::CorruptMarker
));
}
#[test]
fn marker_with_version_returns_version() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join(".bundled");
fs::write(&path, "1.5\n").unwrap();
assert!(matches!(
read_marker_version(&path),
MarkerState::Version(v) if v == "1.5"
));
}
#[test]
fn is_legacy_bundled_matches_identical_content() {
let tmp = TempDir::new().unwrap();
let skill_dir = tmp.path().join("test-skill");
fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = make_skill_md("1.0");
fs::write(skill_dir.join("SKILL.md"), &skill_md_content).unwrap();
let managed = TempDir::new().unwrap();
let report1 = provision_bundled_skills(managed.path()).expect("first provision");
assert!(report1.failed.is_empty());
for name in &report1.installed {
let marker = managed.path().join(name).join(".bundled");
if marker.exists() {
fs::remove_file(&marker).unwrap();
}
}
let report2 = provision_bundled_skills(managed.path()).expect("second provision");
assert!(
report2.failed.is_empty(),
"no failures on re-provision: {:?}",
report2.failed
);
assert!(
report2.installed.is_empty(),
"no new installs expected on re-provision"
);
assert!(
report2.skipped.is_empty(),
"no skills should be skipped when content matches embedded"
);
assert!(
!report2.updated.is_empty(),
"all skills without marker must be migrated to updated"
);
for name in &report2.updated {
let marker = managed.path().join(name).join(".bundled");
assert!(
marker.exists(),
"{name}: .bundled marker missing after migration"
);
}
}
#[test]
fn is_legacy_bundled_skips_modified_skill() {
let managed = TempDir::new().unwrap();
let report1 = provision_bundled_skills(managed.path()).expect("first provision");
assert!(report1.failed.is_empty());
assert!(!report1.installed.is_empty());
for name in &report1.installed {
let skill_dir = managed.path().join(name);
let marker = skill_dir.join(".bundled");
if marker.exists() {
fs::remove_file(&marker).unwrap();
}
let skill_md = skill_dir.join("SKILL.md");
if skill_md.exists() {
let mut content = fs::read_to_string(&skill_md).unwrap();
content.push_str("\n# user modification\n");
fs::write(&skill_md, content).unwrap();
}
}
let report2 = provision_bundled_skills(managed.path()).expect("second provision");
assert!(
report2.failed.is_empty(),
"no failures: {:?}",
report2.failed
);
assert!(
report2.updated.is_empty(),
"modified skills must not be updated"
);
assert!(
report2.installed.is_empty(),
"no re-installs expected when dir exists"
);
assert!(
!report2.skipped.is_empty(),
"modified skills must be skipped"
);
}
#[test]
fn provision_to_empty_dir_installs_all_skills() {
let tmp = TempDir::new().unwrap();
let managed = tmp.path();
let report = provision_bundled_skills(managed).expect("provision should succeed");
assert!(
report.failed.is_empty(),
"unexpected failures: {:?}",
report.failed
);
assert!(report.skipped.is_empty(), "no skills should be skipped");
assert!(report.updated.is_empty(), "no skills should be updated");
assert!(
!report.installed.is_empty(),
"at least one skill must be installed"
);
for name in &report.installed {
let skill_dir = managed.join(name);
assert!(
skill_dir.join("SKILL.md").exists(),
"{name}: SKILL.md missing"
);
let marker = skill_dir.join(".bundled");
assert!(marker.exists(), "{name}: .bundled marker missing");
let version = fs::read_to_string(&marker).unwrap();
assert!(
!version.trim().is_empty(),
"{name}: .bundled marker is empty"
);
}
}
}