use ralph_core::{CliConfig, HatBackend};
use std::fmt;
use std::io::Write;
use tempfile::NamedTempFile;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Text,
StreamJson,
PiStreamJson,
Acp,
}
#[derive(Debug, Clone)]
pub struct CustomBackendError;
impl fmt::Display for CustomBackendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "custom backend requires a command to be specified")
}
}
impl std::error::Error for CustomBackendError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptMode {
Arg,
Stdin,
}
#[derive(Debug, Clone)]
pub struct CliBackend {
pub command: String,
pub args: Vec<String>,
pub prompt_mode: PromptMode,
pub prompt_flag: Option<String>,
pub output_format: OutputFormat,
pub env_vars: Vec<(String, String)>,
}
impl CliBackend {
pub fn from_config(config: &CliConfig) -> Result<Self, CustomBackendError> {
let mut backend = match config.backend.as_str() {
"claude" => Self::claude(),
"kiro" => Self::kiro(),
"kiro-acp" => Self::kiro_acp(),
"gemini" => Self::gemini(),
"codex" => Self::codex(),
"amp" => Self::amp(),
"copilot" => Self::copilot(),
"opencode" => Self::opencode(),
"pi" => Self::pi(),
"custom" => return Self::custom(config),
_ => Self::claude(), };
backend.args.extend(config.args.iter().cloned());
if backend.command == "codex" {
Self::reconcile_codex_args(&mut backend.args);
}
if let Some(ref cmd) = config.command {
backend.command = cmd.clone();
}
Ok(backend)
}
pub fn claude() -> Self {
Self {
command: "claude".to_string(),
args: vec![
"--dangerously-skip-permissions".to_string(),
"--verbose".to_string(),
"--output-format".to_string(),
"stream-json".to_string(),
"--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("-p".to_string()),
output_format: OutputFormat::StreamJson,
env_vars: vec![],
}
}
pub fn claude_interactive() -> Self {
Self {
command: "claude".to_string(),
args: vec![
"--dangerously-skip-permissions".to_string(),
"--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet".to_string(),
],
prompt_mode: PromptMode::Arg,
prompt_flag: None,
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn kiro() -> Self {
Self {
command: "kiro-cli".to_string(),
args: vec![
"chat".to_string(),
"--no-interactive".to_string(),
"--trust-all-tools".to_string(),
],
prompt_mode: PromptMode::Arg,
prompt_flag: None,
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn kiro_with_agent(agent: String, extra_args: &[String]) -> Self {
let mut backend = Self {
command: "kiro-cli".to_string(),
args: vec![
"chat".to_string(),
"--no-interactive".to_string(),
"--trust-all-tools".to_string(),
"--agent".to_string(),
agent,
],
prompt_mode: PromptMode::Arg,
prompt_flag: None,
output_format: OutputFormat::Text,
env_vars: vec![],
};
backend.args.extend(extra_args.iter().cloned());
backend
}
pub fn kiro_acp() -> Self {
Self::kiro_acp_with_options(None, None)
}
pub fn kiro_acp_with_options(agent: Option<&str>, model: Option<&str>) -> Self {
let mut args = vec!["acp".to_string()];
if let Some(name) = agent {
args.push("--agent".to_string());
args.push(name.to_string());
}
if let Some(m) = model {
args.push("--model".to_string());
args.push(m.to_string());
}
Self {
command: "kiro-cli".to_string(),
args,
prompt_mode: PromptMode::Stdin,
prompt_flag: None,
output_format: OutputFormat::Acp,
env_vars: vec![],
}
}
pub fn from_name_with_args(
name: &str,
extra_args: &[String],
) -> Result<Self, CustomBackendError> {
let mut backend = Self::from_name(name)?;
backend.args.extend(extra_args.iter().cloned());
if backend.command == "codex" {
Self::reconcile_codex_args(&mut backend.args);
}
Ok(backend)
}
pub fn from_name(name: &str) -> Result<Self, CustomBackendError> {
match name {
"claude" => Ok(Self::claude()),
"kiro" => Ok(Self::kiro()),
"kiro-acp" => Ok(Self::kiro_acp()),
"gemini" => Ok(Self::gemini()),
"codex" => Ok(Self::codex()),
"amp" => Ok(Self::amp()),
"copilot" => Ok(Self::copilot()),
"opencode" => Ok(Self::opencode()),
"pi" => Ok(Self::pi()),
_ => Err(CustomBackendError),
}
}
pub fn from_hat_backend(hat_backend: &HatBackend) -> Result<Self, CustomBackendError> {
match hat_backend {
HatBackend::Named(name) => Self::from_name(name),
HatBackend::NamedWithArgs { backend_type, args } => {
Self::from_name_with_args(backend_type, args)
}
HatBackend::KiroAgent {
backend_type,
agent,
args,
} => {
if backend_type == "kiro-acp" {
Ok(Self::kiro_acp_with_options(Some(agent), None))
} else {
Ok(Self::kiro_with_agent(agent.clone(), args))
}
}
HatBackend::Custom { command, args } => Ok(Self {
command: command.clone(),
args: args.clone(),
prompt_mode: PromptMode::Arg,
prompt_flag: None,
output_format: OutputFormat::Text,
env_vars: vec![],
}),
}
}
pub fn gemini() -> Self {
Self {
command: "gemini".to_string(),
args: vec!["--yolo".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("-p".to_string()),
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn codex() -> Self {
Self {
command: "codex".to_string(),
args: vec!["exec".to_string(), "--yolo".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: None, output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn amp() -> Self {
Self {
command: "amp".to_string(),
args: vec!["--dangerously-allow-all".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("-x".to_string()),
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn copilot() -> Self {
Self {
command: "copilot".to_string(),
args: vec!["--allow-all-tools".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("-p".to_string()),
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn copilot_tui() -> Self {
Self {
command: "copilot".to_string(),
args: vec![], prompt_mode: PromptMode::Arg,
prompt_flag: None, output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn claude_interactive_teams() -> Self {
Self {
command: "claude".to_string(),
args: vec![
"--dangerously-skip-permissions".to_string(),
"--disallowedTools=TodoWrite".to_string(),
],
prompt_mode: PromptMode::Arg,
prompt_flag: None,
output_format: OutputFormat::Text,
env_vars: vec![(
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
"1".to_string(),
)],
}
}
pub fn for_interactive_prompt(backend_name: &str) -> Result<Self, CustomBackendError> {
match backend_name {
"claude" => Ok(Self::claude_interactive()),
"kiro" => Ok(Self::kiro_interactive()),
"gemini" => Ok(Self::gemini_interactive()),
"codex" => Ok(Self::codex_interactive()),
"amp" => Ok(Self::amp_interactive()),
"copilot" => Ok(Self::copilot_interactive()),
"opencode" => Ok(Self::opencode_interactive()),
"pi" => Ok(Self::pi_interactive()),
_ => Err(CustomBackendError),
}
}
pub fn kiro_interactive() -> Self {
Self {
command: "kiro-cli".to_string(),
args: vec!["chat".to_string(), "--trust-all-tools".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: None,
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn gemini_interactive() -> Self {
Self {
command: "gemini".to_string(),
args: vec!["--yolo".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("-i".to_string()), output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn codex_interactive() -> Self {
Self {
command: "codex".to_string(),
args: vec![], prompt_mode: PromptMode::Arg,
prompt_flag: None, output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn amp_interactive() -> Self {
Self {
command: "amp".to_string(),
args: vec![],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("-x".to_string()),
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn copilot_interactive() -> Self {
Self {
command: "copilot".to_string(),
args: vec![],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("-p".to_string()),
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn opencode() -> Self {
Self {
command: "opencode".to_string(),
args: vec!["run".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: None, output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn opencode_tui() -> Self {
Self {
command: "opencode".to_string(),
args: vec!["run".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: None, output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn opencode_interactive() -> Self {
Self {
command: "opencode".to_string(),
args: vec![],
prompt_mode: PromptMode::Arg,
prompt_flag: Some("--prompt".to_string()),
output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn pi() -> Self {
Self {
command: "pi".to_string(),
args: vec![
"-p".to_string(),
"--mode".to_string(),
"json".to_string(),
"--no-session".to_string(),
],
prompt_mode: PromptMode::Arg,
prompt_flag: None, output_format: OutputFormat::PiStreamJson,
env_vars: vec![],
}
}
pub fn pi_interactive() -> Self {
Self {
command: "pi".to_string(),
args: vec!["--no-session".to_string()],
prompt_mode: PromptMode::Arg,
prompt_flag: None, output_format: OutputFormat::Text,
env_vars: vec![],
}
}
pub fn custom(config: &CliConfig) -> Result<Self, CustomBackendError> {
let command = config.command.clone().ok_or(CustomBackendError)?;
let prompt_mode = if config.prompt_mode == "stdin" {
PromptMode::Stdin
} else {
PromptMode::Arg
};
Ok(Self {
command,
args: config.args.clone(),
prompt_mode,
prompt_flag: config.prompt_flag.clone(),
output_format: OutputFormat::Text,
env_vars: vec![],
})
}
pub fn build_command(
&self,
prompt: &str,
interactive: bool,
) -> (String, Vec<String>, Option<String>, Option<NamedTempFile>) {
let mut args = self.args.clone();
if interactive {
args = self.filter_args_for_interactive(args);
}
let (stdin_input, temp_file) = match self.prompt_mode {
PromptMode::Arg => {
let (prompt_text, temp_file) = if self.command == "claude" && prompt.len() > 7000 {
match NamedTempFile::new() {
Ok(mut file) => {
if let Err(e) = file.write_all(prompt.as_bytes()) {
tracing::warn!("Failed to write prompt to temp file: {}", e);
(prompt.to_string(), None)
} else {
let path = file.path().display().to_string();
(
format!("Please read and execute the task in {}", path),
Some(file),
)
}
}
Err(e) => {
tracing::warn!("Failed to create temp file: {}", e);
(prompt.to_string(), None)
}
}
} else {
(prompt.to_string(), None)
};
if let Some(ref flag) = self.prompt_flag {
args.push(flag.clone());
}
args.push(prompt_text);
(None, temp_file)
}
PromptMode::Stdin => (Some(prompt.to_string()), None),
};
tracing::debug!(
command = %self.command,
args_count = args.len(),
prompt_len = prompt.len(),
interactive = interactive,
uses_stdin = stdin_input.is_some(),
uses_temp_file = temp_file.is_some(),
"Built CLI command"
);
tracing::trace!(prompt = %prompt, "Full prompt content");
(self.command.clone(), args, stdin_input, temp_file)
}
fn filter_args_for_interactive(&self, args: Vec<String>) -> Vec<String> {
match self.command.as_str() {
"kiro-cli" => args
.into_iter()
.filter(|a| a != "--no-interactive")
.collect(),
"codex" => args.into_iter().filter(|a| a != "--full-auto").collect(),
"amp" => args
.into_iter()
.filter(|a| a != "--dangerously-allow-all")
.collect(),
"copilot" => args
.into_iter()
.filter(|a| a != "--allow-all-tools")
.collect(),
_ => args, }
}
fn reconcile_codex_args(args: &mut Vec<String>) {
let had_dangerous_bypass = args
.iter()
.any(|arg| arg == "--dangerously-bypass-approvals-and-sandbox");
if had_dangerous_bypass {
args.retain(|arg| arg != "--dangerously-bypass-approvals-and-sandbox");
if !args.iter().any(|arg| arg == "--yolo") {
if let Some(pos) = args.iter().position(|arg| arg == "exec") {
args.insert(pos + 1, "--yolo".to_string());
} else {
args.push("--yolo".to_string());
}
}
}
if args.iter().any(|arg| arg == "--yolo") {
args.retain(|arg| arg != "--full-auto");
let mut seen_yolo = false;
args.retain(|arg| {
if arg == "--yolo" {
if seen_yolo {
return false;
}
seen_yolo = true;
}
true
});
if !seen_yolo {
if let Some(pos) = args.iter().position(|arg| arg == "exec") {
args.insert(pos + 1, "--yolo".to_string());
} else {
args.push("--yolo".to_string());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_claude_backend() {
let backend = CliBackend::claude();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "claude");
assert_eq!(
args,
vec![
"--dangerously-skip-permissions",
"--verbose",
"--output-format",
"stream-json",
"--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
"-p",
"test prompt"
]
);
assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::StreamJson);
}
#[test]
fn test_claude_interactive_backend() {
let backend = CliBackend::claude_interactive();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "claude");
assert_eq!(
args,
vec![
"--dangerously-skip-permissions",
"--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
"test prompt"
]
);
assert!(stdin.is_none()); assert_eq!(backend.output_format, OutputFormat::Text);
assert_eq!(backend.prompt_flag, None);
}
#[test]
fn test_claude_large_prompt_uses_temp_file() {
let backend = CliBackend::claude();
let large_prompt = "x".repeat(7001);
let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
assert_eq!(cmd, "claude");
assert!(temp.is_some());
assert!(args.iter().any(|a| a.contains("Please read and execute")));
}
#[test]
fn test_non_claude_large_prompt() {
let backend = CliBackend::kiro();
let large_prompt = "x".repeat(7001);
let (cmd, args, stdin, temp) = backend.build_command(&large_prompt, false);
assert_eq!(cmd, "kiro-cli");
assert_eq!(args[3], large_prompt);
assert!(stdin.is_none());
assert!(temp.is_none());
}
#[test]
fn test_kiro_backend() {
let backend = CliBackend::kiro();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "kiro-cli");
assert_eq!(
args,
vec![
"chat",
"--no-interactive",
"--trust-all-tools",
"test prompt"
]
);
assert!(stdin.is_none());
}
#[test]
fn test_gemini_backend() {
let backend = CliBackend::gemini();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "gemini");
assert_eq!(args, vec!["--yolo", "-p", "test prompt"]);
assert!(stdin.is_none());
}
#[test]
fn test_codex_backend() {
let backend = CliBackend::codex();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "codex");
assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
assert!(stdin.is_none());
}
#[test]
fn test_amp_backend() {
let backend = CliBackend::amp();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "amp");
assert_eq!(args, vec!["--dangerously-allow-all", "-x", "test prompt"]);
assert!(stdin.is_none());
}
#[test]
fn test_copilot_backend() {
let backend = CliBackend::copilot();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "copilot");
assert_eq!(args, vec!["--allow-all-tools", "-p", "test prompt"]);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::Text);
}
#[test]
fn test_copilot_tui_backend() {
let backend = CliBackend::copilot_tui();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "copilot");
assert_eq!(args, vec!["test prompt"]);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::Text);
assert_eq!(backend.prompt_flag, None);
}
#[test]
fn test_from_config() {
let config = CliConfig {
backend: "claude".to_string(),
command: None,
prompt_mode: "arg".to_string(),
..Default::default()
};
let backend = CliBackend::from_config(&config).unwrap();
assert_eq!(backend.command, "claude");
assert_eq!(backend.prompt_mode, PromptMode::Arg);
assert_eq!(backend.prompt_flag, Some("-p".to_string()));
}
#[test]
fn test_from_config_command_override() {
let config = CliConfig {
backend: "claude".to_string(),
command: Some("my-custom-claude".to_string()),
prompt_mode: "arg".to_string(),
..Default::default()
};
let backend = CliBackend::from_config(&config).unwrap();
assert_eq!(backend.command, "my-custom-claude");
assert_eq!(backend.prompt_flag, Some("-p".to_string()));
assert_eq!(backend.output_format, OutputFormat::StreamJson);
}
#[test]
fn test_kiro_interactive_mode_omits_no_interactive_flag() {
let backend = CliBackend::kiro();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
assert_eq!(cmd, "kiro-cli");
assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
assert!(stdin.is_none());
assert!(!args.contains(&"--no-interactive".to_string()));
}
#[test]
fn test_codex_interactive_mode_omits_full_auto() {
let backend = CliBackend::codex();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
assert_eq!(cmd, "codex");
assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
assert!(stdin.is_none());
assert!(!args.contains(&"--full-auto".to_string()));
}
#[test]
fn test_amp_interactive_mode_no_flags() {
let backend = CliBackend::amp();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
assert_eq!(cmd, "amp");
assert_eq!(args, vec!["-x", "test prompt"]);
assert!(stdin.is_none());
assert!(!args.contains(&"--dangerously-allow-all".to_string()));
}
#[test]
fn test_copilot_interactive_mode_omits_allow_all_tools() {
let backend = CliBackend::copilot();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", true);
assert_eq!(cmd, "copilot");
assert_eq!(args, vec!["-p", "test prompt"]);
assert!(stdin.is_none());
assert!(!args.contains(&"--allow-all-tools".to_string()));
}
#[test]
fn test_claude_interactive_mode_unchanged() {
let backend = CliBackend::claude();
let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
let (_, args_interactive, stdin_interactive, _) =
backend.build_command("test prompt", true);
assert_eq!(cmd, "claude");
assert_eq!(args_auto, args_interactive);
assert_eq!(
args_auto,
vec![
"--dangerously-skip-permissions",
"--verbose",
"--output-format",
"stream-json",
"--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
"-p",
"test prompt"
]
);
assert!(stdin_auto.is_none());
assert!(stdin_interactive.is_none());
}
#[test]
fn test_gemini_interactive_mode_unchanged() {
let backend = CliBackend::gemini();
let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
let (_, args_interactive, stdin_interactive, _) =
backend.build_command("test prompt", true);
assert_eq!(cmd, "gemini");
assert_eq!(args_auto, args_interactive);
assert_eq!(args_auto, vec!["--yolo", "-p", "test prompt"]);
assert_eq!(stdin_auto, stdin_interactive);
assert!(stdin_auto.is_none());
}
#[test]
fn test_custom_backend_with_prompt_flag_short() {
let config = CliConfig {
backend: "custom".to_string(),
command: Some("my-agent".to_string()),
prompt_mode: "arg".to_string(),
prompt_flag: Some("-p".to_string()),
..Default::default()
};
let backend = CliBackend::from_config(&config).unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "my-agent");
assert_eq!(args, vec!["-p", "test prompt"]);
assert!(stdin.is_none());
}
#[test]
fn test_custom_backend_with_prompt_flag_long() {
let config = CliConfig {
backend: "custom".to_string(),
command: Some("my-agent".to_string()),
prompt_mode: "arg".to_string(),
prompt_flag: Some("--prompt".to_string()),
..Default::default()
};
let backend = CliBackend::from_config(&config).unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "my-agent");
assert_eq!(args, vec!["--prompt", "test prompt"]);
assert!(stdin.is_none());
}
#[test]
fn test_custom_backend_without_prompt_flag_positional() {
let config = CliConfig {
backend: "custom".to_string(),
command: Some("my-agent".to_string()),
prompt_mode: "arg".to_string(),
prompt_flag: None,
..Default::default()
};
let backend = CliBackend::from_config(&config).unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "my-agent");
assert_eq!(args, vec!["test prompt"]);
assert!(stdin.is_none());
}
#[test]
fn test_custom_backend_without_command_returns_error() {
let config = CliConfig {
backend: "custom".to_string(),
command: None,
prompt_mode: "arg".to_string(),
..Default::default()
};
let result = CliBackend::from_config(&config);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(
err.to_string(),
"custom backend requires a command to be specified"
);
}
#[test]
fn test_kiro_with_agent() {
let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &[]);
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "kiro-cli");
assert_eq!(
args,
vec![
"chat",
"--no-interactive",
"--trust-all-tools",
"--agent",
"my-agent",
"test prompt"
]
);
assert!(stdin.is_none());
}
#[test]
fn test_kiro_with_agent_extra_args() {
let extra_args = vec!["--verbose".to_string(), "--debug".to_string()];
let backend = CliBackend::kiro_with_agent("my-agent".to_string(), &extra_args);
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "kiro-cli");
assert_eq!(
args,
vec![
"chat",
"--no-interactive",
"--trust-all-tools",
"--agent",
"my-agent",
"--verbose",
"--debug",
"test prompt"
]
);
assert!(stdin.is_none());
}
#[test]
fn test_from_name_claude() {
let backend = CliBackend::from_name("claude").unwrap();
assert_eq!(backend.command, "claude");
assert_eq!(backend.prompt_flag, Some("-p".to_string()));
}
#[test]
fn test_from_name_kiro() {
let backend = CliBackend::from_name("kiro").unwrap();
assert_eq!(backend.command, "kiro-cli");
}
#[test]
fn test_from_name_gemini() {
let backend = CliBackend::from_name("gemini").unwrap();
assert_eq!(backend.command, "gemini");
}
#[test]
fn test_from_name_codex() {
let backend = CliBackend::from_name("codex").unwrap();
assert_eq!(backend.command, "codex");
}
#[test]
fn test_from_name_amp() {
let backend = CliBackend::from_name("amp").unwrap();
assert_eq!(backend.command, "amp");
}
#[test]
fn test_from_name_copilot() {
let backend = CliBackend::from_name("copilot").unwrap();
assert_eq!(backend.command, "copilot");
assert_eq!(backend.prompt_flag, Some("-p".to_string()));
}
#[test]
fn test_from_name_invalid() {
let result = CliBackend::from_name("invalid");
assert!(result.is_err());
}
#[test]
fn test_from_hat_backend_named() {
let hat_backend = HatBackend::Named("claude".to_string());
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
assert_eq!(backend.command, "claude");
}
#[test]
fn test_from_hat_backend_kiro_agent() {
let hat_backend = HatBackend::KiroAgent {
backend_type: "kiro".to_string(),
agent: "my-agent".to_string(),
args: vec![],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
let (cmd, args, _, _) = backend.build_command("test", false);
assert_eq!(cmd, "kiro-cli");
assert!(args.contains(&"--agent".to_string()));
assert!(args.contains(&"my-agent".to_string()));
}
#[test]
fn test_from_hat_backend_kiro_acp_agent_uses_acp_executor() {
let hat_backend = HatBackend::KiroAgent {
backend_type: "kiro-acp".to_string(),
agent: "my-agent".to_string(),
args: vec![],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
assert_eq!(backend.command, "kiro-cli");
assert_eq!(backend.output_format, OutputFormat::Acp);
assert!(backend.args.contains(&"acp".to_string()));
assert!(backend.args.contains(&"--agent".to_string()));
assert!(backend.args.contains(&"my-agent".to_string()));
}
#[test]
fn test_from_hat_backend_kiro_agent_with_args() {
let hat_backend = HatBackend::KiroAgent {
backend_type: "kiro".to_string(),
agent: "my-agent".to_string(),
args: vec!["--verbose".to_string()],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
let (cmd, args, _, _) = backend.build_command("test", false);
assert_eq!(cmd, "kiro-cli");
assert!(args.contains(&"--agent".to_string()));
assert!(args.contains(&"my-agent".to_string()));
assert!(args.contains(&"--verbose".to_string()));
}
#[test]
fn test_from_hat_backend_named_with_args() {
let hat_backend = HatBackend::NamedWithArgs {
backend_type: "claude".to_string(),
args: vec!["--model".to_string(), "claude-sonnet-4".to_string()],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
assert_eq!(backend.command, "claude");
assert!(backend.args.contains(&"--model".to_string()));
assert!(backend.args.contains(&"claude-sonnet-4".to_string()));
}
#[test]
fn test_codex_named_with_args_dangerous_bypass_normalizes_to_yolo() {
let hat_backend = HatBackend::NamedWithArgs {
backend_type: "codex".to_string(),
args: vec!["--dangerously-bypass-approvals-and-sandbox".to_string()],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
let (cmd, args, _, _) = backend.build_command("test prompt", false);
assert_eq!(cmd, "codex");
assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
}
#[test]
fn test_codex_named_with_args_yolo_removes_full_auto() {
let hat_backend = HatBackend::NamedWithArgs {
backend_type: "codex".to_string(),
args: vec!["--yolo".to_string()],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
let (cmd, args, _, _) = backend.build_command("test prompt", false);
assert_eq!(cmd, "codex");
assert_eq!(args, vec!["exec", "--yolo", "test prompt"]);
}
#[test]
fn test_from_hat_backend_custom() {
let hat_backend = HatBackend::Custom {
command: "my-cli".to_string(),
args: vec!["--flag".to_string()],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
assert_eq!(backend.command, "my-cli");
assert_eq!(backend.args, vec!["--flag"]);
}
#[test]
fn test_for_interactive_prompt_claude() {
let backend = CliBackend::for_interactive_prompt("claude").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "claude");
assert_eq!(
args,
vec![
"--dangerously-skip-permissions",
"--disallowedTools=TodoWrite,TaskCreate,TaskUpdate,TaskList,TaskGet",
"test prompt"
]
);
assert!(stdin.is_none());
assert_eq!(backend.prompt_flag, None);
}
#[test]
fn test_for_interactive_prompt_kiro() {
let backend = CliBackend::for_interactive_prompt("kiro").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "kiro-cli");
assert_eq!(args, vec!["chat", "--trust-all-tools", "test prompt"]);
assert!(!args.contains(&"--no-interactive".to_string()));
assert!(stdin.is_none());
}
#[test]
fn test_for_interactive_prompt_gemini() {
let backend = CliBackend::for_interactive_prompt("gemini").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "gemini");
assert_eq!(args, vec!["--yolo", "-i", "test prompt"]);
assert_eq!(backend.prompt_flag, Some("-i".to_string()));
assert!(stdin.is_none());
}
#[test]
fn test_for_interactive_prompt_codex() {
let backend = CliBackend::for_interactive_prompt("codex").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "codex");
assert_eq!(args, vec!["test prompt"]);
assert!(!args.contains(&"exec".to_string()));
assert!(!args.contains(&"--full-auto".to_string()));
assert!(stdin.is_none());
}
#[test]
fn test_for_interactive_prompt_amp() {
let backend = CliBackend::for_interactive_prompt("amp").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "amp");
assert_eq!(args, vec!["-x", "test prompt"]);
assert!(!args.contains(&"--dangerously-allow-all".to_string()));
assert!(stdin.is_none());
}
#[test]
fn test_for_interactive_prompt_copilot() {
let backend = CliBackend::for_interactive_prompt("copilot").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "copilot");
assert_eq!(args, vec!["-p", "test prompt"]);
assert!(!args.contains(&"--allow-all-tools".to_string()));
assert!(stdin.is_none());
}
#[test]
fn test_for_interactive_prompt_invalid() {
let result = CliBackend::for_interactive_prompt("invalid_backend");
assert!(result.is_err());
}
#[test]
fn test_opencode_backend() {
let backend = CliBackend::opencode();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "opencode");
assert_eq!(args, vec!["run", "test prompt"]);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::Text);
assert_eq!(backend.prompt_flag, None);
}
#[test]
fn test_opencode_tui_backend() {
let backend = CliBackend::opencode_tui();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "opencode");
assert_eq!(args, vec!["run", "test prompt"]);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::Text);
assert_eq!(backend.prompt_flag, None);
}
#[test]
fn test_opencode_interactive_mode_unchanged() {
let backend = CliBackend::opencode();
let (cmd, args_auto, stdin_auto, _) = backend.build_command("test prompt", false);
let (_, args_interactive, stdin_interactive, _) =
backend.build_command("test prompt", true);
assert_eq!(cmd, "opencode");
assert_eq!(args_auto, args_interactive);
assert_eq!(args_auto, vec!["run", "test prompt"]);
assert!(stdin_auto.is_none());
assert!(stdin_interactive.is_none());
}
#[test]
fn test_from_name_opencode() {
let backend = CliBackend::from_name("opencode").unwrap();
assert_eq!(backend.command, "opencode");
assert_eq!(backend.prompt_flag, None); }
#[test]
fn test_for_interactive_prompt_opencode() {
let backend = CliBackend::for_interactive_prompt("opencode").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "opencode");
assert_eq!(args, vec!["--prompt", "test prompt"]);
assert!(stdin.is_none());
assert_eq!(backend.prompt_flag, Some("--prompt".to_string()));
}
#[test]
fn test_opencode_interactive_launches_tui_not_headless() {
let backend = CliBackend::opencode_interactive();
let (cmd, args, _, _) = backend.build_command("test prompt", true);
assert_eq!(cmd, "opencode");
assert!(
!args.contains(&"run".to_string()),
"opencode_interactive() should not use 'run' subcommand. \
'opencode run' is headless mode, but interactive mode needs TUI. \
Expected: opencode --prompt \"test prompt\", got: opencode {}",
args.join(" ")
);
assert!(
args.contains(&"--prompt".to_string()),
"opencode_interactive() should use --prompt flag for TUI mode. \
Expected args to contain '--prompt', got: {:?}",
args
);
}
#[test]
fn test_pi_backend() {
let backend = CliBackend::pi();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "pi");
assert_eq!(
args,
vec!["-p", "--mode", "json", "--no-session", "test prompt"]
);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
assert_eq!(backend.prompt_flag, None); }
#[test]
fn test_pi_interactive_backend() {
let backend = CliBackend::pi_interactive();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "pi");
assert_eq!(args, vec!["--no-session", "test prompt"]);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::Text);
assert_eq!(backend.prompt_flag, None);
}
#[test]
fn test_from_name_pi() {
let backend = CliBackend::from_name("pi").unwrap();
assert_eq!(backend.command, "pi");
assert_eq!(backend.prompt_flag, None);
assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
}
#[test]
fn test_for_interactive_prompt_pi() {
let backend = CliBackend::for_interactive_prompt("pi").unwrap();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "pi");
assert_eq!(args, vec!["--no-session", "test prompt"]);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::Text);
}
#[test]
fn test_from_config_pi() {
let config = CliConfig {
backend: "pi".to_string(),
command: None,
prompt_mode: "arg".to_string(),
args: vec![
"--provider".to_string(),
"zai".to_string(),
"--model".to_string(),
"glm-5".to_string(),
],
..Default::default()
};
let backend = CliBackend::from_config(&config).unwrap();
let (_cmd, args, _stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(backend.command, "pi");
assert_eq!(backend.output_format, OutputFormat::PiStreamJson);
assert!(args.contains(&"--provider".to_string()));
assert!(args.contains(&"zai".to_string()));
assert!(args.contains(&"--model".to_string()));
assert!(args.contains(&"glm-5".to_string()));
}
#[test]
fn test_from_hat_backend_named_with_args_pi() {
let hat_backend = HatBackend::NamedWithArgs {
backend_type: "pi".to_string(),
args: vec![
"--provider".to_string(),
"anthropic".to_string(),
"--model".to_string(),
"claude-sonnet-4".to_string(),
],
};
let backend = CliBackend::from_hat_backend(&hat_backend).unwrap();
let (cmd, args, _, _) = backend.build_command("test prompt", false);
assert_eq!(cmd, "pi");
assert!(args.contains(&"-p".to_string()));
assert!(args.contains(&"--mode".to_string()));
assert!(args.contains(&"json".to_string()));
assert!(args.contains(&"--no-session".to_string()));
assert!(args.contains(&"--provider".to_string()));
assert!(args.contains(&"anthropic".to_string()));
assert!(args.contains(&"--model".to_string()));
assert!(args.contains(&"claude-sonnet-4".to_string()));
assert!(args.contains(&"test prompt".to_string()));
}
#[test]
fn test_pi_large_prompt_no_temp_file() {
let backend = CliBackend::pi();
let large_prompt = "x".repeat(7001);
let (cmd, args, _stdin, temp) = backend.build_command(&large_prompt, false);
assert_eq!(cmd, "pi");
assert!(temp.is_none());
assert!(args.last().unwrap().len() > 7000);
}
#[test]
fn test_pi_interactive_mode_unchanged() {
let backend = CliBackend::pi();
let (_, args_auto, _, _) = backend.build_command("test prompt", false);
let (_, args_interactive, _, _) = backend.build_command("test prompt", true);
assert_eq!(args_auto, args_interactive);
}
#[test]
fn test_custom_args_can_be_appended() {
let mut backend = CliBackend::opencode();
let custom_args = vec!["--model=gpt-4".to_string(), "--temperature=0.7".to_string()];
backend.args.extend(custom_args.clone());
let (cmd, args, _, _) = backend.build_command("test prompt", false);
assert_eq!(cmd, "opencode");
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();
let model_idx = args.iter().position(|a| a == "--model=gpt-4").unwrap();
assert!(
run_idx < model_idx,
"Original args should come before custom args"
);
}
#[test]
fn test_claude_interactive_teams_backend() {
let backend = CliBackend::claude_interactive_teams();
let (cmd, args, stdin, _temp) = backend.build_command("test prompt", false);
assert_eq!(cmd, "claude");
assert_eq!(
args,
vec![
"--dangerously-skip-permissions",
"--disallowedTools=TodoWrite",
"test prompt"
]
);
assert!(stdin.is_none());
assert_eq!(backend.output_format, OutputFormat::Text);
assert_eq!(backend.prompt_flag, None);
assert_eq!(
backend.env_vars,
vec![(
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string(),
"1".to_string()
)]
);
}
#[test]
fn test_env_vars_default_empty() {
assert!(CliBackend::claude().env_vars.is_empty());
assert!(CliBackend::claude_interactive().env_vars.is_empty());
assert!(CliBackend::kiro().env_vars.is_empty());
assert!(CliBackend::gemini().env_vars.is_empty());
assert!(CliBackend::codex().env_vars.is_empty());
assert!(CliBackend::amp().env_vars.is_empty());
assert!(CliBackend::copilot().env_vars.is_empty());
assert!(CliBackend::opencode().env_vars.is_empty());
assert!(CliBackend::pi().env_vars.is_empty());
}
}