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.
19pub async fn run_claude(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
20    let mut command_args = Vec::new();
21
22    // Global args first (before subcommand)
23    command_args.extend(claude.global_args.clone());
24
25    // Then command-specific args
26    command_args.extend(args);
27
28    debug!(binary = %claude.binary.display(), args = ?command_args, "executing claude command");
29
30    let output = if let Some(timeout) = claude.timeout {
31        run_with_timeout(
32            &claude.binary,
33            &command_args,
34            &claude.env,
35            claude.working_dir.as_deref(),
36            timeout,
37        )
38        .await?
39    } else {
40        run_internal(
41            &claude.binary,
42            &command_args,
43            &claude.env,
44            claude.working_dir.as_deref(),
45        )
46        .await?
47    };
48
49    Ok(output)
50}
51
52/// Run a claude command and allow specific non-zero exit codes.
53pub async fn run_claude_allow_exit_codes(
54    claude: &Claude,
55    args: Vec<String>,
56    allowed_codes: &[i32],
57) -> Result<CommandOutput> {
58    let output = run_claude(claude, args).await;
59
60    match output {
61        Err(Error::CommandFailed { exit_code, .. }) if allowed_codes.contains(&exit_code) => {
62            // Re-run to get the output without the error — this is a bit wasteful
63            // but keeps the API clean. In practice, we capture before erroring.
64            // TODO: refactor to capture output before checking exit code
65            Ok(CommandOutput {
66                stdout: String::new(),
67                stderr: String::new(),
68                exit_code,
69                success: false,
70            })
71        }
72        other => other,
73    }
74}
75
76async fn run_internal(
77    binary: &std::path::Path,
78    args: &[String],
79    env: &std::collections::HashMap<String, String>,
80    working_dir: Option<&std::path::Path>,
81) -> Result<CommandOutput> {
82    let mut cmd = Command::new(binary);
83    cmd.args(args);
84
85    // Remove CLAUDECODE env var to prevent nested session detection
86    cmd.env_remove("CLAUDECODE");
87
88    if let Some(dir) = working_dir {
89        cmd.current_dir(dir);
90    }
91
92    for (key, value) in env {
93        cmd.env(key, value);
94    }
95
96    let output = cmd.output().await?;
97
98    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
99    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
100    let exit_code = output.status.code().unwrap_or(-1);
101
102    if !output.status.success() {
103        return Err(Error::CommandFailed {
104            command: format!("{} {}", binary.display(), args.join(" ")),
105            exit_code,
106            stdout,
107            stderr,
108        });
109    }
110
111    Ok(CommandOutput {
112        stdout,
113        stderr,
114        exit_code,
115        success: true,
116    })
117}
118
119async fn run_with_timeout(
120    binary: &std::path::Path,
121    args: &[String],
122    env: &std::collections::HashMap<String, String>,
123    working_dir: Option<&std::path::Path>,
124    timeout: Duration,
125) -> Result<CommandOutput> {
126    tokio::time::timeout(timeout, run_internal(binary, args, env, working_dir))
127        .await
128        .map_err(|_| Error::Timeout {
129            timeout_seconds: timeout.as_secs(),
130        })?
131}