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=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
100 ],
101 prompt_mode: PromptMode::Arg,
102 prompt_flag: Some("-p".to_string()),
103 output_format: OutputFormat::StreamJson,
104 }
105 }
106
107 pub fn claude_interactive() -> Self {
117 Self {
118 command: "claude".to_string(),
119 args: vec![
120 "--dangerously-skip-permissions".to_string(),
121 "--disallowedTools=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![
236 "exec".to_string(),
237 "--yolo".to_string(),
238 "--full-auto".to_string(),
239 ],
240 prompt_mode: PromptMode::Arg,
241 prompt_flag: None, output_format: OutputFormat::Text,
243 }
244 }
245
246 pub fn amp() -> Self {
248 Self {
249 command: "amp".to_string(),
250 args: vec!["--dangerously-allow-all".to_string()],
251 prompt_mode: PromptMode::Arg,
252 prompt_flag: Some("-x".to_string()),
253 output_format: OutputFormat::Text,
254 }
255 }
256
257 pub fn copilot() -> Self {
262 Self {
263 command: "copilot".to_string(),
264 args: vec!["--allow-all-tools".to_string()],
265 prompt_mode: PromptMode::Arg,
266 prompt_flag: Some("-p".to_string()),
267 output_format: OutputFormat::Text,
268 }
269 }
270
271 pub fn copilot_tui() -> Self {
277 Self {
278 command: "copilot".to_string(),
279 args: vec![], prompt_mode: PromptMode::Arg,
281 prompt_flag: None, output_format: OutputFormat::Text,
283 }
284 }
285
286 pub fn for_interactive_prompt(backend_name: &str) -> Result<Self, CustomBackendError> {
305 match backend_name {
306 "claude" => Ok(Self::claude_interactive()),
307 "kiro" => Ok(Self::kiro_interactive()),
308 "gemini" => Ok(Self::gemini_interactive()),
309 "codex" => Ok(Self::codex_interactive()),
310 "amp" => Ok(Self::amp_interactive()),
311 "copilot" => Ok(Self::copilot_interactive()),
312 "opencode" => Ok(Self::opencode_interactive()),
313 _ => Err(CustomBackendError),
314 }
315 }
316
317 pub fn kiro_interactive() -> Self {
322 Self {
323 command: "kiro-cli".to_string(),
324 args: vec!["chat".to_string(), "--trust-all-tools".to_string()],
325 prompt_mode: PromptMode::Arg,
326 prompt_flag: None,
327 output_format: OutputFormat::Text,
328 }
329 }
330
331 pub fn gemini_interactive() -> Self {
336 Self {
337 command: "gemini".to_string(),
338 args: vec!["--yolo".to_string()],
339 prompt_mode: PromptMode::Arg,
340 prompt_flag: Some("-i".to_string()), output_format: OutputFormat::Text,
342 }
343 }
344
345 pub fn codex_interactive() -> Self {
350 Self {
351 command: "codex".to_string(),
352 args: vec![], prompt_mode: PromptMode::Arg,
354 prompt_flag: None, output_format: OutputFormat::Text,
356 }
357 }
358
359 pub fn amp_interactive() -> Self {
364 Self {
365 command: "amp".to_string(),
366 args: vec![],
367 prompt_mode: PromptMode::Arg,
368 prompt_flag: Some("-x".to_string()),
369 output_format: OutputFormat::Text,
370 }
371 }
372
373 pub fn copilot_interactive() -> Self {
378 Self {
379 command: "copilot".to_string(),
380 args: vec![],
381 prompt_mode: PromptMode::Arg,
382 prompt_flag: Some("-p".to_string()),
383 output_format: OutputFormat::Text,
384 }
385 }
386
387 pub fn opencode() -> Self {
397 Self {
398 command: "opencode".to_string(),
399 args: vec!["run".to_string()],
400 prompt_mode: PromptMode::Arg,
401 prompt_flag: None, output_format: OutputFormat::Text,
403 }
404 }
405
406 pub fn opencode_tui() -> Self {
414 Self {
415 command: "opencode".to_string(),
416 args: vec!["run".to_string()],
417 prompt_mode: PromptMode::Arg,
418 prompt_flag: None, output_format: OutputFormat::Text,
420 }
421 }
422
423 pub fn opencode_interactive() -> Self {
433 Self {
434 command: "opencode".to_string(),
435 args: vec![],
436 prompt_mode: PromptMode::Arg,
437 prompt_flag: Some("--prompt".to_string()),
438 output_format: OutputFormat::Text,
439 }
440 }
441
442 pub fn custom(config: &CliConfig) -> Result<Self, CustomBackendError> {
447 let command = config.command.clone().ok_or(CustomBackendError)?;
448 let prompt_mode = if config.prompt_mode == "stdin" {
449 PromptMode::Stdin
450 } else {
451 PromptMode::Arg
452 };
453
454 Ok(Self {
455 command,
456 args: config.args.clone(),
457 prompt_mode,
458 prompt_flag: config.prompt_flag.clone(),
459 output_format: OutputFormat::Text,
460 })
461 }
462
463 pub fn build_command(
469 &self,
470 prompt: &str,
471 interactive: bool,
472 ) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
473 let mut args = self.args.clone();
474
475 if interactive {
477 args = self.filter_args_for_interactive(args);
478 }
479
480 let (stdin_input, temp_file) = match self.prompt_mode {
482 PromptMode::Arg => {
483 let (prompt_text, temp_file) = if self.command == "claude" && prompt.len() > 7000 {
484 match NamedTempFile::new() {
486 Ok(mut file) => {
487 if let Err(e) = file.write_all(prompt.as_bytes()) {
488 tracing::warn!("Failed to write prompt to temp file: {}", e);
489 (prompt.to_string(), None)
490 } else {
491 let path = file.path().display().to_string();
492 (
493 format!("Please read and execute the task in {}", path),
494 Some(file),
495 )
496 }
497 }
498 Err(e) => {
499 tracing::warn!("Failed to create temp file: {}", e);
500 (prompt.to_string(), None)
501 }
502 }
503 } else {
504 (prompt.to_string(), None)
505 };
506
507 if let Some(ref flag) = self.prompt_flag {
508 args.push(flag.clone());
509 }
510 args.push(prompt_text);
511 (None, temp_file)
512 }
513 PromptMode::Stdin => (Some(prompt.to_string()), None),
514 };
515
516 tracing::debug!(
518 command = %self.command,
519 args_count = args.len(),
520 prompt_len = prompt.len(),
521 interactive = interactive,
522 uses_stdin = stdin_input.is_some(),
523 uses_temp_file = temp_file.is_some(),
524 "Built CLI command"
525 );
526 tracing::trace!(prompt = %prompt, "Full prompt content");
528
529 (self.command.clone(), args, stdin_input, temp_file)
530 }
531
532 fn filter_args_for_interactive(&self, args: Vec<String>) -> Vec<String> {
534 match self.command.as_str() {
535 "kiro-cli" => args
536 .into_iter()
537 .filter(|a| a != "--no-interactive")
538 .collect(),
539 "codex" => args.into_iter().filter(|a| a != "--full-auto").collect(),
540 "amp" => args
541 .into_iter()
542 .filter(|a| a != "--dangerously-allow-all")
543 .collect(),
544 "copilot" => args
545 .into_iter()
546 .filter(|a| a != "--allow-all-tools")
547 .collect(),
548 _ => args, }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn test_claude_backend() {
559 let backend = CliBackend::claude();
560 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
561
562 assert_eq!(cmd, "claude");
563 assert_eq!(
564 args,
565 vec![
566 "--dangerously-skip-permissions",
567 "--verbose",
568 "--output-format",
569 "stream-json",
570 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
571 "-p",
572 "test prompt"
573 ]
574 );
575 assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::StreamJson);
577 }
578
579 #[test]
580 fn test_claude_interactive_backend() {
581 let backend = CliBackend::claude_interactive();
582 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
583
584 assert_eq!(cmd, "claude");
585 assert_eq!(
589 args,
590 vec![
591 "--dangerously-skip-permissions",
592 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
593 "test prompt"
594 ]
595 );
596 assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::Text);
598 assert_eq!(backend.prompt_flag, None);
599 }
600
601 #[test]
602 fn test_claude_large_prompt_uses_temp_file() {
603 let backend = CliBackend::claude();
605 let large_prompt = "x".repeat(7001);
606 let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
607
608 assert_eq!(cmd, "claude");
609 assert!(temp.is_some());
611 assert!(args.iter().any(|a| a.contains("Please read and execute")));
613 }
614
615 #[test]
616 fn test_non_claude_large_prompt() {
617 let backend = CliBackend::kiro();
618 let large_prompt = "x".repeat(7001);
619 let (cmd, args, stdin, temp) = backend.build_command(&large_prompt, false);
620
621 assert_eq!(cmd, "kiro-cli");
622 assert_eq!(args[3], large_prompt);
623 assert!(stdin.is_none());
624 assert!(temp.is_none());
625 }
626
627 #[test]
628 fn test_kiro_backend() {
629 let backend = CliBackend::kiro();
630 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
631
632 assert_eq!(cmd, "kiro-cli");
633 assert_eq!(
634 args,
635 vec![
636 "chat",
637 "--no-interactive",
638 "--trust-all-tools",
639 "test prompt"
640 ]
641 );
642 assert!(stdin.is_none());
643 }
644
645 #[test]
646 fn test_gemini_backend() {
647 let backend = CliBackend::gemini();
648 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
649
650 assert_eq!(cmd, "gemini");
651 assert_eq!(args, vec!["--yolo", "-p", "test prompt"]);
652 assert!(stdin.is_none());
653 }
654
655 #[test]
656 fn test_codex_backend() {
657 let backend = CliBackend::codex();
658 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
659
660 assert_eq!(cmd, "codex");
661 assert_eq!(args, vec!["exec", "--yolo", "--full-auto", "test prompt"]);
662 assert!(stdin.is_none());
663 }
664
665 #[test]
666 fn test_amp_backend() {
667 let backend = CliBackend::amp();
668 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
669
670 assert_eq!(cmd, "amp");
671 assert_eq!(args, vec!["--dangerously-allow-all", "-x", "test prompt"]);
672 assert!(stdin.is_none());
673 }
674
675 #[test]
676 fn test_copilot_backend() {
677 let backend = CliBackend::copilot();
678 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
679
680 assert_eq!(cmd, "copilot");
681 assert_eq!(args, vec!["--allow-all-tools", "-p", "test prompt"]);
682 assert!(stdin.is_none());
683 assert_eq!(backend.output_format, OutputFormat::Text);
684 }
685
686 #[test]
687 fn test_copilot_tui_backend() {
688 let backend = CliBackend::copilot_tui();
689 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
690
691 assert_eq!(cmd, "copilot");
692 assert_eq!(args, vec!["test prompt"]);
694 assert!(stdin.is_none());
695 assert_eq!(backend.output_format, OutputFormat::Text);
696 assert_eq!(backend.prompt_flag, None);
697 }
698
699 #[test]
700 fn test_from_config() {
701 let config = CliConfig {
703 backend: "claude".to_string(),
704 command: None,
705 prompt_mode: "arg".to_string(),
706 ..Default::default()
707 };
708 let backend = CliBackend::from_config(&config).unwrap();
709
710 assert_eq!(backend.command, "claude");
711 assert_eq!(backend.prompt_mode, PromptMode::Arg);
712 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
713 }
714
715 #[test]
716 fn test_from_config_command_override() {
717 let config = CliConfig {
718 backend: "claude".to_string(),
719 command: Some("my-custom-claude".to_string()),
720 prompt_mode: "arg".to_string(),
721 ..Default::default()
722 };
723 let backend = CliBackend::from_config(&config).unwrap();
724
725 assert_eq!(backend.command, "my-custom-claude");
726 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
727 assert_eq!(backend.output_format, OutputFormat::StreamJson);
728 }
729
730 #[test]
731 fn test_kiro_interactive_mode_omits_no_interactive_flag() {
732 let backend = CliBackend::kiro();
733 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
734
735 assert_eq!(cmd, "kiro-cli");
736 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
737 assert!(stdin.is_none());
738 assert!(!args.contains(&"--no-interactive".to_string()));
739 }
740
741 #[test]
742 fn test_codex_interactive_mode_omits_full_auto() {
743 let backend = CliBackend::codex();
744 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
745
746 assert_eq!(cmd, "codex");
747 assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
748 assert!(stdin.is_none());
749 assert!(!args.contains(&"--full-auto".to_string()));
750 }
751
752 #[test]
753 fn test_amp_interactive_mode_no_flags() {
754 let backend = CliBackend::amp();
755 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
756
757 assert_eq!(cmd, "amp");
758 assert_eq!(args, vec!["-x", "test prompt"]);
759 assert!(stdin.is_none());
760 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
761 }
762
763 #[test]
764 fn test_copilot_interactive_mode_omits_allow_all_tools() {
765 let backend = CliBackend::copilot();
766 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
767
768 assert_eq!(cmd, "copilot");
769 assert_eq!(args, vec!["-p", "test prompt"]);
770 assert!(stdin.is_none());
771 assert!(!args.contains(&"--allow-all-tools".to_string()));
772 }
773
774 #[test]
775 fn test_claude_interactive_mode_unchanged() {
776 let backend = CliBackend::claude();
777 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
778 let (_, args_interactive, stdin_interactive, _) =
779 backend.build_command("test prompt", true);
780
781 assert_eq!(cmd, "claude");
782 assert_eq!(args_auto, args_interactive);
783 assert_eq!(
784 args_auto,
785 vec![
786 "--dangerously-skip-permissions",
787 "--verbose",
788 "--output-format",
789 "stream-json",
790 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
791 "-p",
792 "test prompt"
793 ]
794 );
795 assert!(stdin_auto.is_none());
797 assert!(stdin_interactive.is_none());
798 }
799
800 #[test]
801 fn test_gemini_interactive_mode_unchanged() {
802 let backend = CliBackend::gemini();
803 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
804 let (_, args_interactive, stdin_interactive, _) =
805 backend.build_command("test prompt", true);
806
807 assert_eq!(cmd, "gemini");
808 assert_eq!(args_auto, args_interactive);
809 assert_eq!(args_auto, vec!["--yolo", "-p", "test prompt"]);
810 assert_eq!(stdin_auto, stdin_interactive);
811 assert!(stdin_auto.is_none());
812 }
813
814 #[test]
815 fn test_custom_backend_with_prompt_flag_short() {
816 let config = CliConfig {
817 backend: "custom".to_string(),
818 command: Some("my-agent".to_string()),
819 prompt_mode: "arg".to_string(),
820 prompt_flag: Some("-p".to_string()),
821 ..Default::default()
822 };
823 let backend = CliBackend::from_config(&config).unwrap();
824 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
825
826 assert_eq!(cmd, "my-agent");
827 assert_eq!(args, vec!["-p", "test prompt"]);
828 assert!(stdin.is_none());
829 }
830
831 #[test]
832 fn test_custom_backend_with_prompt_flag_long() {
833 let config = CliConfig {
834 backend: "custom".to_string(),
835 command: Some("my-agent".to_string()),
836 prompt_mode: "arg".to_string(),
837 prompt_flag: Some("--prompt".to_string()),
838 ..Default::default()
839 };
840 let backend = CliBackend::from_config(&config).unwrap();
841 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
842
843 assert_eq!(cmd, "my-agent");
844 assert_eq!(args, vec!["--prompt", "test prompt"]);
845 assert!(stdin.is_none());
846 }
847
848 #[test]
849 fn test_custom_backend_without_prompt_flag_positional() {
850 let config = CliConfig {
851 backend: "custom".to_string(),
852 command: Some("my-agent".to_string()),
853 prompt_mode: "arg".to_string(),
854 prompt_flag: None,
855 ..Default::default()
856 };
857 let backend = CliBackend::from_config(&config).unwrap();
858 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
859
860 assert_eq!(cmd, "my-agent");
861 assert_eq!(args, vec!["test prompt"]);
862 assert!(stdin.is_none());
863 }
864
865 #[test]
866 fn test_custom_backend_without_command_returns_error() {
867 let config = CliConfig {
868 backend: "custom".to_string(),
869 command: None,
870 prompt_mode: "arg".to_string(),
871 ..Default::default()
872 };
873 let result = CliBackend::from_config(&config);
874
875 assert!(result.is_err());
876 let err = result.unwrap_err();
877 assert_eq!(
878 err.to_string(),
879 "custom backend requires a command to be specified"
880 );
881 }
882
883 #[test]
884 fn test_kiro_with_agent() {
885 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &[]);
886 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
887
888 assert_eq!(cmd, "kiro-cli");
889 assert_eq!(
890 args,
891 vec![
892 "chat",
893 "--no-interactive",
894 "--trust-all-tools",
895 "--agent",
896 "my-agent",
897 "test prompt"
898 ]
899 );
900 assert!(stdin.is_none());
901 }
902
903 #[test]
904 fn test_kiro_with_agent_extra_args() {
905 let extra_args = vec!["--verbose".to_string(), "--debug".to_string()];
906 let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &extra_args);
907 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
908
909 assert_eq!(cmd, "kiro-cli");
910 assert_eq!(
911 args,
912 vec![
913 "chat",
914 "--no-interactive",
915 "--trust-all-tools",
916 "--agent",
917 "my-agent",
918 "--verbose",
919 "--debug",
920 "test prompt"
921 ]
922 );
923 assert!(stdin.is_none());
924 }
925
926 #[test]
927 fn test_from_name_claude() {
928 let backend = CliBackend::from_name("claude").unwrap();
929 assert_eq!(backend.command, "claude");
930 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
931 }
932
933 #[test]
934 fn test_from_name_kiro() {
935 let backend = CliBackend::from_name("kiro").unwrap();
936 assert_eq!(backend.command, "kiro-cli");
937 }
938
939 #[test]
940 fn test_from_name_gemini() {
941 let backend = CliBackend::from_name("gemini").unwrap();
942 assert_eq!(backend.command, "gemini");
943 }
944
945 #[test]
946 fn test_from_name_codex() {
947 let backend = CliBackend::from_name("codex").unwrap();
948 assert_eq!(backend.command, "codex");
949 }
950
951 #[test]
952 fn test_from_name_amp() {
953 let backend = CliBackend::from_name("amp").unwrap();
954 assert_eq!(backend.command, "amp");
955 }
956
957 #[test]
958 fn test_from_name_copilot() {
959 let backend = CliBackend::from_name("copilot").unwrap();
960 assert_eq!(backend.command, "copilot");
961 assert_eq!(backend.prompt_flag, Some("-p".to_string()));
962 }
963
964 #[test]
965 fn test_from_name_invalid() {
966 let result = CliBackend::from_name("invalid");
967 assert!(result.is_err());
968 }
969
970 #[test]
971 fn test_from_hat_backend_named() {
972 let hat_backend = HatBackend::Named("claude".to_string());
973 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
974 assert_eq!(backend.command, "claude");
975 }
976
977 #[test]
978 fn test_from_hat_backend_kiro_agent() {
979 let hat_backend = HatBackend::KiroAgent {
980 backend_type: "kiro".to_string(),
981 agent: "my-agent".to_string(),
982 args: vec![],
983 };
984 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
985 let (cmd, args, _, _) = backend.build_command("test", false);
986 assert_eq!(cmd, "kiro-cli");
987 assert!(args.contains(&"--agent".to_string()));
988 assert!(args.contains(&"my-agent".to_string()));
989 }
990
991 #[test]
992 fn test_from_hat_backend_kiro_agent_with_args() {
993 let hat_backend = HatBackend::KiroAgent {
994 backend_type: "kiro".to_string(),
995 agent: "my-agent".to_string(),
996 args: vec!["--verbose".to_string()],
997 };
998 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
999 let (cmd, args, _, _) = backend.build_command("test", false);
1000 assert_eq!(cmd, "kiro-cli");
1001 assert!(args.contains(&"--agent".to_string()));
1002 assert!(args.contains(&"my-agent".to_string()));
1003 assert!(args.contains(&"--verbose".to_string()));
1004 }
1005
1006 #[test]
1007 fn test_from_hat_backend_named_with_args() {
1008 let hat_backend = HatBackend::NamedWithArgs {
1009 backend_type: "claude".to_string(),
1010 args: vec!["--model".to_string(), "claude-sonnet-4".to_string()],
1011 };
1012 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1013 assert_eq!(backend.command, "claude");
1014 assert!(backend.args.contains(&"--model".to_string()));
1015 assert!(backend.args.contains(&"claude-sonnet-4".to_string()));
1016 }
1017
1018 #[test]
1019 fn test_from_hat_backend_custom() {
1020 let hat_backend = HatBackend::Custom {
1021 command: "my-cli".to_string(),
1022 args: vec!["--flag".to_string()],
1023 };
1024 let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
1025 assert_eq!(backend.command, "my-cli");
1026 assert_eq!(backend.args, vec!["--flag"]);
1027 }
1028
1029 #[test]
1034 fn test_for_interactive_prompt_claude() {
1035 let backend = CliBackend::for_interactive_prompt("claude").unwrap();
1036 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1037
1038 assert_eq!(cmd, "claude");
1039 assert_eq!(
1041 args,
1042 vec![
1043 "--dangerously-skip-permissions",
1044 "--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
1045 "test prompt"
1046 ]
1047 );
1048 assert!(stdin.is_none());
1049 assert_eq!(backend.prompt_flag, None);
1050 }
1051
1052 #[test]
1053 fn test_for_interactive_prompt_kiro() {
1054 let backend = CliBackend::for_interactive_prompt("kiro").unwrap();
1055 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1056
1057 assert_eq!(cmd, "kiro-cli");
1058 assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
1060 assert!(!args.contains(&"--no-interactive".to_string()));
1061 assert!(stdin.is_none());
1062 }
1063
1064 #[test]
1065 fn test_for_interactive_prompt_gemini() {
1066 let backend = CliBackend::for_interactive_prompt("gemini").unwrap();
1067 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1068
1069 assert_eq!(cmd, "gemini");
1070 assert_eq!(args, vec!["--yolo", "-i", "test prompt"]);
1072 assert_eq!(backend.prompt_flag, Some("-i".to_string()));
1073 assert!(stdin.is_none());
1074 }
1075
1076 #[test]
1077 fn test_for_interactive_prompt_codex() {
1078 let backend = CliBackend::for_interactive_prompt("codex").unwrap();
1079 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1080
1081 assert_eq!(cmd, "codex");
1082 assert_eq!(args, vec!["test prompt"]);
1084 assert!(!args.contains(&"exec".to_string()));
1085 assert!(!args.contains(&"--full-auto".to_string()));
1086 assert!(stdin.is_none());
1087 }
1088
1089 #[test]
1090 fn test_for_interactive_prompt_amp() {
1091 let backend = CliBackend::for_interactive_prompt("amp").unwrap();
1092 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1093
1094 assert_eq!(cmd, "amp");
1095 assert_eq!(args, vec!["-x", "test prompt"]);
1097 assert!(!args.contains(&"--dangerously-allow-all".to_string()));
1098 assert!(stdin.is_none());
1099 }
1100
1101 #[test]
1102 fn test_for_interactive_prompt_copilot() {
1103 let backend = CliBackend::for_interactive_prompt("copilot").unwrap();
1104 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1105
1106 assert_eq!(cmd, "copilot");
1107 assert_eq!(args, vec!["-p", "test prompt"]);
1109 assert!(!args.contains(&"--allow-all-tools".to_string()));
1110 assert!(stdin.is_none());
1111 }
1112
1113 #[test]
1114 fn test_for_interactive_prompt_invalid() {
1115 let result = CliBackend::for_interactive_prompt("invalid_backend");
1116 assert!(result.is_err());
1117 }
1118
1119 #[test]
1124 fn test_opencode_backend() {
1125 let backend = CliBackend::opencode();
1126 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1127
1128 assert_eq!(cmd, "opencode");
1129 assert_eq!(args, vec!["run", "test prompt"]);
1131 assert!(stdin.is_none());
1132 assert_eq!(backend.output_format, OutputFormat::Text);
1133 assert_eq!(backend.prompt_flag, None);
1134 }
1135
1136 #[test]
1137 fn test_opencode_tui_backend() {
1138 let backend = CliBackend::opencode_tui();
1139 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1140
1141 assert_eq!(cmd, "opencode");
1142 assert_eq!(args, vec!["run", "test prompt"]);
1144 assert!(stdin.is_none());
1145 assert_eq!(backend.output_format, OutputFormat::Text);
1146 assert_eq!(backend.prompt_flag, None);
1147 }
1148
1149 #[test]
1150 fn test_opencode_interactive_mode_unchanged() {
1151 let backend = CliBackend::opencode();
1153 let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
1154 let (_, args_interactive, stdin_interactive, _) =
1155 backend.build_command("test prompt", true);
1156
1157 assert_eq!(cmd, "opencode");
1158 assert_eq!(args_auto, args_interactive);
1160 assert_eq!(args_auto, vec!["run", "test prompt"]);
1161 assert!(stdin_auto.is_none());
1162 assert!(stdin_interactive.is_none());
1163 }
1164
1165 #[test]
1166 fn test_from_name_opencode() {
1167 let backend = CliBackend::from_name("opencode").unwrap();
1168 assert_eq!(backend.command, "opencode");
1169 assert_eq!(backend.prompt_flag, None); }
1171
1172 #[test]
1173 fn test_for_interactive_prompt_opencode() {
1174 let backend = CliBackend::for_interactive_prompt("opencode").unwrap();
1175 let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
1176
1177 assert_eq!(cmd, "opencode");
1178 assert_eq!(args, vec!["--prompt", "test prompt"]);
1180 assert!(stdin.is_none());
1181 assert_eq!(backend.prompt_flag, Some("--prompt".to_string()));
1182 }
1183
1184 #[test]
1185 fn test_opencode_interactive_launches_tui_not_headless() {
1186 let backend = CliBackend::opencode_interactive();
1196 let (cmd, args, _, _) = backend.build_command("test prompt", true);
1197
1198 assert_eq!(cmd, "opencode");
1199 assert!(
1202 !args.contains(&"run".to_string()),
1203 "opencode_interactive() should not use 'run' subcommand. \
1204 'opencode run' is headless mode, but interactive mode needs TUI. \
1205 Expected: opencode --prompt \"test prompt\", got: opencode {}",
1206 args.join(" ")
1207 );
1208 assert!(
1210 args.contains(&"--prompt".to_string()),
1211 "opencode_interactive() should use --prompt flag for TUI mode. \
1212 Expected args to contain '--prompt', got: {:?}",
1213 args
1214 );
1215 }
1216
1217 #[test]
1218 fn test_custom_args_can_be_appended() {
1219 let mut backend = CliBackend::opencode();
1222
1223 let custom_args = vec!["--model=gpt-4".to_string(), "--temperature=0.7".to_string()];
1225 backend.args.extend(custom_args.clone());
1226
1227 let (cmd, args, _, _) = backend.build_command("test prompt", false);
1229
1230 assert_eq!(cmd, "opencode");
1231 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();
1239 let model_idx = args.iter().position(|a| a == "--model=gpt-4").unwrap();
1240 assert!(
1241 run_idx < model_idx,
1242 "Original args should come before custom args"
1243 );
1244 }
1245}