use crate::config::write_json_file; use crate::error::AppError; use crate::provider::OpenCodeProviderConfig; use crate::settings::get_opencode_override_dir; use indexmap::IndexMap; use serde_json::{json, Map, Value}; use std::path::PathBuf; const STANDARD_OMO_PLUGIN_PREFIXES: [&str; 2] = ["oh-my-openagent", "oh-my-opencode"]; const SLIM_OMO_PLUGIN_PREFIXES: [&str; 1] = ["oh-my-opencode-slim"]; fn matches_plugin_prefix(plugin_name: &str, prefix: &str) -> bool { plugin_name == prefix || plugin_name .strip_prefix(prefix) .map(|suffix| suffix.starts_with('@')) .unwrap_or(false) } fn matches_any_plugin_prefix(plugin_name: &str, prefixes: &[&str]) -> bool { prefixes .iter() .any(|prefix| matches_plugin_prefix(plugin_name, prefix)) } fn canonicalize_plugin_name(plugin_name: &str) -> String { if let Some(suffix) = plugin_name.strip_prefix("oh-my-opencode") { if suffix.is_empty() || suffix.starts_with('@') { return format!("oh-my-openagent{suffix}"); } } plugin_name.to_string() } pub fn get_opencode_dir() -> PathBuf { if let Some(override_dir) = get_opencode_override_dir() { return override_dir; } dirs::home_dir() .map(|h| h.join(".config").join("opencode")) .unwrap_or_else(|| PathBuf::from(".config").join("opencode")) } pub fn get_opencode_config_path() -> PathBuf { get_opencode_dir().join("opencode.json") } #[allow(dead_code)] pub fn get_opencode_env_path() -> PathBuf { get_opencode_dir().join(".env") } pub fn read_opencode_config() -> Result { let path = get_opencode_config_path(); if !path.exists() { return Ok(json!({ "$schema": "https://opencode.ai/config.json" })); } let content = std::fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?; serde_json::from_str(&content).map_err(|e| AppError::json(&path, e)) } pub fn write_opencode_config(config: &Value) -> Result<(), AppError> { let path = get_opencode_config_path(); write_json_file(&path, config)?; log::debug!("OpenCode config written to {path:?}"); Ok(()) } pub fn get_providers() -> Result, AppError> { let config = read_opencode_config()?; Ok(config .get("provider") .and_then(|v| v.as_object()) .cloned() .unwrap_or_default()) } pub fn set_provider(id: &str, config: Value) -> Result<(), AppError> { let mut full_config = read_opencode_config()?; if full_config.get("provider").is_none() { full_config["provider"] = json!({}); } if let Some(providers) = full_config .get_mut("provider") .and_then(|v| v.as_object_mut()) { providers.insert(id.to_string(), config); } write_opencode_config(&full_config) } pub fn remove_provider(id: &str) -> Result<(), AppError> { let mut config = read_opencode_config()?; if let Some(providers) = config.get_mut("provider").and_then(|v| v.as_object_mut()) { providers.remove(id); } write_opencode_config(&config) } pub fn get_typed_providers() -> Result, AppError> { let providers = get_providers()?; let mut result = IndexMap::new(); for (id, value) in providers { match serde_json::from_value::(value.clone()) { Ok(config) => { result.insert(id, config); } Err(e) => { log::warn!("Failed to parse provider '{id}': {e}"); } } } Ok(result) } pub fn set_typed_provider(id: &str, config: &OpenCodeProviderConfig) -> Result<(), AppError> { let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?; set_provider(id, value) } pub fn get_mcp_servers() -> Result, AppError> { let config = read_opencode_config()?; Ok(config .get("mcp") .and_then(|v| v.as_object()) .cloned() .unwrap_or_default()) } pub fn set_mcp_server(id: &str, config: Value) -> Result<(), AppError> { let mut full_config = read_opencode_config()?; if full_config.get("mcp").is_none() { full_config["mcp"] = json!({}); } if let Some(mcp) = full_config.get_mut("mcp").and_then(|v| v.as_object_mut()) { mcp.insert(id.to_string(), config); } write_opencode_config(&full_config) } pub fn remove_mcp_server(id: &str) -> Result<(), AppError> { let mut config = read_opencode_config()?; if let Some(mcp) = config.get_mut("mcp").and_then(|v| v.as_object_mut()) { mcp.remove(id); } write_opencode_config(&config) } pub fn add_plugin(plugin_name: &str) -> Result<(), AppError> { let mut config = read_opencode_config()?; let normalized_plugin_name = canonicalize_plugin_name(plugin_name); let plugins = config.get_mut("plugin").and_then(|v| v.as_array_mut()); match plugins { Some(arr) => { // Mutual exclusion: standard OMO and OMO Slim cannot coexist as plugins if matches_any_plugin_prefix(&normalized_plugin_name, &STANDARD_OMO_PLUGIN_PREFIXES) { arr.retain(|v| { v.as_str() .map(|s| { !matches_any_plugin_prefix(s, &STANDARD_OMO_PLUGIN_PREFIXES) && !matches_any_plugin_prefix(s, &SLIM_OMO_PLUGIN_PREFIXES) }) .unwrap_or(true) }); } else if matches_any_plugin_prefix(&normalized_plugin_name, &SLIM_OMO_PLUGIN_PREFIXES) { arr.retain(|v| { v.as_str() .map(|s| { !matches_any_plugin_prefix(s, &STANDARD_OMO_PLUGIN_PREFIXES) && !matches_any_plugin_prefix(s, &SLIM_OMO_PLUGIN_PREFIXES) }) .unwrap_or(true) }); } let already_exists = arr .iter() .any(|v| v.as_str() == Some(normalized_plugin_name.as_str())); if !already_exists { arr.push(Value::String(normalized_plugin_name)); } } None => { config["plugin"] = json!([normalized_plugin_name]); } } write_opencode_config(&config) } pub fn remove_plugins_by_prefixes(prefixes: &[&str]) -> Result<(), AppError> { let mut config = read_opencode_config()?; if let Some(arr) = config.get_mut("plugin").and_then(|v| v.as_array_mut()) { arr.retain(|v| { v.as_str() .map(|s| !matches_any_plugin_prefix(s, prefixes)) .unwrap_or(true) }); if arr.is_empty() { config.as_object_mut().map(|obj| obj.remove("plugin")); } } write_opencode_config(&config) }