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 {
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    // 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.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}