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 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: claude, copilot)"),
}
}