1pub mod adapter;
26pub mod adapters;
27pub mod content;
28pub mod merge;
29pub mod tool;
30
31use std::collections::HashMap;
32use std::path::{Path, PathBuf};
33
34pub use adapter::{BootstrapContext, DetectionResult, ToolAdapter};
35pub use tool::{MergeStrategy, OutputFormat, Tool};
36
37use crate::error::Result;
38use adapters::*;
39
40pub struct SyncExecutor {
42 adapters: HashMap<Tool, Box<dyn ToolAdapter>>,
43}
44
45impl SyncExecutor {
46 pub fn new() -> Self {
48 let mut adapters: HashMap<Tool, Box<dyn ToolAdapter>> = HashMap::new();
49
50 adapters.insert(Tool::Cursor, Box::new(CursorAdapter));
51 adapters.insert(Tool::ClaudeCode, Box::new(ClaudeCodeAdapter));
52 adapters.insert(Tool::Copilot, Box::new(CopilotAdapter));
53 adapters.insert(Tool::Continue, Box::new(ContinueAdapter));
54 adapters.insert(Tool::Windsurf, Box::new(WindsurfAdapter));
55 adapters.insert(Tool::Cline, Box::new(ClineAdapter));
56 adapters.insert(Tool::Aider, Box::new(AiderAdapter));
57 adapters.insert(Tool::Generic, Box::new(GenericAdapter));
58
59 Self { adapters }
60 }
61
62 pub fn detect_tools(&self, project_root: &Path) -> Vec<Tool> {
64 self.adapters
65 .iter()
66 .filter_map(|(tool, adapter)| {
67 let result = adapter.detect(project_root);
68 if result.detected && *tool != Tool::Generic {
70 Some(*tool)
71 } else {
72 None
73 }
74 })
75 .collect()
76 }
77
78 pub fn detect_all(&self, project_root: &Path) -> Vec<DetectionResult> {
80 self.adapters
81 .values()
82 .map(|adapter| adapter.detect(project_root))
83 .collect()
84 }
85
86 pub fn bootstrap_tool(&self, tool: Tool, project_root: &Path) -> Result<BootstrapResult> {
88 let adapter = self.adapters.get(&tool).ok_or_else(|| {
89 crate::error::AcpError::Other(format!("No adapter for tool: {:?}", tool))
90 })?;
91
92 let context = BootstrapContext { project_root, tool };
93
94 let content = adapter.generate(&context)?;
96
97 adapter.validate(&content)?;
99
100 let output_path = project_root.join(tool.output_path());
102
103 if let Some(parent) = output_path.parent() {
105 if !parent.exists() {
106 std::fs::create_dir_all(parent)?;
107 }
108 }
109
110 let action = if output_path.exists() {
112 let existing = std::fs::read_to_string(&output_path)?;
113 let (start_marker, end_marker) = adapter.section_markers();
114
115 let merged = if start_marker.is_empty() {
116 if tool == Tool::Continue {
118 merge::merge_json(&existing, &content)
119 .map_err(|e| crate::error::AcpError::Other(e.to_string()))?
120 } else {
121 content.clone()
122 }
123 } else {
124 merge::merge_content(
125 adapter.merge_strategy(),
126 &existing,
127 &content,
128 start_marker,
129 end_marker,
130 )
131 };
132
133 std::fs::write(&output_path, merged)?;
134 BootstrapAction::Merged
135 } else {
136 let (start_marker, end_marker) = adapter.section_markers();
138 let final_content = if !start_marker.is_empty() {
139 format!("{}\n{}\n{}", start_marker, content, end_marker)
140 } else {
141 content
142 };
143
144 std::fs::write(&output_path, final_content)?;
145 BootstrapAction::Created
146 };
147
148 Ok(BootstrapResult {
149 tool,
150 output_path,
151 action,
152 })
153 }
154
155 pub fn bootstrap_all(&self, project_root: &Path) -> Vec<Result<BootstrapResult>> {
157 let mut tools = self.detect_tools(project_root);
158
159 if !project_root.join("AGENTS.md").exists() {
161 tools.push(Tool::Generic);
162 }
163
164 tools
165 .into_iter()
166 .map(|tool| self.bootstrap_tool(tool, project_root))
167 .collect()
168 }
169}
170
171impl Default for SyncExecutor {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177#[derive(Debug)]
179pub struct BootstrapResult {
180 pub tool: Tool,
181 pub output_path: PathBuf,
182 pub action: BootstrapAction,
183}
184
185#[derive(Debug, PartialEq, Eq)]
187pub enum BootstrapAction {
188 Created,
190 Merged,
192 Skipped,
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use tempfile::TempDir;
200
201 #[test]
202 fn test_sync_executor_creation() {
203 let executor = SyncExecutor::new();
204 assert_eq!(executor.adapters.len(), 8);
205 }
206
207 #[test]
208 fn test_detect_tools_empty_project() {
209 let temp = TempDir::new().unwrap();
210 let executor = SyncExecutor::new();
211 let detected = executor.detect_tools(temp.path());
212
213 assert!(!detected.contains(&Tool::Cursor));
216 assert!(!detected.contains(&Tool::Copilot));
217 assert!(!detected.contains(&Tool::Generic));
218 }
219
220 #[test]
221 fn test_detect_tools_with_cursorrules() {
222 let temp = TempDir::new().unwrap();
223 std::fs::write(temp.path().join(".cursorrules"), "").unwrap();
224
225 let executor = SyncExecutor::new();
226 let detected = executor.detect_tools(temp.path());
227
228 assert!(detected.contains(&Tool::Cursor));
229 }
230
231 #[test]
232 fn test_bootstrap_creates_file() {
233 let temp = TempDir::new().unwrap();
234 let executor = SyncExecutor::new();
235
236 let result = executor.bootstrap_tool(Tool::Generic, temp.path()).unwrap();
237
238 assert_eq!(result.action, BootstrapAction::Created);
239 assert!(result.output_path.exists());
240
241 let content = std::fs::read_to_string(&result.output_path).unwrap();
242 assert!(content.contains("ACP Context"));
243 }
244
245 #[test]
246 fn test_bootstrap_merges_existing() {
247 let temp = TempDir::new().unwrap();
248 let existing_content = "# My Project\n\nSome existing content.";
249 std::fs::write(temp.path().join(".cursorrules"), existing_content).unwrap();
250
251 let executor = SyncExecutor::new();
252 let result = executor.bootstrap_tool(Tool::Cursor, temp.path()).unwrap();
253
254 assert_eq!(result.action, BootstrapAction::Merged);
255
256 let content = std::fs::read_to_string(&result.output_path).unwrap();
257 assert!(content.contains("My Project"));
258 assert!(content.contains("ACP Context"));
259 assert!(content.contains("BEGIN ACP GENERATED"));
260 }
261}