clipmem 0.5.4

macOS clipboard memory backed by SQLite and searchable from agent runtimes
Documentation
use std::path::Path;

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

use crate::cli::schema::{OpenClawInstallSkillArgs, OpenClawUninstallSkillArgs};

use super::package::{packaged_openclaw_files, resolve_openclaw_skill_dir};
use super::support::install_packaged_skill;

pub(in crate::cli) fn openclaw_install_skill(args: &OpenClawInstallSkillArgs) -> Result<()> {
    let target_dir = resolve_openclaw_skill_dir(args.dest.as_deref(), args.shared)?;
    if target_dir.exists() {
        if args.force {
            std::fs::remove_dir_all(&target_dir).with_context(|| {
                format!(
                    "failed to remove existing skill at {}",
                    target_dir.display()
                )
            })?;
        } else {
            return Err(anyhow!(
                "skill directory already exists at {} (pass --force to replace it)",
                target_dir.display()
            ));
        }
    }

    install_openclaw_package(&target_dir)?;

    println!("Installed OpenClaw skill into {}", target_dir.display());
    println!("Skill file: {}", target_dir.join("SKILL.md").display());
    println!(
        "Reload OpenClaw skills or restart OpenClaw if the skill does not appear immediately."
    );
    Ok(())
}

pub(in crate::cli) fn openclaw_uninstall_skill(args: &OpenClawUninstallSkillArgs) -> Result<()> {
    let target_dir = resolve_openclaw_skill_dir(args.dest.as_deref(), args.shared)?;
    if !target_dir.exists() {
        println!("No installed skill found at {}", target_dir.display());
        return Ok(());
    }

    std::fs::remove_dir_all(&target_dir)
        .with_context(|| format!("failed to remove {}", target_dir.display()))?;
    println!("Removed OpenClaw skill from {}", target_dir.display());
    Ok(())
}

pub(in crate::cli) fn install_openclaw_package(target_dir: &Path) -> Result<()> {
    install_packaged_skill(target_dir, packaged_openclaw_files())
}

#[cfg(test)]
mod tests {
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::super::package::{
        packaged_openclaw_files, resolve_openclaw_skill_dir, OPENCLAW_SKILL_NAME,
    };
    use super::super::support::{install_packaged_skill, PackagedSkillFile};

    fn temp_skill_dir(name: &str) -> std::path::PathBuf {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system clock should be after epoch")
            .as_nanos();
        std::env::temp_dir().join(format!("clipmem-{name}-{}-{nonce}", std::process::id()))
    }

    #[test]
    fn resolve_openclaw_skill_dir_uses_explicit_destination() {
        let explicit = std::path::Path::new("/tmp/custom-openclaw-skill");

        assert_eq!(
            resolve_openclaw_skill_dir(Some(explicit), false)
                .expect("explicit path should resolve"),
            explicit
        );
    }

    #[test]
    fn packaged_openclaw_files_include_skill_and_setup_script() {
        let files = packaged_openclaw_files();

        assert!(files
            .iter()
            .any(|file| file.relative_path == "SKILL.md"
                && file.contents.contains(OPENCLAW_SKILL_NAME)));
        assert!(files
            .iter()
            .any(|file| file.relative_path == "scripts/check-setup.sh"
                && file.contents.starts_with("#!/")));
    }

    #[test]
    fn install_packaged_skill_writes_nested_files_and_marks_scripts_executable() {
        let target_dir = temp_skill_dir("packaged-skill");
        let files = [
            PackagedSkillFile {
                relative_path: "SKILL.md",
                contents: "name: test\n",
            },
            PackagedSkillFile {
                relative_path: "scripts/check-setup.sh",
                contents: "#!/bin/sh\nexit 0\n",
            },
        ];

        install_packaged_skill(&target_dir, &files).expect("package should install");

        let skill_path = target_dir.join("SKILL.md");
        let script_path = target_dir.join("scripts/check-setup.sh");
        assert_eq!(
            std::fs::read_to_string(&skill_path).expect("skill file should be readable"),
            "name: test\n"
        );
        assert!(script_path.is_file());

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = std::fs::metadata(&script_path)
                .expect("script metadata should be readable")
                .permissions()
                .mode();
            assert_ne!(mode & 0o111, 0);
        }

        let _ = std::fs::remove_dir_all(target_dir);
    }
}