1use crate::error::{Error, Result};
38use std::path::PathBuf;
39use std::process::Stdio;
40use tokio::process::{Child, Command};
41use tracing::debug;
42use uuid::Uuid;
43
44#[derive(Debug, Clone, Copy)]
46pub enum PermissionMode {
47 AcceptEdits,
48 BypassPermissions,
49 Default,
50 Plan,
51}
52
53impl PermissionMode {
54 fn as_str(&self) -> &'static str {
55 match self {
56 PermissionMode::AcceptEdits => "acceptEdits",
57 PermissionMode::BypassPermissions => "bypassPermissions",
58 PermissionMode::Default => "default",
59 PermissionMode::Plan => "plan",
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
72pub struct ClaudeCliBuilder {
73 command: PathBuf,
74 prompt: Option<String>,
75 debug: Option<String>,
76 verbose: bool,
77 dangerously_skip_permissions: bool,
78 allowed_tools: Vec<String>,
79 disallowed_tools: Vec<String>,
80 mcp_config: Vec<String>,
81 append_system_prompt: Option<String>,
82 permission_mode: Option<PermissionMode>,
83 continue_conversation: bool,
84 resume: Option<String>,
85 model: Option<String>,
86 fallback_model: Option<String>,
87 settings: Option<String>,
88 add_dir: Vec<PathBuf>,
89 ide: bool,
90 strict_mcp_config: bool,
91 session_id: Option<Uuid>,
92 oauth_token: Option<String>,
93 api_key: Option<String>,
94}
95
96impl Default for ClaudeCliBuilder {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl ClaudeCliBuilder {
103 pub fn new() -> Self {
105 Self {
106 command: PathBuf::from("claude"),
107 prompt: None,
108 debug: None,
109 verbose: false,
110 dangerously_skip_permissions: false,
111 allowed_tools: Vec::new(),
112 disallowed_tools: Vec::new(),
113 mcp_config: Vec::new(),
114 append_system_prompt: None,
115 permission_mode: None,
116 continue_conversation: false,
117 resume: None,
118 model: None,
119 fallback_model: None,
120 settings: None,
121 add_dir: Vec::new(),
122 ide: false,
123 strict_mcp_config: false,
124 session_id: None,
125 oauth_token: None,
126 api_key: None,
127 }
128 }
129
130 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
132 self.command = path.into();
133 self
134 }
135
136 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
138 self.prompt = Some(prompt.into());
139 self
140 }
141
142 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
144 self.debug = filter.map(|s| s.into());
145 self
146 }
147
148 pub fn verbose(mut self, verbose: bool) -> Self {
150 self.verbose = verbose;
151 self
152 }
153
154 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
156 self.dangerously_skip_permissions = skip;
157 self
158 }
159
160 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
162 where
163 I: IntoIterator<Item = S>,
164 S: Into<String>,
165 {
166 self.allowed_tools
167 .extend(tools.into_iter().map(|s| s.into()));
168 self
169 }
170
171 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
173 where
174 I: IntoIterator<Item = S>,
175 S: Into<String>,
176 {
177 self.disallowed_tools
178 .extend(tools.into_iter().map(|s| s.into()));
179 self
180 }
181
182 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
184 where
185 I: IntoIterator<Item = S>,
186 S: Into<String>,
187 {
188 self.mcp_config
189 .extend(configs.into_iter().map(|s| s.into()));
190 self
191 }
192
193 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
195 self.append_system_prompt = Some(prompt.into());
196 self
197 }
198
199 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
201 self.permission_mode = Some(mode);
202 self
203 }
204
205 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
207 self.continue_conversation = continue_conv;
208 self
209 }
210
211 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
213 self.resume = session_id.map(|s| s.into());
214 self
215 }
216
217 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
219 self.model = Some(model.into());
220 self
221 }
222
223 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
225 self.fallback_model = Some(model.into());
226 self
227 }
228
229 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
231 self.settings = Some(settings.into());
232 self
233 }
234
235 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
237 where
238 I: IntoIterator<Item = P>,
239 P: Into<PathBuf>,
240 {
241 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
242 self
243 }
244
245 pub fn ide(mut self, ide: bool) -> Self {
247 self.ide = ide;
248 self
249 }
250
251 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
253 self.strict_mcp_config = strict;
254 self
255 }
256
257 pub fn session_id(mut self, id: Uuid) -> Self {
259 self.session_id = Some(id);
260 self
261 }
262
263 pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
265 let token_str = token.into();
266 if !token_str.starts_with("sk-ant-oat") {
267 eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
268 }
269 self.oauth_token = Some(token_str);
270 self
271 }
272
273 pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
275 let key_str = key.into();
276 if !key_str.starts_with("sk-ant-api") {
277 eprintln!("Warning: API key should start with 'sk-ant-api'");
278 }
279 self.api_key = Some(key_str);
280 self
281 }
282
283 fn build_args(&self) -> Vec<String> {
285 let mut args = vec![
288 "--print".to_string(),
289 "--verbose".to_string(),
290 "--output-format".to_string(),
291 "stream-json".to_string(),
292 "--input-format".to_string(),
293 "stream-json".to_string(),
294 ];
295
296 if let Some(ref debug) = self.debug {
297 args.push("--debug".to_string());
298 if !debug.is_empty() {
299 args.push(debug.clone());
300 }
301 }
302
303 if self.dangerously_skip_permissions {
304 args.push("--dangerously-skip-permissions".to_string());
305 }
306
307 if !self.allowed_tools.is_empty() {
308 args.push("--allowed-tools".to_string());
309 args.extend(self.allowed_tools.clone());
310 }
311
312 if !self.disallowed_tools.is_empty() {
313 args.push("--disallowed-tools".to_string());
314 args.extend(self.disallowed_tools.clone());
315 }
316
317 if !self.mcp_config.is_empty() {
318 args.push("--mcp-config".to_string());
319 args.extend(self.mcp_config.clone());
320 }
321
322 if let Some(ref prompt) = self.append_system_prompt {
323 args.push("--append-system-prompt".to_string());
324 args.push(prompt.clone());
325 }
326
327 if let Some(ref mode) = self.permission_mode {
328 args.push("--permission-mode".to_string());
329 args.push(mode.as_str().to_string());
330 }
331
332 if self.continue_conversation {
333 args.push("--continue".to_string());
334 }
335
336 if let Some(ref session) = self.resume {
337 args.push("--resume".to_string());
338 args.push(session.clone());
339 }
340
341 if let Some(ref model) = self.model {
342 args.push("--model".to_string());
343 args.push(model.clone());
344 }
345
346 if let Some(ref model) = self.fallback_model {
347 args.push("--fallback-model".to_string());
348 args.push(model.clone());
349 }
350
351 if let Some(ref settings) = self.settings {
352 args.push("--settings".to_string());
353 args.push(settings.clone());
354 }
355
356 if !self.add_dir.is_empty() {
357 args.push("--add-dir".to_string());
358 for dir in &self.add_dir {
359 args.push(dir.to_string_lossy().to_string());
360 }
361 }
362
363 if self.ide {
364 args.push("--ide".to_string());
365 }
366
367 if self.strict_mcp_config {
368 args.push("--strict-mcp-config".to_string());
369 }
370
371 args.push("--session-id".to_string());
373 let session_uuid = self.session_id.unwrap_or_else(|| {
374 let uuid = Uuid::new_v4();
375 debug!("[CLI] Generated session UUID: {}", uuid);
376 uuid
377 });
378 args.push(session_uuid.to_string());
379
380 if let Some(ref prompt) = self.prompt {
382 args.push(prompt.clone());
383 }
384
385 args
386 }
387
388 pub async fn spawn(self) -> Result<Child> {
390 let args = self.build_args();
391
392 debug!(
394 "[CLI] Executing command: {} {}",
395 self.command.display(),
396 args.join(" ")
397 );
398 eprintln!("Executing: {} {}", self.command.display(), args.join(" "));
399
400 let mut cmd = Command::new(&self.command);
401 cmd.args(&args)
402 .stdin(Stdio::piped())
403 .stdout(Stdio::piped())
404 .stderr(Stdio::piped());
405
406 if let Some(ref token) = self.oauth_token {
408 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
409 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
410 }
411
412 if let Some(ref key) = self.api_key {
414 cmd.env("ANTHROPIC_API_KEY", key);
415 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
416 }
417
418 let child = cmd.spawn().map_err(Error::Io)?;
419
420 Ok(child)
421 }
422
423 pub fn build_command(self) -> Command {
425 let args = self.build_args();
426 let mut cmd = Command::new(&self.command);
427 cmd.args(&args)
428 .stdin(Stdio::piped())
429 .stdout(Stdio::piped())
430 .stderr(Stdio::piped());
431
432 if let Some(ref token) = self.oauth_token {
434 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
435 }
436
437 if let Some(ref key) = self.api_key {
439 cmd.env("ANTHROPIC_API_KEY", key);
440 }
441
442 cmd
443 }
444
445 pub fn spawn_sync(self) -> std::io::Result<std::process::Child> {
447 let args = self.build_args();
448
449 debug!(
451 "[CLI] Executing sync command: {} {}",
452 self.command.display(),
453 args.join(" ")
454 );
455 eprintln!("Executing: {} {}", self.command.display(), args.join(" "));
456
457 let mut cmd = std::process::Command::new(&self.command);
458 cmd.args(&args)
459 .stdin(Stdio::piped())
460 .stdout(Stdio::piped())
461 .stderr(Stdio::piped());
462
463 if let Some(ref token) = self.oauth_token {
465 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
466 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
467 }
468
469 if let Some(ref key) = self.api_key {
471 cmd.env("ANTHROPIC_API_KEY", key);
472 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
473 }
474
475 cmd.spawn()
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn test_streaming_flags_always_present() {
485 let builder = ClaudeCliBuilder::new();
486 let args = builder.build_args();
487
488 assert!(args.contains(&"--print".to_string()));
490 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
492 assert!(args.contains(&"stream-json".to_string()));
493 assert!(args.contains(&"--input-format".to_string()));
494 }
495
496 #[test]
497 fn test_with_prompt() {
498 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
499 let args = builder.build_args();
500
501 assert_eq!(args.last().unwrap(), "Hello, Claude!");
502 }
503
504 #[test]
505 fn test_with_model() {
506 let builder = ClaudeCliBuilder::new()
507 .model("sonnet")
508 .fallback_model("opus");
509 let args = builder.build_args();
510
511 assert!(args.contains(&"--model".to_string()));
512 assert!(args.contains(&"sonnet".to_string()));
513 assert!(args.contains(&"--fallback-model".to_string()));
514 assert!(args.contains(&"opus".to_string()));
515 }
516
517 #[test]
518 fn test_with_debug() {
519 let builder = ClaudeCliBuilder::new().debug(Some("api"));
520 let args = builder.build_args();
521
522 assert!(args.contains(&"--debug".to_string()));
523 assert!(args.contains(&"api".to_string()));
524 }
525
526 #[test]
527 fn test_with_oauth_token() {
528 let valid_token = "sk-ant-oat-123456789";
529 let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
530
531 let args = builder.clone().build_args();
533 assert!(!args.contains(&valid_token.to_string()));
534
535 assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
537 }
538
539 #[test]
540 fn test_oauth_token_validation() {
541 let invalid_token = "invalid-token-123";
543 let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
544 assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
545 }
546
547 #[test]
548 fn test_with_api_key() {
549 let valid_key = "sk-ant-api-987654321";
550 let builder = ClaudeCliBuilder::new().api_key(valid_key);
551
552 let args = builder.clone().build_args();
554 assert!(!args.contains(&valid_key.to_string()));
555
556 assert_eq!(builder.api_key, Some(valid_key.to_string()));
558 }
559
560 #[test]
561 fn test_api_key_validation() {
562 let invalid_key = "invalid-api-key";
564 let builder = ClaudeCliBuilder::new().api_key(invalid_key);
565 assert_eq!(builder.api_key, Some(invalid_key.to_string()));
566 }
567
568 #[test]
569 fn test_both_auth_methods() {
570 let oauth = "sk-ant-oat-123";
571 let api_key = "sk-ant-api-456";
572 let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
573
574 assert_eq!(builder.oauth_token, Some(oauth.to_string()));
575 assert_eq!(builder.api_key, Some(api_key.to_string()));
576 }
577}