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