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