use crate::agents::Agent;
use std::fs;
use std::io;
use std::path::PathBuf;
pub const SKILL_BODY: &str = include_str!("../../assets/wizard-skill/SKILL.md");
pub const SKILL_NAME: &str = "jarvy-setup";
#[derive(Debug, thiserror::Error)]
pub enum SkillDropError {
#[error("agent `{agent}` has no skills dir on this platform")]
NoSkillsDir { agent: String },
#[error("write {path}: {source}")]
Write {
path: PathBuf,
#[source]
source: io::Error,
},
}
pub fn install_path(agent: Agent) -> Result<PathBuf, SkillDropError> {
let dir = agent
.skills_dir()
.ok_or_else(|| SkillDropError::NoSkillsDir {
agent: agent.slug().to_string(),
})?;
Ok(dir.join(SKILL_NAME).join("SKILL.md"))
}
pub fn install(agent: Agent) -> Result<PathBuf, SkillDropError> {
let path = install_path(agent)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| SkillDropError::Write {
path: parent.to_path_buf(),
source: e,
})?;
}
fs::write(&path, SKILL_BODY).map_err(|e| SkillDropError::Write {
path: path.clone(),
source: e,
})?;
Ok(path)
}
pub fn invocation_phrase(agent: Agent) -> &'static str {
match agent {
Agent::ClaudeCode => "set up jarvy for this project",
Agent::Cursor | Agent::Windsurf | Agent::Cline | Agent::Continue => {
"set up jarvy for this project"
}
Agent::Codex => "set up jarvy for this project",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn install_path_contains_skill_name() {
for &agent in Agent::ALL {
let p = install_path(agent).expect("every agent has a skills_dir");
let s = p.to_string_lossy().into_owned();
assert!(s.contains(SKILL_NAME), "path missing skill name: {s}");
assert!(
s.ends_with("SKILL.md"),
"path doesn't end with SKILL.md: {s}"
);
}
}
#[test]
fn skill_body_has_frontmatter() {
assert!(
SKILL_BODY.starts_with("---\n"),
"SKILL.md must start with YAML frontmatter"
);
assert!(
SKILL_BODY.contains("name: jarvy-setup"),
"SKILL.md frontmatter must declare name: jarvy-setup"
);
}
}