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> {
20 let mut command_args = Vec::new();
21
22 command_args.extend(claude.global_args.clone());
24
25 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
52pub 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 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 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}