1use crate::error::{Error, Result};
13use crate::models::EffortLevel;
14use log::debug;
15use std::path::PathBuf;
16use std::process::Stdio;
17use uuid::Uuid;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PermissionMode {
22 AcceptEdits,
23 BypassPermissions,
24 Default,
25 Delegate,
26 DontAsk,
27 Plan,
28}
29
30impl PermissionMode {
31 pub fn as_str(&self) -> &'static str {
33 match self {
34 PermissionMode::AcceptEdits => "acceptEdits",
35 PermissionMode::BypassPermissions => "bypassPermissions",
36 PermissionMode::Default => "default",
37 PermissionMode::Delegate => "delegate",
38 PermissionMode::DontAsk => "dontAsk",
39 PermissionMode::Plan => "plan",
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum InputFormat {
47 Text,
48 StreamJson,
49}
50
51impl InputFormat {
52 pub fn as_str(&self) -> &'static str {
54 match self {
55 InputFormat::Text => "text",
56 InputFormat::StreamJson => "stream-json",
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum OutputFormat {
64 Text,
65 Json,
66 StreamJson,
67}
68
69impl OutputFormat {
70 pub fn as_str(&self) -> &'static str {
72 match self {
73 OutputFormat::Text => "text",
74 OutputFormat::Json => "json",
75 OutputFormat::StreamJson => "stream-json",
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
97pub enum CliFlag {
98 AddDir(Vec<PathBuf>),
100 Agent(String),
102 Agents(String),
105 AllowDangerouslySkipPermissions,
107 AllowedTools(Vec<String>),
109 AppendSystemPrompt(String),
111 Betas(Vec<String>),
113 Chrome,
115 ClientDataUrl(String),
121 Continue,
123 DangerouslySkipPermissions,
125 Debug(Option<String>),
127 DebugFile(PathBuf),
129 DisableSlashCommands,
131 DisallowedTools(Vec<String>),
133 Effort(EffortLevel),
135 FallbackModel(String),
137 File(Vec<String>),
139 ForkSession,
141 FromPr(Option<String>),
143 IncludePartialMessages,
145 InputFormat(InputFormat),
147 JsonSchema(String),
149 MaxBudgetUsd(f64),
151 MaxThinkingTokens(u32),
153 McpConfig(Vec<String>),
155 McpDebug,
157 Model(String),
159 NoChrome,
161 NoSessionPersistence,
163 OutputFormat(OutputFormat),
165 PermissionMode(PermissionMode),
167 PermissionPromptTool(String),
169 PluginDir(Vec<PathBuf>),
171 Print,
173 ReplayUserMessages,
175 Resume(Option<String>),
177 SessionId(String),
179 SettingSources(String),
181 Settings(String),
183 StrictMcpConfig,
185 SystemPrompt(String),
187 Tools(Vec<String>),
189 Verbose,
191}
192
193impl CliFlag {
194 pub fn as_flag(&self) -> &'static str {
196 match self {
197 CliFlag::AddDir(_) => "--add-dir",
198 CliFlag::Agent(_) => "--agent",
199 CliFlag::Agents(_) => "--agents",
200 CliFlag::AllowDangerouslySkipPermissions => "--allow-dangerously-skip-permissions",
201 CliFlag::AllowedTools(_) => "--allowed-tools",
202 CliFlag::AppendSystemPrompt(_) => "--append-system-prompt",
203 CliFlag::Betas(_) => "--betas",
204 CliFlag::Chrome => "--chrome",
205 CliFlag::ClientDataUrl(_) => "--client-data-url",
206 CliFlag::Continue => "--continue",
207 CliFlag::DangerouslySkipPermissions => "--dangerously-skip-permissions",
208 CliFlag::Debug(_) => "--debug",
209 CliFlag::DebugFile(_) => "--debug-file",
210 CliFlag::DisableSlashCommands => "--disable-slash-commands",
211 CliFlag::DisallowedTools(_) => "--disallowed-tools",
212 CliFlag::Effort(_) => "--effort",
213 CliFlag::FallbackModel(_) => "--fallback-model",
214 CliFlag::File(_) => "--file",
215 CliFlag::ForkSession => "--fork-session",
216 CliFlag::FromPr(_) => "--from-pr",
217 CliFlag::IncludePartialMessages => "--include-partial-messages",
218 CliFlag::InputFormat(_) => "--input-format",
219 CliFlag::JsonSchema(_) => "--json-schema",
220 CliFlag::MaxBudgetUsd(_) => "--max-budget-usd",
221 CliFlag::MaxThinkingTokens(_) => "--max-thinking-tokens",
222 CliFlag::McpConfig(_) => "--mcp-config",
223 CliFlag::McpDebug => "--mcp-debug",
224 CliFlag::Model(_) => "--model",
225 CliFlag::NoChrome => "--no-chrome",
226 CliFlag::NoSessionPersistence => "--no-session-persistence",
227 CliFlag::OutputFormat(_) => "--output-format",
228 CliFlag::PermissionMode(_) => "--permission-mode",
229 CliFlag::PermissionPromptTool(_) => "--permission-prompt-tool",
230 CliFlag::PluginDir(_) => "--plugin-dir",
231 CliFlag::Print => "--print",
232 CliFlag::ReplayUserMessages => "--replay-user-messages",
233 CliFlag::Resume(_) => "--resume",
234 CliFlag::SessionId(_) => "--session-id",
235 CliFlag::SettingSources(_) => "--setting-sources",
236 CliFlag::Settings(_) => "--settings",
237 CliFlag::StrictMcpConfig => "--strict-mcp-config",
238 CliFlag::SystemPrompt(_) => "--system-prompt",
239 CliFlag::Tools(_) => "--tools",
240 CliFlag::Verbose => "--verbose",
241 }
242 }
243
244 pub fn to_args(&self) -> Vec<String> {
246 let flag = self.as_flag().to_string();
247 match self {
248 CliFlag::AllowDangerouslySkipPermissions
250 | CliFlag::Chrome
251 | CliFlag::Continue
252 | CliFlag::DangerouslySkipPermissions
253 | CliFlag::DisableSlashCommands
254 | CliFlag::ForkSession
255 | CliFlag::IncludePartialMessages
256 | CliFlag::McpDebug
257 | CliFlag::NoChrome
258 | CliFlag::NoSessionPersistence
259 | CliFlag::Print
260 | CliFlag::ReplayUserMessages
261 | CliFlag::StrictMcpConfig
262 | CliFlag::Verbose => vec![flag],
263
264 CliFlag::Debug(filter) => match filter {
266 Some(f) => vec![flag, f.clone()],
267 None => vec![flag],
268 },
269 CliFlag::FromPr(value) | CliFlag::Resume(value) => match value {
270 Some(v) => vec![flag, v.clone()],
271 None => vec![flag],
272 },
273
274 CliFlag::Agent(v)
276 | CliFlag::Agents(v)
277 | CliFlag::AppendSystemPrompt(v)
278 | CliFlag::ClientDataUrl(v)
279 | CliFlag::FallbackModel(v)
280 | CliFlag::JsonSchema(v)
281 | CliFlag::Model(v)
282 | CliFlag::PermissionPromptTool(v)
283 | CliFlag::SessionId(v)
284 | CliFlag::SettingSources(v)
285 | CliFlag::Settings(v)
286 | CliFlag::SystemPrompt(v) => vec![flag, v.clone()],
287
288 CliFlag::Effort(level) => vec![flag, level.as_str().to_string()],
290 CliFlag::InputFormat(f) => vec![flag, f.as_str().to_string()],
291 CliFlag::OutputFormat(f) => vec![flag, f.as_str().to_string()],
292 CliFlag::PermissionMode(m) => vec![flag, m.as_str().to_string()],
293
294 CliFlag::MaxBudgetUsd(amount) => vec![flag, amount.to_string()],
296 CliFlag::MaxThinkingTokens(tokens) => vec![flag, tokens.to_string()],
297
298 CliFlag::DebugFile(p) => vec![flag, p.to_string_lossy().to_string()],
300
301 CliFlag::AllowedTools(items)
303 | CliFlag::Betas(items)
304 | CliFlag::DisallowedTools(items)
305 | CliFlag::File(items)
306 | CliFlag::McpConfig(items)
307 | CliFlag::Tools(items) => {
308 let mut args = vec![flag];
309 args.extend(items.clone());
310 args
311 }
312
313 CliFlag::AddDir(paths) | CliFlag::PluginDir(paths) => {
315 let mut args = vec![flag];
316 args.extend(paths.iter().map(|p| p.to_string_lossy().to_string()));
317 args
318 }
319 }
320 }
321
322 pub fn all_flags() -> Vec<(&'static str, &'static str)> {
335 vec![
336 ("AddDir", "--add-dir"),
337 ("Agent", "--agent"),
338 ("Agents", "--agents"),
339 (
340 "AllowDangerouslySkipPermissions",
341 "--allow-dangerously-skip-permissions",
342 ),
343 ("AllowedTools", "--allowed-tools"),
344 ("AppendSystemPrompt", "--append-system-prompt"),
345 ("Betas", "--betas"),
346 ("Chrome", "--chrome"),
347 ("ClientDataUrl", "--client-data-url"),
348 ("Continue", "--continue"),
349 (
350 "DangerouslySkipPermissions",
351 "--dangerously-skip-permissions",
352 ),
353 ("Debug", "--debug"),
354 ("DebugFile", "--debug-file"),
355 ("DisableSlashCommands", "--disable-slash-commands"),
356 ("DisallowedTools", "--disallowed-tools"),
357 ("Effort", "--effort"),
358 ("FallbackModel", "--fallback-model"),
359 ("File", "--file"),
360 ("ForkSession", "--fork-session"),
361 ("FromPr", "--from-pr"),
362 ("IncludePartialMessages", "--include-partial-messages"),
363 ("InputFormat", "--input-format"),
364 ("JsonSchema", "--json-schema"),
365 ("MaxBudgetUsd", "--max-budget-usd"),
366 ("MaxThinkingTokens", "--max-thinking-tokens"),
367 ("McpConfig", "--mcp-config"),
368 ("McpDebug", "--mcp-debug"),
369 ("Model", "--model"),
370 ("NoChrome", "--no-chrome"),
371 ("NoSessionPersistence", "--no-session-persistence"),
372 ("OutputFormat", "--output-format"),
373 ("PermissionMode", "--permission-mode"),
374 ("PermissionPromptTool", "--permission-prompt-tool"),
375 ("PluginDir", "--plugin-dir"),
376 ("Print", "--print"),
377 ("ReplayUserMessages", "--replay-user-messages"),
378 ("Resume", "--resume"),
379 ("SessionId", "--session-id"),
380 ("SettingSources", "--setting-sources"),
381 ("Settings", "--settings"),
382 ("StrictMcpConfig", "--strict-mcp-config"),
383 ("SystemPrompt", "--system-prompt"),
384 ("Tools", "--tools"),
385 ("Verbose", "--verbose"),
386 ]
387 }
388}
389
390#[derive(Debug, Clone)]
398pub struct ClaudeCliBuilder {
399 command: PathBuf,
400 working_directory: Option<PathBuf>,
401 prompt: Option<String>,
402 debug: Option<String>,
403 verbose: bool,
404 dangerously_skip_permissions: bool,
405 allowed_tools: Vec<String>,
406 disallowed_tools: Vec<String>,
407 mcp_config: Vec<String>,
408 append_system_prompt: Option<String>,
409 permission_mode: Option<PermissionMode>,
410 continue_conversation: bool,
411 resume: Option<String>,
412 fork_session: bool,
413 model: Option<String>,
414 fallback_model: Option<String>,
415 effort: Option<EffortLevel>,
416 settings: Option<String>,
417 add_dir: Vec<PathBuf>,
418 ide: bool,
419 strict_mcp_config: bool,
420 session_id: Option<Uuid>,
421 oauth_token: Option<String>,
422 api_key: Option<String>,
423 permission_prompt_tool: Option<String>,
425 allow_recursion: bool,
427 max_thinking_tokens: Option<u32>,
429}
430
431impl Default for ClaudeCliBuilder {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437impl ClaudeCliBuilder {
438 pub fn new() -> Self {
440 Self {
441 command: PathBuf::from("claude"),
442 working_directory: None,
443 prompt: None,
444 debug: None,
445 verbose: false,
446 dangerously_skip_permissions: false,
447 allowed_tools: Vec::new(),
448 disallowed_tools: Vec::new(),
449 mcp_config: Vec::new(),
450 append_system_prompt: None,
451 permission_mode: None,
452 continue_conversation: false,
453 resume: None,
454 fork_session: false,
455 model: None,
456 fallback_model: None,
457 effort: None,
458 settings: None,
459 add_dir: Vec::new(),
460 ide: false,
461 strict_mcp_config: false,
462 session_id: None,
463 oauth_token: None,
464 api_key: None,
465 permission_prompt_tool: None,
466 allow_recursion: false,
467 max_thinking_tokens: None,
468 }
469 }
470
471 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
473 self.command = path.into();
474 self
475 }
476
477 pub fn working_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
479 self.working_directory = Some(path.into());
480 self
481 }
482
483 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
485 self.prompt = Some(prompt.into());
486 self
487 }
488
489 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
491 self.debug = filter.map(|s| s.into());
492 self
493 }
494
495 pub fn verbose(mut self, verbose: bool) -> Self {
497 self.verbose = verbose;
498 self
499 }
500
501 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
503 self.dangerously_skip_permissions = skip;
504 self
505 }
506
507 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
509 where
510 I: IntoIterator<Item = S>,
511 S: Into<String>,
512 {
513 self.allowed_tools
514 .extend(tools.into_iter().map(|s| s.into()));
515 self
516 }
517
518 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
520 where
521 I: IntoIterator<Item = S>,
522 S: Into<String>,
523 {
524 self.disallowed_tools
525 .extend(tools.into_iter().map(|s| s.into()));
526 self
527 }
528
529 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
531 where
532 I: IntoIterator<Item = S>,
533 S: Into<String>,
534 {
535 self.mcp_config
536 .extend(configs.into_iter().map(|s| s.into()));
537 self
538 }
539
540 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
542 self.append_system_prompt = Some(prompt.into());
543 self
544 }
545
546 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
548 self.permission_mode = Some(mode);
549 self
550 }
551
552 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
554 self.continue_conversation = continue_conv;
555 self
556 }
557
558 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
560 self.resume = session_id.map(|s| s.into());
561 self
562 }
563
564 pub fn fork_session(mut self, fork: bool) -> Self {
571 self.fork_session = fork;
572 self
573 }
574
575 pub fn fork_from<S: Into<String>>(mut self, source_session_id: S) -> Self {
592 self.resume = Some(source_session_id.into());
593 self.fork_session = true;
594 self
595 }
596
597 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
599 self.model = Some(model.into());
600 self
601 }
602
603 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
605 self.fallback_model = Some(model.into());
606 self
607 }
608
609 pub fn effort(mut self, level: EffortLevel) -> Self {
615 self.effort = Some(level);
616 self
617 }
618
619 pub fn max_thinking_tokens(mut self, tokens: u32) -> Self {
621 self.max_thinking_tokens = Some(tokens);
622 self
623 }
624
625 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
627 self.settings = Some(settings.into());
628 self
629 }
630
631 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
633 where
634 I: IntoIterator<Item = P>,
635 P: Into<PathBuf>,
636 {
637 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
638 self
639 }
640
641 pub fn ide(mut self, ide: bool) -> Self {
643 self.ide = ide;
644 self
645 }
646
647 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
649 self.strict_mcp_config = strict;
650 self
651 }
652
653 pub fn session_id(mut self, id: Uuid) -> Self {
655 self.session_id = Some(id);
656 self
657 }
658
659 pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
661 let token_str = token.into();
662 if !token_str.starts_with("sk-ant-oat") {
663 eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
664 }
665 self.oauth_token = Some(token_str);
666 self
667 }
668
669 pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
671 let key_str = key.into();
672 if !key_str.starts_with("sk-ant-api") {
673 eprintln!("Warning: API key should start with 'sk-ant-api'");
674 }
675 self.api_key = Some(key_str);
676 self
677 }
678
679 pub fn permission_prompt_tool<S: Into<String>>(mut self, tool: S) -> Self {
694 self.permission_prompt_tool = Some(tool.into());
695 self
696 }
697
698 #[cfg(feature = "integration-tests")]
701 pub fn allow_recursion(mut self) -> Self {
702 self.allow_recursion = true;
703 self
704 }
705
706 fn resolve_command(&self) -> Result<PathBuf> {
708 if self.command.is_absolute() {
709 return Ok(self.command.clone());
710 }
711 which::which(&self.command).map_err(|_| Error::BinaryNotFound {
712 name: self.command.display().to_string(),
713 })
714 }
715
716 fn build_args(&self) -> Vec<String> {
718 let mut args = vec![
721 "--print".to_string(),
722 "--verbose".to_string(),
723 "--output-format".to_string(),
724 "stream-json".to_string(),
725 "--input-format".to_string(),
726 "stream-json".to_string(),
727 ];
728
729 if let Some(ref debug) = self.debug {
730 args.push("--debug".to_string());
731 if !debug.is_empty() {
732 args.push(debug.clone());
733 }
734 }
735
736 if self.dangerously_skip_permissions {
737 args.push("--dangerously-skip-permissions".to_string());
738 }
739
740 if !self.allowed_tools.is_empty() {
741 args.push("--allowed-tools".to_string());
742 args.extend(self.allowed_tools.clone());
743 }
744
745 if !self.disallowed_tools.is_empty() {
746 args.push("--disallowed-tools".to_string());
747 args.extend(self.disallowed_tools.clone());
748 }
749
750 if !self.mcp_config.is_empty() {
751 args.push("--mcp-config".to_string());
752 args.extend(self.mcp_config.clone());
753 }
754
755 if let Some(ref prompt) = self.append_system_prompt {
756 args.push("--append-system-prompt".to_string());
757 args.push(prompt.clone());
758 }
759
760 if let Some(ref mode) = self.permission_mode {
761 args.push("--permission-mode".to_string());
762 args.push(mode.as_str().to_string());
763 }
764
765 if self.continue_conversation {
766 args.push("--continue".to_string());
767 }
768
769 if let Some(ref session) = self.resume {
770 args.push("--resume".to_string());
771 args.push(session.clone());
772 }
773
774 if self.fork_session {
775 args.push("--fork-session".to_string());
776 }
777
778 if let Some(ref model) = self.model {
779 args.push("--model".to_string());
780 args.push(model.clone());
781 }
782
783 if let Some(ref model) = self.fallback_model {
784 args.push("--fallback-model".to_string());
785 args.push(model.clone());
786 }
787
788 if let Some(ref level) = self.effort {
789 args.push("--effort".to_string());
790 args.push(level.to_string());
791 }
792
793 if let Some(tokens) = self.max_thinking_tokens {
794 args.push("--max-thinking-tokens".to_string());
795 args.push(tokens.to_string());
796 }
797
798 if let Some(ref settings) = self.settings {
799 args.push("--settings".to_string());
800 args.push(settings.clone());
801 }
802
803 if !self.add_dir.is_empty() {
804 args.push("--add-dir".to_string());
805 for dir in &self.add_dir {
806 args.push(dir.to_string_lossy().to_string());
807 }
808 }
809
810 if self.ide {
811 args.push("--ide".to_string());
812 }
813
814 if self.strict_mcp_config {
815 args.push("--strict-mcp-config".to_string());
816 }
817
818 if let Some(ref tool) = self.permission_prompt_tool {
819 args.push("--permission-prompt-tool".to_string());
820 args.push(tool.clone());
821 }
822
823 if (self.resume.is_none() && !self.continue_conversation) || self.fork_session {
828 args.push("--session-id".to_string());
829 let session_uuid = self.session_id.unwrap_or_else(|| {
830 let uuid = Uuid::new_v4();
831 debug!("[CLI] Generated session UUID: {}", uuid);
832 uuid
833 });
834 args.push(session_uuid.to_string());
835 }
836
837 if let Some(ref prompt) = self.prompt {
839 args.push(prompt.clone());
840 }
841
842 args
843 }
844
845 #[cfg(feature = "async-client")]
847 pub async fn spawn(self) -> Result<tokio::process::Child> {
848 let resolved = self.resolve_command()?;
849 let args = self.build_args();
850
851 debug!(
852 "[CLI] Executing command: {} {}",
853 resolved.display(),
854 args.join(" ")
855 );
856
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 let child = cmd.spawn().map_err(Error::Io)?;
881
882 Ok(child)
883 }
884
885 #[cfg(feature = "async-client")]
887 pub fn build_command(self) -> Result<tokio::process::Command> {
888 let resolved = self.resolve_command()?;
889 let args = self.build_args();
890 let mut cmd = tokio::process::Command::new(&resolved);
891 cmd.args(&args)
892 .stdin(Stdio::piped())
893 .stdout(Stdio::piped())
894 .stderr(Stdio::piped());
895
896 if let Some(ref dir) = self.working_directory {
897 cmd.current_dir(dir);
898 }
899
900 if self.allow_recursion {
901 cmd.env_remove("CLAUDECODE");
902 }
903
904 if let Some(ref token) = self.oauth_token {
905 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
906 }
907
908 if let Some(ref key) = self.api_key {
909 cmd.env("ANTHROPIC_API_KEY", key);
910 }
911
912 crate::process::configure_no_window(cmd.as_std_mut());
913 Ok(cmd)
914 }
915
916 pub fn spawn_sync(self) -> Result<std::process::Child> {
918 let resolved = self.resolve_command()?;
919 let args = self.build_args();
920
921 debug!(
922 "[CLI] Executing sync command: {} {}",
923 resolved.display(),
924 args.join(" ")
925 );
926
927 let mut cmd = std::process::Command::new(&resolved);
928 cmd.args(&args)
929 .stdin(Stdio::piped())
930 .stdout(Stdio::piped())
931 .stderr(Stdio::piped());
932
933 if let Some(ref dir) = self.working_directory {
934 cmd.current_dir(dir);
935 }
936
937 if self.allow_recursion {
938 cmd.env_remove("CLAUDECODE");
939 }
940
941 if let Some(ref token) = self.oauth_token {
942 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
943 }
944
945 if let Some(ref key) = self.api_key {
946 cmd.env("ANTHROPIC_API_KEY", key);
947 }
948
949 crate::process::configure_no_window(&mut cmd);
950 cmd.spawn().map_err(Error::Io)
951 }
952}
953
954#[cfg(test)]
955mod tests {
956 use super::*;
957
958 #[test]
959 fn test_working_directory() {
960 let builder = ClaudeCliBuilder::new().working_directory("workspace");
961 assert_eq!(builder.working_directory, Some(PathBuf::from("workspace")));
962 }
963
964 #[test]
965 fn test_streaming_flags_always_present() {
966 let builder = ClaudeCliBuilder::new();
967 let args = builder.build_args();
968
969 assert!(args.contains(&"--print".to_string()));
971 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
973 assert!(args.contains(&"stream-json".to_string()));
974 assert!(args.contains(&"--input-format".to_string()));
975 }
976
977 #[test]
978 fn test_with_prompt() {
979 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
980 let args = builder.build_args();
981
982 assert_eq!(args.last().unwrap(), "Hello, Claude!");
983 }
984
985 #[test]
986 fn test_with_model() {
987 let builder = ClaudeCliBuilder::new()
988 .model("sonnet")
989 .fallback_model("opus");
990 let args = builder.build_args();
991
992 assert!(args.contains(&"--model".to_string()));
993 assert!(args.contains(&"sonnet".to_string()));
994 assert!(args.contains(&"--fallback-model".to_string()));
995 assert!(args.contains(&"opus".to_string()));
996 }
997
998 #[test]
999 fn test_with_effort() {
1000 let args = ClaudeCliBuilder::new()
1001 .effort(EffortLevel::Xhigh)
1002 .build_args();
1003 let i = args.iter().position(|a| a == "--effort").expect("--effort");
1004 assert_eq!(args[i + 1], "xhigh");
1005 assert!(!ClaudeCliBuilder::new()
1006 .build_args()
1007 .contains(&"--effort".to_string()));
1008 assert_eq!(
1009 CliFlag::Effort(EffortLevel::Max).to_args(),
1010 vec!["--effort", "max"]
1011 );
1012 }
1013
1014 #[test]
1015 fn test_with_debug() {
1016 let builder = ClaudeCliBuilder::new().debug(Some("api"));
1017 let args = builder.build_args();
1018
1019 assert!(args.contains(&"--debug".to_string()));
1020 assert!(args.contains(&"api".to_string()));
1021 }
1022
1023 #[test]
1024 fn test_with_oauth_token() {
1025 let valid_token = "sk-ant-oat-123456789";
1026 let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
1027
1028 let args = builder.clone().build_args();
1030 assert!(!args.contains(&valid_token.to_string()));
1031
1032 assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
1034 }
1035
1036 #[test]
1037 fn test_oauth_token_validation() {
1038 let invalid_token = "invalid-token-123";
1040 let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
1041 assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
1042 }
1043
1044 #[test]
1045 fn test_with_api_key() {
1046 let valid_key = "sk-ant-api-987654321";
1047 let builder = ClaudeCliBuilder::new().api_key(valid_key);
1048
1049 let args = builder.clone().build_args();
1051 assert!(!args.contains(&valid_key.to_string()));
1052
1053 assert_eq!(builder.api_key, Some(valid_key.to_string()));
1055 }
1056
1057 #[test]
1058 fn test_api_key_validation() {
1059 let invalid_key = "invalid-api-key";
1061 let builder = ClaudeCliBuilder::new().api_key(invalid_key);
1062 assert_eq!(builder.api_key, Some(invalid_key.to_string()));
1063 }
1064
1065 #[test]
1066 fn test_both_auth_methods() {
1067 let oauth = "sk-ant-oat-123";
1068 let api_key = "sk-ant-api-456";
1069 let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
1070
1071 assert_eq!(builder.oauth_token, Some(oauth.to_string()));
1072 assert_eq!(builder.api_key, Some(api_key.to_string()));
1073 }
1074
1075 #[test]
1076 fn test_permission_prompt_tool() {
1077 let builder = ClaudeCliBuilder::new().permission_prompt_tool("stdio");
1078 let args = builder.build_args();
1079
1080 assert!(args.contains(&"--permission-prompt-tool".to_string()));
1081 assert!(args.contains(&"stdio".to_string()));
1082 }
1083
1084 #[test]
1085 fn test_permission_prompt_tool_not_present_by_default() {
1086 let builder = ClaudeCliBuilder::new();
1087 let args = builder.build_args();
1088
1089 assert!(!args.contains(&"--permission-prompt-tool".to_string()));
1090 }
1091
1092 #[test]
1093 fn test_session_id_present_for_new_session() {
1094 let builder = ClaudeCliBuilder::new();
1095 let args = builder.build_args();
1096
1097 assert!(
1098 args.contains(&"--session-id".to_string()),
1099 "New sessions should have --session-id"
1100 );
1101 }
1102
1103 #[test]
1104 fn test_session_id_not_present_with_resume() {
1105 let builder = ClaudeCliBuilder::new().resume(Some("existing-uuid".to_string()));
1108 let args = builder.build_args();
1109
1110 assert!(
1111 args.contains(&"--resume".to_string()),
1112 "Should have --resume flag"
1113 );
1114 assert!(
1115 !args.contains(&"--session-id".to_string()),
1116 "--session-id should NOT be present when resuming"
1117 );
1118 }
1119
1120 #[test]
1121 fn test_session_id_not_present_with_continue() {
1122 let builder = ClaudeCliBuilder::new().continue_conversation(true);
1124 let args = builder.build_args();
1125
1126 assert!(
1127 args.contains(&"--continue".to_string()),
1128 "Should have --continue flag"
1129 );
1130 assert!(
1131 !args.contains(&"--session-id".to_string()),
1132 "--session-id should NOT be present when continuing"
1133 );
1134 }
1135
1136 #[test]
1137 fn test_fork_from_assembles_resume_fork_and_new_session_id() {
1138 let new_id = Uuid::new_v4();
1139 let args = ClaudeCliBuilder::new()
1140 .fork_from("source-uuid")
1141 .session_id(new_id)
1142 .build_args();
1143
1144 let resume_pos = args.iter().position(|a| a == "--resume").unwrap();
1145 assert_eq!(args[resume_pos + 1], "source-uuid");
1146 assert!(args.contains(&"--fork-session".to_string()));
1147 let sid_pos = args.iter().position(|a| a == "--session-id").unwrap();
1148 assert_eq!(args[sid_pos + 1], new_id.to_string());
1149 }
1150
1151 #[test]
1152 fn test_fork_from_generates_session_id_when_unset() {
1153 let args = ClaudeCliBuilder::new()
1154 .fork_from("source-uuid")
1155 .build_args();
1156
1157 assert!(args.contains(&"--fork-session".to_string()));
1158 let sid_pos = args.iter().position(|a| a == "--session-id").unwrap();
1159 assert!(
1160 Uuid::parse_str(&args[sid_pos + 1]).is_ok(),
1161 "generated fork session id should be a UUID"
1162 );
1163 }
1164
1165 #[test]
1166 fn test_fork_session_with_continue_emits_session_id() {
1167 let args = ClaudeCliBuilder::new()
1168 .continue_conversation(true)
1169 .fork_session(true)
1170 .build_args();
1171
1172 assert!(args.contains(&"--continue".to_string()));
1173 assert!(args.contains(&"--fork-session".to_string()));
1174 assert!(args.contains(&"--session-id".to_string()));
1175 }
1176}