//! Claude Code skill installation for makima commands.
//!
//! This module installs makima CLI commands as Claude Code skills
//! to ~/.claude/skills/ on daemon startup. Skills allow Claude Code
//! instances to use makima commands via slash commands like
//! `/makima-supervisor`, `/makima-contract`, and `/makima-directive`.
use std::path::PathBuf;
use tokio::fs;
use super::skills;
/// Get the global Claude skills directory (~/.claude/skills/)
fn skills_dir() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".claude").join("skills"))
}
/// Check if a skill needs to be installed or updated
async fn needs_install(skill_dir: &PathBuf, expected_content: &str) -> bool {
let skill_file = skill_dir.join("SKILL.md");
match fs::read_to_string(&skill_file).await {
Ok(content) => content != expected_content,
Err(_) => true, // File doesn't exist or can't be read
}
}
/// Install or update all makima skills to ~/.claude/skills/
///
/// This function:
/// 1. Creates the skills directory if needed
/// 2. For each skill, checks if it needs updating
/// 3. Writes/updates the SKILL.md file only if content differs
///
/// Returns Ok(()) on success, or an error message on failure.
pub async fn install_skills() -> Result<(), String> {
let base_dir = skills_dir()
.ok_or_else(|| "Could not determine home directory".to_string())?;
// Create base skills directory if needed
fs::create_dir_all(&base_dir)
.await
.map_err(|e| format!("Failed to create skills directory: {}", e))?;
let mut installed_count = 0;
for (name, content) in skills::ALL_SKILLS {
let skill_dir = base_dir.join(name);
if needs_install(&skill_dir, content).await {
// Create skill directory
fs::create_dir_all(&skill_dir)
.await
.map_err(|e| format!("Failed to create {} directory: {}", name, e))?;
// Write SKILL.md
let skill_file = skill_dir.join("SKILL.md");
fs::write(&skill_file, content)
.await
.map_err(|e| format!("Failed to write {}/SKILL.md: {}", name, e))?;
eprintln!(" Installed skill: {}", name);
installed_count += 1;
}
}
if installed_count == 0 {
eprintln!(" All {} skills up to date", skills::ALL_SKILLS.len());
}
Ok(())
}
/// Check if all skills are properly installed
///
/// Returns a list of (skill_name, is_installed) pairs.
pub async fn verify_skills() -> Vec<(String, bool)> {
let mut results = Vec::new();
if let Some(base_dir) = skills_dir() {
for (name, content) in skills::ALL_SKILLS {
let skill_dir = base_dir.join(name);
let installed = !needs_install(&skill_dir, content).await;
results.push((name.to_string(), installed));
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skills_dir() {
// Should return a valid path on most systems
let dir = skills_dir();
assert!(dir.is_some());
let path = dir.unwrap();
assert!(path.ends_with(".claude/skills"));
}
#[test]
fn test_skill_content_not_empty() {
// Verify all skills have content
for (name, content) in skills::ALL_SKILLS {
assert!(!content.is_empty(), "Skill {} has no content", name);
assert!(
content.starts_with("---"),
"Skill {} missing YAML frontmatter",
name
);
}
}
}