diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index f70fa7fd5..44dbd4c02 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -1818,6 +1818,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonptr" version = "0.6.3" @@ -2370,8 +2381,9 @@ dependencies = [ [[package]] name = "openwork" -version = "0.3.0" +version = "0.3.2" dependencies = [ + "json5", "serde", "serde_json", "tauri", @@ -2478,6 +2490,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.8.0" @@ -4467,6 +4522,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 010c4f2ee..bbbf4ae97 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" tauri-build = { version = "2", features = [] } [dependencies] +json5 = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" tauri = { version = "2", features = [] } diff --git a/packages/desktop/src-tauri/src/config.rs b/packages/desktop/src-tauri/src/config.rs index f0f99ee8f..c7cd238db 100644 --- a/packages/desktop/src-tauri/src/config.rs +++ b/packages/desktop/src-tauri/src/config.rs @@ -4,13 +4,14 @@ use std::path::PathBuf; use crate::types::{ExecResult, OpencodeConfigFile}; -pub fn resolve_opencode_config_path(scope: &str, project_dir: &str) -> Result { +fn opencode_config_candidates(scope: &str, project_dir: &str) -> Result<(PathBuf, PathBuf), String> { match scope { "project" => { if project_dir.trim().is_empty() { return Err("projectDir is required".to_string()); } - Ok(PathBuf::from(project_dir).join("opencode.json")) + let root = PathBuf::from(project_dir); + Ok((root.join("opencode.jsonc"), root.join("opencode.json"))) } "global" => { let base = if let Ok(dir) = env::var("XDG_CONFIG_HOME") { @@ -21,12 +22,27 @@ pub fn resolve_opencode_config_path(scope: &str, project_dir: &str) -> Result Err("scope must be 'project' or 'global'".to_string()), } } +pub fn resolve_opencode_config_path(scope: &str, project_dir: &str) -> Result { + let (jsonc_path, json_path) = opencode_config_candidates(scope, project_dir)?; + + if jsonc_path.exists() { + return Ok(jsonc_path); + } + + if json_path.exists() { + return Ok(json_path); + } + + Ok(jsonc_path) +} + pub fn read_opencode_config(scope: &str, project_dir: &str) -> Result { let path = resolve_opencode_config_path(scope.trim(), project_dir)?; let exists = path.exists(); diff --git a/packages/desktop/src-tauri/src/engine/spawn.rs b/packages/desktop/src-tauri/src/engine/spawn.rs index 0eace0552..503fc3eef 100644 --- a/packages/desktop/src-tauri/src/engine/spawn.rs +++ b/packages/desktop/src-tauri/src/engine/spawn.rs @@ -38,11 +38,20 @@ pub fn build_engine_command(program: &Path, hostname: &str, port: u16, project_d command.env("XDG_DATA_HOME", xdg_data_home); } - if let Some(xdg_config_home) = maybe_infer_xdg_home( + let xdg_config_home = maybe_infer_xdg_home( "XDG_CONFIG_HOME", candidate_xdg_config_dirs(), - Path::new("opencode/opencode.json"), - ) { + Path::new("opencode/opencode.jsonc"), + ) + .or_else(|| { + maybe_infer_xdg_home( + "XDG_CONFIG_HOME", + candidate_xdg_config_dirs(), + Path::new("opencode/opencode.json"), + ) + }); + + if let Some(xdg_config_home) = xdg_config_home { command.env("XDG_CONFIG_HOME", xdg_config_home); } diff --git a/packages/desktop/src-tauri/src/workspace/files.rs b/packages/desktop/src-tauri/src/workspace/files.rs index d87144acb..75f5cdd95 100644 --- a/packages/desktop/src-tauri/src/workspace/files.rs +++ b/packages/desktop/src-tauri/src/workspace/files.rs @@ -158,11 +158,22 @@ pub fn ensure_workspace_files(workspace_path: &str, preset: &str) -> Result<(), .map_err(|e| format!("Failed to create .openwork/templates: {e}"))?; seed_templates(&templates_dir)?; - let config_path = root.join("opencode.json"); - let mut config: serde_json::Value = if config_path.exists() { + let config_path_jsonc = root.join("opencode.jsonc"); + let config_path_json = root.join("opencode.json"); + let config_path = if config_path_jsonc.exists() { + config_path_jsonc + } else if config_path_json.exists() { + config_path_json + } else { + config_path_jsonc + }; + + let config_exists = config_path.exists(); + let mut config_changed = !config_exists; + let mut config: serde_json::Value = if config_exists { let raw = fs::read_to_string(&config_path) .map_err(|e| format!("Failed to read {}: {e}", config_path.display()))?; - serde_json::from_str(&raw).unwrap_or_else(|_| serde_json::json!({})) + json5::from_str(&raw).unwrap_or_else(|_| serde_json::json!({})) } else { serde_json::json!({ "$schema": "https://opencode.ai/config.json" @@ -173,6 +184,7 @@ pub fn ensure_workspace_files(workspace_path: &str, preset: &str) -> Result<(), config = serde_json::json!({ "$schema": "https://opencode.ai/config.json" }); + config_changed = true; } let required_plugins: Vec<&str> = match preset { @@ -196,7 +208,10 @@ pub fn ensure_workspace_files(workspace_path: &str, preset: &str) -> Result<(), _ => vec![], }; - let merged = merge_plugins(existing_plugins, &required_plugins); + let merged = merge_plugins(existing_plugins.clone(), &required_plugins); + if merged != existing_plugins { + config_changed = true; + } if let Some(obj) = config.as_object_mut() { obj.insert( "plugin".to_string(), @@ -205,8 +220,10 @@ pub fn ensure_workspace_files(workspace_path: &str, preset: &str) -> Result<(), } } - fs::write(&config_path, serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?) - .map_err(|e| format!("Failed to write {}: {e}", config_path.display()))?; + if config_changed { + fs::write(&config_path, serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?) + .map_err(|e| format!("Failed to write {}: {e}", config_path.display()))?; + } let openwork_path = root.join(".opencode").join("openwork.json"); if !openwork_path.exists() {