#[derive(
Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct SkillRow {
pub name: String,
pub description: String,
pub dir: String,
pub skill_md: String,
pub valid: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub problem: Option<String>,
}
#[derive(
Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct SkillsReport {
pub skills: Vec<SkillRow>,
}
#[cfg(test)]
mod report_tests {
use super::*;
fn row(name: &str, description: &str) -> SkillRow {
SkillRow {
name: name.into(),
description: description.into(),
dir: format!("/p/{name}"),
skill_md: format!("/p/{name}/SKILL.md"),
valid: true,
problem: None,
}
}
#[test]
fn a_valid_row_carries_no_problem_key() {
let wire = serde_json::to_value(row("alpha", "d")).expect("ser");
assert!(wire.get("problem").is_none(), "{wire}");
assert_eq!(wire["dir"], "/p/alpha");
assert_eq!(wire["skill_md"], "/p/alpha/SKILL.md");
}
}
use std::collections::BTreeSet;
use crate::skills::root::SkillsRoot;
use bitrouter_orchestrator::harness::skills::format::{DiscoveredSkill, discover_all_skills};
pub struct InstalledSkills {
roots: Vec<SkillsRoot>,
}
impl InstalledSkills {
pub fn new(roots: Vec<SkillsRoot>) -> Self {
Self { roots }
}
pub fn report(&self) -> SkillsReport {
let mut skills = Vec::new();
let mut seen: BTreeSet<std::path::PathBuf> = BTreeSet::new();
for root in &self.roots {
for found in discover_all_skills(&root.discovery_root()) {
if !seen.insert(found.skill_md.clone()) {
continue;
}
skills.push(row(&found));
}
}
skills.sort_by(|a, b| (&a.name, &a.dir).cmp(&(&b.name, &b.dir)));
SkillsReport { skills }
}
}
fn row(found: &DiscoveredSkill) -> SkillRow {
let problem = found.problem();
SkillRow {
name: found.name(),
description: found.description().to_string(),
dir: found.dir.display().to_string(),
skill_md: found.skill_md.display().to_string(),
valid: problem.is_none(),
problem,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn install(root: &std::path::Path, rel: &str, dir: &str, body: &str) -> std::path::PathBuf {
let mut skill_dir = root.to_path_buf();
for segment in rel.split('/').filter(|segment| !segment.is_empty()) {
skill_dir.push(segment);
}
skill_dir.push(dir);
std::fs::create_dir_all(&skill_dir).expect("mkdir");
std::fs::write(skill_dir.join("SKILL.md"), body).expect("write");
skill_dir
}
fn valid(name: &str, description: &str) -> String {
format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n\nDo it.\n")
}
fn project(root: &std::path::Path) -> InstalledSkills {
InstalledSkills::new(
SkillsRoot::cli_scope(false, root.to_path_buf()).expect("project scope"),
)
}
#[test]
fn every_conventional_layout_is_one_listing() {
let dir = tempfile::tempdir().expect("tempdir");
install(
dir.path(),
".claude/skills",
"installed",
&valid("installed", "d"),
);
install(dir.path(), "skills", "bundled", &valid("bundled", "d"));
let names: Vec<_> = project(dir.path())
.report()
.skills
.into_iter()
.map(|s| s.name)
.collect();
assert_eq!(names, vec!["bundled".to_string(), "installed".to_string()]);
}
#[test]
fn a_broken_skill_is_listed_and_marked() {
let dir = tempfile::tempdir().expect("tempdir");
install(dir.path(), ".claude/skills", "ok", &valid("ok", "d"));
install(
dir.path(),
".claude/skills",
"broken",
"---\nname: broken\n---\n",
);
let skills = project(dir.path()).report().skills;
assert_eq!(skills.len(), 2);
let broken = skills.iter().find(|s| s.name == "broken").expect("listed");
assert!(!broken.valid);
assert!(broken.problem.is_some());
let ok = skills.iter().find(|s| s.name == "ok").expect("listed");
assert!(ok.valid && ok.problem.is_none());
}
#[test]
fn a_row_carries_both_the_directory_and_the_skill_md() {
let dir = tempfile::tempdir().expect("tempdir");
let installed = install(dir.path(), ".claude/skills", "alpha", &valid("alpha", "d"));
let row = project(dir.path()).report().skills.remove(0);
assert_eq!(row.dir, installed.display().to_string());
assert_eq!(
row.skill_md,
installed.join("SKILL.md").display().to_string()
);
}
#[cfg(unix)]
#[test]
fn a_symlinked_skill_directory_is_not_listed() {
let outside = tempfile::tempdir().expect("tempdir");
install(outside.path(), "", "escape", &valid("escape", "d"));
let dir = tempfile::tempdir().expect("tempdir");
let skills = dir.path().join(".claude").join("skills");
std::fs::create_dir_all(&skills).expect("mkdir");
std::os::unix::fs::symlink(outside.path().join("escape"), skills.join("escape"))
.expect("symlink");
assert!(
project(dir.path()).report().skills.is_empty(),
"a symlink beneath the root must not be traversed"
);
}
}