Skip to main content

bn/commands/
config_cmd.rs

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