Avoid overwriting invalid or missing OpenCode config
This commit is contained in:
@@ -300,25 +300,106 @@ fn resolve_opencode_config_path() -> PathBuf {
|
||||
.join("opencode.json")
|
||||
}
|
||||
|
||||
pub async fn ensure_global_config(mcp: &McpRegistry) -> anyhow::Result<()> {
|
||||
let config_path = resolve_opencode_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
enum ConfigLoadResult {
|
||||
Missing,
|
||||
Invalid(String),
|
||||
Ok(Value),
|
||||
}
|
||||
|
||||
async fn load_config_object(path: &Path) -> ConfigLoadResult {
|
||||
if !path.exists() {
|
||||
return ConfigLoadResult::Missing;
|
||||
}
|
||||
|
||||
let contents = match tokio::fs::read_to_string(path).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => return ConfigLoadResult::Invalid(err.to_string()),
|
||||
};
|
||||
|
||||
let parsed: Value = match serde_json::from_str(&contents) {
|
||||
Ok(value) => value,
|
||||
Err(err) => return ConfigLoadResult::Invalid(err.to_string()),
|
||||
};
|
||||
|
||||
if !parsed.is_object() {
|
||||
return ConfigLoadResult::Invalid("OpenCode config root is not a JSON object".to_string());
|
||||
}
|
||||
|
||||
ConfigLoadResult::Ok(parsed)
|
||||
}
|
||||
|
||||
async fn backup_config(path: &Path) {
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let parent = match path.parent() {
|
||||
Some(parent) => parent,
|
||||
None => return,
|
||||
};
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("opencode.json");
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S");
|
||||
let backup_path = parent.join(format!("{}.bak.{}", filename, timestamp));
|
||||
|
||||
if let Err(err) = tokio::fs::copy(path, &backup_path).await {
|
||||
tracing::warn!(path = %path.display(), error = %err, "Failed to backup OpenCode config");
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_json_atomic(path: &Path, root: &Value) -> anyhow::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut root: Value = if config_path.exists() {
|
||||
let contents = tokio::fs::read_to_string(&config_path)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
serde_json::from_str(&contents).unwrap_or_else(|_| json!({}))
|
||||
} else {
|
||||
json!({})
|
||||
};
|
||||
backup_config(path).await;
|
||||
|
||||
if !root.is_object() {
|
||||
root = json!({});
|
||||
let payload = serde_json::to_string_pretty(root)?;
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("OpenCode config path has no parent"))?;
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("opencode.json");
|
||||
let tmp_path = parent.join(format!(
|
||||
".{}.tmp.{}",
|
||||
filename,
|
||||
chrono::Utc::now().timestamp_millis()
|
||||
));
|
||||
|
||||
tokio::fs::write(&tmp_path, payload).await?;
|
||||
if let Err(err) = tokio::fs::rename(&tmp_path, path).await {
|
||||
let _ = tokio::fs::remove_file(&tmp_path).await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ensure_global_config(mcp: &McpRegistry) -> anyhow::Result<()> {
|
||||
let config_path = resolve_opencode_config_path();
|
||||
let mut root: Value = match load_config_object(&config_path).await {
|
||||
ConfigLoadResult::Ok(value) => value,
|
||||
ConfigLoadResult::Missing => {
|
||||
tracing::warn!(
|
||||
path = %config_path.display(),
|
||||
"OpenCode config is missing; skipping global config sync"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
ConfigLoadResult::Invalid(err) => {
|
||||
tracing::warn!(
|
||||
path = %config_path.display(),
|
||||
error = %err,
|
||||
"OpenCode config is invalid; skipping global config sync"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let mcp_configs = mcp.list_configs().await;
|
||||
let mut mcp_entries = serde_json::Map::new();
|
||||
for config in mcp_configs.iter().filter(|c| c.enabled) {
|
||||
@@ -347,14 +428,15 @@ pub async fn ensure_global_config(mcp: &McpRegistry) -> anyhow::Result<()> {
|
||||
.or_insert_with(|| json!({}))
|
||||
.as_object_mut()
|
||||
.expect("tools object");
|
||||
tools_obj.insert("bash".to_string(), json!(false));
|
||||
if !tools_obj.contains_key("bash") {
|
||||
tools_obj.insert("bash".to_string(), json!(false));
|
||||
}
|
||||
tools_obj.insert("desktop_*".to_string(), json!(true));
|
||||
tools_obj.insert("playwright_*".to_string(), json!(true));
|
||||
tools_obj.insert("browser_*".to_string(), json!(true));
|
||||
tools_obj.insert("workspace_*".to_string(), json!(true));
|
||||
|
||||
let payload = serde_json::to_string_pretty(&root)?;
|
||||
tokio::fs::write(&config_path, payload).await?;
|
||||
write_json_atomic(&config_path, &root).await?;
|
||||
tracing::info!(path = %config_path.display(), "Ensured OpenCode global config");
|
||||
|
||||
Ok(())
|
||||
@@ -379,23 +461,25 @@ fn package_base(spec: &str) -> String {
|
||||
|
||||
pub async fn sync_global_plugins(plugins: &HashMap<String, Plugin>) -> anyhow::Result<()> {
|
||||
let config_path = resolve_opencode_config_path();
|
||||
if let Some(parent) = config_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut root: Value = if config_path.exists() {
|
||||
let contents = tokio::fs::read_to_string(&config_path)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
serde_json::from_str(&contents).unwrap_or_else(|_| json!({}))
|
||||
} else {
|
||||
json!({})
|
||||
let mut root: Value = match load_config_object(&config_path).await {
|
||||
ConfigLoadResult::Ok(value) => value,
|
||||
ConfigLoadResult::Missing => {
|
||||
tracing::warn!(
|
||||
path = %config_path.display(),
|
||||
"OpenCode config is missing; skipping plugin sync"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
ConfigLoadResult::Invalid(err) => {
|
||||
tracing::warn!(
|
||||
path = %config_path.display(),
|
||||
error = %err,
|
||||
"OpenCode config is invalid; skipping plugin sync"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !root.is_object() {
|
||||
root = json!({});
|
||||
}
|
||||
|
||||
let root_obj = root.as_object_mut().expect("config object");
|
||||
let existing_plugins: Vec<String> = root_obj
|
||||
.get("plugin")
|
||||
@@ -430,8 +514,7 @@ pub async fn sync_global_plugins(plugins: &HashMap<String, Plugin>) -> anyhow::R
|
||||
|
||||
root_obj.insert("plugin".to_string(), json!(merged));
|
||||
|
||||
let payload = serde_json::to_string_pretty(&root)?;
|
||||
tokio::fs::write(&config_path, payload).await?;
|
||||
write_json_atomic(&config_path, &root).await?;
|
||||
tracing::info!(path = %config_path.display(), "Synced OpenCode global plugins");
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user