use std::path::{Path, PathBuf};
use super::change::{Action, Change, read};
use crate::harness::{Harness, Level};
pub const FILES: [(&str, &str); 5] = [
("SKILL.md", include_str!("../../assets/skills/borg/SKILL.md")),
("references/collectives.md", include_str!("../../assets/skills/borg/references/collectives.md")),
("references/team.md", include_str!("../../assets/skills/borg/references/team.md")),
("references/vectors.md", include_str!("../../assets/skills/borg/references/vectors.md")),
("references/troubleshooting.md", include_str!("../../assets/skills/borg/references/troubleshooting.md")),
];
const RETIRED: [(&str, &[&str]); 1] = [(
"references/hive.md",
&[
"e489258ae65ce54c7b30b8d30e18754424946420a39cb03e45daa1cadb602380",
"e5f0eff3bddbfc883290fbefc781bcee3286562f06d95a64496514c97b686d94",
"c305e7b93c700d7f38c9b7968116fc4ece56a4f5b2f05f6bc47dadeecab84741",
"15f4dc8013f7584988d7ce035af1589917ad9e2b52f744792b5a42973f41c939",
"d11ef6672729f9d669a708e7439ef653da14eed88e908dcaed7d2dbb94e65fbb",
"23e607385e63f7b202e6bebf3dc8617023ec632f88958073354e9d6c0629be10",
],
)];
fn retired(dir: &Path) -> Vec<(PathBuf, String)> {
RETIRED
.iter()
.filter_map(|(rel, known)| {
let path = dir.join(rel);
let text = read(&path).ok().flatten()?;
known.contains(&blake3::hash(text.as_bytes()).to_hex().as_str()).then_some((path, text))
})
.collect()
}
pub fn dir(h: &dyn Harness, home: &Path, var: &dyn Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
let up = 1 + usize::from(!h.wiring().adapter().is_empty());
let dir = h.target(home, var).ancestors().nth(up)?.join("skills/borg");
(!h.hidden()).then_some(dir)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum State {
Absent,
Current,
Stale,
Linked,
Unreadable(String),
}
fn linked(dir: &Path) -> bool {
std::fs::symlink_metadata(dir).is_ok_and(|m| m.file_type().is_symlink())
}
fn current(dir: &Path) -> Result<Vec<Option<String>>, String> {
FILES.iter().map(|(rel, _)| read(&dir.join(rel)).map_err(|e| format!("{}: cannot read ({e}); nothing was changed", dir.join(rel).display()))).collect()
}
pub fn plan(action: Action, dir: Option<&Path>) -> Result<(Vec<Change>, Vec<String>), String> {
let Some(dir) = dir else { return Ok((Vec::new(), Vec::new())) };
if linked(dir) {
return Ok((Vec::new(), vec![format!("{}: left alone, a symlink (yours); the borg skill is not managed there", dir.display())]));
}
let befores = current(dir)?;
let mut changes = Vec::new();
let mut foreign = Vec::new();
for ((rel, text), before) in FILES.iter().zip(befores) {
let path = dir.join(rel);
let ours = before.as_deref() == Some(*text);
match action {
Action::Install if !ours => changes.push(Change { path, before, after: Some((*text).to_string()), through_links: false, backup: true }),
Action::Uninstall if ours => changes.push(Change { path, before, after: None, through_links: false, backup: false }),
Action::Uninstall if before.is_some() => foreign.push(path.display().to_string()),
_ => {}
}
}
for (path, text) in retired(dir) {
changes.push(Change { path, before: Some(text), after: None, through_links: false, backup: false });
}
let notes = foreign.into_iter().map(|p| format!("{p}: left alone, its content is not this borg's skill (yours, or an older borg's)")).collect();
Ok((changes, notes))
}
pub fn tidy(dir: &Path) {
if linked(dir) {
return;
}
let _ = std::fs::remove_dir(dir.join("references"));
let _ = std::fs::remove_dir(dir);
}
#[must_use]
pub fn state(dir: &Path) -> State {
if linked(dir) {
return State::Linked;
}
match current(dir) {
Err(e) => State::Unreadable(e),
Ok(found) if found.iter().all(Option::is_none) => State::Absent,
Ok(found) => {
let same = FILES.iter().zip(&found).all(|((_, text), f)| f.as_deref() == Some(*text));
if same && retired(dir).is_empty() { State::Current } else { State::Stale }
}
}
}
#[must_use]
pub fn status(name: &str, dir: Option<&Path>) -> Option<String> {
let dir = dir?;
Some(match state(dir) {
State::Absent => "skill not installed".to_string(),
State::Current => format!("skill installed ({})", dir.display()),
State::Stale => format!("skill STALE: {} differs from this borg's (borg install {name})", dir.display()),
State::Linked => format!("skill: {} is a symlink (yours)", dir.display()),
State::Unreadable(e) => format!("skill UNREADABLE: {e}"),
})
}
#[must_use]
pub fn checks(name: &str, dir: Option<&Path>, wired: bool) -> Vec<(Level, String)> {
let Some(dir) = dir else { return Vec::new() };
let fix = format!("borg install {name} --apply");
match state(dir) {
State::Absent if wired => vec![(Level::Warn, format!("{name}: the borg skill is missing at {} ({fix})", dir.display()))],
State::Absent => Vec::new(),
State::Current => vec![(Level::Ok, format!("{name}: borg skill installed at {}", dir.display()))],
State::Stale => vec![(Level::Warn, format!("{name}: the borg skill at {} differs from this borg's, older or edited ({fix})", dir.display()))],
State::Linked => vec![(Level::Ok, format!("{name}: the borg skill at {} is a symlink (yours); borg leaves it alone", dir.display()))],
State::Unreadable(e) => vec![(Level::Warn, format!("{name}: borg skill {e}"))],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::install::change::apply;
fn tmp(name: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("borg-skill-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn run(action: Action, dir: &Path, stamp: &str) -> (usize, Vec<String>) {
let (changes, notes) = plan(action, Some(dir)).unwrap();
for c in &changes {
apply(c, stamp).unwrap();
}
tidy(dir);
(changes.len(), notes)
}
#[test]
fn the_skill_lands_in_each_harness_own_config_directory() {
let home = Path::new("/h");
for h in crate::harness::visible() {
let d = dir(h, home, &|_| None).unwrap();
assert!(d.starts_with(home) && d.ends_with("skills/borg"), "{}", d.display());
assert_eq!(dir(h, home, &|_| Some(PathBuf::from("/v"))).unwrap(), Path::new("/v/skills/borg"), "{}", h.name());
}
assert!(crate::harness::ALL.iter().filter(|h| h.hidden()).all(|h| dir(*h, home, &|_| None).is_none()));
}
#[test]
fn the_skill_names_itself_and_says_when_to_load() {
let (name, skill) = FILES[0];
assert_eq!(name, "SKILL.md");
let front = skill.strip_prefix("---\n").and_then(|s| s.split_once("\n---\n")).map(|(f, _)| f).expect("frontmatter first");
assert!(front.lines().any(|l| l == "name: borg"), "{front}");
let desc = front.lines().find_map(|l| l.strip_prefix("description: ")).expect("a description");
assert!(desc.len() <= 1024, "opencode's limit");
for word in ["borg", "collective", "lesson", "drone", "queen", "mesh", "vectors"] {
assert!(desc.contains(word), "the description triggers on {word}");
}
for (rel, _) in &FILES[1..] {
assert!(skill.contains(rel), "SKILL.md links {rel}");
}
}
#[test]
fn install_copies_every_file_once_and_uninstall_removes_exactly_them() {
let home = tmp("cycle");
let dir = home.join("skills/borg");
assert_eq!(state(&dir), State::Absent);
assert_eq!(run(Action::Install, &dir, "a").0, FILES.len());
assert_eq!(state(&dir), State::Current);
assert_eq!(run(Action::Install, &dir, "b").0, 0, "idempotent");
assert_eq!(run(Action::Uninstall, &dir, "c").0, FILES.len());
assert!(!dir.exists(), "the empty skill directory goes too");
assert!(home.join("skills").is_dir(), "the harness's skills directory stays");
assert_eq!(run(Action::Uninstall, &dir, "d").0, 0);
}
#[test]
fn an_edited_file_is_stale_replaced_with_a_backup_and_never_uninstalled() {
let dir = tmp("edited").join("borg");
run(Action::Install, &dir, "a");
std::fs::write(dir.join("references/team.md"), "my notes").unwrap();
assert_eq!(state(&dir), State::Stale);
assert!(checks("x", Some(&dir), true)[0].1.contains("borg install x --apply"), "the harness's own fix, not all");
let (n, notes) = run(Action::Uninstall, &dir, "u");
assert_eq!((n, notes.len()), (FILES.len() - 1, 1), "{notes:?}");
assert_eq!(std::fs::read_to_string(dir.join("references/team.md")).unwrap(), "my notes", "left alone");
assert_eq!(run(Action::Install, &dir, "i").0, FILES.len());
assert_eq!(std::fs::read_to_string(dir.join("references/team.md.borg-backup-i")).unwrap(), "my notes", "backed up");
assert_eq!(state(&dir), State::Current);
}
#[test]
fn an_older_borgs_retired_file_goes_and_an_edited_one_stays() {
let dir = tmp("retired").join("borg");
run(Action::Install, &dir, "a");
let old = dir.join("references/hive.md");
let shipped = std::process::Command::new("git")
.args(["show", "188c262:crates/borg/assets/skills/borg/references/hive.md"])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output()
.ok()
.filter(|o| o.status.success());
if let Some(o) = shipped {
std::fs::write(&old, &o.stdout).unwrap();
assert_eq!(state(&dir), State::Stale, "a leftover of an older borg asks for a reinstall");
assert_eq!(run(Action::Install, &dir, "b").0, 1);
assert!(!old.exists());
}
std::fs::write(&old, "my notes").unwrap();
assert_eq!(state(&dir), State::Current, "the engineer's own file is not borg's");
assert_eq!(run(Action::Install, &dir, "c").0, 0);
assert_eq!(std::fs::read_to_string(&old).unwrap(), "my notes");
}
#[cfg(unix)]
#[test]
fn symlinks_a_linked_file_is_replaced_and_a_linked_directory_left_alone() {
let home = tmp("links");
let outside = home.join("checkout.md");
std::fs::write(&outside, "checkout").unwrap();
let dir = home.join("borg");
std::fs::create_dir_all(&dir).unwrap();
std::os::unix::fs::symlink(&outside, dir.join("SKILL.md")).unwrap();
run(Action::Install, &dir, "a");
assert_eq!(std::fs::read_to_string(&outside).unwrap(), "checkout", "not written through");
assert_eq!(state(&dir), State::Current);
let linked_dir = home.join("linked");
std::os::unix::fs::symlink(home.join("elsewhere"), &linked_dir).unwrap();
let (n, notes) = run(Action::Install, &linked_dir, "b");
assert_eq!((n, notes.len()), (0, 1));
assert!(!home.join("elsewhere").exists());
assert_eq!(state(&linked_dir), State::Linked);
}
#[test]
fn doctor_warns_on_a_missing_skill_only_where_borg_is_wired() {
let dir = tmp("doctor").join("borg");
assert!(checks("x", Some(&dir), false).is_empty());
assert_eq!(checks("x", Some(&dir), true)[0].0, Level::Warn);
assert!(checks("x", None, true).is_empty(), "a harness without skills");
assert_eq!(status("x", None), None);
run(Action::Install, &dir, "a");
assert_eq!(checks("x", Some(&dir), true)[0].0, Level::Ok);
assert!(status("x", Some(&dir)).unwrap().starts_with("skill installed"));
}
}