1use serde::Deserialize;
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5#[derive(Debug, Deserialize, Clone)]
6pub struct Settings {
7 pub agents: Vec<AgentConfig>,
8}
9
10#[derive(Debug, Deserialize, Clone)]
11pub struct AgentConfig {
12 pub command: String,
13 #[serde(default)]
14 pub args: Vec<String>,
15 #[serde(default)]
16 pub models: Option<HashMap<String, String>>,
17}
18
19impl Default for Settings {
20 fn default() -> Self {
21 Self {
22 agents: vec![AgentConfig {
23 command: "claude".to_string(),
24 args: vec![],
25 models: None,
26 }],
27 }
28 }
29}
30
31impl Settings {
32 pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
33 let path = Self::settings_path()?;
34 if !path.exists() {
35 return Ok(Settings::default());
36 }
37 let content = std::fs::read_to_string(&path)?;
38 let settings: Settings = serde_json::from_str(&content)?;
39 Ok(settings)
40 }
41
42 fn settings_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
43 let home = dirs::home_dir().ok_or("HOME directory not found")?;
44 Ok(home.join(".seher").join("settings.json"))
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 fn sample_settings_path() -> PathBuf {
53 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
54 .join("examples")
55 .join("settings.json")
56 }
57
58 #[test]
59 fn test_parse_sample_settings() {
60 let content = std::fs::read_to_string(sample_settings_path())
61 .expect("examples/settings.json not found");
62 let settings: Settings = serde_json::from_str(&content).expect("failed to parse settings");
63
64 assert_eq!(settings.agents.len(), 2);
65 }
66
67 #[test]
68 fn test_sample_settings_claude_agent() {
69 let content = std::fs::read_to_string(sample_settings_path()).unwrap();
70 let settings: Settings = serde_json::from_str(&content).unwrap();
71
72 let claude = &settings.agents[0];
73 assert_eq!(claude.command, "claude");
74 assert_eq!(
75 claude.args,
76 [
77 "--permission-mode",
78 "bypassPermissions",
79 "--model",
80 "{model}"
81 ]
82 );
83
84 let models = claude.models.as_ref().expect("models should be present");
85 assert_eq!(models.get("high").map(String::as_str), Some("opus"));
86 assert_eq!(models.get("low").map(String::as_str), Some("sonnet"));
87 }
88
89 #[test]
90 fn test_sample_settings_copilot_agent() {
91 let content = std::fs::read_to_string(sample_settings_path()).unwrap();
92 let settings: Settings = serde_json::from_str(&content).unwrap();
93
94 let copilot = &settings.agents[1];
95 assert_eq!(copilot.command, "copilot");
96 assert_eq!(copilot.args, ["--model", "{model}", "--yolo"]);
97
98 let models = copilot.models.as_ref().expect("models should be present");
99 assert_eq!(
100 models.get("high").map(String::as_str),
101 Some("claude-opus-4.5")
102 );
103 assert_eq!(
104 models.get("low").map(String::as_str),
105 Some("claude-sonnet-4.5")
106 );
107 }
108
109 #[test]
110 fn test_parse_minimal_settings_without_models() {
111 let json = r#"{"agents": [{"command": "claude"}]}"#;
112 let settings: Settings =
113 serde_json::from_str(json).expect("failed to parse minimal settings");
114
115 assert_eq!(settings.agents.len(), 1);
116 assert_eq!(settings.agents[0].command, "claude");
117 assert!(settings.agents[0].args.is_empty());
118 assert!(settings.agents[0].models.is_none());
119 }
120
121 #[test]
122 fn test_parse_settings_with_args_no_models() {
123 let json = r#"{"agents": [{"command": "claude", "args": ["--permission-mode", "bypassPermissions"]}]}"#;
124 let settings: Settings = serde_json::from_str(json).unwrap();
125
126 assert_eq!(
127 settings.agents[0].args,
128 ["--permission-mode", "bypassPermissions"]
129 );
130 assert!(settings.agents[0].models.is_none());
131 }
132}