clipmem 0.6.0

macOS clipboard memory backed by SQLite and searchable from agent runtimes
Documentation
use std::collections::HashSet;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};

use super::doctor::{AgentDoctorCheck, AgentDoctorStatus};

#[derive(Debug, Clone, Copy)]
pub(in crate::cli) struct PackagedSkillFile {
    pub(in crate::cli) relative_path: &'static str,
    pub(in crate::cli) contents: &'static str,
}

pub(in crate::cli) fn install_packaged_skill(
    target_dir: &Path,
    files: &[PackagedSkillFile],
) -> Result<()> {
    std::fs::create_dir_all(target_dir)
        .with_context(|| format!("failed to create {}", target_dir.display()))?;

    for file in files {
        let path = target_dir.join(file.relative_path);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("failed to create {}", parent.display()))?;
        }
        std::fs::write(&path, file.contents)
            .with_context(|| format!("failed to write {}", path.display()))?;
        if file.relative_path.ends_with(".sh") {
            set_executable(&path)
                .with_context(|| format!("failed to mark {} executable", path.display()))?;
        }
    }

    Ok(())
}

#[cfg(unix)]
pub(in crate::cli) fn set_executable(path: &Path) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    let mut perms = std::fs::metadata(path)?.permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(path, perms)?;
    Ok(())
}

#[cfg(not(unix))]
pub(in crate::cli) fn set_executable(_path: &Path) -> Result<()> {
    // On non-Unix targets there is no concept of the executable bit to set;
    // scripts are invoked via their interpreter explicitly.
    Ok(())
}

pub(in crate::cli) fn home_dir() -> Result<PathBuf> {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .ok_or_else(|| anyhow!("HOME is not set"))
}

pub(in crate::cli) fn find_executable(name: &str) -> Option<PathBuf> {
    let path = std::env::var_os("PATH")?;
    std::env::split_paths(&path).find_map(|dir| {
        let candidate = dir.join(name);
        candidate.is_file().then_some(candidate)
    })
}

pub(in crate::cli) fn binary_check(
    label: &str,
    path: Option<&PathBuf>,
    next_steps: &[&str],
) -> AgentDoctorCheck {
    match path {
        Some(path) => AgentDoctorCheck {
            status: AgentDoctorStatus::Ok,
            label: label.to_string(),
            detail: format!("Found {}", path.display()),
            next_steps: Vec::new(),
        },
        None => AgentDoctorCheck {
            status: AgentDoctorStatus::Fail,
            label: label.to_string(),
            detail: format!("{label} is missing"),
            next_steps: next_steps.iter().map(|s| s.to_string()).collect(),
        },
    }
}

pub(in crate::cli) fn validate_packaged_skill_file(path: &Path, relative_path: &str) -> Result<()> {
    if !path.is_file() {
        return Err(anyhow!("packaged file is missing: {}", path.display()));
    }

    if relative_path.ends_with(".sh") {
        ensure_executable(path)
            .with_context(|| format!("packaged script is not executable: {}", path.display()))?;
    }

    Ok(())
}

#[cfg(unix)]
pub(in crate::cli) fn ensure_executable(path: &Path) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
    if mode & 0o111 == 0 {
        return Err(anyhow!("mode {mode:o} does not include any execute bit"));
    }

    Ok(())
}

#[cfg(not(unix))]
pub(in crate::cli) fn ensure_executable(_path: &Path) -> Result<()> {
    Ok(())
}

pub(in crate::cli) fn referenced_markdown_files(content: &str) -> Vec<PathBuf> {
    let mut seen = HashSet::new();
    let mut references = Vec::new();

    for line in content.lines() {
        let mut remainder = line;
        while let Some(start) = remainder.find("(references/") {
            let after = &remainder[start + 1..];
            let Some(end) = after.find(')') else {
                break;
            };
            let candidate = &after[..end];
            if candidate.ends_with(".md") && seen.insert(candidate.to_string()) {
                references.push(PathBuf::from(candidate));
            }
            remainder = &after[end + 1..];
        }
    }

    references
}

pub(in crate::cli) fn parse_skill_frontmatter(content: &str) -> Result<Vec<&str>> {
    let mut lines = content.lines();
    if lines.next() != Some("---") {
        return Err(anyhow!("skill frontmatter must start with `---`"));
    }

    let mut frontmatter_lines = Vec::new();
    for line in lines {
        if line == "---" {
            return Ok(frontmatter_lines);
        }
        frontmatter_lines.push(line);
    }

    Err(anyhow!("skill frontmatter is missing the closing `---`"))
}

pub(in crate::cli) fn frontmatter_value<'a>(lines: &'a [&str], key: &str) -> Option<&'a str> {
    let prefix = format!("{key}:");
    lines
        .iter()
        .find_map(|line| line.strip_prefix(&prefix).map(str::trim))
}

pub(in crate::cli) fn required_frontmatter_value<'a>(
    lines: &'a [&str],
    key: &str,
) -> Result<&'a str> {
    frontmatter_value(lines, key)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| anyhow!("skill frontmatter must include `{key}`"))
}

pub(in crate::cli) fn validate_required_skill_references(
    content: &str,
    required_references: &[&str],
) -> Result<()> {
    let references = referenced_markdown_files(content);
    for required in required_references {
        if !references
            .iter()
            .any(|reference| reference == Path::new(required))
        {
            return Err(anyhow!("skill content must reference `{required}`"));
        }
    }
    Ok(())
}