use crate::model::{Agent, Rule, Skill};
use anyhow::{Context, Result};
use serde_yaml_ng::{Mapping, Value};
pub mod claude;
pub mod copilot;
pub mod directives;
pub mod format;
pub mod opencode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Client {
Claude,
Copilot,
OpenCode,
}
impl Client {
pub fn name(self) -> &'static str {
match self {
Client::Claude => "claude",
Client::Copilot => "copilot",
Client::OpenCode => "opencode",
}
}
}
impl std::str::FromStr for Client {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"claude" => Ok(Client::Claude),
"copilot" => Ok(Client::Copilot),
"opencode" => Ok(Client::OpenCode),
other => anyhow::bail!("unknown client identifier: {}", other),
}
}
}
pub fn render_skill(skill: &Skill, body: &str, client: Client) -> Result<String> {
let processed_body = directives::process(body, client)?;
let frontmatter = match client {
Client::Claude => claude::skill_frontmatter(skill)?,
Client::Copilot => copilot::skill_frontmatter(skill)?,
Client::OpenCode => opencode::skill_frontmatter(skill)?,
};
let combined = assemble(&frontmatter, &processed_body);
format::format_markdown(&combined)
}
pub fn render_rule(rule: &Rule, body: &str, client: Client) -> Result<String> {
let processed_body = directives::process(body, client)?;
let frontmatter = match client {
Client::Claude => claude::rule_frontmatter(rule)?,
Client::Copilot => copilot::rule_frontmatter(rule)?,
Client::OpenCode => opencode::rule_frontmatter(rule)?,
};
let combined = assemble(&frontmatter, &processed_body);
format::format_markdown(&combined)
}
pub fn render_agent(agent: &Agent, body: &str, client: Client) -> Result<String> {
let processed_body = directives::process(body, client)?;
let frontmatter = match client {
Client::Claude => claude::agent_frontmatter(agent)?,
Client::Copilot => copilot::agent_frontmatter(agent)?,
Client::OpenCode => opencode::agent_frontmatter(agent)?,
};
let combined = assemble(&frontmatter, &processed_body);
format::format_markdown(&combined)
}
fn assemble(frontmatter: &str, body: &str) -> String {
let body = body.trim_start_matches('\n');
if body.is_empty() {
format!("---\n{}---\n", frontmatter)
} else {
format!("---\n{}---\n\n{}", frontmatter, body)
}
}
pub(crate) fn build_skill_frontmatter(
skill: &Skill,
passthrough: Option<&Value>,
) -> Result<String> {
let mut map = Mapping::new();
map.insert(Value::from("name"), Value::from(skill.name.clone()));
map.insert(
Value::from("description"),
Value::from(skill.description.clone()),
);
for (k, v) in &skill.extra {
map.insert(Value::from(k.clone()), v.clone());
}
if let Some(Value::Mapping(m)) = passthrough {
for (k, v) in m {
map.insert(k.clone(), v.clone());
}
}
serde_yaml_ng::to_string(&map).context("serializing skill frontmatter")
}