mcp_sync/targets/
opencode.rs1use 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
12pub 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 {
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}