settings.rs

  1use anyhow::Result;
  2use serde_json::Value;
  3
  4use crate::migrations::migrate_settings;
  5
  6const AGENT_SERVERS_KEY: &str = "agent_servers";
  7
  8struct BuiltinMapping {
  9    old_key: &'static str,
 10    registry_key: &'static str,
 11}
 12
 13const BUILTIN_MAPPINGS: &[BuiltinMapping] = &[
 14    BuiltinMapping {
 15        old_key: "gemini",
 16        registry_key: "gemini",
 17    },
 18    BuiltinMapping {
 19        old_key: "claude",
 20        registry_key: "claude-acp",
 21    },
 22    BuiltinMapping {
 23        old_key: "codex",
 24        registry_key: "codex-acp",
 25    },
 26];
 27
 28const REGISTRY_COMPATIBLE_FIELDS: &[&str] = &[
 29    "env",
 30    "default_mode",
 31    "default_model",
 32    "favorite_models",
 33    "default_config_options",
 34    "favorite_config_option_values",
 35];
 36
 37pub fn migrate_builtin_agent_servers_to_registry(value: &mut Value) -> Result<()> {
 38    migrate_settings(value, &mut migrate_one)
 39}
 40
 41fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
 42    let Some(agent_servers) = obj.get_mut(AGENT_SERVERS_KEY) else {
 43        return Ok(());
 44    };
 45    let Some(servers_map) = agent_servers.as_object_mut() else {
 46        return Ok(());
 47    };
 48
 49    for mapping in BUILTIN_MAPPINGS {
 50        migrate_builtin_entry(servers_map, mapping);
 51    }
 52
 53    Ok(())
 54}
 55
 56fn migrate_builtin_entry(
 57    servers_map: &mut serde_json::Map<String, Value>,
 58    mapping: &BuiltinMapping,
 59) {
 60    // Check if the old key exists and needs migration before taking ownership.
 61    let needs_migration = servers_map
 62        .get(mapping.old_key)
 63        .and_then(|v| v.as_object())
 64        .is_some_and(|obj| !obj.contains_key("type"));
 65
 66    if !needs_migration {
 67        return;
 68    }
 69
 70    // When the registry key differs from the old key and the target already
 71    // exists, just remove the stale old entry to avoid overwriting user data.
 72    if mapping.old_key != mapping.registry_key && servers_map.contains_key(mapping.registry_key) {
 73        servers_map.remove(mapping.old_key);
 74        return;
 75    }
 76
 77    let Some(old_entry) = servers_map.remove(mapping.old_key) else {
 78        return;
 79    };
 80    let Some(old_obj) = old_entry.as_object() else {
 81        return;
 82    };
 83
 84    let has_command = old_obj.contains_key("command");
 85    let ignore_system_version = old_obj
 86        .get("ignore_system_version")
 87        .and_then(|v| v.as_bool());
 88
 89    // A custom entry is needed when the user configured a custom binary
 90    // or explicitly opted into using the system version via
 91    // `ignore_system_version: false` (only meaningful for gemini).
 92    let needs_custom = has_command
 93        || (mapping.old_key == "gemini" && matches!(ignore_system_version, Some(false)));
 94
 95    if needs_custom {
 96        let local_key = format!("{}-custom", mapping.registry_key);
 97
 98        // Don't overwrite an existing `-custom` entry.
 99        if servers_map.contains_key(&local_key) {
100            return;
101        }
102
103        let mut custom_obj = serde_json::Map::new();
104        custom_obj.insert("type".to_string(), Value::String("custom".to_string()));
105
106        if has_command {
107            if let Some(command) = old_obj.get("command") {
108                custom_obj.insert("command".to_string(), command.clone());
109            }
110            if let Some(args) = old_obj.get("args") {
111                if !args.as_array().is_some_and(|a| a.is_empty()) {
112                    custom_obj.insert("args".to_string(), args.clone());
113                }
114            }
115        } else {
116            // ignore_system_version: false — the user wants the binary from $PATH
117            custom_obj.insert(
118                "command".to_string(),
119                Value::String(mapping.old_key.to_string()),
120            );
121        }
122
123        // Carry over all compatible fields to the custom entry.
124        for &field in REGISTRY_COMPATIBLE_FIELDS {
125            if let Some(value) = old_obj.get(field) {
126                match value {
127                    Value::Array(arr) if arr.is_empty() => continue,
128                    Value::Object(map) if map.is_empty() => continue,
129                    Value::Null => continue,
130                    _ => {
131                        custom_obj.insert(field.to_string(), value.clone());
132                    }
133                }
134            }
135        }
136
137        servers_map.insert(local_key, Value::Object(custom_obj));
138    } else {
139        // Build a registry entry with compatible fields only.
140        let mut registry_obj = serde_json::Map::new();
141        registry_obj.insert("type".to_string(), Value::String("registry".to_string()));
142
143        for &field in REGISTRY_COMPATIBLE_FIELDS {
144            if let Some(value) = old_obj.get(field) {
145                match value {
146                    Value::Array(arr) if arr.is_empty() => continue,
147                    Value::Object(map) if map.is_empty() => continue,
148                    Value::Null => continue,
149                    _ => {
150                        registry_obj.insert(field.to_string(), value.clone());
151                    }
152                }
153            }
154        }
155
156        servers_map.insert(
157            mapping.registry_key.to_string(),
158            Value::Object(registry_obj),
159        );
160    }
161}