skiller 0.1.1

Symlink manager for AI tool skill bundles
use anyhow::{bail, Result};
use std::collections::HashMap;
use std::path::PathBuf;

use crate::config::{Config, Target};

pub fn builtin_paths() -> HashMap<&'static str, PathBuf> {
    let home = dirs::home_dir().unwrap_or_default();
    HashMap::from([
        ("claude",    home.join(".claude/skills")),
        ("codex",     home.join(".codex/skills")),
        ("opencode",  home.join(".config/opencode/skills")),
        ("openclaw",  home.join(".openclaw/skills")),
        ("hermes",    home.join(".hermes/skills")),
    ])
}

pub fn cmd_add(cfg: &mut Config, type_name: &str, path: Option<&str>) -> Result<()> {
    if cfg.targets.iter().any(|t| t.r#type == type_name) {
        bail!("target '{}' already exists", type_name);
    }

    let resolved = if let Some(p) = path {
        PathBuf::from(shellexpand::tilde(p).as_ref())
    } else {
        let builtins = builtin_paths();
        builtins
            .get(type_name)
            .cloned()
            .ok_or_else(|| anyhow::anyhow!(
                "unknown type '{}' — provide a path or use a built-in: {}",
                type_name,
                builtin_paths().keys().cloned().collect::<Vec<_>>().join(", ")
            ))?
    };

    cfg.targets.push(Target { r#type: type_name.to_owned(), path: resolved.clone() });
    cfg.save()?;
    println!("added target '{}' → {}", type_name, resolved.display());
    Ok(())
}

pub fn cmd_remove(cfg: &mut Config, type_name: &str) -> Result<()> {
    let before = cfg.targets.len();
    cfg.targets.retain(|t| t.r#type != type_name);
    if cfg.targets.len() == before {
        bail!("target '{}' not found", type_name);
    }
    cfg.save()?;
    println!("removed target '{}'", type_name);
    Ok(())
}

pub fn cmd_list(cfg: &Config) -> Result<()> {
    if cfg.targets.is_empty() {
        println!("no targets configured");
        return Ok(());
    }
    let source = cfg.source.as_deref();
    for t in &cfg.targets {
        let status = link_status(&t.path, source);
        println!("{:<12} {}  [{}]", t.r#type, t.path.display(), status);
    }
    Ok(())
}

pub fn link_status(target: &std::path::Path, source: Option<&std::path::Path>) -> &'static str {
    match target.symlink_metadata() {
        Err(_) => "not linked",
        Ok(m) if m.file_type().is_symlink() => {
            if let (Ok(dst), Some(src)) = (std::fs::read_link(target), source) {
                if dst == src { "linked" } else { "conflict (wrong link)" }
            } else {
                "linked (unknown source)"
            }
        }
        Ok(_) => "conflict (real dir)",
    }
}