Skip to main content

bn/commands/
config_cmd.rs

1use std::path::Path;
2
3use anyhow::{anyhow, Result};
4
5use crate::config::{Config, GlobalConfig};
6
7/// Get a configuration value by key
8pub fn cmd_config_get(beans_dir: &Path, key: &str) -> Result<()> {
9    let config = Config::load(beans_dir)?;
10
11    let value = match key {
12        "project" => config.project,
13        "next_id" => config.next_id.to_string(),
14        "auto_close_parent" => config.auto_close_parent.to_string(),
15        "run" => config.run.unwrap_or_default(),
16        "plan" => config.plan.unwrap_or_default(),
17        "max_concurrent" => config.max_concurrent.to_string(),
18        "poll_interval" => config.poll_interval.to_string(),
19        "rules_file" => config.rules_file.unwrap_or_else(|| "RULES.md".to_string()),
20        "on_close" => config.on_close.unwrap_or_default(),
21        "on_fail" => config.on_fail.unwrap_or_default(),
22        "post_plan" => config.post_plan.unwrap_or_default(),
23        "user" => {
24            if let Some(user) = config.user {
25                user
26            } else if let Ok(global) = GlobalConfig::load() {
27                global.user.unwrap_or_default()
28            } else {
29                String::new()
30            }
31        }
32        "user.email" => {
33            if let Some(email) = config.user_email {
34                email
35            } else if let Ok(global) = GlobalConfig::load() {
36                global.user_email.unwrap_or_default()
37            } else {
38                String::new()
39            }
40        }
41        _ => return Err(anyhow!("Unknown config key: {}", key)),
42    };
43
44    println!("{}", value);
45    Ok(())
46}
47
48/// Set a configuration value by key
49pub fn cmd_config_set(beans_dir: &Path, key: &str, value: &str) -> Result<()> {
50    let mut config = Config::load(beans_dir)?;
51
52    match key {
53        "project" => {
54            config.project = value.to_string();
55        }
56        "next_id" => {
57            config.next_id = value
58                .parse()
59                .map_err(|_| anyhow!("Invalid value for next_id: {}", value))?;
60        }
61        "auto_close_parent" => {
62            config.auto_close_parent = value.parse().map_err(|_| {
63                anyhow!(
64                    "Invalid value for auto_close_parent: {} (expected true/false)",
65                    value
66                )
67            })?;
68        }
69        "run" => {
70            if value.is_empty() || value == "none" || value == "unset" {
71                config.run = None;
72            } else {
73                config.run = Some(value.to_string());
74            }
75        }
76        "plan" => {
77            if value.is_empty() || value == "none" || value == "unset" {
78                config.plan = None;
79            } else {
80                config.plan = Some(value.to_string());
81            }
82        }
83        "max_concurrent" => {
84            config.max_concurrent = value.parse().map_err(|_| {
85                anyhow!(
86                    "Invalid value for max_concurrent: {} (expected positive integer)",
87                    value
88                )
89            })?;
90        }
91        "poll_interval" => {
92            config.poll_interval = value.parse().map_err(|_| {
93                anyhow!(
94                    "Invalid value for poll_interval: {} (expected positive integer)",
95                    value
96                )
97            })?;
98        }
99        "rules_file" => {
100            if value.is_empty() || value == "none" || value == "unset" {
101                config.rules_file = None;
102            } else {
103                config.rules_file = Some(value.to_string());
104            }
105        }
106        "on_close" => {
107            if value.is_empty() || value == "none" || value == "unset" {
108                config.on_close = None;
109            } else {
110                config.on_close = Some(value.to_string());
111            }
112        }
113        "on_fail" => {
114            if value.is_empty() || value == "none" || value == "unset" {
115                config.on_fail = None;
116            } else {
117                config.on_fail = Some(value.to_string());
118            }
119        }
120        "post_plan" => {
121            if value.is_empty() || value == "none" || value == "unset" {
122                config.post_plan = None;
123            } else {
124                config.post_plan = Some(value.to_string());
125            }
126        }
127        "user" => {
128            if value.is_empty() || value == "none" || value == "unset" {
129                config.user = None;
130            } else {
131                config.user = Some(value.to_string());
132            }
133        }
134        "user.email" => {
135            if value.is_empty() || value == "none" || value == "unset" {
136                config.user_email = None;
137            } else {
138                config.user_email = Some(value.to_string());
139            }
140        }
141        _ => return Err(anyhow!("Unknown config key: {}", key)),
142    }
143
144    config.save(beans_dir)?;
145    println!("Set {} = {}", key, value);
146    Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::fs;
153
154    fn setup_test_dir() -> tempfile::TempDir {
155        let dir = tempfile::tempdir().unwrap();
156        fs::write(
157            dir.path().join("config.yaml"),
158            "project: test\nnext_id: 1\nauto_close_parent: true\n",
159        )
160        .unwrap();
161        dir
162    }
163
164    #[test]
165    fn get_unknown_key_returns_error() {
166        let dir = setup_test_dir();
167        let result = cmd_config_get(dir.path(), "unknown_key");
168        assert!(result.is_err());
169        assert!(result
170            .unwrap_err()
171            .to_string()
172            .contains("Unknown config key"));
173    }
174
175    #[test]
176    fn set_unknown_key_returns_error() {
177        let dir = setup_test_dir();
178        let result = cmd_config_set(dir.path(), "unknown_key", "value");
179        assert!(result.is_err());
180        assert!(result
181            .unwrap_err()
182            .to_string()
183            .contains("Unknown config key"));
184    }
185
186    #[test]
187    fn get_run_returns_empty_when_unset() {
188        let dir = setup_test_dir();
189        let result = cmd_config_get(dir.path(), "run");
190        assert!(result.is_ok());
191    }
192
193    #[test]
194    fn set_run_stores_command_template() {
195        let dir = setup_test_dir();
196        cmd_config_set(dir.path(), "run", "claude -p 'implement bean {id}'").unwrap();
197
198        let config = Config::load(dir.path()).unwrap();
199        assert_eq!(
200            config.run,
201            Some("claude -p 'implement bean {id}'".to_string())
202        );
203    }
204
205    #[test]
206    fn set_run_to_none_clears_it() {
207        let dir = setup_test_dir();
208        cmd_config_set(dir.path(), "run", "some command").unwrap();
209        cmd_config_set(dir.path(), "run", "none").unwrap();
210
211        let config = Config::load(dir.path()).unwrap();
212        assert_eq!(config.run, None);
213    }
214
215    #[test]
216    fn set_run_to_empty_clears_it() {
217        let dir = setup_test_dir();
218        cmd_config_set(dir.path(), "run", "some command").unwrap();
219        cmd_config_set(dir.path(), "run", "").unwrap();
220
221        let config = Config::load(dir.path()).unwrap();
222        assert_eq!(config.run, None);
223    }
224}