mod claude;
mod copilot;
use anyhow::{bail, Result};
use std::path::{Path, PathBuf};
pub struct MaterializedSkill {
pub name: String,
pub path: PathBuf,
}
pub struct VendorStatus {
pub location: PathBuf,
pub present: Vec<String>,
pub missing: Vec<String>,
pub stale: Vec<String>,
pub notes: Vec<String>,
}
pub fn classify(skills_dir: &Path, expected: &[String]) -> VendorStatus {
let (mut present, mut missing) = (Vec::new(), Vec::new());
for name in expected {
if skills_dir.join(name).is_dir() {
present.push(name.clone());
} else {
missing.push(name.clone());
}
}
let mut stale = Vec::new();
if let Ok(entries) = std::fs::read_dir(skills_dir) {
for e in entries.flatten() {
if e.path().is_dir() {
if let Ok(n) = e.file_name().into_string() {
if !expected.iter().any(|x| x == &n) {
stale.push(n);
}
}
}
}
}
stale.sort();
VendorStatus {
location: skills_dir.to_path_buf(),
present,
missing,
stale,
notes: Vec::new(),
}
}
pub trait Vendor {
#[allow(dead_code)] fn name(&self) -> &'static str;
fn materialize(
&self,
project_root: &Path,
project_id: &str,
skills: &[MaterializedSkill],
) -> Result<()>;
fn clean(&self, project_root: &Path, project_id: &str) -> Result<()>;
fn status(&self, project_root: &Path, expected: &[String]) -> Result<VendorStatus>;
}
pub const ALL_TARGETS: &[&str] = &["claude", "copilot"];
pub fn for_target(target: &str) -> Result<Box<dyn Vendor>> {
match target {
"claude" => Ok(Box::new(claude::Claude)),
"copilot" => Ok(Box::new(copilot::Copilot)),
other => bail!(
"unknown target `{other}` (supported: {})",
ALL_TARGETS.join(", ")
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_targets_resolve_and_report_their_own_name() {
for t in ALL_TARGETS {
let v = for_target(t).expect("advertised target must resolve");
assert_eq!(&v.name(), t, "vendor name must match its target key");
}
}
}