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