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),
103 AllowDangerouslySkipPermissions,
105 AllowedTools(Vec<String>),
107 AppendSystemPrompt(String),
109 Betas(Vec<String>),
111 Chrome,
113 Continue,
115 DangerouslySkipPermissions,
117 Debug(Option<String>),
119 DebugFile(PathBuf),
121 DisableSlashCommands,
123 DisallowedTools(Vec<String>),
125 FallbackModel(String),
127 File(Vec<String>),
129 ForkSession,
131 FromPr(Option<String>),
133 IncludePartialMessages,
135 InputFormat(InputFormat),
137 JsonSchema(String),
139 MaxBudgetUsd(f64),
141 MaxThinkingTokens(u32),
143 McpConfig(Vec<String>),
145 McpDebug,
147 Model(String),
149 NoChrome,
151 NoSessionPersistence,
153 OutputFormat(OutputFormat),
155 PermissionMode(PermissionMode),
157 PermissionPromptTool(String),
159 PluginDir(Vec<PathBuf>),
161 Print,
163 ReplayUserMessages,
165 Resume(Option<String>),
167 SessionId(String),
169 SettingSources(String),
171 Settings(String),
173 StrictMcpConfig,
175 SystemPrompt(String),
177 Tools(Vec<String>),
179 Verbose,
181}
182
183impl CliFlag {
184 pub fn as_flag(&self) -> &'static str {
186 match self {
187 CliFlag::AddDir(_) => "--add-dir",
188 CliFlag::Agent(_) => "--agent",
189 CliFlag::Agents(_) => "--agents",
190 CliFlag::AllowDangerouslySkipPermissions => "--allow-dangerously-skip-permissions",
191 CliFlag::AllowedTools(_) => "--allowed-tools",
192 CliFlag::AppendSystemPrompt(_) => "--append-system-prompt",
193 CliFlag::Betas(_) => "--betas",
194 CliFlag::Chrome => "--chrome",
195 CliFlag::Continue => "--continue",
196 CliFlag::DangerouslySkipPermissions => "--dangerously-skip-permissions",
197 CliFlag::Debug(_) => "--debug",
198 CliFlag::DebugFile(_) => "--debug-file",
199 CliFlag::DisableSlashCommands => "--disable-slash-commands",
200 CliFlag::DisallowedTools(_) => "--disallowed-tools",
201 CliFlag::FallbackModel(_) => "--fallback-model",
202 CliFlag::File(_) => "--file",
203 CliFlag::ForkSession => "--fork-session",
204 CliFlag::FromPr(_) => "--from-pr",
205 CliFlag::IncludePartialMessages => "--include-partial-messages",
206 CliFlag::InputFormat(_) => "--input-format",
207 CliFlag::JsonSchema(_) => "--json-schema",
208 CliFlag::MaxBudgetUsd(_) => "--max-budget-usd",
209 CliFlag::MaxThinkingTokens(_) => "--max-thinking-tokens",
210 CliFlag::McpConfig(_) => "--mcp-config",
211 CliFlag::McpDebug => "--mcp-debug",
212 CliFlag::Model(_) => "--model",
213 CliFlag::NoChrome => "--no-chrome",
214 CliFlag::NoSessionPersistence => "--no-session-persistence",
215 CliFlag::OutputFormat(_) => "--output-format",
216 CliFlag::PermissionMode(_) => "--permission-mode",
217 CliFlag::PermissionPromptTool(_) => "--permission-prompt-tool",
218 CliFlag::PluginDir(_) => "--plugin-dir",
219 CliFlag::Print => "--print",
220 CliFlag::ReplayUserMessages => "--replay-user-messages",
221 CliFlag::Resume(_) => "--resume",
222 CliFlag::SessionId(_) => "--session-id",
223 CliFlag::SettingSources(_) => "--setting-sources",
224 CliFlag::Settings(_) => "--settings",
225 CliFlag::StrictMcpConfig => "--strict-mcp-config",
226 CliFlag::SystemPrompt(_) => "--system-prompt",
227 CliFlag::Tools(_) => "--tools",
228 CliFlag::Verbose => "--verbose",
229 }
230 }
231
232 pub fn to_args(&self) -> Vec<String> {
234 let flag = self.as_flag().to_string();
235 match self {
236 CliFlag::AllowDangerouslySkipPermissions
238 | CliFlag::Chrome
239 | CliFlag::Continue
240 | CliFlag::DangerouslySkipPermissions
241 | CliFlag::DisableSlashCommands
242 | CliFlag::ForkSession
243 | CliFlag::IncludePartialMessages
244 | CliFlag::McpDebug
245 | CliFlag::NoChrome
246 | CliFlag::NoSessionPersistence
247 | CliFlag::Print
248 | CliFlag::ReplayUserMessages
249 | CliFlag::StrictMcpConfig
250 | CliFlag::Verbose => vec![flag],
251
252 CliFlag::Debug(filter) => match filter {
254 Some(f) => vec![flag, f.clone()],
255 None => vec![flag],
256 },
257 CliFlag::FromPr(value) | CliFlag::Resume(value) => match value {
258 Some(v) => vec![flag, v.clone()],
259 None => vec![flag],
260 },
261
262 CliFlag::Agent(v)
264 | CliFlag::Agents(v)
265 | CliFlag::AppendSystemPrompt(v)
266 | CliFlag::FallbackModel(v)
267 | CliFlag::JsonSchema(v)
268 | CliFlag::Model(v)
269 | CliFlag::PermissionPromptTool(v)
270 | CliFlag::SessionId(v)
271 | CliFlag::SettingSources(v)
272 | CliFlag::Settings(v)
273 | CliFlag::SystemPrompt(v) => vec![flag, v.clone()],
274
275 CliFlag::InputFormat(f) => vec![flag, f.as_str().to_string()],
277 CliFlag::OutputFormat(f) => vec![flag, f.as_str().to_string()],
278 CliFlag::PermissionMode(m) => vec![flag, m.as_str().to_string()],
279
280 CliFlag::MaxBudgetUsd(amount) => vec![flag, amount.to_string()],
282 CliFlag::MaxThinkingTokens(tokens) => vec![flag, tokens.to_string()],
283
284 CliFlag::DebugFile(p) => vec![flag, p.to_string_lossy().to_string()],
286
287 CliFlag::AllowedTools(items)
289 | CliFlag::Betas(items)
290 | CliFlag::DisallowedTools(items)
291 | CliFlag::File(items)
292 | CliFlag::McpConfig(items)
293 | CliFlag::Tools(items) => {
294 let mut args = vec![flag];
295 args.extend(items.clone());
296 args
297 }
298
299 CliFlag::AddDir(paths) | CliFlag::PluginDir(paths) => {
301 let mut args = vec![flag];
302 args.extend(paths.iter().map(|p| p.to_string_lossy().to_string()));
303 args
304 }
305 }
306 }
307
308 pub fn all_flags() -> Vec<(&'static str, &'static str)> {
321 vec![
322 ("AddDir", "--add-dir"),
323 ("Agent", "--agent"),
324 ("Agents", "--agents"),
325 (
326 "AllowDangerouslySkipPermissions",
327 "--allow-dangerously-skip-permissions",
328 ),
329 ("AllowedTools", "--allowed-tools"),
330 ("AppendSystemPrompt", "--append-system-prompt"),
331 ("Betas", "--betas"),
332 ("Chrome", "--chrome"),
333 ("Continue", "--continue"),
334 (
335 "DangerouslySkipPermissions",
336 "--dangerously-skip-permissions",
337 ),
338 ("Debug", "--debug"),
339 ("DebugFile", "--debug-file"),
340 ("DisableSlashCommands", "--disable-slash-commands"),
341 ("DisallowedTools", "--disallowed-tools"),
342 ("FallbackModel", "--fallback-model"),
343 ("File", "--file"),
344 ("ForkSession", "--fork-session"),
345 ("FromPr", "--from-pr"),
346 ("IncludePartialMessages", "--include-partial-messages"),
347 ("InputFormat", "--input-format"),
348 ("JsonSchema", "--json-schema"),
349 ("MaxBudgetUsd", "--max-budget-usd"),
350 ("MaxThinkingTokens", "--max-thinking-tokens"),
351 ("McpConfig", "--mcp-config"),
352 ("McpDebug", "--mcp-debug"),
353 ("Model", "--model"),
354 ("NoChrome", "--no-chrome"),
355 ("NoSessionPersistence", "--no-session-persistence"),
356 ("OutputFormat", "--output-format"),
357 ("PermissionMode", "--permission-mode"),
358 ("PermissionPromptTool", "--permission-prompt-tool"),
359 ("PluginDir", "--plugin-dir"),
360 ("Print", "--print"),
361 ("ReplayUserMessages", "--replay-user-messages"),
362 ("Resume", "--resume"),
363 ("SessionId", "--session-id"),
364 ("SettingSources", "--setting-sources"),
365 ("Settings", "--settings"),
366 ("StrictMcpConfig", "--strict-mcp-config"),
367 ("SystemPrompt", "--system-prompt"),
368 ("Tools", "--tools"),
369 ("Verbose", "--verbose"),
370 ]
371 }
372}
373
374#[derive(Debug, Clone)]
382pub struct ClaudeCliBuilder {
383 command: PathBuf,
384 working_directory: Option<PathBuf>,
385 prompt: Option<String>,
386 debug: Option<String>,
387 verbose: bool,
388 dangerously_skip_permissions: bool,
389 allowed_tools: Vec<String>,
390 disallowed_tools: Vec<String>,
391 mcp_config: Vec<String>,
392 append_system_prompt: Option<String>,
393 permission_mode: Option<PermissionMode>,
394 continue_conversation: bool,
395 resume: Option<String>,
396 fork_session: bool,
397 model: Option<String>,
398 fallback_model: Option<String>,
399 settings: Option<String>,
400 add_dir: Vec<PathBuf>,
401 ide: bool,
402 strict_mcp_config: bool,
403 session_id: Option<Uuid>,
404 oauth_token: Option<String>,
405 api_key: Option<String>,
406 permission_prompt_tool: Option<String>,
408 allow_recursion: bool,
410 max_thinking_tokens: Option<u32>,
412}
413
414impl Default for ClaudeCliBuilder {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420impl ClaudeCliBuilder {
421 pub fn new() -> Self {
423 Self {
424 command: PathBuf::from("claude"),
425 working_directory: None,
426 prompt: None,
427 debug: None,
428 verbose: false,
429 dangerously_skip_permissions: false,
430 allowed_tools: Vec::new(),
431 disallowed_tools: Vec::new(),
432 mcp_config: Vec::new(),
433 append_system_prompt: None,
434 permission_mode: None,
435 continue_conversation: false,
436 resume: None,
437 fork_session: false,
438 model: None,
439 fallback_model: None,
440 settings: None,
441 add_dir: Vec::new(),
442 ide: false,
443 strict_mcp_config: false,
444 session_id: None,
445 oauth_token: None,
446 api_key: None,
447 permission_prompt_tool: None,
448 allow_recursion: false,
449 max_thinking_tokens: None,
450 }
451 }
452
453 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
455 self.command = path.into();
456 self
457 }
458
459 pub fn working_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
461 self.working_directory = Some(path.into());
462 self
463 }
464
465 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
467 self.prompt = Some(prompt.into());
468 self
469 }
470
471 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
473 self.debug = filter.map(|s| s.into());
474 self
475 }
476
477 pub fn verbose(mut self, verbose: bool) -> Self {
479 self.verbose = verbose;
480 self
481 }
482
483 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
485 self.dangerously_skip_permissions = skip;
486 self
487 }
488
489 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
491 where
492 I: IntoIterator<Item = S>,
493 S: Into<String>,
494 {
495 self.allowed_tools
496 .extend(tools.into_iter().map(|s| s.into()));
497 self
498 }
499
500 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
502 where
503 I: IntoIterator<Item = S>,
504 S: Into<String>,
505 {
506 self.disallowed_tools
507 .extend(tools.into_iter().map(|s| s.into()));
508 self
509 }
510
511 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
513 where
514 I: IntoIterator<Item = S>,
515 S: Into<String>,
516 {
517 self.mcp_config
518 .extend(configs.into_iter().map(|s| s.into()));
519 self
520 }
521
522 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
524 self.append_system_prompt = Some(prompt.into());
525 self
526 }
527
528 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
530 self.permission_mode = Some(mode);
531 self
532 }
533
534 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
536 self.continue_conversation = continue_conv;
537 self
538 }
539
540 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
542 self.resume = session_id.map(|s| s.into());
543 self
544 }
545
546 pub fn fork_session(mut self, fork: bool) -> Self {
553 self.fork_session = fork;
554 self
555 }
556
557 pub fn fork_from<S: Into<String>>(mut self, source_session_id: S) -> Self {
574 self.resume = Some(source_session_id.into());
575 self.fork_session = true;
576 self
577 }
578
579 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
581 self.model = Some(model.into());
582 self
583 }
584
585 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
587 self.fallback_model = Some(model.into());
588 self
589 }
590
591 pub fn max_thinking_tokens(mut self, tokens: u32) -> Self {
593 self.max_thinking_tokens = Some(tokens);
594 self
595 }
596
597 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
599 self.settings = Some(settings.into());
600 self
601 }
602
603 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
605 where
606 I: IntoIterator<Item = P>,
607 P: Into<PathBuf>,
608 {
609 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
610 self
611 }
612
613 pub fn ide(mut self, ide: bool) -> Self {
615 self.ide = ide;
616 self
617 }
618
619 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
621 self.strict_mcp_config = strict;
622 self
623 }
624
625 pub fn session_id(mut self, id: Uuid) -> Self {
627 self.session_id = Some(id);
628 self
629 }
630
631 pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
633 let token_str = token.into();
634 if !token_str.starts_with("sk-ant-oat") {
635 eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
636 }
637 self.oauth_token = Some(token_str);
638 self
639 }
640
641 pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
643 let key_str = key.into();
644 if !key_str.starts_with("sk-ant-api") {
645 eprintln!("Warning: API key should start with 'sk-ant-api'");
646 }
647 self.api_key = Some(key_str);
648 self
649 }
650
651 pub fn permission_prompt_tool<S: Into<String>>(mut self, tool: S) -> Self {
666 self.permission_prompt_tool = Some(tool.into());
667 self
668 }
669
670 #[cfg(feature = "integration-tests")]
673 pub fn allow_recursion(mut self) -> Self {
674 self.allow_recursion = true;
675 self
676 }
677
678 fn resolve_command(&self) -> Result<PathBuf> {
680 if self.command.is_absolute() {
681 return Ok(self.command.clone());
682 }
683 which::which(&self.command).map_err(|_| Error::BinaryNotFound {
684 name: self.command.display().to_string(),
685 })
686 }
687
688 fn build_args(&self) -> Vec<String> {
690 let mut args = vec![
693 "--print".to_string(),
694 "--verbose".to_string(),
695 "--output-format".to_string(),
696 "stream-json".to_string(),
697 "--input-format".to_string(),
698 "stream-json".to_string(),
699 ];
700
701 if let Some(ref debug) = self.debug {
702 args.push("--debug".to_string());
703 if !debug.is_empty() {
704 args.push(debug.clone());
705 }
706 }
707
708 if self.dangerously_skip_permissions {
709 args.push("--dangerously-skip-permissions".to_string());
710 }
711
712 if !self.allowed_tools.is_empty() {
713 args.push("--allowed-tools".to_string());
714 args.extend(self.allowed_tools.clone());
715 }
716
717 if !self.disallowed_tools.is_empty() {
718 args.push("--disallowed-tools".to_string());
719 args.extend(self.disallowed_tools.clone());
720 }
721
722 if !self.mcp_config.is_empty() {
723 args.push("--mcp-config".to_string());
724 args.extend(self.mcp_config.clone());
725 }
726
727 if let Some(ref prompt) = self.append_system_prompt {
728 args.push("--append-system-prompt".to_string());
729 args.push(prompt.clone());
730 }
731
732 if let Some(ref mode) = self.permission_mode {
733 args.push("--permission-mode".to_string());
734 args.push(mode.as_str().to_string());
735 }
736
737 if self.continue_conversation {
738 args.push("--continue".to_string());
739 }
740
741 if let Some(ref session) = self.resume {
742 args.push("--resume".to_string());
743 args.push(session.clone());
744 }
745
746 if self.fork_session {
747 args.push("--fork-session".to_string());
748 }
749
750 if let Some(ref model) = self.model {
751 args.push("--model".to_string());
752 args.push(model.clone());
753 }
754
755 if let Some(ref model) = self.fallback_model {
756 args.push("--fallback-model".to_string());
757 args.push(model.clone());
758 }
759
760 if let Some(tokens) = self.max_thinking_tokens {
761 args.push("--max-thinking-tokens".to_string());
762 args.push(tokens.to_string());
763 }
764
765 if let Some(ref settings) = self.settings {
766 args.push("--settings".to_string());
767 args.push(settings.clone());
768 }
769
770 if !self.add_dir.is_empty() {
771 args.push("--add-dir".to_string());
772 for dir in &self.add_dir {
773 args.push(dir.to_string_lossy().to_string());
774 }
775 }
776
777 if self.ide {
778 args.push("--ide".to_string());
779 }
780
781 if self.strict_mcp_config {
782 args.push("--strict-mcp-config".to_string());
783 }
784
785 if let Some(ref tool) = self.permission_prompt_tool {
786 args.push("--permission-prompt-tool".to_string());
787 args.push(tool.clone());
788 }
789
790 if (self.resume.is_none() && !self.continue_conversation) || self.fork_session {
795 args.push("--session-id".to_string());
796 let session_uuid = self.session_id.unwrap_or_else(|| {
797 let uuid = Uuid::new_v4();
798 debug!("[CLI] Generated session UUID: {}", uuid);
799 uuid
800 });
801 args.push(session_uuid.to_string());
802 }
803
804 if let Some(ref prompt) = self.prompt {
806 args.push(prompt.clone());
807 }
808
809 args
810 }
811
812 #[cfg(feature = "async-client")]
814 pub async fn spawn(self) -> Result<tokio::process::Child> {
815 let resolved = self.resolve_command()?;
816 let args = self.build_args();
817
818 debug!(
819 "[CLI] Executing command: {} {}",
820 resolved.display(),
821 args.join(" ")
822 );
823
824 let mut cmd = tokio::process::Command::new(&resolved);
825 cmd.args(&args)
826 .stdin(Stdio::piped())
827 .stdout(Stdio::piped())
828 .stderr(Stdio::piped());
829
830 if let Some(ref dir) = self.working_directory {
831 cmd.current_dir(dir);
832 }
833
834 if self.allow_recursion {
835 cmd.env_remove("CLAUDECODE");
836 }
837
838 if let Some(ref token) = self.oauth_token {
839 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
840 }
841
842 if let Some(ref key) = self.api_key {
843 cmd.env("ANTHROPIC_API_KEY", key);
844 }
845
846 crate::process::configure_no_window(cmd.as_std_mut());
847 let child = cmd.spawn().map_err(Error::Io)?;
848
849 Ok(child)
850 }
851
852 #[cfg(feature = "async-client")]
854 pub fn build_command(self) -> Result<tokio::process::Command> {
855 let resolved = self.resolve_command()?;
856 let args = self.build_args();
857 let mut cmd = tokio::process::Command::new(&resolved);
858 cmd.args(&args)
859 .stdin(Stdio::piped())
860 .stdout(Stdio::piped())
861 .stderr(Stdio::piped());
862
863 if let Some(ref dir) = self.working_directory {
864 cmd.current_dir(dir);
865 }
866
867 if self.allow_recursion {
868 cmd.env_remove("CLAUDECODE");
869 }
870
871 if let Some(ref token) = self.oauth_token {
872 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
873 }
874
875 if let Some(ref key) = self.api_key {
876 cmd.env("ANTHROPIC_API_KEY", key);
877 }
878
879 crate::process::configure_no_window(cmd.as_std_mut());
880 Ok(cmd)
881 }
882
883 pub fn spawn_sync(self) -> Result<std::process::Child> {
885 let resolved = self.resolve_command()?;
886 let args = self.build_args();
887
888 debug!(
889 "[CLI] Executing sync command: {} {}",
890 resolved.display(),
891 args.join(" ")
892 );
893
894 let mut cmd = std::process::Command::new(&resolved);
895 cmd.args(&args)
896 .stdin(Stdio::piped())
897 .stdout(Stdio::piped())
898 .stderr(Stdio::piped());
899
900 if let Some(ref dir) = self.working_directory {
901 cmd.current_dir(dir);
902 }
903
904 if self.allow_recursion {
905 cmd.env_remove("CLAUDECODE");
906 }
907
908 if let Some(ref token) = self.oauth_token {
909 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
910 }
911
912 if let Some(ref key) = self.api_key {
913 cmd.env("ANTHROPIC_API_KEY", key);
914 }
915
916 crate::process::configure_no_window(&mut cmd);
917 cmd.spawn().map_err(Error::Io)
918 }
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924
925 #[test]
926 fn test_working_directory() {
927 let builder = ClaudeCliBuilder::new().working_directory("workspace");
928 assert_eq!(builder.working_directory, Some(PathBuf::from("workspace")));
929 }
930
931 #[test]
932 fn test_streaming_flags_always_present() {
933 let builder = ClaudeCliBuilder::new();
934 let args = builder.build_args();
935
936 assert!(args.contains(&"--print".to_string()));
938 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
940 assert!(args.contains(&"stream-json".to_string()));
941 assert!(args.contains(&"--input-format".to_string()));
942 }
943
944 #[test]
945 fn test_with_prompt() {
946 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
947 let args = builder.build_args();
948
949 assert_eq!(args.last().unwrap(), "Hello, Claude!");
950 }
951
952 #[test]
953 fn test_with_model() {
954 let builder = ClaudeCliBuilder::new()
955 .model("sonnet")
956 .fallback_model("opus");
957 let args = builder.build_args();
958
959 assert!(args.contains(&"--model".to_string()));
960 assert!(args.contains(&"sonnet".to_string()));
961 assert!(args.contains(&"--fallback-model".to_string()));
962 assert!(args.contains(&"opus".to_string()));
963 }
964
965 #[test]
966 fn test_with_debug() {
967 let builder = ClaudeCliBuilder::new().debug(Some("api"));
968 let args = builder.build_args();
969
970 assert!(args.contains(&"--debug".to_string()));
971 assert!(args.contains(&"api".to_string()));
972 }
973
974 #[test]
975 fn test_with_oauth_token() {
976 let valid_token = "sk-ant-oat-123456789";
977 let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
978
979 let args = builder.clone().build_args();
981 assert!(!args.contains(&valid_token.to_string()));
982
983 assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
985 }
986
987 #[test]
988 fn test_oauth_token_validation() {
989 let invalid_token = "invalid-token-123";
991 let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
992 assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
993 }
994
995 #[test]
996 fn test_with_api_key() {
997 let valid_key = "sk-ant-api-987654321";
998 let builder = ClaudeCliBuilder::new().api_key(valid_key);
999
1000 let args = builder.clone().build_args();
1002 assert!(!args.contains(&valid_key.to_string()));
1003
1004 assert_eq!(builder.api_key, Some(valid_key.to_string()));
1006 }
1007
1008 #[test]
1009 fn test_api_key_validation() {
1010 let invalid_key = "invalid-api-key";
1012 let builder = ClaudeCliBuilder::new().api_key(invalid_key);
1013 assert_eq!(builder.api_key, Some(invalid_key.to_string()));
1014 }
1015
1016 #[test]
1017 fn test_both_auth_methods() {
1018 let oauth = "sk-ant-oat-123";
1019 let api_key = "sk-ant-api-456";
1020 let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
1021
1022 assert_eq!(builder.oauth_token, Some(oauth.to_string()));
1023 assert_eq!(builder.api_key, Some(api_key.to_string()));
1024 }
1025
1026 #[test]
1027 fn test_permission_prompt_tool() {
1028 let builder = ClaudeCliBuilder::new().permission_prompt_tool("stdio");
1029 let args = builder.build_args();
1030
1031 assert!(args.contains(&"--permission-prompt-tool".to_string()));
1032 assert!(args.contains(&"stdio".to_string()));
1033 }
1034
1035 #[test]
1036 fn test_permission_prompt_tool_not_present_by_default() {
1037 let builder = ClaudeCliBuilder::new();
1038 let args = builder.build_args();
1039
1040 assert!(!args.contains(&"--permission-prompt-tool".to_string()));
1041 }
1042
1043 #[test]
1044 fn test_session_id_present_for_new_session() {
1045 let builder = ClaudeCliBuilder::new();
1046 let args = builder.build_args();
1047
1048 assert!(
1049 args.contains(&"--session-id".to_string()),
1050 "New sessions should have --session-id"
1051 );
1052 }
1053
1054 #[test]
1055 fn test_session_id_not_present_with_resume() {
1056 let builder = ClaudeCliBuilder::new().resume(Some("existing-uuid".to_string()));
1059 let args = builder.build_args();
1060
1061 assert!(
1062 args.contains(&"--resume".to_string()),
1063 "Should have --resume flag"
1064 );
1065 assert!(
1066 !args.contains(&"--session-id".to_string()),
1067 "--session-id should NOT be present when resuming"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_session_id_not_present_with_continue() {
1073 let builder = ClaudeCliBuilder::new().continue_conversation(true);
1075 let args = builder.build_args();
1076
1077 assert!(
1078 args.contains(&"--continue".to_string()),
1079 "Should have --continue flag"
1080 );
1081 assert!(
1082 !args.contains(&"--session-id".to_string()),
1083 "--session-id should NOT be present when continuing"
1084 );
1085 }
1086
1087 #[test]
1088 fn test_fork_from_assembles_resume_fork_and_new_session_id() {
1089 let new_id = Uuid::new_v4();
1090 let args = ClaudeCliBuilder::new()
1091 .fork_from("source-uuid")
1092 .session_id(new_id)
1093 .build_args();
1094
1095 let resume_pos = args.iter().position(|a| a == "--resume").unwrap();
1096 assert_eq!(args[resume_pos + 1], "source-uuid");
1097 assert!(args.contains(&"--fork-session".to_string()));
1098 let sid_pos = args.iter().position(|a| a == "--session-id").unwrap();
1099 assert_eq!(args[sid_pos + 1], new_id.to_string());
1100 }
1101
1102 #[test]
1103 fn test_fork_from_generates_session_id_when_unset() {
1104 let args = ClaudeCliBuilder::new()
1105 .fork_from("source-uuid")
1106 .build_args();
1107
1108 assert!(args.contains(&"--fork-session".to_string()));
1109 let sid_pos = args.iter().position(|a| a == "--session-id").unwrap();
1110 assert!(
1111 Uuid::parse_str(&args[sid_pos + 1]).is_ok(),
1112 "generated fork session id should be a UUID"
1113 );
1114 }
1115
1116 #[test]
1117 fn test_fork_session_with_continue_emits_session_id() {
1118 let args = ClaudeCliBuilder::new()
1119 .continue_conversation(true)
1120 .fork_session(true)
1121 .build_args();
1122
1123 assert!(args.contains(&"--continue".to_string()));
1124 assert!(args.contains(&"--fork-session".to_string()));
1125 assert!(args.contains(&"--session-id".to_string()));
1126 }
1127}