use crate::io::{parse_jsonc_cst, read_file, write_file, ApiResult, CstRootNode, FromPath, InputOutput};
#[cfg(feature = "std")]
use crate::prelude::Path;
use crate::prelude::PathBuf;
use crate::schema::agent::Provider;
use crate::util::MimeType;
use alloc::collections::BTreeMap;
use color_eyre::eyre::eyre;
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::skip_serializing_none;
pub type PermissionObjectConfig = StringMap<PermissionRuleConfig>;
pub type StringMap<T> = BTreeMap<String, T>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AgentMode {
Subagent,
Primary,
All,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AutoUpdateConfig {
Bool(bool),
Notify(Notify),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BoolOrMap<T> {
Bool(bool),
Map(StringMap<T>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LayoutConfig {
Auto,
Stretch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum LspConfig {
Disabled {
disabled: bool,
},
Server(LspServerConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum McpConfig {
#[serde(rename = "local")]
Local(McpLocalConfig),
#[serde(rename = "remote")]
Remote(McpRemoteConfig),
#[serde(untagged)]
EnabledOnly {
enabled: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum McpOAuthOrFalse {
Config(McpOAuthConfig),
Disabled(bool),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Notify {
Notify,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionAction {
Ask,
Allow,
Deny,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PermissionConfig {
Action(PermissionAction),
Object(PermissionObjectConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PermissionRuleConfig {
Action(PermissionAction),
Object(StringMap<PermissionAction>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PluginConfig {
Name(String),
WithOptions(String, Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PolicyAction {
#[serde(rename = "provider.use")]
ProviderUse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PolicyEffect {
Allow,
Deny,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ReferenceConfig {
String(String),
Git(ReferenceGit),
Local(ReferenceLocal),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ShareConfig {
Manual,
Auto,
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AgentConfig {
pub model: Option<String>,
pub variant: Option<String>,
pub temperature: Option<f64>,
pub top_p: Option<f64>,
pub prompt: Option<String>,
#[deprecated(note = "Use `tools` instead")]
pub tools: Option<StringMap<bool>>,
pub disable: Option<bool>,
pub description: Option<String>,
pub mode: Option<AgentMode>,
pub hidden: Option<bool>,
pub options: Option<Value>,
pub color: Option<String>,
pub steps: Option<u64>,
#[deprecated(note = "Use `steps` instead")]
#[serde(rename = "maxSteps")]
pub max_steps: Option<u64>,
pub permission: Option<PermissionConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct AttachmentConfig {
pub image: Option<ImageAttachmentConfig>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(skip)]
pub cst: Option<CstRootNode>,
#[serde(skip)]
pub path: Option<PathBuf>,
pub agent: Option<StringMap<AgentConfig>>,
pub attachment: Option<AttachmentConfig>,
#[deprecated(note = "Use `share` instead")]
pub autoshare: Option<bool>,
pub autoupdate: Option<AutoUpdateConfig>,
pub command: Option<StringMap<CommandConfig>>,
pub compaction: Option<CompactionConfig>,
pub default_agent: Option<String>,
pub disabled_providers: Option<Vec<String>>,
pub enabled_providers: Option<Vec<String>>,
pub enterprise: Option<EnterpriseConfig>,
pub experimental: Option<ExperimentalConfig>,
pub formatter: Option<BoolOrMap<FormatterConfig>>,
pub instructions: Option<Vec<String>>,
#[deprecated(note = "Remove this field; layout is no longer configurable")]
pub layout: Option<LayoutConfig>,
#[serde(rename = "logLevel")]
pub log_level: Option<LogLevel>,
pub lsp: Option<BoolOrMap<LspConfig>>,
pub mcp: Option<StringMap<McpConfig>>,
#[deprecated(note = "Use `agent` instead")]
pub mode: Option<StringMap<AgentConfig>>,
pub model: Option<String>,
pub permission: Option<PermissionConfig>,
pub plugin: Option<Vec<PluginConfig>>,
pub provider: Option<StringMap<Value>>,
#[deprecated(note = "Use `references` instead")]
pub reference: Option<StringMap<ReferenceConfig>>,
pub references: Option<StringMap<ReferenceConfig>>,
#[serde(rename = "$schema")]
pub schema: Option<String>,
pub server: Option<ServerConfig>,
pub share: Option<ShareConfig>,
pub shell: Option<String>,
pub skills: Option<SkillsConfig>,
pub small_model: Option<String>,
pub snapshot: Option<bool>,
pub tool_output: Option<ToolOutputConfig>,
pub tools: Option<StringMap<bool>>,
pub username: Option<String>,
pub watcher: Option<WatcherConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CommandConfig {
pub template: String,
pub description: Option<String>,
pub agent: Option<String>,
pub model: Option<String>,
pub variant: Option<String>,
pub subtask: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct CompactionConfig {
pub auto: Option<bool>,
pub prune: Option<bool>,
pub tail_turns: Option<u64>,
pub preserve_recent_tokens: Option<u64>,
pub reserved: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct EnterpriseConfig {
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ExperimentalConfig {
pub disable_paste_summary: Option<bool>,
pub batch_tool: Option<bool>,
#[serde(rename = "openTelemetry")]
pub open_telemetry: Option<bool>,
pub primary_tools: Option<Vec<String>>,
pub continue_loop_on_deny: Option<bool>,
pub mcp_timeout: Option<u64>,
pub policies: Option<Vec<ExperimentalPolicy>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExperimentalPolicy {
pub action: PolicyAction,
pub effect: PolicyEffect,
pub resource: Provider,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FormatterConfig {
pub disabled: Option<bool>,
pub command: Option<Vec<String>>,
pub environment: Option<StringMap<String>>,
pub extensions: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ImageAttachmentConfig {
pub auto_resize: Option<bool>,
pub max_width: Option<u64>,
pub max_height: Option<u64>,
pub max_base64_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LspServerConfig {
pub command: Vec<String>,
pub extensions: Option<Vec<String>>,
pub disabled: Option<bool>,
pub env: Option<StringMap<String>>,
pub initialization: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct McpLocalConfig {
pub command: Vec<String>,
pub cwd: Option<String>,
pub environment: Option<StringMap<String>>,
pub enabled: Option<bool>,
pub timeout: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct McpOAuthConfig {
#[serde(rename = "clientId")]
pub client_id: Option<String>,
#[serde(rename = "clientSecret")]
pub client_secret: Option<String>,
pub scope: Option<String>,
#[serde(rename = "callbackPort")]
pub callback_port: Option<u16>,
#[serde(rename = "redirectUri")]
pub redirect_uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct McpRemoteConfig {
pub url: String,
pub enabled: Option<bool>,
pub headers: Option<StringMap<String>>,
pub oauth: Option<McpOAuthOrFalse>,
pub timeout: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReferenceGit {
pub repository: String,
pub branch: Option<String>,
pub description: Option<String>,
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReferenceLocal {
pub path: String,
pub description: Option<String>,
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ServerConfig {
pub port: Option<u64>,
pub hostname: Option<String>,
pub mdns: Option<bool>,
#[serde(rename = "mdnsDomain")]
pub mdns_domain: Option<String>,
pub cors: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct SkillsConfig {
pub paths: Option<Vec<String>>,
pub urls: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct ToolOutputConfig {
pub max_lines: Option<u64>,
pub max_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct WatcherConfig {
pub ignore: Option<Vec<String>>,
}
impl Config {
#[cfg(feature = "std")]
pub fn load(path: impl AsRef<Path>) -> Self {
path.as_ref()
.is_file()
.then(|| Self::read(path.as_ref()).ok())
.flatten()
.unwrap_or_default()
}
pub fn with_provider(self, provider: StringMap<Value>) -> Self {
Self {
provider: Some(provider),
..self
}
}
pub fn with_model(self, model: String) -> Self {
Self { model: Some(model), ..self }
}
fn parse_json(content: impl AsRef<str>) -> ApiResult<Self> {
serde_json::from_str(content.as_ref()).map_err(|why| eyre!("JSON parse error — {why}"))
}
pub fn parse_jsonc(content: &str) -> ApiResult<Self> {
parse_jsonc_cst::<Config>(content).map(|(mut config, cst)| {
config.cst = Some(cst);
config
})
}
#[cfg(feature = "std")]
pub fn from_path(path: impl AsRef<Path>) -> ApiResult<Self> {
Self::read(path.as_ref())
}
#[cfg(feature = "std")]
pub fn resolve() -> Option<Self> {
let home = BaseDirs::new().map(|dirs| dirs.config_dir().to_path_buf());
let candidates = [
Some(PathBuf::from("opencode.jsonc")),
Some(PathBuf::from("opencode.json")),
home.as_ref().map(|p| p.join("opencode").join("opencode.jsonc")),
home.as_ref().map(|p| p.join("opencode").join("opencode.json")),
];
candidates
.into_iter()
.flatten()
.find_map(|path| if path.is_file() { Self::read(&path).ok() } else { None })
}
fn to_jsonc_string(&self) -> ApiResult<String> {
match &self.cst {
| Some(cst) => Ok(cst.to_string()),
| None => serde_json::to_string_pretty(self).map_err(|why| eyre!("Failed to serialize JSONC config — {why}")),
}
}
}
impl InputOutput for Config {
fn read(path: impl Into<PathBuf>) -> ApiResult<Self> {
let source = path.into();
match MimeType::from_path(&source) {
| MimeType::Json => Self::read_json(source.clone()),
| MimeType::Jsonc => Self::read_jsonc(source.clone()),
| _ => Err(eyre!("Unsupported OpenCode configuration file extension")),
}
.map(|config| Self {
path: Some(source),
..config
})
}
fn read_json(path: PathBuf) -> ApiResult<Self> {
read_file(path.clone())
.and_then(|content| Self::parse_json(&content).map_err(|why| eyre!("Failed to read JSON config `{}` — {}", path.display(), why)))
}
fn read_jsonc(path: PathBuf) -> ApiResult<Self> {
read_file(path.clone())
.and_then(|content| Self::parse_jsonc(&content).map_err(|why| eyre!("Failed to read JSONC config `{}` — {}", path.display(), why)))
}
fn read_yaml(_path: PathBuf) -> ApiResult<Self> {
Err(eyre!("YAML format is not supported for OpenCode configuration"))
}
fn write(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let target = path.into();
match MimeType::from_path(&target) {
| MimeType::Json | MimeType::Jsonc => self.write_json(&target),
| _ => Err(eyre!("Unsupported OpenCode configuration file extension")),
}
}
fn write_json(&self, path: impl Into<PathBuf>) -> ApiResult<()> {
let target = path.into();
self.to_jsonc_string().and_then(|content| write_file(target.clone(), content))
}
fn write_yaml(&self, _path: impl Into<PathBuf>) -> ApiResult<()> {
Err(eyre!("YAML format is not supported for OpenCode configuration"))
}
}