stynx_code_config/application/
load_config.rs1use crate::domain::config::Settings;
2use std::path::Path;
3
4pub fn load_config() -> Settings {
5 let global = load_file(&global_settings_path());
6 let project = load_file(&project_settings_path());
7 merge(global, project)
8}
9
10fn global_settings_path() -> std::path::PathBuf {
11 let home = super::platform::home_dir()
12 .unwrap_or_else(|| std::path::PathBuf::from("."));
13 home.join(".stynx").join("settings.json")
14}
15
16fn project_settings_path() -> std::path::PathBuf {
17 std::env::current_dir()
18 .unwrap_or_else(|_| std::path::PathBuf::from("."))
19 .join(".stynx")
20 .join("settings.json")
21}
22
23fn load_file(path: &Path) -> Settings {
24 match std::fs::read_to_string(path) {
25 Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
26 Err(_) => Settings::default(),
27 }
28}
29
30fn merge(global: Settings, project: Settings) -> Settings {
31 let mut allow = global.permissions.allow;
32 allow.extend(project.permissions.allow);
33
34 let mut deny = global.permissions.deny;
35 deny.extend(project.permissions.deny);
36
37 let mut pre_tool_use = global.hooks.pre_tool_use;
38 pre_tool_use.extend(project.hooks.pre_tool_use);
39 let mut post_tool_use = global.hooks.post_tool_use;
40 post_tool_use.extend(project.hooks.post_tool_use);
41 let mut stop = global.hooks.stop;
42 stop.extend(project.hooks.stop);
43 let mut session_start = global.hooks.session_start;
44 session_start.extend(project.hooks.session_start);
45
46 let mut interns = global.interns;
47 for p in project.interns {
48 if let Some(slot) = interns.iter_mut().find(|i| i.name == p.name) {
49 *slot = p;
50 } else {
51 interns.push(p);
52 }
53 }
54
55 Settings {
56 permissions: crate::domain::config::PermissionSettings { allow, deny },
57 model: project.model.or(global.model),
58 hooks: crate::domain::config::HooksConfig { pre_tool_use, post_tool_use, stop, session_start },
59 max_turns: project.max_turns.or(global.max_turns),
60 max_tokens: project.max_tokens.or(global.max_tokens),
61 effort: project.effort.or(global.effort),
62 commit_attribution: project.commit_attribution || global.commit_attribution,
63 interns,
64 main_provider: project.main_provider.or(global.main_provider),
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::domain::config::{HooksConfig, PermissionSettings, Settings};
72
73 #[test]
74 fn test_merge_concatenates_permissions() {
75 let global = Settings {
76 permissions: PermissionSettings {
77 allow: vec!["read".into(), "glob".into()],
78 deny: vec!["bash(rm -rf *)".into()],
79 },
80 model: Some("global-model".into()),
81 ..Default::default()
82 };
83 let project = Settings {
84 permissions: PermissionSettings {
85 allow: vec!["grep".into()],
86 deny: vec![],
87 },
88 model: None,
89 ..Default::default()
90 };
91 let merged = merge(global, project);
92 assert_eq!(merged.permissions.allow, vec!["read", "glob", "grep"]);
93 assert_eq!(merged.permissions.deny, vec!["bash(rm -rf *)"]);
94 assert_eq!(merged.model, Some("global-model".into()));
95 }
96
97 #[test]
98 fn test_merge_project_model_overrides() {
99 let global = Settings {
100 model: Some("global-model".into()),
101 ..Default::default()
102 };
103 let project = Settings {
104 model: Some("project-model".into()),
105 ..Default::default()
106 };
107 let merged = merge(global, project);
108 assert_eq!(merged.model, Some("project-model".into()));
109 }
110
111 #[test]
112 fn test_merge_effort_project_overrides_global() {
113 let global = Settings {
114 effort: Some("medium".into()),
115 ..Default::default()
116 };
117 let project = Settings {
118 effort: Some("high".into()),
119 ..Default::default()
120 };
121 let merged = merge(global, project);
122 assert_eq!(merged.effort, Some("high".into()));
123 }
124
125 #[test]
126 fn test_merge_effort_falls_back_to_global() {
127 let global = Settings {
128 effort: Some("low".into()),
129 ..Default::default()
130 };
131 let project = Settings::default();
132 let merged = merge(global, project);
133 assert_eq!(merged.effort, Some("low".into()));
134 }
135
136 #[test]
137 fn test_merge_effort_none_when_both_unset() {
138 let merged = merge(Settings::default(), Settings::default());
139 assert_eq!(merged.effort, None);
140 }
141}