1use crate::error::{Error, Result};
7use std::path::PathBuf;
8use std::process::Stdio;
9use tokio::process::{Child, Command};
10use tracing::debug;
11
12#[derive(Debug, Clone, Copy)]
14pub enum PermissionMode {
15 AcceptEdits,
16 BypassPermissions,
17 Default,
18 Plan,
19}
20
21impl PermissionMode {
22 fn as_str(&self) -> &'static str {
23 match self {
24 PermissionMode::AcceptEdits => "acceptEdits",
25 PermissionMode::BypassPermissions => "bypassPermissions",
26 PermissionMode::Default => "default",
27 PermissionMode::Plan => "plan",
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
40pub struct ClaudeCliBuilder {
41 command: PathBuf,
42 prompt: Option<String>,
43 debug: Option<String>,
44 verbose: bool,
45 dangerously_skip_permissions: bool,
46 allowed_tools: Vec<String>,
47 disallowed_tools: Vec<String>,
48 mcp_config: Vec<String>,
49 append_system_prompt: Option<String>,
50 permission_mode: Option<PermissionMode>,
51 continue_conversation: bool,
52 resume: Option<String>,
53 model: Option<String>,
54 fallback_model: Option<String>,
55 settings: Option<String>,
56 add_dir: Vec<PathBuf>,
57 ide: bool,
58 strict_mcp_config: bool,
59 session_id: Option<String>,
60}
61
62impl Default for ClaudeCliBuilder {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl ClaudeCliBuilder {
69 pub fn new() -> Self {
71 Self {
72 command: PathBuf::from("claude"),
73 prompt: None,
74 debug: None,
75 verbose: false,
76 dangerously_skip_permissions: false,
77 allowed_tools: Vec::new(),
78 disallowed_tools: Vec::new(),
79 mcp_config: Vec::new(),
80 append_system_prompt: None,
81 permission_mode: None,
82 continue_conversation: false,
83 resume: None,
84 model: None,
85 fallback_model: None,
86 settings: None,
87 add_dir: Vec::new(),
88 ide: false,
89 strict_mcp_config: false,
90 session_id: None,
91 }
92 }
93
94 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
96 self.command = path.into();
97 self
98 }
99
100 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
102 self.prompt = Some(prompt.into());
103 self
104 }
105
106 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
108 self.debug = filter.map(|s| s.into());
109 self
110 }
111
112 pub fn verbose(mut self, verbose: bool) -> Self {
114 self.verbose = verbose;
115 self
116 }
117
118 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
120 self.dangerously_skip_permissions = skip;
121 self
122 }
123
124 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
126 where
127 I: IntoIterator<Item = S>,
128 S: Into<String>,
129 {
130 self.allowed_tools
131 .extend(tools.into_iter().map(|s| s.into()));
132 self
133 }
134
135 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
137 where
138 I: IntoIterator<Item = S>,
139 S: Into<String>,
140 {
141 self.disallowed_tools
142 .extend(tools.into_iter().map(|s| s.into()));
143 self
144 }
145
146 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
148 where
149 I: IntoIterator<Item = S>,
150 S: Into<String>,
151 {
152 self.mcp_config
153 .extend(configs.into_iter().map(|s| s.into()));
154 self
155 }
156
157 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
159 self.append_system_prompt = Some(prompt.into());
160 self
161 }
162
163 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
165 self.permission_mode = Some(mode);
166 self
167 }
168
169 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
171 self.continue_conversation = continue_conv;
172 self
173 }
174
175 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
177 self.resume = session_id.map(|s| s.into());
178 self
179 }
180
181 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
183 self.model = Some(model.into());
184 self
185 }
186
187 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
189 self.fallback_model = Some(model.into());
190 self
191 }
192
193 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
195 self.settings = Some(settings.into());
196 self
197 }
198
199 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
201 where
202 I: IntoIterator<Item = P>,
203 P: Into<PathBuf>,
204 {
205 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
206 self
207 }
208
209 pub fn ide(mut self, ide: bool) -> Self {
211 self.ide = ide;
212 self
213 }
214
215 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
217 self.strict_mcp_config = strict;
218 self
219 }
220
221 pub fn session_id<S: Into<String>>(mut self, id: S) -> Self {
223 self.session_id = Some(id.into());
224 self
225 }
226
227 fn build_args(&self) -> Vec<String> {
229 let mut args = vec![
232 "--print".to_string(),
233 "--verbose".to_string(),
234 "--output-format".to_string(),
235 "stream-json".to_string(),
236 "--input-format".to_string(),
237 "stream-json".to_string(),
238 ];
239
240 if let Some(ref debug) = self.debug {
241 args.push("--debug".to_string());
242 if !debug.is_empty() {
243 args.push(debug.clone());
244 }
245 }
246
247 if self.dangerously_skip_permissions {
248 args.push("--dangerously-skip-permissions".to_string());
249 }
250
251 if !self.allowed_tools.is_empty() {
252 args.push("--allowed-tools".to_string());
253 args.extend(self.allowed_tools.clone());
254 }
255
256 if !self.disallowed_tools.is_empty() {
257 args.push("--disallowed-tools".to_string());
258 args.extend(self.disallowed_tools.clone());
259 }
260
261 if !self.mcp_config.is_empty() {
262 args.push("--mcp-config".to_string());
263 args.extend(self.mcp_config.clone());
264 }
265
266 if let Some(ref prompt) = self.append_system_prompt {
267 args.push("--append-system-prompt".to_string());
268 args.push(prompt.clone());
269 }
270
271 if let Some(ref mode) = self.permission_mode {
272 args.push("--permission-mode".to_string());
273 args.push(mode.as_str().to_string());
274 }
275
276 if self.continue_conversation {
277 args.push("--continue".to_string());
278 }
279
280 if let Some(ref session) = self.resume {
281 args.push("--resume".to_string());
282 args.push(session.clone());
283 }
284
285 if let Some(ref model) = self.model {
286 args.push("--model".to_string());
287 args.push(model.clone());
288 }
289
290 if let Some(ref model) = self.fallback_model {
291 args.push("--fallback-model".to_string());
292 args.push(model.clone());
293 }
294
295 if let Some(ref settings) = self.settings {
296 args.push("--settings".to_string());
297 args.push(settings.clone());
298 }
299
300 if !self.add_dir.is_empty() {
301 args.push("--add-dir".to_string());
302 for dir in &self.add_dir {
303 args.push(dir.to_string_lossy().to_string());
304 }
305 }
306
307 if self.ide {
308 args.push("--ide".to_string());
309 }
310
311 if self.strict_mcp_config {
312 args.push("--strict-mcp-config".to_string());
313 }
314
315 if let Some(ref id) = self.session_id {
316 args.push("--session-id".to_string());
317 args.push(id.clone());
318 }
319
320 if let Some(ref prompt) = self.prompt {
322 args.push(prompt.clone());
323 }
324
325 args
326 }
327
328 pub async fn spawn(self) -> Result<Child> {
330 let args = self.build_args();
331
332 debug!(
334 "[CLI] Executing command: {} {}",
335 self.command.display(),
336 args.join(" ")
337 );
338 eprintln!("Executing: {} {}", self.command.display(), args.join(" "));
339
340 let child = Command::new(&self.command)
341 .args(&args)
342 .stdin(Stdio::piped())
343 .stdout(Stdio::piped())
344 .stderr(Stdio::piped())
345 .spawn()
346 .map_err(Error::Io)?;
347
348 Ok(child)
349 }
350
351 pub fn build_command(self) -> Command {
353 let args = self.build_args();
354 let mut cmd = Command::new(&self.command);
355 cmd.args(&args)
356 .stdin(Stdio::piped())
357 .stdout(Stdio::piped())
358 .stderr(Stdio::piped());
359 cmd
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_streaming_flags_always_present() {
369 let builder = ClaudeCliBuilder::new();
370 let args = builder.build_args();
371
372 assert!(args.contains(&"--print".to_string()));
374 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
376 assert!(args.contains(&"stream-json".to_string()));
377 assert!(args.contains(&"--input-format".to_string()));
378 }
379
380 #[test]
381 fn test_with_prompt() {
382 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
383 let args = builder.build_args();
384
385 assert_eq!(args.last().unwrap(), "Hello, Claude!");
386 }
387
388 #[test]
389 fn test_with_model() {
390 let builder = ClaudeCliBuilder::new()
391 .model("sonnet")
392 .fallback_model("opus");
393 let args = builder.build_args();
394
395 assert!(args.contains(&"--model".to_string()));
396 assert!(args.contains(&"sonnet".to_string()));
397 assert!(args.contains(&"--fallback-model".to_string()));
398 assert!(args.contains(&"opus".to_string()));
399 }
400
401 #[test]
402 fn test_with_debug() {
403 let builder = ClaudeCliBuilder::new().debug(Some("api"));
404 let args = builder.build_args();
405
406 assert!(args.contains(&"--debug".to_string()));
407 assert!(args.contains(&"api".to_string()));
408 }
409}