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