1use crate::error::{Error, Result};
31use std::path::PathBuf;
32use std::process::Stdio;
33use tokio::process::{Child, Command};
34use tracing::debug;
35
36#[derive(Debug, Clone, Copy)]
38pub enum PermissionMode {
39 AcceptEdits,
40 BypassPermissions,
41 Default,
42 Plan,
43}
44
45impl PermissionMode {
46 fn as_str(&self) -> &'static str {
47 match self {
48 PermissionMode::AcceptEdits => "acceptEdits",
49 PermissionMode::BypassPermissions => "bypassPermissions",
50 PermissionMode::Default => "default",
51 PermissionMode::Plan => "plan",
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
64pub struct ClaudeCliBuilder {
65 command: PathBuf,
66 prompt: Option<String>,
67 debug: Option<String>,
68 verbose: bool,
69 dangerously_skip_permissions: bool,
70 allowed_tools: Vec<String>,
71 disallowed_tools: Vec<String>,
72 mcp_config: Vec<String>,
73 append_system_prompt: Option<String>,
74 permission_mode: Option<PermissionMode>,
75 continue_conversation: bool,
76 resume: Option<String>,
77 model: Option<String>,
78 fallback_model: Option<String>,
79 settings: Option<String>,
80 add_dir: Vec<PathBuf>,
81 ide: bool,
82 strict_mcp_config: bool,
83 session_id: Option<String>,
84}
85
86impl Default for ClaudeCliBuilder {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl ClaudeCliBuilder {
93 pub fn new() -> Self {
95 Self {
96 command: PathBuf::from("claude"),
97 prompt: None,
98 debug: None,
99 verbose: false,
100 dangerously_skip_permissions: false,
101 allowed_tools: Vec::new(),
102 disallowed_tools: Vec::new(),
103 mcp_config: Vec::new(),
104 append_system_prompt: None,
105 permission_mode: None,
106 continue_conversation: false,
107 resume: None,
108 model: None,
109 fallback_model: None,
110 settings: None,
111 add_dir: Vec::new(),
112 ide: false,
113 strict_mcp_config: false,
114 session_id: None,
115 }
116 }
117
118 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
120 self.command = path.into();
121 self
122 }
123
124 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
126 self.prompt = Some(prompt.into());
127 self
128 }
129
130 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
132 self.debug = filter.map(|s| s.into());
133 self
134 }
135
136 pub fn verbose(mut self, verbose: bool) -> Self {
138 self.verbose = verbose;
139 self
140 }
141
142 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
144 self.dangerously_skip_permissions = skip;
145 self
146 }
147
148 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
150 where
151 I: IntoIterator<Item = S>,
152 S: Into<String>,
153 {
154 self.allowed_tools
155 .extend(tools.into_iter().map(|s| s.into()));
156 self
157 }
158
159 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
161 where
162 I: IntoIterator<Item = S>,
163 S: Into<String>,
164 {
165 self.disallowed_tools
166 .extend(tools.into_iter().map(|s| s.into()));
167 self
168 }
169
170 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
172 where
173 I: IntoIterator<Item = S>,
174 S: Into<String>,
175 {
176 self.mcp_config
177 .extend(configs.into_iter().map(|s| s.into()));
178 self
179 }
180
181 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
183 self.append_system_prompt = Some(prompt.into());
184 self
185 }
186
187 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
189 self.permission_mode = Some(mode);
190 self
191 }
192
193 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
195 self.continue_conversation = continue_conv;
196 self
197 }
198
199 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
201 self.resume = session_id.map(|s| s.into());
202 self
203 }
204
205 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
207 self.model = Some(model.into());
208 self
209 }
210
211 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
213 self.fallback_model = Some(model.into());
214 self
215 }
216
217 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
219 self.settings = Some(settings.into());
220 self
221 }
222
223 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
225 where
226 I: IntoIterator<Item = P>,
227 P: Into<PathBuf>,
228 {
229 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
230 self
231 }
232
233 pub fn ide(mut self, ide: bool) -> Self {
235 self.ide = ide;
236 self
237 }
238
239 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
241 self.strict_mcp_config = strict;
242 self
243 }
244
245 pub fn session_id<S: Into<String>>(mut self, id: S) -> Self {
247 self.session_id = Some(id.into());
248 self
249 }
250
251 fn build_args(&self) -> Vec<String> {
253 let mut args = vec![
256 "--print".to_string(),
257 "--verbose".to_string(),
258 "--output-format".to_string(),
259 "stream-json".to_string(),
260 "--input-format".to_string(),
261 "stream-json".to_string(),
262 ];
263
264 if let Some(ref debug) = self.debug {
265 args.push("--debug".to_string());
266 if !debug.is_empty() {
267 args.push(debug.clone());
268 }
269 }
270
271 if self.dangerously_skip_permissions {
272 args.push("--dangerously-skip-permissions".to_string());
273 }
274
275 if !self.allowed_tools.is_empty() {
276 args.push("--allowed-tools".to_string());
277 args.extend(self.allowed_tools.clone());
278 }
279
280 if !self.disallowed_tools.is_empty() {
281 args.push("--disallowed-tools".to_string());
282 args.extend(self.disallowed_tools.clone());
283 }
284
285 if !self.mcp_config.is_empty() {
286 args.push("--mcp-config".to_string());
287 args.extend(self.mcp_config.clone());
288 }
289
290 if let Some(ref prompt) = self.append_system_prompt {
291 args.push("--append-system-prompt".to_string());
292 args.push(prompt.clone());
293 }
294
295 if let Some(ref mode) = self.permission_mode {
296 args.push("--permission-mode".to_string());
297 args.push(mode.as_str().to_string());
298 }
299
300 if self.continue_conversation {
301 args.push("--continue".to_string());
302 }
303
304 if let Some(ref session) = self.resume {
305 args.push("--resume".to_string());
306 args.push(session.clone());
307 }
308
309 if let Some(ref model) = self.model {
310 args.push("--model".to_string());
311 args.push(model.clone());
312 }
313
314 if let Some(ref model) = self.fallback_model {
315 args.push("--fallback-model".to_string());
316 args.push(model.clone());
317 }
318
319 if let Some(ref settings) = self.settings {
320 args.push("--settings".to_string());
321 args.push(settings.clone());
322 }
323
324 if !self.add_dir.is_empty() {
325 args.push("--add-dir".to_string());
326 for dir in &self.add_dir {
327 args.push(dir.to_string_lossy().to_string());
328 }
329 }
330
331 if self.ide {
332 args.push("--ide".to_string());
333 }
334
335 if self.strict_mcp_config {
336 args.push("--strict-mcp-config".to_string());
337 }
338
339 if let Some(ref id) = self.session_id {
340 args.push("--session-id".to_string());
341 args.push(id.clone());
342 }
343
344 if let Some(ref prompt) = self.prompt {
346 args.push(prompt.clone());
347 }
348
349 args
350 }
351
352 pub async fn spawn(self) -> Result<Child> {
354 let args = self.build_args();
355
356 debug!(
358 "[CLI] Executing command: {} {}",
359 self.command.display(),
360 args.join(" ")
361 );
362 eprintln!("Executing: {} {}", self.command.display(), args.join(" "));
363
364 let child = Command::new(&self.command)
365 .args(&args)
366 .stdin(Stdio::piped())
367 .stdout(Stdio::piped())
368 .stderr(Stdio::piped())
369 .spawn()
370 .map_err(Error::Io)?;
371
372 Ok(child)
373 }
374
375 pub fn build_command(self) -> Command {
377 let args = self.build_args();
378 let mut cmd = Command::new(&self.command);
379 cmd.args(&args)
380 .stdin(Stdio::piped())
381 .stdout(Stdio::piped())
382 .stderr(Stdio::piped());
383 cmd
384 }
385
386 pub fn spawn_sync(self) -> std::io::Result<std::process::Child> {
388 let args = self.build_args();
389
390 debug!(
392 "[CLI] Executing sync command: {} {}",
393 self.command.display(),
394 args.join(" ")
395 );
396 eprintln!("Executing: {} {}", self.command.display(), args.join(" "));
397
398 std::process::Command::new(&self.command)
399 .args(&args)
400 .stdin(Stdio::piped())
401 .stdout(Stdio::piped())
402 .stderr(Stdio::piped())
403 .spawn()
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn test_streaming_flags_always_present() {
413 let builder = ClaudeCliBuilder::new();
414 let args = builder.build_args();
415
416 assert!(args.contains(&"--print".to_string()));
418 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
420 assert!(args.contains(&"stream-json".to_string()));
421 assert!(args.contains(&"--input-format".to_string()));
422 }
423
424 #[test]
425 fn test_with_prompt() {
426 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
427 let args = builder.build_args();
428
429 assert_eq!(args.last().unwrap(), "Hello, Claude!");
430 }
431
432 #[test]
433 fn test_with_model() {
434 let builder = ClaudeCliBuilder::new()
435 .model("sonnet")
436 .fallback_model("opus");
437 let args = builder.build_args();
438
439 assert!(args.contains(&"--model".to_string()));
440 assert!(args.contains(&"sonnet".to_string()));
441 assert!(args.contains(&"--fallback-model".to_string()));
442 assert!(args.contains(&"opus".to_string()));
443 }
444
445 #[test]
446 fn test_with_debug() {
447 let builder = ClaudeCliBuilder::new().debug(Some("api"));
448 let args = builder.build_args();
449
450 assert!(args.contains(&"--debug".to_string()));
451 assert!(args.contains(&"api".to_string()));
452 }
453}