Skip to main content

claude_wrapper/
exec.rs

1use std::time::Duration;
2
3use tokio::process::Command;
4use tracing::debug;
5
6use crate::Claude;
7use crate::error::{Error, Result};
8
9/// Raw output from a claude CLI invocation.
10#[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
18/// Run a claude command with the given arguments.
19///
20/// If the [`Claude`] client has a retry policy set, transient errors will be
21/// retried according to that policy. A per-command retry policy can be passed
22/// to override the client default.
23pub async fn run_claude(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
24    run_claude_with_retry(claude, args, None).await
25}
26
27/// Run a claude command with an optional per-command retry policy override.
28pub 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    // Global args first (before subcommand)
47    command_args.extend(claude.global_args.clone());
48
49    // Then command-specific args
50    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
76/// Run a claude command and allow specific non-zero exit codes.
77pub 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            // Re-run to get the output without the error — this is a bit wasteful
87            // but keeps the API clean. In practice, we capture before erroring.
88            // TODO: refactor to capture output before checking exit code
89            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    // Prevent child from inheriting/blocking on parent's stdin.
110    cmd.stdin(std::process::Stdio::null());
111
112    // Remove Claude Code env vars to prevent nested session detection
113    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}