Skip to main content

stynx_code_config/application/
load_config.rs

1use 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}