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