use crate::error::JanyError;
use std::path::{Path, PathBuf};
const FILES: &[(&str, &str)] = &[
("SKILL.md", include_str!("../skill/jany-register/SKILL.md")),
("reference.md", include_str!("../skill/jany-register/reference.md")),
("template/schema.toml", include_str!("../skill/jany-register/template/schema.toml")),
("template/assemble.sh", include_str!("../skill/jany-register/template/assemble.sh")),
("template/cases.toml", include_str!("../skill/jany-register/template/cases.toml")),
("examples/find/schema.toml", include_str!("../examples/find/schema.toml")),
("examples/find/assemble.sh", include_str!("../examples/find/assemble.sh")),
("examples/find/cases.toml", include_str!("../examples/find/cases.toml")),
("examples/curl/schema.toml", include_str!("../examples/curl/schema.toml")),
("examples/curl/assemble.sh", include_str!("../examples/curl/assemble.sh")),
("examples/curl/cases.toml", include_str!("../examples/curl/cases.toml")),
];
const COMMANDS: &[(&str, &[(&str, &str)])] = &[
("find", &[
("schema.toml", include_str!("../examples/find/schema.toml")),
("assemble.sh", include_str!("../examples/find/assemble.sh")),
("cases.toml", include_str!("../examples/find/cases.toml")),
]),
("curl", &[
("schema.toml", include_str!("../examples/curl/schema.toml")),
("assemble.sh", include_str!("../examples/curl/assemble.sh")),
("cases.toml", include_str!("../examples/curl/cases.toml")),
]),
("docker/run", &[
("schema.toml", include_str!("../examples/docker/run/schema.toml")),
("assemble.sh", include_str!("../examples/docker/run/assemble.sh")),
("cases.toml", include_str!("../examples/docker/run/cases.toml")),
]),
];
pub fn install_commands(cmd_dir: &Path) -> Result<Vec<String>, JanyError> {
let mut placed = Vec::new();
for (name, files) in COMMANDS {
let dir = cmd_dir.join(name);
if dir.join("schema.toml").exists() {
continue;
}
std::fs::create_dir_all(&dir)?;
for (rel, body) in *files {
let p = dir.join(rel);
std::fs::write(&p, body)?;
set_executable(&p, rel.ends_with(".sh"))?;
}
placed.push(dir.display().to_string());
}
Ok(placed)
}
pub fn skill_dir() -> Option<PathBuf> {
if let Some(d) = std::env::var_os("JANY_SKILL_DIR") {
return Some(PathBuf::from(d));
}
let home = std::env::var_os("HOME")?;
Some(Path::new(&home).join(".agents").join("skills").join("jany-register"))
}
pub fn install() -> Result<Vec<String>, JanyError> {
let dir = skill_dir().ok_or_else(|| JanyError::Config("cannot determine skill dir (HOME unset)".into()))?;
let mut changed = Vec::new();
for (rel, body) in FILES {
let p = dir.join(rel);
if std::fs::read_to_string(&p).map(|cur| cur == *body).unwrap_or(false) {
continue;
}
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&p, body)?;
set_executable(&p, rel.ends_with(".sh"))?;
changed.push(p.display().to_string());
}
if let Some(home) = std::env::var_os("HOME") {
for tool in [".claude", ".codex"] {
let skills = Path::new(&home).join(tool).join("skills");
let link = skills.join("jany-register");
if skills.is_dir() && std::fs::symlink_metadata(&link).is_err() {
#[cfg(unix)]
if std::os::unix::fs::symlink(&dir, &link).is_ok() {
changed.push(format!("{} -> {}", link.display(), dir.display()));
}
}
}
}
Ok(changed)
}
fn set_executable(p: &Path, on: bool) -> std::io::Result<()> {
#[cfg(unix)]
if on {
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(p)?.permissions();
perm.set_mode(perm.mode() | 0o111);
std::fs::set_permissions(p, perm)?;
}
Ok(())
}
pub fn register(cmd_dir: &Path, names: &[String]) -> Result<i32, JanyError> {
if names.is_empty() {
return Err(JanyError::Usage("jany --register <name> [sub …] e.g. jany --register docker run".into()));
}
for n in names {
if n.is_empty() || n.contains('/') || n.starts_with('.') || n.starts_with('-') {
return Err(JanyError::Usage(format!("bad command name `{n}`: use plain words (docker run)")));
}
}
let name = names.join(" ");
let argv0 = names.iter().map(|n| format!("{n:?}")).collect::<Vec<_>>().join(", ");
let dir = names.iter().fold(cmd_dir.to_path_buf(), |d, n| d.join(n));
if dir.join("schema.toml").exists() {
return Err(JanyError::Usage(format!("{} already exists; edit it or remove it first", dir.join("schema.toml").display())));
}
std::fs::create_dir_all(&dir)?;
for rel in ["schema.toml", "assemble.sh", "cases.toml"] {
let body = FILES.iter().find(|(r, _)| *r == format!("template/{rel}")).map(|(_, b)| *b).expect("template embedded");
let p = dir.join(rel);
std::fs::write(&p, body.replace("__ARGV0__", &argv0).replace("__NAME__", &name))?;
set_executable(&p, rel.ends_with(".sh"))?;
eprintln!("wrote {}", p.display());
}
let skill = skill_dir().map(|d| d.display().to_string()).unwrap_or_else(|| "~/.agents/skills/jany-register".into());
eprintln!(
"\nnext: fill them in with the skill (Claude Code / Codex): /jany-register {name}\n reference: {skill}/reference.md\n then run: jany --test {name}"
);
Ok(0)
}