mcp_sync/target.rs
1//! # Target Trait
2//!
3//! Defines the interface for sync targets (Antigravity, Claude, Codex, OpenCode).
4//! This trait allows for polymorphic handling and easy addition of new targets.
5
6use crate::canon::Canon;
7use anyhow::Result;
8use std::path::{Path, PathBuf};
9
10/// Options passed to sync operations.
11#[derive(Debug, Clone)]
12pub struct SyncOptions {
13 /// Remove servers not present in canonical config.
14 pub clean: bool,
15 /// Print changes but don't write files.
16 pub dry_run: bool,
17 /// Enable verbose output.
18 pub verbose: bool,
19}
20
21impl SyncOptions {
22 /// Creates new sync options.
23 pub fn new(clean: bool, dry_run: bool, verbose: bool) -> Self {
24 Self { clean, dry_run, verbose }
25 }
26}
27
28/// Trait for MCP configuration sync targets.
29///
30/// Each target (e.g., Antigravity, Claude) implements this trait to provide
31/// target-specific file paths and sync logic.
32///
33/// # Example
34/// ```ignore
35/// struct MyTarget;
36///
37/// impl Target for MyTarget {
38/// fn name(&self) -> &'static str { "my-target" }
39/// fn global_path(&self) -> Result<PathBuf> { Ok(home()?.join(".my-config.json")) }
40/// fn project_path(&self, root: &Path) -> PathBuf { root.join(".my-config.json") }
41/// fn sync(&self, path: &Path, canon: &Canon, opts: &SyncOptions) -> Result<()> {
42/// // Implementation
43/// Ok(())
44/// }
45/// }
46/// ```
47pub trait Target: Send + Sync {
48 /// Returns the human-readable name of this target.
49 fn name(&self) -> &'static str;
50
51 /// Returns the path to the global (user-level) configuration file.
52 fn global_path(&self) -> Result<PathBuf>;
53
54 /// Returns the path to the project-level configuration file.
55 fn project_path(&self, project_root: &Path) -> PathBuf;
56
57 /// Syncs the canonical configuration to the target file.
58 ///
59 /// # Arguments
60 /// * `path` - The target file path (global or project)
61 /// * `canon` - The parsed canonical configuration
62 /// * `opts` - Sync options (clean, dry_run, verbose)
63 fn sync(&self, path: &Path, canon: &Canon, opts: &SyncOptions) -> Result<()>;
64
65 /// Syncs to the global configuration file.
66 fn sync_global(&self, canon: &Canon, opts: &SyncOptions) -> Result<()> {
67 let path = self.global_path()?;
68 self.sync(&path, canon, opts)
69 }
70
71 /// Syncs to the project configuration file.
72 fn sync_project(&self, project_root: &Path, canon: &Canon, opts: &SyncOptions) -> Result<()> {
73 let path = self.project_path(project_root);
74 self.sync(&path, canon, opts)
75 }
76}
77
78// ─────────────────────────── Target Manager ───────────────────────────
79
80/// Manager for loading and managing sync targets.
81///
82/// Supports both built-in targets (Antigravity, Claude, Codex, OpenCode)
83/// and dynamically loaded targets from shared libraries.
84///
85/// # Example
86/// ```ignore
87/// use mcp_sync::TargetManager;
88///
89/// let mut mgr = TargetManager::new();
90///
91/// // Load all built-in targets
92/// mgr.load_builtin_all();
93///
94/// // Load a custom target from dylib
95/// unsafe {
96/// mgr.load_dynamic(Path::new("./my_target.dylib"))?;
97/// }
98///
99/// // Sync all targets
100/// for target in mgr.targets() {
101/// target.sync_global(&canon, &opts)?;
102/// }
103/// ```
104pub struct TargetManager {
105 targets: Vec<Box<dyn Target>>,
106 #[allow(dead_code)]
107 libraries: Vec<libloading::Library>,
108}
109
110impl TargetManager {
111 /// Creates a new empty target manager.
112 pub fn new() -> Self {
113 Self {
114 targets: Vec::new(),
115 libraries: Vec::new(),
116 }
117 }
118
119 /// Creates a target manager pre-loaded with all built-in targets.
120 pub fn with_builtins() -> Self {
121 let mut mgr = Self::new();
122 mgr.load_builtin_all();
123 mgr
124 }
125
126 /// Loads all built-in targets.
127 pub fn load_builtin_all(&mut self) {
128 use crate::targets::{AntigravityTarget, ClaudeTarget, CodexTarget, OpenCodeTarget};
129
130 self.targets.push(Box::new(AntigravityTarget));
131 self.targets.push(Box::new(ClaudeTarget));
132 self.targets.push(Box::new(CodexTarget));
133 self.targets.push(Box::new(OpenCodeTarget));
134 }
135
136 /// Loads a specific built-in target by name.
137 pub fn load_builtin(&mut self, name: &str) -> Result<()> {
138 use crate::targets::{AntigravityTarget, ClaudeTarget, CodexTarget, OpenCodeTarget};
139 use anyhow::bail;
140
141 match name.to_lowercase().as_str() {
142 "antigravity" => self.targets.push(Box::new(AntigravityTarget)),
143 "claude" => self.targets.push(Box::new(ClaudeTarget)),
144 "codex" => self.targets.push(Box::new(CodexTarget)),
145 "opencode" => self.targets.push(Box::new(OpenCodeTarget)),
146 _ => bail!("unknown built-in target: {}", name),
147 }
148 Ok(())
149 }
150
151 /// Loads a custom target from a dynamic library.
152 ///
153 /// The library must export a `create_target` function:
154 /// ```ignore
155 /// #[unsafe(no_mangle)]
156 /// pub extern "C" fn create_target() -> *mut dyn Target {
157 /// Box::into_raw(Box::new(MyTarget::new()))
158 /// }
159 /// ```
160 ///
161 /// # Safety
162 /// The caller must ensure the library is compatible and the
163 /// `create_target` function returns a valid Target pointer.
164 pub unsafe fn load_dynamic(&mut self, path: &Path) -> Result<()> {
165 use anyhow::Context;
166
167 let lib = unsafe {
168 libloading::Library::new(path)
169 .with_context(|| format!("failed to load library {:?}", path))?
170 };
171
172 let create_fn: libloading::Symbol<unsafe extern "C" fn() -> *mut dyn Target> = unsafe {
173 lib.get(b"create_target")
174 .with_context(|| "library missing create_target symbol")?
175 };
176
177 let raw_ptr = unsafe { create_fn() };
178 let target = unsafe { Box::from_raw(raw_ptr) };
179
180 self.targets.push(target);
181 self.libraries.push(lib);
182
183 Ok(())
184 }
185
186 /// Returns an iterator over all loaded targets.
187 pub fn targets(&self) -> impl Iterator<Item = &dyn Target> {
188 self.targets.iter().map(|b| b.as_ref())
189 }
190
191 /// Returns a mutable iterator over all loaded targets.
192 pub fn targets_mut(&mut self) -> impl Iterator<Item = &mut Box<dyn Target>> {
193 self.targets.iter_mut()
194 }
195
196 /// Returns the number of loaded targets.
197 pub fn count(&self) -> usize {
198 self.targets.len()
199 }
200
201 /// Returns true if no targets are loaded.
202 pub fn is_empty(&self) -> bool {
203 self.targets.is_empty()
204 }
205
206 /// Finds a target by name.
207 pub fn find(&self, name: &str) -> Option<&dyn Target> {
208 self.targets.iter()
209 .map(|b| b.as_ref())
210 .find(|t| t.name().eq_ignore_ascii_case(name))
211 }
212
213 /// Syncs all targets to their global configuration files.
214 pub fn sync_all_global(&self, canon: &Canon, opts: &SyncOptions) -> Result<()> {
215 for target in self.targets() {
216 target.sync_global(canon, opts)?;
217 }
218 Ok(())
219 }
220
221 /// Syncs all targets to their project configuration files.
222 pub fn sync_all_project(&self, project_root: &Path, canon: &Canon, opts: &SyncOptions) -> Result<()> {
223 for target in self.targets() {
224 target.sync_project(project_root, canon, opts)?;
225 }
226 Ok(())
227 }
228}
229
230impl Default for TargetManager {
231 fn default() -> Self {
232 Self::new()
233 }
234}
235