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, LEGACY_RULES_DIR, 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 pub(super) fn legacy_standalone_layout(
27 &self,
28 scope: &Scope,
29 ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
30 let root = self.project_root(scope)?;
31 let rules_dir = root.join(LEGACY_RULES_DIR);
32 Ok(instructions_dir::StandaloneLayout {
33 config_dir: rules_dir.clone(),
34 instruction_dir: rules_dir,
35 })
36 }
37}
38
39impl InstructionSurface for ClineAgent {
40 fn id(&self) -> &'static str {
41 "cline"
42 }
43
44 fn supported_instruction_scopes(&self) -> &'static [ScopeKind] {
45 &[ScopeKind::Local]
46 }
47
48 fn instruction_status(
49 &self,
50 scope: &Scope,
51 name: &str,
52 expected_owner: &str,
53 ) -> Result<StatusReport, AgentConfigError> {
54 let primary_status = instructions_dir::standalone_status(
55 self.standalone_layout(scope)?,
56 name,
57 expected_owner,
58 )?;
59 if matches!(
60 primary_status.status,
61 crate::status::InstallStatus::InstalledOwned { .. }
62 | crate::status::InstallStatus::InstalledOtherOwner { .. }
63 ) {
64 Ok(primary_status)
65 } else {
66 let legacy_layout = self.legacy_standalone_layout(scope)?;
67 let legacy_status =
68 instructions_dir::standalone_status(legacy_layout, name, expected_owner)?;
69 if matches!(
70 legacy_status.status,
71 crate::status::InstallStatus::InstalledOwned { .. }
72 | crate::status::InstallStatus::InstalledOtherOwner { .. }
73 ) {
74 Ok(legacy_status)
75 } else {
76 Ok(primary_status)
77 }
78 }
79 }
80
81 fn plan_install_instruction(
82 &self,
83 scope: &Scope,
84 spec: &InstructionSpec,
85 ) -> Result<InstallPlan, AgentConfigError> {
86 instructions_dir::standalone_plan_install(
87 InstructionSurface::id(self),
88 scope,
89 self.standalone_layout(scope),
90 spec,
91 )
92 }
93
94 fn plan_uninstall_instruction(
95 &self,
96 scope: &Scope,
97 name: &str,
98 owner_tag: &str,
99 ) -> Result<UninstallPlan, AgentConfigError> {
100 let mut plan = instructions_dir::standalone_plan_uninstall(
101 InstructionSurface::id(self),
102 scope,
103 self.standalone_layout(scope),
104 name,
105 owner_tag,
106 )?;
107 if let Ok(legacy_layout) = self.legacy_standalone_layout(scope) {
108 let legacy_plan = instructions_dir::standalone_plan_uninstall(
109 InstructionSurface::id(self),
110 scope,
111 Ok(legacy_layout),
112 name,
113 owner_tag,
114 )?;
115 plan.changes.extend(legacy_plan.changes);
116 }
117 Ok(plan)
118 }
119
120 fn install_instruction(
121 &self,
122 scope: &Scope,
123 spec: &InstructionSpec,
124 ) -> Result<InstallReport, AgentConfigError> {
125 instructions_dir::standalone_install(scope, self.standalone_layout(scope)?, spec)
126 }
127
128 fn uninstall_instruction(
129 &self,
130 scope: &Scope,
131 name: &str,
132 owner_tag: &str,
133 ) -> Result<UninstallReport, AgentConfigError> {
134 let mut report = instructions_dir::standalone_uninstall(
135 scope,
136 self.standalone_layout(scope)?,
137 name,
138 owner_tag,
139 )?;
140 if let Ok(legacy_layout) = self.legacy_standalone_layout(scope) {
141 let legacy_report =
142 instructions_dir::standalone_uninstall(scope, legacy_layout, name, owner_tag)?;
143 report.merge(legacy_report);
144 }
145 Ok(report)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::spec::InstructionPlacement;
153 use tempfile::tempdir;
154
155 fn instruction_spec(name: &str, owner: &str) -> InstructionSpec {
156 InstructionSpec::builder(name)
157 .owner(owner)
158 .placement(InstructionPlacement::StandaloneFile)
159 .body("# Test instruction\n")
160 .build()
161 }
162
163 #[test]
164 fn instruction_writes_to_rules_dir() {
165 let dir = tempdir().unwrap();
166 let agent = ClineAgent::new();
167 let scope = Scope::Local(dir.path().to_path_buf());
168 agent
169 .install_instruction(&scope, &instruction_spec("test-rule", "myapp"))
170 .unwrap();
171 assert!(dir.path().join(".cline/rules/test-rule.md").exists());
172 }
173
174 #[test]
175 fn instruction_uninstall_removes_file() {
176 let dir = tempdir().unwrap();
177 let agent = ClineAgent::new();
178 let scope = Scope::Local(dir.path().to_path_buf());
179 agent
180 .install_instruction(&scope, &instruction_spec("test-rule", "myapp"))
181 .unwrap();
182 agent
183 .uninstall_instruction(&scope, "test-rule", "myapp")
184 .unwrap();
185 assert!(!dir.path().join(".cline/rules/test-rule.md").exists());
186 }
187
188 #[test]
189 fn instruction_rejects_global_scope() {
190 let agent = ClineAgent::new();
191 let spec = instruction_spec("test-rule", "myapp");
192 let plan = agent
193 .plan_install_instruction(&Scope::Global, &spec)
194 .unwrap();
195 assert!(matches!(plan.status, crate::plan::PlanStatus::Refused));
196 }
197}