1use std::path::{Path, PathBuf};
9
10use serde_json::{Map, Value};
11
12pub mod permission_validation;
13pub mod settings_cache;
14pub mod tool_validation_config;
15pub mod validation;
16
17pub use crate::services::mcp::ConfigScope;
19
20#[cfg(test)]
21#[path = "tests/settings_tests.rs"]
22mod settings_tests;
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum EditableSettingSource {
27 UserSettings,
29 ProjectSettings,
31 LocalSettings,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum SettingSource {
38 UserSettings,
39 ProjectSettings,
40 LocalSettings,
41 PolicySettings,
42 FlagSettings,
43}
44
45pub fn get_settings_file_path_for_source(source: &EditableSettingSource) -> Option<PathBuf> {
48 match source {
49 EditableSettingSource::UserSettings => {
50 dirs::home_dir().map(|home| home.join(".ai").join("settings.json"))
51 }
52 EditableSettingSource::ProjectSettings => {
53 std::env::current_dir().ok().map(|cwd| cwd.join(".ai").join("settings.json"))
54 }
55 EditableSettingSource::LocalSettings => {
56 std::env::current_dir().ok().map(|cwd| cwd.join(".ai").join("settings.local.json"))
57 }
58 }
59}
60
61pub fn read_settings_file(path: &Path) -> Option<Value> {
63 let content = std::fs::read_to_string(path).ok()?;
64 if content.trim().is_empty() {
65 return Some(Value::Object(serde_json::Map::new()));
66 }
67 serde_json::from_str(&content).ok()
68}
69
70fn deep_merge(base: &Value, overlay: &Value) -> Value {
74 match (base, overlay) {
75 (Value::Object(base_map), Value::Object(overlay_map)) => {
76 let mut result = base_map.clone();
77 for (key, overlay_val) in overlay_map {
78 if overlay_val.is_null() {
79 result.remove(key);
80 } else {
81 let base_val = result.get(key);
82 result.insert(
83 key.clone(),
84 match base_val {
85 Some(b) => deep_merge(b, overlay_val),
86 None => overlay_val.clone(),
87 },
88 );
89 }
90 }
91 Value::Object(result)
92 }
93 (_, Value::Array(overlay_arr)) => overlay.clone(),
94 (_, other) => other.clone(),
95 }
96}
97
98pub fn get_settings_for_source(source: &EditableSettingSource) -> Option<Value> {
101 let path = get_settings_file_path_for_source(source)?;
102 read_settings_file(&path)
103}
104
105pub fn update_settings_for_source(
109 source: &EditableSettingSource,
110 settings: &Value,
111) -> Result<(), String> {
112 let file_path =
113 get_settings_file_path_for_source(source).ok_or("Cannot determine settings path")?;
114
115 if let Some(parent) = file_path.parent() {
117 std::fs::create_dir_all(parent)
118 .map_err(|e| format!("Failed to create settings directory: {}", e))?;
119 }
120
121 let existing = read_settings_file(&file_path).unwrap_or(Value::Object(serde_json::Map::new()));
123
124 let merged = deep_merge(&existing, settings);
126
127 let json_str = serde_json::to_string_pretty(&merged)
129 .map_err(|e| format!("Failed to serialize settings: {}", e))?;
130
131 std::fs::write(&file_path, json_str + "\n")
132 .map_err(|e| format!("Failed to write settings file: {}", e))?;
133
134 Ok(())
135}
136
137pub fn add_permission_rules_to_settings(
140 rules: &[String],
141 behavior: &str, source: &EditableSettingSource,
143) -> Result<(), String> {
144 let existing = get_settings_for_source(source).unwrap_or(Value::Object(serde_json::Map::new()));
145
146 let current_rules: Vec<String> = existing
148 .get("permissions")
149 .and_then(|p| p.get(behavior))
150 .and_then(|r| r.as_array())
151 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
152 .unwrap_or_default();
153
154 let mut all_rules = current_rules;
156 for rule in rules {
157 if !all_rules.contains(rule) {
158 all_rules.push(rule.clone());
159 }
160 }
161
162 let mut perms = serde_json::Map::new();
164 perms.insert(
165 behavior.to_string(),
166 Value::Array(all_rules.into_iter().map(Value::String).collect()),
167 );
168 let settings = Value::Object(
169 [("permissions".to_string(), Value::Object(perms))]
170 .into_iter()
171 .collect::<Map<_, _>>(),
172 );
173
174 update_settings_for_source(source, &settings)
175}
176
177pub fn remove_permission_rules_from_settings(
179 rules: &[String],
180 behavior: &str,
181 source: &EditableSettingSource,
182) -> Result<(), String> {
183 let current_rules: Vec<String> = match get_settings_for_source(source) {
184 Some(s) => s.get("permissions")
185 .and_then(|p| p.get(behavior))
186 .and_then(|r| r.as_array())
187 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
188 .unwrap_or_default(),
189 None => Vec::new(),
190 };
191
192 let rules_to_remove: std::collections::HashSet<&str> = rules.iter().map(|s| s.as_str()).collect();
193 let filtered: Vec<String> =
194 current_rules.into_iter().filter(|r| !rules_to_remove.contains(r.as_str())).collect();
195
196 let mut perms = serde_json::Map::new();
197 perms.insert(
198 behavior.to_string(),
199 Value::Array(filtered.into_iter().map(Value::String).collect()),
200 );
201 let settings = Value::Object(
202 [("permissions".to_string(), Value::Object(perms))]
203 .into_iter()
204 .collect::<Map<_, _>>(),
205 );
206
207 update_settings_for_source(source, &settings)
208}
209
210pub fn replace_permission_rules_in_settings(
212 rules: &[String],
213 behavior: &str,
214 source: &EditableSettingSource,
215) -> Result<(), String> {
216 let mut perms = serde_json::Map::new();
217 perms.insert(
218 behavior.to_string(),
219 Value::Array(rules.iter().map(|r| Value::String(r.clone())).collect()),
220 );
221 let settings = Value::Object(
222 [("permissions".to_string(), Value::Object(perms))]
223 .into_iter()
224 .collect::<Map<_, _>>(),
225 );
226
227 update_settings_for_source(source, &settings)
228}
229
230pub fn add_directories_to_settings(
232 directories: &[String],
233 source: &EditableSettingSource,
234) -> Result<(), String> {
235 let current_dirs: Vec<String> = match get_settings_for_source(source) {
236 Some(s) => s.get("permissions")
237 .and_then(|p| p.get("additionalDirectories"))
238 .and_then(|r| r.as_array())
239 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
240 .unwrap_or_default(),
241 None => Vec::new(),
242 };
243
244 let existing: std::collections::HashSet<String> = current_dirs.iter().cloned().collect();
245 let mut all_dirs = current_dirs;
246 for dir in directories {
247 if !existing.contains(dir) {
248 all_dirs.push(dir.clone());
249 }
250 }
251
252 let mut perms = serde_json::Map::new();
253 perms.insert(
254 "additionalDirectories".to_string(),
255 Value::Array(all_dirs.into_iter().map(Value::String).collect()),
256 );
257 let settings = Value::Object(
258 [("permissions".to_string(), Value::Object(perms))]
259 .into_iter()
260 .collect::<Map<_, _>>(),
261 );
262
263 update_settings_for_source(source, &settings)
264}
265
266pub fn remove_directories_from_settings(
268 directories: &[String],
269 source: &EditableSettingSource,
270) -> Result<(), String> {
271 let current_dirs: Vec<String> = match get_settings_for_source(source) {
272 Some(s) => s.get("permissions")
273 .and_then(|p| p.get("additionalDirectories"))
274 .and_then(|r| r.as_array())
275 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
276 .unwrap_or_default(),
277 None => Vec::new(),
278 };
279
280 let dirs_to_remove: std::collections::HashSet<&str> =
281 directories.iter().map(|s| s.as_str()).collect();
282 let filtered: Vec<String> =
283 current_dirs.into_iter().filter(|d| !dirs_to_remove.contains(d.as_str())).collect();
284
285 let mut perms = serde_json::Map::new();
286 perms.insert(
287 "additionalDirectories".to_string(),
288 Value::Array(filtered.into_iter().map(Value::String).collect()),
289 );
290 let settings = Value::Object(
291 [("permissions".to_string(), Value::Object(perms))]
292 .into_iter()
293 .collect::<Map<_, _>>(),
294 );
295
296 update_settings_for_source(source, &settings)
297}
298
299pub fn set_permission_mode_in_settings(
301 mode: &str,
302 source: &EditableSettingSource,
303) -> Result<(), String> {
304 let mut perms = serde_json::Map::new();
305 perms.insert("defaultMode".to_string(), Value::String(mode.to_string()));
306 let settings = Value::Object(
307 [("permissions".to_string(), Value::Object(perms))]
308 .into_iter()
309 .collect::<Map<_, _>>(),
310 );
311
312 update_settings_for_source(source, &settings)
313}