agentdiff 0.1.26

Audit and trace autonomous AI code contributions in git repositories
use crate::cli::{InstallSkillArgs, SkillScope};
use crate::util::{ok, print_command_header, warn};
use anyhow::{Context, Result};
use colored::Colorize;
use std::path::{Path, PathBuf};

const SKILL_CONTENT: &str = include_str!("../../templates/skills/agentdiff-context/SKILL.md");
const SKILL_REL_PATH: &[&str] = &["agentdiff-context", "SKILL.md"];

pub fn run(repo_root: &Path, args: &InstallSkillArgs) -> Result<()> {
    print_command_header("install-skill");

    let path = skill_path(repo_root, &args.scope)?;
    if path.exists() && !args.force {
        println!(
            "  {} {} already exists — skipping (use --force to overwrite)",
            warn(),
            path.display()
        );
        println!();
        return Ok(());
    }

    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("creating {}", parent.display()))?;
    }
    std::fs::write(&path, SKILL_CONTENT).with_context(|| format!("writing {}", path.display()))?;

    println!("  {} wrote {}", ok(), path.display());
    println!();
    println!("  Next steps:");
    match args.scope {
        SkillScope::Project => {
            println!(
                "    1. Commit {}",
                ".cursor/skills/agentdiff-context/SKILL.md".cyan()
            );
            println!("    2. Ask agents to use AgentDiff context before editing traced files");
        }
        SkillScope::Global => {
            println!("    1. Restart or refresh Cursor agents so the global skill is discovered");
            println!(
                "    2. Prefer project scope when repository-specific guidance should be versioned"
            );
        }
    }
    println!();
    Ok(())
}

fn skill_path(repo_root: &Path, scope: &SkillScope) -> Result<PathBuf> {
    match scope {
        SkillScope::Project => Ok(repo_root
            .join(".cursor")
            .join("skills")
            .join(SKILL_REL_PATH[0])
            .join(SKILL_REL_PATH[1])),
        SkillScope::Global => {
            let home = dirs::home_dir().context("could not determine home directory")?;
            Ok(home
                .join(".agents")
                .join("skills")
                .join(SKILL_REL_PATH[0])
                .join(SKILL_REL_PATH[1]))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn project_scope_writes_under_repo_cursor_skills() {
        let path = skill_path(Path::new("/repo"), &SkillScope::Project).unwrap();
        assert_eq!(
            path,
            PathBuf::from("/repo/.cursor/skills/agentdiff-context/SKILL.md")
        );
    }

    #[test]
    fn embedded_skill_has_expected_frontmatter() {
        assert!(SKILL_CONTENT.contains("name: agentdiff-context"));
        assert!(SKILL_CONTENT.contains("agentdiff context path/to/file --json"));
    }
}