mcp_sync/targets/
opencode.rs

1//! OpenCode target implementation.
2
3use crate::{
4    canon::{canon_names, Canon},
5    target::{SyncOptions, Target},
6    utils::{home, json_get_obj_mut, json_obj_mut, log_verbose, read_json_file, write_json_file},
7};
8use anyhow::{anyhow, Result};
9use serde_json::Value as JsonValue;
10use std::path::{Path, PathBuf};
11
12/// OpenCode target.
13///
14/// File format: `{ "$schema": "...", "mcp": { name: {...} } }`
15pub struct OpenCodeTarget;
16
17impl Target for OpenCodeTarget {
18    fn name(&self) -> &'static str {
19        "OpenCode"
20    }
21
22    fn global_path(&self) -> Result<PathBuf> {
23        let home = home()?;
24        
25        #[cfg(target_os = "macos")]
26        let path = home.join(".config").join("opencode").join("opencode.json");
27        
28        #[cfg(target_os = "windows")]
29        let path = home
30            .join("AppData")
31            .join("Roaming")
32            .join("opencode")
33            .join("opencode.json");
34        
35        #[cfg(target_os = "linux")]
36        let path = home.join(".config").join("opencode").join("opencode.json");
37        
38        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
39        let path = home.join(".config").join("opencode").join("opencode.json");
40        
41        Ok(path)
42    }
43
44    fn project_path(&self, project_root: &Path) -> PathBuf {
45        project_root.join("opencode.json")
46    }
47
48    fn sync(&self, path: &Path, canon: &Canon, opts: &SyncOptions) -> Result<()> {
49        let mut root = read_json_file(path)?;
50
51        // Ensure $schema is present
52        {
53            let obj = json_obj_mut(&mut root)?;
54            obj.entry("$schema".to_string())
55                .or_insert_with(|| JsonValue::String("https://opencode.ai/config.json".into()));
56        }
57
58        let mcp = json_get_obj_mut(&mut root, "mcp")?;
59
60        for (name, s) in &canon.servers {
61            let cfg = if s.kind() == "http" {
62                let url = s.url.clone().ok_or_else(|| anyhow!("server {}: http missing url", name))?;
63                let mut obj = serde_json::Map::new();
64                obj.insert("type".into(), JsonValue::String("remote".into()));
65                obj.insert("url".into(), JsonValue::String(url));
66                obj.insert("enabled".into(), JsonValue::Bool(s.enabled()));
67                if let Some(h) = &s.headers {
68                    obj.insert("headers".into(), serde_json::to_value(h)?);
69                }
70                JsonValue::Object(obj)
71            } else {
72                let cmd = s.command.clone().ok_or_else(|| anyhow!("server {}: stdio missing command", name))?;
73                let mut obj = serde_json::Map::new();
74                obj.insert("type".into(), JsonValue::String("local".into()));
75                let mut cmd_arr = vec![JsonValue::String(cmd)];
76                for a in s.args.clone().unwrap_or_default() {
77                    cmd_arr.push(JsonValue::String(a));
78                }
79                obj.insert("command".into(), JsonValue::Array(cmd_arr));
80                obj.insert("enabled".into(), JsonValue::Bool(s.enabled()));
81                if let Some(env) = &s.env {
82                    obj.insert("environment".into(), serde_json::to_value(env)?);
83                }
84                JsonValue::Object(obj)
85            };
86            mcp.insert(name.clone(), cfg);
87        }
88
89        if opts.clean {
90            let canon_set = canon_names(canon);
91            let keys: Vec<String> = mcp.keys().cloned().collect();
92            for k in keys {
93                if !canon_set.contains(&k) {
94                    mcp.remove(&k);
95                }
96            }
97        }
98
99        let label = if path.starts_with(home().unwrap_or_default()) {
100            "global"
101        } else {
102            "project"
103        };
104        log_verbose(opts.verbose, format!("{} {} -> {:?}", self.name(), label, path));
105        write_json_file(path, &root, opts.dry_run)
106    }
107}