1use std::time::Duration;
2
3use tokio::process::Command;
4use tracing::debug;
5
6use crate::Claude;
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone)]
11pub struct CommandOutput {
12 pub stdout: String,
13 pub stderr: String,
14 pub exit_code: i32,
15 pub success: bool,
16}
17
18pub async fn run_claude(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
24 run_claude_with_retry(claude, args, None).await
25}
26
27pub async fn run_claude_with_retry(
29 claude: &Claude,
30 args: Vec<String>,
31 retry_override: Option<&crate::retry::RetryPolicy>,
32) -> Result<CommandOutput> {
33 let policy = retry_override.or(claude.retry_policy.as_ref());
34
35 match policy {
36 Some(policy) => {
37 crate::retry::with_retry(policy, || run_claude_once(claude, args.clone())).await
38 }
39 None => run_claude_once(claude, args).await,
40 }
41}
42
43async fn run_claude_once(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
44 let mut command_args = Vec::new();
45
46 command_args.extend(claude.global_args.clone());
48
49 command_args.extend(args);
51
52 debug!(binary = %claude.binary.display(), args = ?command_args, "executing claude command");
53
54 let output = if let Some(timeout) = claude.timeout {
55 run_with_timeout(
56 &claude.binary,
57 &command_args,
58 &claude.env,
59 claude.working_dir.as_deref(),
60 timeout,
61 )
62 .await?
63 } else {
64 run_internal(
65 &claude.binary,
66 &command_args,
67 &claude.env,
68 claude.working_dir.as_deref(),
69 )
70 .await?
71 };
72
73 Ok(output)
74}
75
76pub async fn run_claude_allow_exit_codes(
78 claude: &Claude,
79 args: Vec<String>,
80 allowed_codes: &[i32],
81) -> Result<CommandOutput> {
82 let output = run_claude(claude, args).await;
83
84 match output {
85 Err(Error::CommandFailed { exit_code, .. }) if allowed_codes.contains(&exit_code) => {
86 Ok(CommandOutput {
90 stdout: String::new(),
91 stderr: String::new(),
92 exit_code,
93 success: false,
94 })
95 }
96 other => other,
97 }
98}
99
100async fn run_internal(
101 binary: &std::path::Path,
102 args: &[String],
103 env: &std::collections::HashMap<String, String>,
104 working_dir: Option<&std::path::Path>,
105) -> Result<CommandOutput> {
106 let mut cmd = Command::new(binary);
107 cmd.args(args);
108
109 cmd.stdin(std::process::Stdio::null());
111
112 cmd.env_remove("CLAUDECODE");
114 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
115
116 if let Some(dir) = working_dir {
117 cmd.current_dir(dir);
118 }
119
120 for (key, value) in env {
121 cmd.env(key, value);
122 }
123
124 let output = cmd.output().await?;
125
126 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
127 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
128 let exit_code = output.status.code().unwrap_or(-1);
129
130 if !output.status.success() {
131 return Err(Error::CommandFailed {
132 command: format!("{} {}", binary.display(), args.join(" ")),
133 exit_code,
134 stdout,
135 stderr,
136 });
137 }
138
139 Ok(CommandOutput {
140 stdout,
141 stderr,
142 exit_code,
143 success: true,
144 })
145}
146
147async fn run_with_timeout(
148 binary: &std::path::Path,
149 args: &[String],
150 env: &std::collections::HashMap<String, String>,
151 working_dir: Option<&std::path::Path>,
152 timeout: Duration,
153) -> Result<CommandOutput> {
154 tokio::time::timeout(timeout, run_internal(binary, args, env, working_dir))
155 .await
156 .map_err(|_| Error::Timeout {
157 timeout_seconds: timeout.as_secs(),
158 })?
159}