use anyhow::Result;
use std::path::Path;
use agent_kit::skill::SkillConfig;
const BUNDLED_SKILL: &str = include_str!("../SKILL.md");
const BUNDLED_RUNBOOKS: &[(&str, &str)] = &[
("compact-exchange.md", include_str!("../runbooks/compact-exchange.md")),
("transfer-extract.md", include_str!("../runbooks/transfer-extract.md")),
];
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn config() -> SkillConfig {
SkillConfig::new("agent-doc", BUNDLED_SKILL, VERSION)
}
fn resolve_root() -> Option<std::path::PathBuf> {
let output = std::process::Command::new("git")
.args(["rev-parse", "--show-superproject-working-tree"])
.output()
.ok()?;
let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !root.is_empty() {
return Some(std::path::PathBuf::from(root));
}
let output = std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.ok()?;
let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !root.is_empty() {
return Some(std::path::PathBuf::from(root));
}
None
}
fn install_runbooks(root: Option<&Path>) -> Result<()> {
let resolved = root.map(|p| p.to_path_buf()).or_else(resolve_root);
let base = resolved.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let runbooks_dir = base.join(".claude/skills/agent-doc/runbooks");
std::fs::create_dir_all(&runbooks_dir)?;
for (name, content) in BUNDLED_RUNBOOKS {
let path = runbooks_dir.join(name);
let needs_write = !path.exists()
|| std::fs::read_to_string(&path)
.map(|existing| existing != *content)
.unwrap_or(true);
if needs_write {
std::fs::write(&path, content)?;
}
}
Ok(())
}
#[allow(dead_code)]
pub fn install_at(root: Option<&Path>) -> Result<()> {
let resolved = root.map(|p| p.to_path_buf()).or_else(resolve_root);
config().install(resolved.as_deref())?;
install_runbooks(resolved.as_deref())
}
#[allow(dead_code)]
pub fn install() -> Result<()> {
install_at(None)
}
pub fn install_and_check_updated() -> Result<bool> {
let cfg = config();
let resolved = resolve_root();
let path = cfg.skill_path(resolved.as_deref());
let was_current = path.exists()
&& std::fs::read_to_string(&path)
.map(|existing| existing == cfg.content)
.unwrap_or(false);
cfg.install(resolved.as_deref())?;
install_runbooks(resolved.as_deref())?;
Ok(!was_current)
}
pub fn install_for(env: agent_kit::detect::Environment) -> Result<()> {
let resolved = resolve_root();
config().install_for(env, resolved.as_deref())?;
install_runbooks(resolved.as_deref())
}
pub fn install_all() -> Result<()> {
let resolved = resolve_root();
config().install_all(resolved.as_deref())?;
install_runbooks(resolved.as_deref())
}
pub fn check_at(root: Option<&Path>) -> Result<()> {
let resolved = root.map(|p| p.to_path_buf()).or_else(resolve_root);
let up_to_date = config().check(resolved.as_deref())?;
if !up_to_date {
std::process::exit(1);
}
Ok(())
}
pub fn check() -> Result<()> {
check_at(None)
}
#[cfg(test)]
mod tests {
use super::*;
use agent_kit::detect::Environment;
#[test]
fn bundled_skill_is_not_empty() {
assert!(!BUNDLED_SKILL.is_empty());
}
#[test]
fn bundled_skill_contains_agent_doc() {
assert!(BUNDLED_SKILL.contains("agent-doc"));
}
fn test_config() -> SkillConfig {
SkillConfig::with_environment("agent-doc", BUNDLED_SKILL, VERSION, Environment::ClaudeCode)
}
fn expected_path(dir: &std::path::Path) -> std::path::PathBuf {
test_config().skill_path(Some(dir))
}
fn install_test(root: Option<&std::path::Path>) -> anyhow::Result<()> {
test_config().install(root)
}
#[test]
fn install_creates_file() {
let dir = tempfile::tempdir().unwrap();
install_test(Some(dir.path())).unwrap();
let path = expected_path(dir.path());
assert!(path.exists(), "skill not found at {}", path.display());
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, BUNDLED_SKILL);
}
#[test]
fn install_idempotent() {
let dir = tempfile::tempdir().unwrap();
install_test(Some(dir.path())).unwrap();
install_test(Some(dir.path())).unwrap();
let path = expected_path(dir.path());
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, BUNDLED_SKILL);
}
#[test]
fn check_not_installed() {
let dir = tempfile::tempdir().unwrap();
let path = expected_path(dir.path());
assert!(!path.exists());
}
#[test]
fn install_creates_runbooks() {
let dir = tempfile::tempdir().unwrap();
install_test(Some(dir.path())).unwrap();
super::install_runbooks(Some(dir.path())).unwrap();
let runbook_path = dir.path().join(".claude/skills/agent-doc/runbooks/compact-exchange.md");
assert!(runbook_path.exists(), "runbook not found at {}", runbook_path.display());
let content = std::fs::read_to_string(&runbook_path).unwrap();
assert!(content.contains("Compact Exchange"));
}
#[test]
fn install_overwrites_outdated() {
let dir = tempfile::tempdir().unwrap();
let path = expected_path(dir.path());
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "old content").unwrap();
install_test(Some(dir.path())).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, BUNDLED_SKILL);
}
}