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 {
86 exit_code,
87 stdout,
88 stderr,
89 ..
90 }) if allowed_codes.contains(&exit_code) => Ok(CommandOutput {
91 stdout,
92 stderr,
93 exit_code,
94 success: false,
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.map_err(|e| Error::Io {
125 message: format!("failed to spawn claude: {e}"),
126 source: e,
127 working_dir: working_dir.map(|p| p.to_path_buf()),
128 })?;
129
130 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
131 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
132 let exit_code = output.status.code().unwrap_or(-1);
133
134 if !output.status.success() {
135 return Err(Error::CommandFailed {
136 command: format!("{} {}", binary.display(), args.join(" ")),
137 exit_code,
138 stdout,
139 stderr,
140 working_dir: working_dir.map(|p| p.to_path_buf()),
141 });
142 }
143
144 Ok(CommandOutput {
145 stdout,
146 stderr,
147 exit_code,
148 success: true,
149 })
150}
151
152async fn run_with_timeout(
153 binary: &std::path::Path,
154 args: &[String],
155 env: &std::collections::HashMap<String, String>,
156 working_dir: Option<&std::path::Path>,
157 timeout: Duration,
158) -> Result<CommandOutput> {
159 tokio::time::timeout(timeout, run_internal(binary, args, env, working_dir))
160 .await
161 .map_err(|_| Error::Timeout {
162 timeout_seconds: timeout.as_secs(),
163 })?
164}