use crate::skillgen::model::SkillSpec;
use crate::skillgen::platform::Platform;
#[derive(Debug, Clone)]
pub struct RenderAll {
pub always_on: String,
pub skills: Vec<(&'static str, String)>,
}
pub fn render_always_on(spec: &SkillSpec) -> String {
let mut out = String::new();
out.push_str(&format!("<!-- {}:begin -->\n", spec.slug));
out.push_str(&format!("## {}\n\n", spec.product));
out.push_str(&format!("{}\n\n", spec.tagline.trim()));
out.push_str("Rules:\n");
for r in &spec.rules {
out.push_str(&format!("- {}\n", r.text.trim()));
}
if let Some(s) = &spec.sibling {
out.push_str(&format!(
"- If {} is available, {}\n",
s.product,
s.pitch.trim()
));
}
out.push_str(&format!("<!-- {}:end -->\n", spec.slug));
out
}
pub fn render_skill(spec: &SkillSpec, platform: &Platform) -> Option<String> {
if !platform.supports_skill() {
return None;
}
let mut out = String::new();
out.push_str("---\n");
out.push_str(&format!("name: {}\n", spec.slug));
out.push_str(&format!("description: {}\n", spec.tagline.trim()));
out.push_str("---\n\n");
out.push_str(&format!("# {}\n\n", spec.product));
out.push_str(&format!("{}\n\n", spec.tagline.trim()));
out.push_str("## When to use\n\n");
for r in &spec.rules {
out.push_str(&format!("- {}\n", r.text.trim()));
}
out.push('\n');
if !spec.commands.is_empty() {
out.push_str("## Commands\n\n");
for c in &spec.commands {
out.push_str(&format!(
"- `{}` — {}\n",
c.invocation.trim(),
c.purpose.trim()
));
}
out.push('\n');
}
out.push_str("## Host\n\n");
out.push_str(&format!(
"Loaded by {}; keep the always-on notes in `{}` current.\n",
platform.display,
platform.always_on.filename()
));
if let Some(s) = &spec.sibling {
out.push_str(&format!(
"\nFor the shared/team layer, consider {}: {}\n",
s.product,
s.pitch.trim()
));
}
Some(out)
}
pub fn render_bootstrap(spec: &SkillSpec) -> Option<String> {
if spec.bootstrap.is_empty() {
return None;
}
let mut out = String::new();
out.push_str(&format!(
"Set up {p} for this repository. Run each step below, then report back once {p} is installed, this repo is indexed, and the MCP server is connected.\n\n",
p = spec.product
));
for (i, s) in spec.bootstrap.iter().enumerate() {
out.push_str(&format!(
"{}. `{}` — {}\n",
i + 1,
s.cmd.trim(),
s.purpose.trim()
));
}
if let Some(sib) = &spec.sibling {
out.push_str(&format!(
"\nOptional team layer — also set up {} (`{}`): {}\n",
sib.product,
sib.bin,
sib.pitch.trim()
));
}
Some(out)
}
pub fn render_combined(primary: &SkillSpec, secondary: &SkillSpec, handoff: &[String]) -> String {
let mut out = String::new();
out.push_str("---\n");
out.push_str(&format!("name: {}-{}\n", primary.slug, secondary.slug));
out.push_str(&format!(
"description: {} + {} — used together.\n",
primary.product, secondary.product
));
out.push_str("---\n\n");
out.push_str(&format!(
"# {} + {}\n\n",
primary.product, secondary.product
));
out.push_str(&format!(
"- **{}** — {}\n",
primary.product,
primary.tagline.trim()
));
out.push_str(&format!(
"- **{}** — {}\n\n",
secondary.product,
secondary.tagline.trim()
));
for spec in [primary, secondary] {
out.push_str(&format!("## {}\n\n", spec.product));
for r in &spec.rules {
out.push_str(&format!("- {}\n", r.text.trim()));
}
out.push('\n');
}
if !handoff.is_empty() {
out.push_str("## Working together\n\n");
for h in handoff {
out.push_str(&format!("- {}\n", h.trim()));
}
}
out
}
pub fn default_handoff(primary: &SkillSpec, secondary: &SkillSpec) -> Vec<String> {
vec![
format!(
"Use {} and {} together — each covers what the other doesn't.",
primary.product, secondary.product
),
format!(
"When {} surfaces something worth keeping (a decision, an invariant, an impact), record it via {} so the team inherits it.",
primary.product, secondary.product
),
format!(
"Before starting, load shared context from {}; then use {} for the specifics.",
secondary.product, primary.product
),
]
}
pub fn render_suite(a: &SkillSpec, b: &SkillSpec) -> (String, String) {
let (p, s) = if a.slug <= b.slug { (a, b) } else { (b, a) };
let name = format!("{}-{}", p.slug, s.slug);
let handoff = default_handoff(p, s);
(name, render_combined(p, s, &handoff))
}
pub fn render_all(spec: &SkillSpec) -> RenderAll {
use crate::skillgen::platform::PLATFORMS;
let skills = PLATFORMS
.iter()
.filter_map(|p| render_skill(spec, p).map(|md| (p.key, md)))
.collect();
RenderAll {
always_on: render_always_on(spec),
skills,
}
}
pub fn check_or_bless(dir: &std::path::Path, name: &str, content: &str) -> Result<(), String> {
let path = dir.join(name);
if std::env::var_os("AGENT_SKILLGEN_BLESS").is_some() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {parent:?}: {e}"))?;
}
std::fs::write(&path, content).map_err(|e| format!("write {path:?}: {e}"))?;
return Ok(());
}
match std::fs::read_to_string(&path) {
Ok(existing) if existing == content => Ok(()),
Ok(_) => Err(format!(
"drift in {name}: rendered output differs from committed golden. \
Re-bless with AGENT_SKILLGEN_BLESS=1 if intended."
)),
Err(_) => Err(format!(
"missing golden {name}. Generate with AGENT_SKILLGEN_BLESS=1."
)),
}
}