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