Skip to main content

agent_rules_tool/
format.rs

1//! Rule format identifiers and auto-detection from frontmatter.
2
3use clap::ValueEnum;
4use serde_json::Value;
5
6/// CLI-facing rule format selector (mirrors [`RuleFormat`] for `clap`).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
8pub enum RuleFormatArg {
9    /// Canonical [agent-rules-spec](https://github.com/rameshsunkara/agent-rules-spec) format.
10    Agents,
11    /// Cursor `.cursor/rules` format.
12    Cursor,
13    /// Windsurf `.windsurf/rules` format.
14    Windsurf,
15    /// GitHub Copilot `.github/instructions` format.
16    Copilot,
17    /// Cline `.clinerules` format.
18    Cline,
19    /// Claude `.claude/rules` format.
20    Claude,
21    /// JetBrains AI Assistant format.
22    Jetbrains,
23    /// Amazon Q format.
24    Amazonq,
25    /// Detect format from frontmatter or path.
26    Auto,
27}
28
29/// Supported agent rule file formats.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum RuleFormat {
32    /// Canonical [agent-rules-spec](https://github.com/rameshsunkara/agent-rules-spec) format.
33    Agents,
34    /// Cursor `.cursor/rules` format.
35    Cursor,
36    /// Windsurf `.windsurf/rules` format.
37    Windsurf,
38    /// GitHub Copilot `.github/instructions` format.
39    Copilot,
40    /// Cline `.clinerules` format.
41    Cline,
42    /// Claude `.claude/rules` format.
43    Claude,
44    /// JetBrains AI Assistant format.
45    Jetbrains,
46    /// Amazon Q format.
47    AmazonQ,
48    /// Detect format from frontmatter or path.
49    Auto,
50}
51
52impl From<RuleFormatArg> for RuleFormat {
53    fn from(arg: RuleFormatArg) -> Self {
54        match arg {
55            RuleFormatArg::Agents => RuleFormat::Agents,
56            RuleFormatArg::Cursor => RuleFormat::Cursor,
57            RuleFormatArg::Windsurf => RuleFormat::Windsurf,
58            RuleFormatArg::Copilot => RuleFormat::Copilot,
59            RuleFormatArg::Cline => RuleFormat::Cline,
60            RuleFormatArg::Claude => RuleFormat::Claude,
61            RuleFormatArg::Jetbrains => RuleFormat::Jetbrains,
62            RuleFormatArg::Amazonq => RuleFormat::AmazonQ,
63            RuleFormatArg::Auto => RuleFormat::Auto,
64        }
65    }
66}
67
68impl RuleFormat {
69    /// Default output directory for this format when translating to disk.
70    pub fn default_output_dir(self) -> &'static str {
71        match self {
72            RuleFormat::Agents => ".agents/rules",
73            RuleFormat::Cursor => ".cursor/rules",
74            RuleFormat::Windsurf => ".windsurf/rules",
75            RuleFormat::Copilot => ".github/instructions",
76            RuleFormat::Cline => ".clinerules",
77            RuleFormat::Claude => ".claude/rules",
78            RuleFormat::Jetbrains => ".aiassistant/rules",
79            RuleFormat::AmazonQ => ".amazonq/rules",
80            RuleFormat::Auto => ".agents/rules",
81        }
82    }
83
84    /// Infer format from frontmatter shape (heuristic field matching).
85    pub fn detect_from_frontmatter(frontmatter: &Value) -> RuleFormat {
86        if frontmatter.is_null() {
87            return RuleFormat::Agents;
88        }
89        let obj = match frontmatter.as_object() {
90            Some(o) if !o.is_empty() => o,
91            _ => return RuleFormat::Agents,
92        };
93
94        if obj.contains_key("applyTo") {
95            return RuleFormat::Copilot;
96        }
97
98        if obj.contains_key("globs") {
99            if obj.contains_key("alwaysApply") {
100                return RuleFormat::Cursor;
101            }
102            if let Some(trigger) = obj.get("trigger").and_then(|v| v.as_str())
103                && matches!(trigger, "glob" | "always_on" | "manual" | "model_decision")
104            {
105                return RuleFormat::Windsurf;
106            }
107            return RuleFormat::Cursor;
108        }
109
110        if obj.contains_key("paths") {
111            if let Some(trigger) = obj.get("trigger").and_then(|v| v.as_str())
112                && matches!(trigger, "always" | "auto" | "manual")
113            {
114                return RuleFormat::Agents;
115            }
116            return RuleFormat::Claude;
117        }
118
119        if obj.contains_key("trigger") {
120            return RuleFormat::Agents;
121        }
122
123        RuleFormat::Agents
124    }
125
126    /// Resolve [`RuleFormat::Auto`] via [`Self::detect_from_frontmatter`]; otherwise return `self`.
127    pub fn resolve(self, frontmatter: &Value) -> RuleFormat {
128        if self == RuleFormat::Auto {
129            Self::detect_from_frontmatter(frontmatter)
130        } else {
131            self
132        }
133    }
134}