1use ralph_core::{CliConfig, HatBackend};
4use std::fmt;
5use std::io::Write;
6use tempfile::NamedTempFile;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum OutputFormat {
14 #[default]
16 Text,
17 StreamJson,
19}
20
21#[derive(Debug, Clone)]
23pub struct CustomBackendError;
24
25impl fmt::Display for CustomBackendError {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 write!(f, "custom backend requires a command to be specified")
28 }
29}
30
31impl std::error::Error for CustomBackendError {}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum PromptMode {
36 Arg,
38 Stdin,
40}
41
42#[derive(Debug, Clone)]
44pub struct CliBackend {
45 pub command: String,
47 pub args: Vec<String>,
49 pub prompt_mode: PromptMode,
51 pub prompt_flag: Option<String>,
53 pub output_format: OutputFormat,
55}
56
57impl CliBackend {
58 pub fn from_config(config: &CliConfig) -> Result<Self, CustomBackendError> {
63 let mut backend = match config.backend.as_str() {
64 "claude" => Self::claude(),
65 "kiro" => Self::kiro(),
66 "gemini" => Self::gemini(),
67 "codex" => Self::codex(),
68 "amp" => Self::amp(),
69 "copilot" => Self::copilot(),
70 "opencode" => Self::opencode(),
71 "custom" => return Self::custom(config),
72 _ => Self::claude(), };
74
75 if let Some(ref cmd) = config.command {
77 backend.command = cmd.clone();
78 }
79
80 Ok(backend)
81 }
82
83 pub fn claude() -> Self {
92 Self {
93 command: "claude".to_string(),
94 args: vec![
95 "--dangerously-skip-permissions".to_string(),
96 "--verbose".to_string(),
97 "--output-format".to_string(),
98 "stream-json".to_string(),
99 "--disallowedTools".to_string(),
100 "TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
101 ],
102 prompt_mode: PromptMode::Arg,
103 prompt_flag: Some("-p".to_string()),
104 output_format: OutputFormat::StreamJson,
105 }
106 }
107
108 pub fn claude_interactive() -> Self {
116 Self {
117 command: "claude".to_string(),
118 args: vec![
119 "--dangerously-skip-permissions".to_string(),
120 "--disallowedTools".to_string(),
121 "TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
122 ],
123 prompt_mode: PromptMode::Arg,
124 prompt_flag: None,
125 output_format: OutputFormat::Text,
126 }
127 }
128
129 pub fn kiro() -> Self {
133 Self {
134 command: "kiro-cli".to_string(),
135 args: vec![
136 "chat".to_string(),
137 "--no-interactive".to_string(),
138 "--trust-all-tools".to_string(),
139 ],
140 prompt_mode: PromptMode::Arg,
141 prompt_flag: None,
142 output_format: OutputFormat::Text,
143 }
144 }
145
146 pub fn kiro_with_agent(agent: String, extra_args: &[String]) -> Self {
150 let mut backend = Self {
151 command: "kiro-cli".to_string(),
152 args: vec![
153 "chat".to_string(),
154 "--no-interactive".to_string(),
155 "--trust-all-tools".to_string(),
156 "--agent".to_string(),
157 agent,
158 ],
159 prompt_mode: PromptMode::Arg,
160 prompt_flag: None,
161 output_format: OutputFormat::Text,
162 };
163 backend.args.extend(extra_args.iter().cloned());
164 backend
165 }
166
167 pub fn from_name_with_args(
172 name: &str,
173 extra_args: &[String],
174 ) -> Result<Self, CustomBackendError> {
175 let mut backend = Self::from_name(name)?;
176 backend.args.extend(extra_args.iter().cloned());
177 Ok(backend)
178 }
179
180 pub fn from_name(name: &str) -> Result<Self, CustomBackendError> {
185 match name {
186 "claude" => Ok(Self::claude()),
187 "kiro" => Ok(Self::kiro()),
188 "gemini" => Ok(Self::gemini()),
189 "codex" => Ok(Self::codex()),
190 "amp" => Ok(Self::amp()),
191 "copilot" => Ok(Self::copilot()),
192 "opencode" => Ok(Self::opencode()),
193 _ => Err(CustomBackendError),
194 }
195 }
196
197 pub fn from_hat_backend(hat_backend: &HatBackend) -> Result<Self, CustomBackendError> {
202 match hat_backend {
203 HatBackend::Named(name) => Self::from_name(name),
204 HatBackend::NamedWithArgs { backend_type, args } => {
205 Self::from_name_with_args(backend_type, args)
206 }
207 HatBackend::KiroAgent { agent, args, .. } => {
208 Ok(Self::kiro_with_agent(agent.clone(), args))
209 }
210 HatBackend::Custom { command, args } => Ok(Self {
211 command: command.clone(),
212 args: args.clone(),
213 prompt_mode: PromptMode::Arg,
214 prompt_flag: None,
215 output_format: OutputFormat::Text,
216 }),
217 }
218 }
219
220 pub fn gemini() -> Self {
222 Self {
223 command: "gemini".to_string(),
224 args: vec!["--yolo".to_string()],
225 prompt_mode: PromptMode::Arg,
226 prompt_flag: Some("-p".to_string()),
227 output_format: OutputFormat::Text,
228 }
229 }
230
231 pub fn codex() -> Self {
233 Self {
234 command: "codex".to_string(),
235 args: vec!["exec".to_string(), "--full-auto".to_string()],
236 prompt_mode: PromptMode::Arg,
237 prompt_flag: None, output_format: OutputFormat::Text,
239 }
240 }
241
242 pub fn amp() -> Self {
244 Self {
245 command: "amp".to_string(),
246 args: vec!["--dangerously-allow-all".to_string()],
247 prompt_mode: PromptMode::Arg,
248 prompt_flag: Some("-x".to_string()),
249 output_format: OutputFormat::Text,
250 }
251 }
252
253 pub fn copilot() -> Self {
258 Self {
259 command: "copilot".to_string(),
260 args: vec!["--allow-all-tools".to_string()],
261 prompt_mode: PromptMode::Arg,
262 prompt_flag: Some("-p".to_string()),
263 output_format: OutputFormat::Text,
264 }
265 }
266
267 pub fn copilot_tui() -> Self {
273 Self {
274 command: "copilot".to_string(),
275 args: vec![], prompt_mode: PromptMode::Arg,
277 prompt_flag: None, output_format: OutputFormat::Text,
279 }
280 }
281
282 pub fn for_interactive_prompt(backend_name: &str) -> Result<Self, CustomBackendError> {
301 match backend_name {
302 "claude" => Ok(Self::claude_interactive()),
303 "kiro" => Ok(Self::kiro_interactive()),
304 "gemini" => Ok(Self::gemini_interactive()),
305 "codex" => Ok(Self::codex_interactive()),
306 "amp" => Ok(Self::amp_interactive()),
307 "copilot" => Ok(Self::copilot_interactive()),
308 "opencode" => Ok(Self::opencode_interactive()),
309 _ => Err(CustomBackendError),
310 }
311 }
312
313 pub fn kiro_interactive() -> Self {
318 Self {
319 command: "kiro-cli".to_string(),
320 args: vec!["chat".to_string(), "--trust-all-tools".to_string()],
321 prompt_mode: PromptMode::Arg,
322 prompt_flag: None,
323 output_format: OutputFormat::Text,
324 }
325 }
326
327 pub fn gemini_interactive() -> Self {
332 Self {
333 command: "gemini".to_string(),
334 args: vec!["--yolo".to_string()],
335 prompt_mode: PromptMode::Arg,
336 prompt_flag: Some("-i".to_string()), output_format: OutputFormat::Text,
338 }
339 }
340
341 pub fn codex_interactive() -> Self {
346 Self {
347 command: "codex".to_string(),
348 args: vec![], prompt_mode: PromptMode::Arg,
350 prompt_flag: None, output_format: OutputFormat::Text,
352 }
353 }
354
355 pub fn amp_interactive() -> Self {
360 Self {
361 command: "amp".to_string(),
362 args: vec![],
363 prompt_mode: PromptMode::Arg,
364 prompt_flag: Some("-x".to_string()),
365 output_format: OutputFormat::Text,
366 }
367 }
368
369 pub fn copilot_interactive() -> Self {
374 Self {
375 command: "copilot".to_string(),
376 args: vec![],
377 prompt_mode: PromptMode::Arg,
378 prompt_flag: Some("-p".to_string()),
379 output_format: OutputFormat::Text,
380 }
381 }
382
383 pub fn opencode() -> Self {
393 Self {
394 command: "opencode".to_string(),
395 args: vec!["run".to_string()],
396 prompt_mode: PromptMode::Arg,
397 prompt_flag: None, output_format: OutputFormat::Text,
399 }
400 }
401
402 pub fn opencode_tui() -> Self {
410 Self {
411 command: "opencode".to_string(),
412 args: vec!["run".to_string()],
413 prompt_mode: PromptMode::Arg,
414 prompt_flag: None, output_format: OutputFormat::Text,
416 }
417 }
418
419 pub fn opencode_interactive() -> Self {
429 Self {
430 command: "opencode".to_string(),
431 args: vec![],
432 prompt_mode: PromptMode::Arg,
433 prompt_flag: Some("--prompt".to_string()),
434 output_format: OutputFormat::Text,
435 }
436 }
437
438 pub fn custom(config: &CliConfig) -> Result<Self, CustomBackendError> {
443 let command = config.command.clone().ok_or(CustomBackendError)?;
444 let prompt_mode = if config.prompt_mode == "stdin" {
445 PromptMode::Stdin
446 } else {
447 PromptMode::Arg
448 };
449
450 Ok(Self {
451 command,
452 args: config.args.clone(),
453 prompt_mode,
454 prompt_flag: config.prompt_flag.clone(),
455 output_format: OutputFormat::Text,
456 })
457 }
458
459 pub fn build_command(
465 &self,
466 prompt: &str,
467 interactive: bool,
468 ) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
469 let mut args = self.args.clone();
470
471 if interactive {
473 args = self.filter_args_for_interactive(args);
474 }
475
476 let (stdin_input, temp_file) = match self.prompt_mode {
478 PromptMode::Arg => {
479 let (prompt_text, temp_file) = if self.command == "claude" && prompt.len() > 7000 {
480 match NamedTempFile::new() {
482 Ok(mut file) => {
483 if let Err(e) = file.write_all(prompt.as_bytes()) {
484 tracing::warn!("Failed to write prompt to temp file: {}", e);
485 (prompt.to_string(), None)
486 } else {
487 let path = file.path().display().to_string();
488 (
489 format!("Please read and execute the task in {}", path),
490 Some(file),
491 )
492 }
493 }
494 Err(e) => {
495 tracing::warn!("Failed to create temp file: {}", e);
496 (prompt.to_string(), None)
497 }
498 }
499 } else {
500 (prompt.to_string(), None)
501 };
502
503 if let Some(ref flag) = self.prompt_flag {
504 args.push(flag.clone());
505 }
506 args.push(prompt_text);
507 (None, temp_file)
508 }
509 PromptMode::Stdin => (Some(prompt.to_string()), None),
510 };
511
512 tracing::debug!(
514 command = %self.command,
515 args_count = args.len(),
516 prompt_len = prompt.len(),
517 interactive = interactive,
518 uses_stdin = stdin_input.is_some(),
519 uses_temp_file = temp_file.is_some(),
520 "Built CLI command"
521 );
522 tracing::trace!(prompt = %prompt, "Full prompt content");
524
525 (self.command.clone(), args, stdin_input, temp_file)
526 }
527
528 fn filter_args_for_interactive(&self, args: Vec<String>) -> Vec<String> {
530 match self.command.as_str() {
531 "kiro-cli" => args
532 .into_iter()
533 .filter(|a| a != "--no-interactive")
534 .collect(),
535 "codex" => args.into_iter().filter(|a| a != "--full-auto").collect(),
536 "amp" => args
537 .into_iter()
538 .filter(|a| a != "--dangerously-allow-all")
539 .collect(),
540 "copilot" => args
541 .into_iter()
542 .filter(|a| a != "--allow-all-tools")
543 .collect(),
544 _ => args, }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_claude_backend() {
555 let backend = CliBackend::claude();
556 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
557
558 assert_eq!(cmd, "claude");
559 assert_eq!(
560 args,
561 vec![
562 "--dangerously-skip-permissions",
563 "--verbose",
564 "--output-format",
565 "stream-json",
566 "--disallowedTools",
567 "TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
568 "-p",
569 "test prompt"
570 ]
571 );
572 assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::StreamJson);
574 }
575
576 #[test]
577 fn test_claude_interactive_backend() {
578 let backend = CliBackend::claude_interactive();
579 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
580
581 assert_eq!(cmd, "claude");
582 assert_eq!(
585 args,
586 vec![
587 "--dangerously-skip-permissions",
588 "--disallowedTools",
589 "TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
590 "test prompt"
591 ]
592 );
593 assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::Text);
595 assert_eq!(backend.prompt_flag, None);
596 }
597
598 #[test]
599 fn test_claude_large_prompt_uses_temp_file() {
600 let backend = CliBackend::claude();
602 let large_prompt = "x".repeat(7001);
603 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
604
605 assert_eq!(cmd, "claude");
606 assert!(temp.is_some());
608 assert!(args.iter().any(|a| a.contains("Please read and execute")));
610 }
611
612 #[test]
613 fn test_non_claude_large_prompt() {
614 let backend = CliBackend::kiro();
615 let large_prompt = "x".repeat(7001);
616 let (cmd, args, stdin, temp) = backend.build_command(&large_prompt, false);
617
618 assert_eq!(cmd, "kiro-cli");
619 assert_eq!(args[3], large_prompt);
620 assert!(stdin.is_none());
621 assert!(temp.is_none());
622 }
623
624 #[test]
625 fn test_kiro_backend() {
626 let backend = CliBackend::kiro();
627 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
628
629 assert_eq!(cmd, "kiro-cli");
630 assert_eq!(
631 args,
632 vec![
633 "chat",
634 "--no-interactive",
635 "--trust-all-tools",
636 "test prompt"
637 ]
638 );
639 assert!(stdin.is_none());
640 }
641
642 #[test]
643 fn test_gemini_backend() {
644 let backend = CliBackend::gemini();
645 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
646
647 assert_eq!(cmd, "gemini");
648 assert_eq!(args, vec!["--yolo", "-p", "test prompt"]);
649 assert!(stdin.is_none());
650 }
651
652 #[test]
653 fn test_codex_backend() {
654 let backend = CliBackend::codex();
655 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
656
657 assert_eq!(cmd, "codex");
658 assert_eq!(args, vec!["exec", "--full-auto", "test prompt"]);
659 assert!(stdin.is_none());
660 }
661
662 #[test]
663 fn test_amp_backend() {
664 let backend = CliBackend::amp();
665 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
666
667 assert_eq!(cmd, "amp");
668 assert_eq!(args, vec!["--dangerously-allow-all", "-x", "test prompt"]);
669 assert!(stdin.is_none());
670 }
671
672 #[test]
673 fn test_copilot_backend() {
674 let backend = CliBackend::copilot();
675 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
676
677 assert_eq!(cmd, "copilot");
678 assert_eq!(args, vec!["--allow-all-tools", "-p", "test prompt"]);
679 assert!(stdin.is_none());
680 assert_eq!(backend.output_format, OutputFormat::Text);
681 }
682
683 #[test]
684 fn test_copilot_tui_backend() {
685 let backend = CliBackend::copilot_tui();
686 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
687
688 assert_eq!(cmd, "copilot");
689 assert_eq!(args, vec!["test prompt"]);
691 assert!(stdin.is_none());
692 assert_eq!(backend.output_format, OutputFormat::Text);
693 assert_eq!(backend.prompt_flag, None);
694 }
695
696 #[test]
697 fn test_from_config() {
698 let config = CliConfig {
700 backend: "claude".to_string(),
701 command: None,
702 prompt_mode: "arg".to_string(),
703 ..Default::default()
704 };
705 let backend = CliBackend::from_config(&config).unwrap();
706
707 assert_eq!(backend.command, "claude");
708 assert_eq!(backend.prompt_mode, PromptMode::Arg);
709 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
710 }
711
712 #[test]
713 fn test_from_config_command_override() {
714 let config = CliConfig {
715 backend: "claude".to_string(),
716 command: Some("my-custom-claude".to_string()),
717 prompt_mode: "arg".to_string(),
718 ..Default::default()
719 };
720 let backend = CliBackend::from_config(&config).unwrap();
721
722 assert_eq!(backend.command, "my-custom-claude");
723 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
724 assert_eq!(backend.output_format, OutputFormat::StreamJson);
725 }
726
727 #[test]
728 fn test_kiro_interactive_mode_omits_no_interactive_flag() {
729 let backend = CliBackend::kiro();
730 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
731
732 assert_eq!(cmd, "kiro-cli");
733 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
734 assert!(stdin.is_none());
735 assert!(!args.contains(&"--no-interactive".to_string()));
736 }
737
738 #[test]
739 fn test_codex_interactive_mode_omits_full_auto() {
740 let backend = CliBackend::codex();
741 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
742
743 assert_eq!(cmd, "codex");
744 assert_eq!(args, vec!["exec", "test prompt"]);
745 assert!(stdin.is_none());
746 assert!(!args.contains(&"--full-auto".to_string()));
747 }
748
749 #[test]
750 fn test_amp_interactive_mode_no_flags() {
751 let backend = CliBackend::amp();
752 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
753
754 assert_eq!(cmd, "amp");
755 assert_eq!(args, vec!["-x", "test prompt"]);
756 assert!(stdin.is_none());
757 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
758 }
759
760 #[test]
761 fn test_copilot_interactive_mode_omits_allow_all_tools() {
762 let backend = CliBackend::copilot();
763 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
764
765 assert_eq!(cmd, "copilot");
766 assert_eq!(args, vec!["-p", "test prompt"]);
767 assert!(stdin.is_none());
768 assert!(!args.contains(&"--allow-all-tools".to_string()));
769 }
770
771 #[test]
772 fn test_claude_interactive_mode_unchanged() {
773 let backend = CliBackend::claude();
774 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
775 let (_, args_interactive, stdin_interactive, _) =
776 backend.build_command("test prompt", true);
777
778 assert_eq!(cmd, "claude");
779 assert_eq!(args_auto, args_interactive);
780 assert_eq!(
781 args_auto,
782 vec![
783 "--dangerously-skip-permissions",
784 "--verbose",
785 "--output-format",
786 "stream-json",
787 "--disallowedTools",
788 "TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
789 "-p",
790 "test prompt"
791 ]
792 );
793 assert!(stdin_auto.is_none());
795 assert!(stdin_interactive.is_none());
796 }
797
798 #[test]
799 fn test_gemini_interactive_mode_unchanged() {
800 let backend = CliBackend::gemini();
801 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
802 let (_, args_interactive, stdin_interactive, _) =
803 backend.build_command("test prompt", true);
804
805 assert_eq!(cmd, "gemini");
806 assert_eq!(args_auto, args_interactive);
807 assert_eq!(args_auto, vec!["--yolo", "-p", "test prompt"]);
808 assert_eq!(stdin_auto, stdin_interactive);
809 assert!(stdin_auto.is_none());
810 }
811
812 #[test]
813 fn test_custom_backend_with_prompt_flag_short() {
814 let config = CliConfig {
815 backend: "custom".to_string(),
816 command: Some("my-agent".to_string()),
817 prompt_mode: "arg".to_string(),
818 prompt_flag: Some("-p".to_string()),
819 ..Default::default()
820 };
821 let backend = CliBackend::from_config(&config).unwrap();
822 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
823
824 assert_eq!(cmd, "my-agent");
825 assert_eq!(args, vec!["-p", "test prompt"]);
826 assert!(stdin.is_none());
827 }
828
829 #[test]
830 fn test_custom_backend_with_prompt_flag_long() {
831 let config = CliConfig {
832 backend: "custom".to_string(),
833 command: Some("my-agent".to_string()),
834 prompt_mode: "arg".to_string(),
835 prompt_flag: Some("--prompt".to_string()),
836 ..Default::default()
837 };
838 let backend = CliBackend::from_config(&config).unwrap();
839 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
840
841 assert_eq!(cmd, "my-agent");
842 assert_eq!(args, vec!["--prompt", "test prompt"]);
843 assert!(stdin.is_none());
844 }
845
846 #[test]
847 fn test_custom_backend_without_prompt_flag_positional() {
848 let config = CliConfig {
849 backend: "custom".to_string(),
850 command: Some("my-agent".to_string()),
851 prompt_mode: "arg".to_string(),
852 prompt_flag: None,
853 ..Default::default()
854 };
855 let backend = CliBackend::from_config(&config).unwrap();
856 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
857
858 assert_eq!(cmd, "my-agent");
859 assert_eq!(args, vec!["test prompt"]);
860 assert!(stdin.is_none());
861 }
862
863 #[test]
864 fn test_custom_backend_without_command_returns_error() {
865 let config = CliConfig {
866 backend: "custom".to_string(),
867 command: None,
868 prompt_mode: "arg".to_string(),
869 ..Default::default()
870 };
871 let result = CliBackend::from_config(&config);
872
873 assert!(result.is_err());
874 let err = result.unwrap_err();
875 assert_eq!(
876 err.to_string(),
877 "custom backend requires a command to be specified"
878 );
879 }
880
881 #[test]
882 fn test_kiro_with_agent() {
883 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &[]);
884 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
885
886 assert_eq!(cmd, "kiro-cli");
887 assert_eq!(
888 args,
889 vec![
890 "chat",
891 "--no-interactive",
892 "--trust-all-tools",
893 "--agent",
894 "my-agent",
895 "test prompt"
896 ]
897 );
898 assert!(stdin.is_none());
899 }
900
901 #[test]
902 fn test_kiro_with_agent_extra_args() {
903 let extra_args = vec!["--verbose".to_string(), "--debug".to_string()];
904 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &extra_args);
905 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
906
907 assert_eq!(cmd, "kiro-cli");
908 assert_eq!(
909 args,
910 vec![
911 "chat",
912 "--no-interactive",
913 "--trust-all-tools",
914 "--agent",
915 "my-agent",
916 "--verbose",
917 "--debug",
918 "test prompt"
919 ]
920 );
921 assert!(stdin.is_none());
922 }
923
924 #[test]
925 fn test_from_name_claude() {
926 let backend = CliBackend::from_name("claude").unwrap();
927 assert_eq!(backend.command, "claude");
928 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
929 }
930
931 #[test]
932 fn test_from_name_kiro() {
933 let backend = CliBackend::from_name("kiro").unwrap();
934 assert_eq!(backend.command, "kiro-cli");
935 }
936
937 #[test]
938 fn test_from_name_gemini() {
939 let backend = CliBackend::from_name("gemini").unwrap();
940 assert_eq!(backend.command, "gemini");
941 }
942
943 #[test]
944 fn test_from_name_codex() {
945 let backend = CliBackend::from_name("codex").unwrap();
946 assert_eq!(backend.command, "codex");
947 }
948
949 #[test]
950 fn test_from_name_amp() {
951 let backend = CliBackend::from_name("amp").unwrap();
952 assert_eq!(backend.command, "amp");
953 }
954
955 #[test]
956 fn test_from_name_copilot() {
957 let backend = CliBackend::from_name("copilot").unwrap();
958 assert_eq!(backend.command, "copilot");
959 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
960 }
961
962 #[test]
963 fn test_from_name_invalid() {
964 let result = CliBackend::from_name("invalid");
965 assert!(result.is_err());
966 }
967
968 #[test]
969 fn test_from_hat_backend_named() {
970 let hat_backend = HatBackend::Named("claude".to_string());
971 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
972 assert_eq!(backend.command, "claude");
973 }
974
975 #[test]
976 fn test_from_hat_backend_kiro_agent() {
977 let hat_backend = HatBackend::KiroAgent {
978 backend_type: "kiro".to_string(),
979 agent: "my-agent".to_string(),
980 args: vec![],
981 };
982 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
983 let (cmd, args, _, _) = backend.build_command("test", false);
984 assert_eq!(cmd, "kiro-cli");
985 assert!(args.contains(&"--agent".to_string()));
986 assert!(args.contains(&"my-agent".to_string()));
987 }
988
989 #[test]
990 fn test_from_hat_backend_kiro_agent_with_args() {
991 let hat_backend = HatBackend::KiroAgent {
992 backend_type: "kiro".to_string(),
993 agent: "my-agent".to_string(),
994 args: vec!["--verbose".to_string()],
995 };
996 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
997 let (cmd, args, _, _) = backend.build_command("test", false);
998 assert_eq!(cmd, "kiro-cli");
999 assert!(args.contains(&"--agent".to_string()));
1000 assert!(args.contains(&"my-agent".to_string()));
1001 assert!(args.contains(&"--verbose".to_string()));
1002 }
1003
1004 #[test]
1005 fn test_from_hat_backend_named_with_args() {
1006 let hat_backend = HatBackend::NamedWithArgs {
1007 backend_type: "claude".to_string(),
1008 args: vec!["--model".to_string(), "claude-sonnet-4".to_string()],
1009 };
1010 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1011 assert_eq!(backend.command, "claude");
1012 assert!(backend.args.contains(&"--model".to_string()));
1013 assert!(backend.args.contains(&"claude-sonnet-4".to_string()));
1014 }
1015
1016 #[test]
1017 fn test_from_hat_backend_custom() {
1018 let hat_backend = HatBackend::Custom {
1019 command: "my-cli".to_string(),
1020 args: vec!["--flag".to_string()],
1021 };
1022 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1023 assert_eq!(backend.command, "my-cli");
1024 assert_eq!(backend.args, vec!["--flag"]);
1025 }
1026
1027 #[test]
1032 fn test_for_interactive_prompt_claude() {
1033 let backend = CliBackend::for_interactive_prompt("claude").unwrap();
1034 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1035
1036 assert_eq!(cmd, "claude");
1037 assert_eq!(
1039 args,
1040 vec![
1041 "--dangerously-skip-permissions",
1042 "--disallowedTools",
1043 "TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1044 "test prompt"
1045 ]
1046 );
1047 assert!(stdin.is_none());
1048 assert_eq!(backend.prompt_flag, None);
1049 }
1050
1051 #[test]
1052 fn test_for_interactive_prompt_kiro() {
1053 let backend = CliBackend::for_interactive_prompt("kiro").unwrap();
1054 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1055
1056 assert_eq!(cmd, "kiro-cli");
1057 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1059 assert!(!args.contains(&"--no-interactive".to_string()));
1060 assert!(stdin.is_none());
1061 }
1062
1063 #[test]
1064 fn test_for_interactive_prompt_gemini() {
1065 let backend = CliBackend::for_interactive_prompt("gemini").unwrap();
1066 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1067
1068 assert_eq!(cmd, "gemini");
1069 assert_eq!(args, vec!["--yolo", "-i", "test prompt"]);
1071 assert_eq!(backend.prompt_flag, Some("-i".to_string()));
1072 assert!(stdin.is_none());
1073 }
1074
1075 #[test]
1076 fn test_for_interactive_prompt_codex() {
1077 let backend = CliBackend::for_interactive_prompt("codex").unwrap();
1078 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1079
1080 assert_eq!(cmd, "codex");
1081 assert_eq!(args, vec!["test prompt"]);
1083 assert!(!args.contains(&"exec".to_string()));
1084 assert!(!args.contains(&"--full-auto".to_string()));
1085 assert!(stdin.is_none());
1086 }
1087
1088 #[test]
1089 fn test_for_interactive_prompt_amp() {
1090 let backend = CliBackend::for_interactive_prompt("amp").unwrap();
1091 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1092
1093 assert_eq!(cmd, "amp");
1094 assert_eq!(args, vec!["-x", "test prompt"]);
1096 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1097 assert!(stdin.is_none());
1098 }
1099
1100 #[test]
1101 fn test_for_interactive_prompt_copilot() {
1102 let backend = CliBackend::for_interactive_prompt("copilot").unwrap();
1103 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1104
1105 assert_eq!(cmd, "copilot");
1106 assert_eq!(args, vec!["-p", "test prompt"]);
1108 assert!(!args.contains(&"--allow-all-tools".to_string()));
1109 assert!(stdin.is_none());
1110 }
1111
1112 #[test]
1113 fn test_for_interactive_prompt_invalid() {
1114 let result = CliBackend::for_interactive_prompt("invalid_backend");
1115 assert!(result.is_err());
1116 }
1117
1118 #[test]
1123 fn test_opencode_backend() {
1124 let backend = CliBackend::opencode();
1125 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1126
1127 assert_eq!(cmd, "opencode");
1128 assert_eq!(args, vec!["run", "test prompt"]);
1130 assert!(stdin.is_none());
1131 assert_eq!(backend.output_format, OutputFormat::Text);
1132 assert_eq!(backend.prompt_flag, None);
1133 }
1134
1135 #[test]
1136 fn test_opencode_tui_backend() {
1137 let backend = CliBackend::opencode_tui();
1138 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1139
1140 assert_eq!(cmd, "opencode");
1141 assert_eq!(args, vec!["run", "test prompt"]);
1143 assert!(stdin.is_none());
1144 assert_eq!(backend.output_format, OutputFormat::Text);
1145 assert_eq!(backend.prompt_flag, None);
1146 }
1147
1148 #[test]
1149 fn test_opencode_interactive_mode_unchanged() {
1150 let backend = CliBackend::opencode();
1152 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1153 let (_, args_interactive, stdin_interactive, _) =
1154 backend.build_command("test prompt", true);
1155
1156 assert_eq!(cmd, "opencode");
1157 assert_eq!(args_auto, args_interactive);
1159 assert_eq!(args_auto, vec!["run", "test prompt"]);
1160 assert!(stdin_auto.is_none());
1161 assert!(stdin_interactive.is_none());
1162 }
1163
1164 #[test]
1165 fn test_from_name_opencode() {
1166 let backend = CliBackend::from_name("opencode").unwrap();
1167 assert_eq!(backend.command, "opencode");
1168 assert_eq!(backend.prompt_flag, None); }
1170
1171 #[test]
1172 fn test_for_interactive_prompt_opencode() {
1173 let backend = CliBackend::for_interactive_prompt("opencode").unwrap();
1174 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1175
1176 assert_eq!(cmd, "opencode");
1177 assert_eq!(args, vec!["--prompt", "test prompt"]);
1179 assert!(stdin.is_none());
1180 assert_eq!(backend.prompt_flag, Some("--prompt".to_string()));
1181 }
1182
1183 #[test]
1184 fn test_opencode_interactive_launches_tui_not_headless() {
1185 let backend = CliBackend::opencode_interactive();
1195 let (cmd, args, _, _) = backend.build_command("test prompt", true);
1196
1197 assert_eq!(cmd, "opencode");
1198 assert!(
1201 !args.contains(&"run".to_string()),
1202 "opencode_interactive() should not use 'run' subcommand. \
1203 'opencode run' is headless mode, but interactive mode needs TUI. \
1204 Expected: opencode --prompt \"test prompt\", got: opencode {}",
1205 args.join(" ")
1206 );
1207 assert!(
1209 args.contains(&"--prompt".to_string()),
1210 "opencode_interactive() should use --prompt flag for TUI mode. \
1211 Expected args to contain '--prompt', got: {:?}",
1212 args
1213 );
1214 }
1215
1216 #[test]
1217 fn test_custom_args_can_be_appended() {
1218 let mut backend = CliBackend::opencode();
1221
1222 let custom_args = vec!["--model=gpt-4".to_string(), "--temperature=0.7".to_string()];
1224 backend.args.extend(custom_args.clone());
1225
1226 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1228
1229 assert_eq!(cmd, "opencode");
1230 assert!(args.contains(&"run".to_string())); assert!(args.contains(&"--model=gpt-4".to_string())); assert!(args.contains(&"--temperature=0.7".to_string())); assert!(args.contains(&"test prompt".to_string())); let run_idx = args.iter().position(|a| a == "run").unwrap();
1238 let model_idx = args.iter().position(|a| a == "--model=gpt-4").unwrap();
1239 assert!(
1240 run_idx < model_idx,
1241 "Original args should come before custom args"
1242 );
1243 }
1244}