Skip to main content

zenith_cli/commands/plugin/
render.rs

1//! Render the per-agent contents of a rule-format skill file.
2//!
3//! Folder-format agents receive the embedded tree verbatim (its `SKILL.md`
4//! frontmatter already follows the Agent Skills standard), so only the
5//! single-file rule agents need rendering here.
6
7use super::agent::Agent;
8use super::assets::{skill_description, skill_md_body};
9
10/// A note prepended to single-file installs: the reference packs are a
11/// folder-skill feature, so here the agent should lean on the CLI's own help.
12const RULE_NOTE: &str = "> **Single-file install.** The `references/` packs, `templates/`, and \
13`themes/` ship with the full folder skill (Claude Code, Codex, OpenCode) and live in the \
14[repo](https://github.com/zenitheditor/zenith). In this agent, drive the self-documenting \
15`zenith` CLI directly — run `zenith --help` and `zenith <command> --help` for exact flags, \
16and read the repo's `examples/*.zen` for syntax.";
17
18/// Render the file contents for a rule-format `agent`.
19pub fn render_rule(agent: Agent) -> String {
20    let body = skill_md_body();
21    let desc = skill_description().unwrap_or_default();
22    match agent {
23        // Cursor `.mdc` rules: frontmatter with `description` + `alwaysApply`.
24        Agent::Cursor => format!(
25            "---\nalwaysApply: false\ndescription: {desc}\n---\n\n{RULE_NOTE}\n\n{body}",
26            desc = yaml_scalar(&desc),
27        ),
28        // Windsurf rules: bare markdown.
29        Agent::Windsurf => format!("{RULE_NOTE}\n\n{body}"),
30        // Everything else: plain markdown with an identifying H1.
31        Agent::Aider
32        | Agent::Zed
33        | Agent::Gemini
34        | Agent::Copilot
35        | Agent::Continue
36        | Agent::Kiro
37        | Agent::Antigravity => format!("# Zenith\n\n{RULE_NOTE}\n\n{body}"),
38        // Folder agents never reach here.
39        Agent::ClaudeCode | Agent::Codex | Agent::OpenCode => body.to_owned(),
40    }
41}
42
43/// Quote a YAML scalar only when it contains characters that would otherwise
44/// break parsing.
45fn yaml_scalar(s: &str) -> String {
46    let needs = s.contains(':')
47        || s.contains('#')
48        || s.contains('"')
49        || s.contains('\'')
50        || s.starts_with(char::is_whitespace)
51        || s.ends_with(char::is_whitespace);
52    if needs {
53        format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
54    } else {
55        s.to_owned()
56    }
57}