use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use crate::backend::Registry;
const SKILL_MD: &str = include_str!("../../skills/SKILL.md");
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn auto_update(registry: &Registry) {
let home = match std::env::var("HOME") {
Ok(h) => h,
Err(_) => return,
};
for (config_dir, agent_name) in [
(".claude", "Claude Code"),
(".codex", "Codex"),
(".forge", "Forge"),
] {
let skill_dir = skill_dir_for_home(&PathBuf::from(&home), config_dir);
let version_file = skill_dir.join(".version");
if !version_file.exists() {
continue;
}
let installed = std::fs::read_to_string(&version_file).unwrap_or_default();
if installed.trim() == VERSION {
continue;
}
let _ = init_agent(registry, config_dir, agent_name);
}
}
pub fn run_init(target: &str, registry: &Registry) -> Result<()> {
match target {
"claude" => init_agent(registry, ".claude", "Claude Code"),
"codex" => init_agent(registry, ".codex", "Codex"),
"forge" => init_agent(registry, ".forge", "Forge"),
_ => bail!("unknown init target: {target} (use: claude, codex, forge, agent-context)"),
}
}
pub fn emit_skill_yaml() {
if let Some(fm) = extract_frontmatter(SKILL_MD) {
println!("---\n{fm}\n---");
} else {
print!("{SKILL_MD}");
}
}
fn extract_frontmatter(doc: &str) -> Option<&str> {
let rest = doc.strip_prefix("---\n")?;
let end = rest.find("\n---\n")?;
Some(&rest[..end])
}
fn init_agent(registry: &Registry, config_dir: &str, agent_name: &str) -> Result<()> {
let home = std::env::var("HOME").context("HOME not set")?;
let skill_dir = skill_dir_for_home(&PathBuf::from(&home), config_dir);
let adapters_dir = skill_dir.join("references/adapters");
let adapters: Vec<(&str, &str)> = registry
.all_backends()
.iter()
.flat_map(|b| b.adapters())
.collect();
let version_file = skill_dir.join(".version");
#[cfg(not(debug_assertions))]
if version_file.exists() {
if let Ok(existing) = std::fs::read_to_string(&version_file) {
if existing.trim() == VERSION {
eprintln!("dbg v{VERSION} skill already up to date");
match ensure_on_path()? {
Some(path) => eprintln!("installed: {}", path.display()),
None => eprintln!("dbg already on PATH"),
}
return Ok(());
}
}
}
let _ = std::fs::remove_file(skill_dir.join(".hash"));
std::fs::create_dir_all(&adapters_dir).context("failed to create skill directory")?;
std::fs::write(skill_dir.join("SKILL.md"), SKILL_MD)?;
for (filename, content) in &adapters {
std::fs::write(adapters_dir.join(filename), content)?;
}
std::fs::write(&version_file, VERSION)?;
eprintln!("dbg v{VERSION} skill installed: {}", skill_dir.display());
match ensure_on_path()? {
Some(path) => eprintln!("installed: {}", path.display()),
None => eprintln!("dbg already on PATH"),
}
eprintln!("done — restart {agent_name} to activate");
Ok(())
}
pub fn ensure_on_path() -> Result<Option<PathBuf>> {
let bin_dir = dirs_home().join(".local/bin");
let dest = bin_dir.join("dbg");
let self_exe = std::env::current_exe()?;
let self_canon = std::fs::canonicalize(&self_exe)?;
if dest.exists() {
if let Ok(dest_canon) = std::fs::canonicalize(&dest) {
if dest_canon == self_canon {
return Ok(None);
}
}
}
if let Ok(found) = which::which("dbg") {
if let Ok(found_canon) = std::fs::canonicalize(&found) {
if found_canon == self_canon {
return Ok(None);
}
}
}
std::fs::create_dir_all(&bin_dir)?;
if dest.exists() || dest.is_symlink() {
std::fs::remove_file(&dest)?;
}
std::fs::copy(&self_exe, &dest)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755))?;
}
Ok(Some(dest))
}
fn skill_dir_for_home(home: &std::path::Path, config_dir: &str) -> PathBuf {
home.join(config_dir).join("skills/dbg")
}
fn dirs_home() -> PathBuf {
std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn init_target_help_mentions_forge() {
let registry = Registry::new();
let err = run_init("not-an-agent", ®istry).unwrap_err().to_string();
assert!(
err.contains("forge"),
"init target help should mention forge: {err}"
);
}
#[test]
fn forge_skill_dir_is_under_user_home() {
let home = PathBuf::from("/home/example");
assert_eq!(
skill_dir_for_home(&home, ".forge"),
PathBuf::from("/home/example/.forge/skills/dbg")
);
}
}