agent_config/agents/cline/
instructions.rs1use 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}