use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::embed::{Skill, SKILLS};
pub const TARGETS: [&str; 3] = ["copilot", "claude", "gemini"];
struct Parsed {
name: String,
description: String,
argument_name: String,
body: String,
}
fn parse(skill: &Skill) -> Result<Parsed, String> {
let raw = skill.raw;
let rest = raw
.strip_prefix("---\n")
.ok_or_else(|| format!("{}: missing frontmatter", skill.name))?;
let end = rest
.find("\n---")
.ok_or_else(|| format!("{}: unterminated frontmatter", skill.name))?;
let fm = &rest[..end];
let after = &rest[end + 4..]; let body = after.trim_start_matches(['\n', '\r']).trim_end().to_string();
let mut description = String::new();
let mut argument_name = "story-id".to_string();
for line in fm.lines() {
let t = line.trim_start();
if t.starts_with('-') {
continue;
}
if let Some((k, v)) = line.split_once(':') {
let key = k.trim();
let val = strip_quotes(v.trim());
match key {
"description" => description = val.to_string(),
"argument_name" if !val.is_empty() => argument_name = val.to_string(),
_ => {}
}
}
}
Ok(Parsed {
name: skill.name.to_string(),
description,
argument_name,
body,
})
}
fn strip_quotes(s: &str) -> &str {
let b = s.as_bytes();
if b.len() >= 2
&& ((b[0] == b'"' && b[b.len() - 1] == b'"') || (b[0] == b'\'' && b[b.len() - 1] == b'\''))
{
&s[1..s.len() - 1]
} else {
s
}
}
fn camel(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut up = false;
for c in s.chars() {
if c == '-' || c == '_' {
up = true;
} else if up {
out.extend(c.to_uppercase());
up = false;
} else {
out.push(c);
}
}
out
}
fn rewrite_arg(body: &str, target: &str, p: &Parsed) -> String {
let repl = match target {
"copilot" => format!("${{input:{}:{}}}", camel(&p.argument_name), p.argument_name),
"claude" => "$ARGUMENTS".to_string(),
"gemini" => "{{args}}".to_string(),
_ => "{{ARG}}".to_string(),
};
body.replace("{{ARG}}", &repl)
}
fn toml_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace("\"\"\"", "\\\"\\\"\\\"")
}
fn write(path: &Path, contents: &str) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, contents)
}
fn emit_target(target: &str, out_root: &Path) -> io::Result<Vec<PathBuf>> {
let mut written = Vec::new();
for skill in SKILLS {
let p = parse(skill).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let prompt = rewrite_arg(&p.body, target, &p);
let (path, contents) = match target {
"copilot" => {
let path = out_root
.join(".github/prompts")
.join(format!("{}.prompt.md", p.name));
let c = format!(
"---\nmode: agent\ndescription: {}\n---\n{}\n",
p.description, prompt
);
(path, c)
}
"claude" => {
let path = out_root
.join(".claude/skills")
.join(&p.name)
.join("SKILL.md");
let c = format!(
"---\nname: {}\ndescription: {}\n---\n{}\n",
p.name, p.description, prompt
);
(path, c)
}
"gemini" => {
let path = out_root
.join(".gemini/commands")
.join(format!("{}.toml", p.name));
let c = format!(
"description = \"{}\"\nprompt = \"\"\"\n{}\n\"\"\"\n",
p.description.replace('"', "\\\""),
toml_escape(&prompt)
);
(path, c)
}
other => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unknown target '{other}'"),
))
}
};
write(&path, &contents)?;
written.push(path);
}
Ok(written)
}
pub fn emit(target: &str, out_root: &Path) -> io::Result<Vec<(String, Vec<PathBuf>)>> {
let targets: Vec<&str> = if target == "all" {
TARGETS.to_vec()
} else if TARGETS.contains(&target) {
vec![target]
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"invalid target '{target}' — use one of: {}, all",
TARGETS.join(", ")
),
));
};
let mut result = Vec::new();
for t in targets {
let files = emit_target(t, out_root)?;
result.push((t.to_string(), files));
}
Ok(result)
}