Skip to main content

agent_config/agents/cline/
instructions.rs

1//! Cline instructions surface. Standalone files under `.clinerules/`.
2
3use crate::error::AgentConfigError;
4use crate::integration::{InstallReport, InstructionSurface, UninstallReport};
5use crate::plan::{InstallPlan, UninstallPlan};
6use crate::scope::{Scope, ScopeKind};
7use crate::spec::InstructionSpec;
8use crate::status::StatusReport;
9use crate::util::instructions_dir;
10
11use super::{ClineAgent, RULES_DIR};
12
13impl ClineAgent {
14    pub(super) fn standalone_layout(
15        &self,
16        scope: &Scope,
17    ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
18        let root = self.project_root(scope)?;
19        let rules_dir = root.join(RULES_DIR);
20        Ok(instructions_dir::StandaloneLayout {
21            config_dir: rules_dir.clone(),
22            instruction_dir: rules_dir,
23        })
24    }
25}
26
27impl InstructionSurface for ClineAgent {
28    fn id(&self) -> &'static str {
29        "cline"
30    }
31
32    fn supported_instruction_scopes(&self) -> &'static [ScopeKind] {
33        &[ScopeKind::Local]
34    }
35
36    fn instruction_status(
37        &self,
38        scope: &Scope,
39        name: &str,
40        expected_owner: &str,
41    ) -> Result<StatusReport, AgentConfigError> {
42        instructions_dir::standalone_status(self.standalone_layout(scope)?, name, expected_owner)
43    }
44
45    fn plan_install_instruction(
46        &self,
47        scope: &Scope,
48        spec: &InstructionSpec,
49    ) -> Result<InstallPlan, AgentConfigError> {
50        instructions_dir::standalone_plan_install(
51            InstructionSurface::id(self),
52            scope,
53            self.standalone_layout(scope),
54            spec,
55        )
56    }
57
58    fn plan_uninstall_instruction(
59        &self,
60        scope: &Scope,
61        name: &str,
62        owner_tag: &str,
63    ) -> Result<UninstallPlan, AgentConfigError> {
64        instructions_dir::standalone_plan_uninstall(
65            InstructionSurface::id(self),
66            scope,
67            self.standalone_layout(scope),
68            name,
69            owner_tag,
70        )
71    }
72
73    fn install_instruction(
74        &self,
75        scope: &Scope,
76        spec: &InstructionSpec,
77    ) -> Result<InstallReport, AgentConfigError> {
78        instructions_dir::standalone_install(scope, self.standalone_layout(scope)?, spec)
79    }
80
81    fn uninstall_instruction(
82        &self,
83        scope: &Scope,
84        name: &str,
85        owner_tag: &str,
86    ) -> Result<UninstallReport, AgentConfigError> {
87        instructions_dir::standalone_uninstall(
88            scope,
89            self.standalone_layout(scope)?,
90            name,
91            owner_tag,
92        )
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::spec::InstructionPlacement;
100    use tempfile::tempdir;
101
102    fn instruction_spec(name: &str, owner: &str) -> InstructionSpec {
103        InstructionSpec::builder(name)
104            .owner(owner)
105            .placement(InstructionPlacement::StandaloneFile)
106            .body("# Test instruction\n")
107            .build()
108    }
109
110    #[test]
111    fn instruction_writes_to_rules_dir() {
112        let dir = tempdir().unwrap();
113        let agent = ClineAgent::new();
114        let scope = Scope::Local(dir.path().to_path_buf());
115        agent
116            .install_instruction(&scope, &instruction_spec("test-rule", "myapp"))
117            .unwrap();
118        assert!(dir.path().join(".clinerules/test-rule.md").exists());
119    }
120
121    #[test]
122    fn instruction_uninstall_removes_file() {
123        let dir = tempdir().unwrap();
124        let agent = ClineAgent::new();
125        let scope = Scope::Local(dir.path().to_path_buf());
126        agent
127            .install_instruction(&scope, &instruction_spec("test-rule", "myapp"))
128            .unwrap();
129        agent
130            .uninstall_instruction(&scope, "test-rule", "myapp")
131            .unwrap();
132        assert!(!dir.path().join(".clinerules/test-rule.md").exists());
133    }
134
135    #[test]
136    fn instruction_rejects_global_scope() {
137        let agent = ClineAgent::new();
138        let spec = instruction_spec("test-rule", "myapp");
139        let plan = agent
140            .plan_install_instruction(&Scope::Global, &spec)
141            .unwrap();
142        assert!(matches!(plan.status, crate::plan::PlanStatus::Refused));
143    }
144}