carryctx 0.0.3

Persistent project context for coding agents
use std::path::PathBuf;

use crate::adapter::xdg::XdgPaths;
use crate::error::CarryCtxError;

const BUNDLED_SKILLS: &[(&str, &[u8])] = &[];

#[derive(Debug, Clone, serde::Serialize)]
pub struct InstalledSkill {
    pub name: String,
    pub path: String,
    pub kind: String,
}

fn skills_dir() -> PathBuf {
    let xdg = XdgPaths::new();
    xdg.data_home.join("carryctx").join("skills")
}

pub fn install_skill(name: Option<&str>) -> Result<Vec<InstalledSkill>, CarryCtxError> {
    let skill_dir = skills_dir();
    crate::adapter::filesystem::ensure_dir(&skill_dir)?;

    let mut installed = Vec::new();

    for (skill_name, data) in BUNDLED_SKILLS {
        if let Some(filter) = name {
            if skill_name != &filter {
                continue;
            }
        }

        let target = skill_dir.join(skill_name);
        crate::adapter::filesystem::ensure_dir(&target)?;

        let file_path = target.join("SKILL.md");
        if !file_path.exists() {
            crate::adapter::filesystem::write_atomic(&file_path, data)?;
        }

        installed.push(InstalledSkill {
            name: skill_name.to_string(),
            path: target.to_string_lossy().to_string(),
            kind: "bundled".into(),
        });
    }

    if installed.is_empty() {
        if let Some(filter) = name {
            return Err(CarryCtxError::resource_not_found(format!(
                "Skill '{filter}' not found in bundled skills."
            )));
        }
    }

    Ok(installed)
}

pub fn list_skills() -> Result<Vec<InstalledSkill>, CarryCtxError> {
    let skill_dir = skills_dir();
    let mut skills = Vec::new();

    if !skill_dir.exists() {
        return Ok(skills);
    }

    let mut dir = std::fs::read_dir(&skill_dir)
        .map_err(|e| CarryCtxError::database_error(format!("Failed to read skills dir: {e}")))?;

    while let Some(Ok(entry)) = dir.next() {
        let path = entry.path();
        if path.is_dir() {
            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                skills.push(InstalledSkill {
                    name: name.to_string(),
                    path: path.to_string_lossy().to_string(),
                    kind: classify_skill(&path),
                });
            }
        }
    }

    skills.sort_by(|a, b| a.name.cmp(&b.name));
    Ok(skills)
}

fn classify_skill(path: &std::path::Path) -> String {
    let skill_md = path.join("SKILL.md");
    if skill_md.exists() {
        "complete".into()
    } else {
        "incomplete".into()
    }
}

pub fn get_skill_path(name: &str) -> Result<String, CarryCtxError> {
    let skill_dir = skills_dir().join(name);
    if !skill_dir.exists() {
        return Err(CarryCtxError::resource_not_found(format!(
            "Skill '{name}' is not installed."
        )));
    }
    Ok(skill_dir.to_string_lossy().to_string())
}