bn/commands/
config_cmd.rs1use std::path::Path;
2
3use anyhow::{anyhow, Result};
4
5use crate::config::Config;
6
7pub 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
31pub 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 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}