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}