1use crate::error::{Error, Result};
13use log::debug;
14use std::path::PathBuf;
15use std::process::Stdio;
16use uuid::Uuid;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PermissionMode {
21 AcceptEdits,
22 BypassPermissions,
23 Default,
24 Delegate,
25 DontAsk,
26 Plan,
27}
28
29impl PermissionMode {
30 pub fn as_str(&self) -> &'static str {
32 match self {
33 PermissionMode::AcceptEdits => "acceptEdits",
34 PermissionMode::BypassPermissions => "bypassPermissions",
35 PermissionMode::Default => "default",
36 PermissionMode::Delegate => "delegate",
37 PermissionMode::DontAsk => "dontAsk",
38 PermissionMode::Plan => "plan",
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum InputFormat {
46 Text,
47 StreamJson,
48}
49
50impl InputFormat {
51 pub fn as_str(&self) -> &'static str {
53 match self {
54 InputFormat::Text => "text",
55 InputFormat::StreamJson => "stream-json",
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum OutputFormat {
63 Text,
64 Json,
65 StreamJson,
66}
67
68impl OutputFormat {
69 pub fn as_str(&self) -> &'static str {
71 match self {
72 OutputFormat::Text => "text",
73 OutputFormat::Json => "json",
74 OutputFormat::StreamJson => "stream-json",
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
96pub enum CliFlag {
97 AddDir(Vec<PathBuf>),
99 Agent(String),
101 Agents(String),
104 AllowDangerouslySkipPermissions,
106 AllowedTools(Vec<String>),
108 AppendSystemPrompt(String),
110 Betas(Vec<String>),
112 Chrome,
114 Continue,
116 DangerouslySkipPermissions,
118 Debug(Option<String>),
120 DebugFile(PathBuf),
122 DisableSlashCommands,
124 DisallowedTools(Vec<String>),
126 FallbackModel(String),
128 File(Vec<String>),
130 ForkSession,
132 FromPr(Option<String>),
134 IncludePartialMessages,
136 InputFormat(InputFormat),
138 JsonSchema(String),
140 MaxBudgetUsd(f64),
142 MaxThinkingTokens(u32),
144 McpConfig(Vec<String>),
146 McpDebug,
148 Model(String),
150 NoChrome,
152 NoSessionPersistence,
154 OutputFormat(OutputFormat),
156 PermissionMode(PermissionMode),
158 PermissionPromptTool(String),
160 PluginDir(Vec<PathBuf>),
162 Print,
164 ReplayUserMessages,
166 Resume(Option<String>),
168 SessionId(String),
170 SettingSources(String),
172 Settings(String),
174 StrictMcpConfig,
176 SystemPrompt(String),
178 Tools(Vec<String>),
180 Verbose,
182}
183
184impl CliFlag {
185 pub fn as_flag(&self) -> &'static str {
187 match self {
188 CliFlag::AddDir(_) => "--add-dir",
189 CliFlag::Agent(_) => "--agent",
190 CliFlag::Agents(_) => "--agents",
191 CliFlag::AllowDangerouslySkipPermissions => "--allow-dangerously-skip-permissions",
192 CliFlag::AllowedTools(_) => "--allowed-tools",
193 CliFlag::AppendSystemPrompt(_) => "--append-system-prompt",
194 CliFlag::Betas(_) => "--betas",
195 CliFlag::Chrome => "--chrome",
196 CliFlag::Continue => "--continue",
197 CliFlag::DangerouslySkipPermissions => "--dangerously-skip-permissions",
198 CliFlag::Debug(_) => "--debug",
199 CliFlag::DebugFile(_) => "--debug-file",
200 CliFlag::DisableSlashCommands => "--disable-slash-commands",
201 CliFlag::DisallowedTools(_) => "--disallowed-tools",
202 CliFlag::FallbackModel(_) => "--fallback-model",
203 CliFlag::File(_) => "--file",
204 CliFlag::ForkSession => "--fork-session",
205 CliFlag::FromPr(_) => "--from-pr",
206 CliFlag::IncludePartialMessages => "--include-partial-messages",
207 CliFlag::InputFormat(_) => "--input-format",
208 CliFlag::JsonSchema(_) => "--json-schema",
209 CliFlag::MaxBudgetUsd(_) => "--max-budget-usd",
210 CliFlag::MaxThinkingTokens(_) => "--max-thinking-tokens",
211 CliFlag::McpConfig(_) => "--mcp-config",
212 CliFlag::McpDebug => "--mcp-debug",
213 CliFlag::Model(_) => "--model",
214 CliFlag::NoChrome => "--no-chrome",
215 CliFlag::NoSessionPersistence => "--no-session-persistence",
216 CliFlag::OutputFormat(_) => "--output-format",
217 CliFlag::PermissionMode(_) => "--permission-mode",
218 CliFlag::PermissionPromptTool(_) => "--permission-prompt-tool",
219 CliFlag::PluginDir(_) => "--plugin-dir",
220 CliFlag::Print => "--print",
221 CliFlag::ReplayUserMessages => "--replay-user-messages",
222 CliFlag::Resume(_) => "--resume",
223 CliFlag::SessionId(_) => "--session-id",
224 CliFlag::SettingSources(_) => "--setting-sources",
225 CliFlag::Settings(_) => "--settings",
226 CliFlag::StrictMcpConfig => "--strict-mcp-config",
227 CliFlag::SystemPrompt(_) => "--system-prompt",
228 CliFlag::Tools(_) => "--tools",
229 CliFlag::Verbose => "--verbose",
230 }
231 }
232
233 pub fn to_args(&self) -> Vec<String> {
235 let flag = self.as_flag().to_string();
236 match self {
237 CliFlag::AllowDangerouslySkipPermissions
239 | CliFlag::Chrome
240 | CliFlag::Continue
241 | CliFlag::DangerouslySkipPermissions
242 | CliFlag::DisableSlashCommands
243 | CliFlag::ForkSession
244 | CliFlag::IncludePartialMessages
245 | CliFlag::McpDebug
246 | CliFlag::NoChrome
247 | CliFlag::NoSessionPersistence
248 | CliFlag::Print
249 | CliFlag::ReplayUserMessages
250 | CliFlag::StrictMcpConfig
251 | CliFlag::Verbose => vec![flag],
252
253 CliFlag::Debug(filter) => match filter {
255 Some(f) => vec![flag, f.clone()],
256 None => vec![flag],
257 },
258 CliFlag::FromPr(value) | CliFlag::Resume(value) => match value {
259 Some(v) => vec![flag, v.clone()],
260 None => vec![flag],
261 },
262
263 CliFlag::Agent(v)
265 | CliFlag::Agents(v)
266 | CliFlag::AppendSystemPrompt(v)
267 | CliFlag::FallbackModel(v)
268 | CliFlag::JsonSchema(v)
269 | CliFlag::Model(v)
270 | CliFlag::PermissionPromptTool(v)
271 | CliFlag::SessionId(v)
272 | CliFlag::SettingSources(v)
273 | CliFlag::Settings(v)
274 | CliFlag::SystemPrompt(v) => vec![flag, v.clone()],
275
276 CliFlag::InputFormat(f) => vec![flag, f.as_str().to_string()],
278 CliFlag::OutputFormat(f) => vec![flag, f.as_str().to_string()],
279 CliFlag::PermissionMode(m) => vec![flag, m.as_str().to_string()],
280
281 CliFlag::MaxBudgetUsd(amount) => vec![flag, amount.to_string()],
283 CliFlag::MaxThinkingTokens(tokens) => vec![flag, tokens.to_string()],
284
285 CliFlag::DebugFile(p) => vec![flag, p.to_string_lossy().to_string()],
287
288 CliFlag::AllowedTools(items)
290 | CliFlag::Betas(items)
291 | CliFlag::DisallowedTools(items)
292 | CliFlag::File(items)
293 | CliFlag::McpConfig(items)
294 | CliFlag::Tools(items) => {
295 let mut args = vec![flag];
296 args.extend(items.clone());
297 args
298 }
299
300 CliFlag::AddDir(paths) | CliFlag::PluginDir(paths) => {
302 let mut args = vec![flag];
303 args.extend(paths.iter().map(|p| p.to_string_lossy().to_string()));
304 args
305 }
306 }
307 }
308
309 pub fn all_flags() -> Vec<(&'static str, &'static str)> {
322 vec![
323 ("AddDir", "--add-dir"),
324 ("Agent", "--agent"),
325 ("Agents", "--agents"),
326 (
327 "AllowDangerouslySkipPermissions",
328 "--allow-dangerously-skip-permissions",
329 ),
330 ("AllowedTools", "--allowed-tools"),
331 ("AppendSystemPrompt", "--append-system-prompt"),
332 ("Betas", "--betas"),
333 ("Chrome", "--chrome"),
334 ("Continue", "--continue"),
335 (
336 "DangerouslySkipPermissions",
337 "--dangerously-skip-permissions",
338 ),
339 ("Debug", "--debug"),
340 ("DebugFile", "--debug-file"),
341 ("DisableSlashCommands", "--disable-slash-commands"),
342 ("DisallowedTools", "--disallowed-tools"),
343 ("FallbackModel", "--fallback-model"),
344 ("File", "--file"),
345 ("ForkSession", "--fork-session"),
346 ("FromPr", "--from-pr"),
347 ("IncludePartialMessages", "--include-partial-messages"),
348 ("InputFormat", "--input-format"),
349 ("JsonSchema", "--json-schema"),
350 ("MaxBudgetUsd", "--max-budget-usd"),
351 ("MaxThinkingTokens", "--max-thinking-tokens"),
352 ("McpConfig", "--mcp-config"),
353 ("McpDebug", "--mcp-debug"),
354 ("Model", "--model"),
355 ("NoChrome", "--no-chrome"),
356 ("NoSessionPersistence", "--no-session-persistence"),
357 ("OutputFormat", "--output-format"),
358 ("PermissionMode", "--permission-mode"),
359 ("PermissionPromptTool", "--permission-prompt-tool"),
360 ("PluginDir", "--plugin-dir"),
361 ("Print", "--print"),
362 ("ReplayUserMessages", "--replay-user-messages"),
363 ("Resume", "--resume"),
364 ("SessionId", "--session-id"),
365 ("SettingSources", "--setting-sources"),
366 ("Settings", "--settings"),
367 ("StrictMcpConfig", "--strict-mcp-config"),
368 ("SystemPrompt", "--system-prompt"),
369 ("Tools", "--tools"),
370 ("Verbose", "--verbose"),
371 ]
372 }
373}
374
375#[derive(Debug, Clone)]
383pub struct ClaudeCliBuilder {
384 command: PathBuf,
385 working_directory: Option<PathBuf>,
386 prompt: Option<String>,
387 debug: Option<String>,
388 verbose: bool,
389 dangerously_skip_permissions: bool,
390 allowed_tools: Vec<String>,
391 disallowed_tools: Vec<String>,
392 mcp_config: Vec<String>,
393 append_system_prompt: Option<String>,
394 permission_mode: Option<PermissionMode>,
395 continue_conversation: bool,
396 resume: Option<String>,
397 fork_session: bool,
398 model: Option<String>,
399 fallback_model: Option<String>,
400 settings: Option<String>,
401 add_dir: Vec<PathBuf>,
402 ide: bool,
403 strict_mcp_config: bool,
404 session_id: Option<Uuid>,
405 oauth_token: Option<String>,
406 api_key: Option<String>,
407 permission_prompt_tool: Option<String>,
409 allow_recursion: bool,
411 max_thinking_tokens: Option<u32>,
413}
414
415impl Default for ClaudeCliBuilder {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421impl ClaudeCliBuilder {
422 pub fn new() -> Self {
424 Self {
425 command: PathBuf::from("claude"),
426 working_directory: None,
427 prompt: None,
428 debug: None,
429 verbose: false,
430 dangerously_skip_permissions: false,
431 allowed_tools: Vec::new(),
432 disallowed_tools: Vec::new(),
433 mcp_config: Vec::new(),
434 append_system_prompt: None,
435 permission_mode: None,
436 continue_conversation: false,
437 resume: None,
438 fork_session: false,
439 model: None,
440 fallback_model: None,
441 settings: None,
442 add_dir: Vec::new(),
443 ide: false,
444 strict_mcp_config: false,
445 session_id: None,
446 oauth_token: None,
447 api_key: None,
448 permission_prompt_tool: None,
449 allow_recursion: false,
450 max_thinking_tokens: None,
451 }
452 }
453
454 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
456 self.command = path.into();
457 self
458 }
459
460 pub fn working_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
462 self.working_directory = Some(path.into());
463 self
464 }
465
466 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
468 self.prompt = Some(prompt.into());
469 self
470 }
471
472 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
474 self.debug = filter.map(|s| s.into());
475 self
476 }
477
478 pub fn verbose(mut self, verbose: bool) -> Self {
480 self.verbose = verbose;
481 self
482 }
483
484 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
486 self.dangerously_skip_permissions = skip;
487 self
488 }
489
490 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
492 where
493 I: IntoIterator<Item = S>,
494 S: Into<String>,
495 {
496 self.allowed_tools
497 .extend(tools.into_iter().map(|s| s.into()));
498 self
499 }
500
501 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
503 where
504 I: IntoIterator<Item = S>,
505 S: Into<String>,
506 {
507 self.disallowed_tools
508 .extend(tools.into_iter().map(|s| s.into()));
509 self
510 }
511
512 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
514 where
515 I: IntoIterator<Item = S>,
516 S: Into<String>,
517 {
518 self.mcp_config
519 .extend(configs.into_iter().map(|s| s.into()));
520 self
521 }
522
523 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
525 self.append_system_prompt = Some(prompt.into());
526 self
527 }
528
529 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
531 self.permission_mode = Some(mode);
532 self
533 }
534
535 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
537 self.continue_conversation = continue_conv;
538 self
539 }
540
541 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
543 self.resume = session_id.map(|s| s.into());
544 self
545 }
546
547 pub fn fork_session(mut self, fork: bool) -> Self {
554 self.fork_session = fork;
555 self
556 }
557
558 pub fn fork_from<S: Into<String>>(mut self, source_session_id: S) -> Self {
575 self.resume = Some(source_session_id.into());
576 self.fork_session = true;
577 self
578 }
579
580 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
582 self.model = Some(model.into());
583 self
584 }
585
586 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
588 self.fallback_model = Some(model.into());
589 self
590 }
591
592 pub fn max_thinking_tokens(mut self, tokens: u32) -> Self {
594 self.max_thinking_tokens = Some(tokens);
595 self
596 }
597
598 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
600 self.settings = Some(settings.into());
601 self
602 }
603
604 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
606 where
607 I: IntoIterator<Item = P>,
608 P: Into<PathBuf>,
609 {
610 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
611 self
612 }
613
614 pub fn ide(mut self, ide: bool) -> Self {
616 self.ide = ide;
617 self
618 }
619
620 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
622 self.strict_mcp_config = strict;
623 self
624 }
625
626 pub fn session_id(mut self, id: Uuid) -> Self {
628 self.session_id = Some(id);
629 self
630 }
631
632 pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
634 let token_str = token.into();
635 if !token_str.starts_with("sk-ant-oat") {
636 eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
637 }
638 self.oauth_token = Some(token_str);
639 self
640 }
641
642 pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
644 let key_str = key.into();
645 if !key_str.starts_with("sk-ant-api") {
646 eprintln!("Warning: API key should start with 'sk-ant-api'");
647 }
648 self.api_key = Some(key_str);
649 self
650 }
651
652 pub fn permission_prompt_tool<S: Into<String>>(mut self, tool: S) -> Self {
667 self.permission_prompt_tool = Some(tool.into());
668 self
669 }
670
671 #[cfg(feature = "integration-tests")]
674 pub fn allow_recursion(mut self) -> Self {
675 self.allow_recursion = true;
676 self
677 }
678
679 fn resolve_command(&self) -> Result<PathBuf> {
681 if self.command.is_absolute() {
682 return Ok(self.command.clone());
683 }
684 which::which(&self.command).map_err(|_| Error::BinaryNotFound {
685 name: self.command.display().to_string(),
686 })
687 }
688
689 fn build_args(&self) -> Vec<String> {
691 let mut args = vec![
694 "--print".to_string(),
695 "--verbose".to_string(),
696 "--output-format".to_string(),
697 "stream-json".to_string(),
698 "--input-format".to_string(),
699 "stream-json".to_string(),
700 ];
701
702 if let Some(ref debug) = self.debug {
703 args.push("--debug".to_string());
704 if !debug.is_empty() {
705 args.push(debug.clone());
706 }
707 }
708
709 if self.dangerously_skip_permissions {
710 args.push("--dangerously-skip-permissions".to_string());
711 }
712
713 if !self.allowed_tools.is_empty() {
714 args.push("--allowed-tools".to_string());
715 args.extend(self.allowed_tools.clone());
716 }
717
718 if !self.disallowed_tools.is_empty() {
719 args.push("--disallowed-tools".to_string());
720 args.extend(self.disallowed_tools.clone());
721 }
722
723 if !self.mcp_config.is_empty() {
724 args.push("--mcp-config".to_string());
725 args.extend(self.mcp_config.clone());
726 }
727
728 if let Some(ref prompt) = self.append_system_prompt {
729 args.push("--append-system-prompt".to_string());
730 args.push(prompt.clone());
731 }
732
733 if let Some(ref mode) = self.permission_mode {
734 args.push("--permission-mode".to_string());
735 args.push(mode.as_str().to_string());
736 }
737
738 if self.continue_conversation {
739 args.push("--continue".to_string());
740 }
741
742 if let Some(ref session) = self.resume {
743 args.push("--resume".to_string());
744 args.push(session.clone());
745 }
746
747 if self.fork_session {
748 args.push("--fork-session".to_string());
749 }
750
751 if let Some(ref model) = self.model {
752 args.push("--model".to_string());
753 args.push(model.clone());
754 }
755
756 if let Some(ref model) = self.fallback_model {
757 args.push("--fallback-model".to_string());
758 args.push(model.clone());
759 }
760
761 if let Some(tokens) = self.max_thinking_tokens {
762 args.push("--max-thinking-tokens".to_string());
763 args.push(tokens.to_string());
764 }
765
766 if let Some(ref settings) = self.settings {
767 args.push("--settings".to_string());
768 args.push(settings.clone());
769 }
770
771 if !self.add_dir.is_empty() {
772 args.push("--add-dir".to_string());
773 for dir in &self.add_dir {
774 args.push(dir.to_string_lossy().to_string());
775 }
776 }
777
778 if self.ide {
779 args.push("--ide".to_string());
780 }
781
782 if self.strict_mcp_config {
783 args.push("--strict-mcp-config".to_string());
784 }
785
786 if let Some(ref tool) = self.permission_prompt_tool {
787 args.push("--permission-prompt-tool".to_string());
788 args.push(tool.clone());
789 }
790
791 if (self.resume.is_none() && !self.continue_conversation) || self.fork_session {
796 args.push("--session-id".to_string());
797 let session_uuid = self.session_id.unwrap_or_else(|| {
798 let uuid = Uuid::new_v4();
799 debug!("[CLI] Generated session UUID: {}", uuid);
800 uuid
801 });
802 args.push(session_uuid.to_string());
803 }
804
805 if let Some(ref prompt) = self.prompt {
807 args.push(prompt.clone());
808 }
809
810 args
811 }
812
813 #[cfg(feature = "async-client")]
815 pub async fn spawn(self) -> Result<tokio::process::Child> {
816 let resolved = self.resolve_command()?;
817 let args = self.build_args();
818
819 debug!(
820 "[CLI] Executing command: {} {}",
821 resolved.display(),
822 args.join(" ")
823 );
824
825 let mut cmd = tokio::process::Command::new(&resolved);
826 cmd.args(&args)
827 .stdin(Stdio::piped())
828 .stdout(Stdio::piped())
829 .stderr(Stdio::piped());
830
831 if let Some(ref dir) = self.working_directory {
832 cmd.current_dir(dir);
833 }
834
835 if self.allow_recursion {
836 cmd.env_remove("CLAUDECODE");
837 }
838
839 if let Some(ref token) = self.oauth_token {
840 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
841 }
842
843 if let Some(ref key) = self.api_key {
844 cmd.env("ANTHROPIC_API_KEY", key);
845 }
846
847 crate::process::configure_no_window(cmd.as_std_mut());
848 let child = cmd.spawn().map_err(Error::Io)?;
849
850 Ok(child)
851 }
852
853 #[cfg(feature = "async-client")]
855 pub fn build_command(self) -> Result<tokio::process::Command> {
856 let resolved = self.resolve_command()?;
857 let args = self.build_args();
858 let mut cmd = tokio::process::Command::new(&resolved);
859 cmd.args(&args)
860 .stdin(Stdio::piped())
861 .stdout(Stdio::piped())
862 .stderr(Stdio::piped());
863
864 if let Some(ref dir) = self.working_directory {
865 cmd.current_dir(dir);
866 }
867
868 if self.allow_recursion {
869 cmd.env_remove("CLAUDECODE");
870 }
871
872 if let Some(ref token) = self.oauth_token {
873 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
874 }
875
876 if let Some(ref key) = self.api_key {
877 cmd.env("ANTHROPIC_API_KEY", key);
878 }
879
880 crate::process::configure_no_window(cmd.as_std_mut());
881 Ok(cmd)
882 }
883
884 pub fn spawn_sync(self) -> Result<std::process::Child> {
886 let resolved = self.resolve_command()?;
887 let args = self.build_args();
888
889 debug!(
890 "[CLI] Executing sync command: {} {}",
891 resolved.display(),
892 args.join(" ")
893 );
894
895 let mut cmd = std::process::Command::new(&resolved);
896 cmd.args(&args)
897 .stdin(Stdio::piped())
898 .stdout(Stdio::piped())
899 .stderr(Stdio::piped());
900
901 if let Some(ref dir) = self.working_directory {
902 cmd.current_dir(dir);
903 }
904
905 if self.allow_recursion {
906 cmd.env_remove("CLAUDECODE");
907 }
908
909 if let Some(ref token) = self.oauth_token {
910 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
911 }
912
913 if let Some(ref key) = self.api_key {
914 cmd.env("ANTHROPIC_API_KEY", key);
915 }
916
917 crate::process::configure_no_window(&mut cmd);
918 cmd.spawn().map_err(Error::Io)
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925
926 #[test]
927 fn test_working_directory() {
928 let builder = ClaudeCliBuilder::new().working_directory("workspace");
929 assert_eq!(builder.working_directory, Some(PathBuf::from("workspace")));
930 }
931
932 #[test]
933 fn test_streaming_flags_always_present() {
934 let builder = ClaudeCliBuilder::new();
935 let args = builder.build_args();
936
937 assert!(args.contains(&"--print".to_string()));
939 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
941 assert!(args.contains(&"stream-json".to_string()));
942 assert!(args.contains(&"--input-format".to_string()));
943 }
944
945 #[test]
946 fn test_with_prompt() {
947 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
948 let args = builder.build_args();
949
950 assert_eq!(args.last().unwrap(), "Hello, Claude!");
951 }
952
953 #[test]
954 fn test_with_model() {
955 let builder = ClaudeCliBuilder::new()
956 .model("sonnet")
957 .fallback_model("opus");
958 let args = builder.build_args();
959
960 assert!(args.contains(&"--model".to_string()));
961 assert!(args.contains(&"sonnet".to_string()));
962 assert!(args.contains(&"--fallback-model".to_string()));
963 assert!(args.contains(&"opus".to_string()));
964 }
965
966 #[test]
967 fn test_with_debug() {
968 let builder = ClaudeCliBuilder::new().debug(Some("api"));
969 let args = builder.build_args();
970
971 assert!(args.contains(&"--debug".to_string()));
972 assert!(args.contains(&"api".to_string()));
973 }
974
975 #[test]
976 fn test_with_oauth_token() {
977 let valid_token = "sk-ant-oat-123456789";
978 let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
979
980 let args = builder.clone().build_args();
982 assert!(!args.contains(&valid_token.to_string()));
983
984 assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
986 }
987
988 #[test]
989 fn test_oauth_token_validation() {
990 let invalid_token = "invalid-token-123";
992 let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
993 assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
994 }
995
996 #[test]
997 fn test_with_api_key() {
998 let valid_key = "sk-ant-api-987654321";
999 let builder = ClaudeCliBuilder::new().api_key(valid_key);
1000
1001 let args = builder.clone().build_args();
1003 assert!(!args.contains(&valid_key.to_string()));
1004
1005 assert_eq!(builder.api_key, Some(valid_key.to_string()));
1007 }
1008
1009 #[test]
1010 fn test_api_key_validation() {
1011 let invalid_key = "invalid-api-key";
1013 let builder = ClaudeCliBuilder::new().api_key(invalid_key);
1014 assert_eq!(builder.api_key, Some(invalid_key.to_string()));
1015 }
1016
1017 #[test]
1018 fn test_both_auth_methods() {
1019 let oauth = "sk-ant-oat-123";
1020 let api_key = "sk-ant-api-456";
1021 let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
1022
1023 assert_eq!(builder.oauth_token, Some(oauth.to_string()));
1024 assert_eq!(builder.api_key, Some(api_key.to_string()));
1025 }
1026
1027 #[test]
1028 fn test_permission_prompt_tool() {
1029 let builder = ClaudeCliBuilder::new().permission_prompt_tool("stdio");
1030 let args = builder.build_args();
1031
1032 assert!(args.contains(&"--permission-prompt-tool".to_string()));
1033 assert!(args.contains(&"stdio".to_string()));
1034 }
1035
1036 #[test]
1037 fn test_permission_prompt_tool_not_present_by_default() {
1038 let builder = ClaudeCliBuilder::new();
1039 let args = builder.build_args();
1040
1041 assert!(!args.contains(&"--permission-prompt-tool".to_string()));
1042 }
1043
1044 #[test]
1045 fn test_session_id_present_for_new_session() {
1046 let builder = ClaudeCliBuilder::new();
1047 let args = builder.build_args();
1048
1049 assert!(
1050 args.contains(&"--session-id".to_string()),
1051 "New sessions should have --session-id"
1052 );
1053 }
1054
1055 #[test]
1056 fn test_session_id_not_present_with_resume() {
1057 let builder = ClaudeCliBuilder::new().resume(Some("existing-uuid".to_string()));
1060 let args = builder.build_args();
1061
1062 assert!(
1063 args.contains(&"--resume".to_string()),
1064 "Should have --resume flag"
1065 );
1066 assert!(
1067 !args.contains(&"--session-id".to_string()),
1068 "--session-id should NOT be present when resuming"
1069 );
1070 }
1071
1072 #[test]
1073 fn test_session_id_not_present_with_continue() {
1074 let builder = ClaudeCliBuilder::new().continue_conversation(true);
1076 let args = builder.build_args();
1077
1078 assert!(
1079 args.contains(&"--continue".to_string()),
1080 "Should have --continue flag"
1081 );
1082 assert!(
1083 !args.contains(&"--session-id".to_string()),
1084 "--session-id should NOT be present when continuing"
1085 );
1086 }
1087
1088 #[test]
1089 fn test_fork_from_assembles_resume_fork_and_new_session_id() {
1090 let new_id = Uuid::new_v4();
1091 let args = ClaudeCliBuilder::new()
1092 .fork_from("source-uuid")
1093 .session_id(new_id)
1094 .build_args();
1095
1096 let resume_pos = args.iter().position(|a| a == "--resume").unwrap();
1097 assert_eq!(args[resume_pos + 1], "source-uuid");
1098 assert!(args.contains(&"--fork-session".to_string()));
1099 let sid_pos = args.iter().position(|a| a == "--session-id").unwrap();
1100 assert_eq!(args[sid_pos + 1], new_id.to_string());
1101 }
1102
1103 #[test]
1104 fn test_fork_from_generates_session_id_when_unset() {
1105 let args = ClaudeCliBuilder::new()
1106 .fork_from("source-uuid")
1107 .build_args();
1108
1109 assert!(args.contains(&"--fork-session".to_string()));
1110 let sid_pos = args.iter().position(|a| a == "--session-id").unwrap();
1111 assert!(
1112 Uuid::parse_str(&args[sid_pos + 1]).is_ok(),
1113 "generated fork session id should be a UUID"
1114 );
1115 }
1116
1117 #[test]
1118 fn test_fork_session_with_continue_emits_session_id() {
1119 let args = ClaudeCliBuilder::new()
1120 .continue_conversation(true)
1121 .fork_session(true)
1122 .build_args();
1123
1124 assert!(args.contains(&"--continue".to_string()));
1125 assert!(args.contains(&"--fork-session".to_string()));
1126 assert!(args.contains(&"--session-id".to_string()));
1127 }
1128}