Skip to main content

call_coding_clis/
exec.rs

1use std::collections::BTreeMap;
2use std::io::{self, Write};
3use std::path::PathBuf;
4use std::process::{Command, Stdio};
5use std::sync::{Arc, Mutex};
6use std::thread;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct CommandSpec {
10    pub argv: Vec<String>,
11    pub stdin_text: Option<String>,
12    pub cwd: Option<PathBuf>,
13    pub env: BTreeMap<String, String>,
14}
15
16impl CommandSpec {
17    pub fn new<I, S>(argv: I) -> Self
18    where
19        I: IntoIterator<Item = S>,
20        S: Into<String>,
21    {
22        Self {
23            argv: argv.into_iter().map(Into::into).collect(),
24            stdin_text: None,
25            cwd: None,
26            env: BTreeMap::new(),
27        }
28    }
29
30    pub fn with_stdin(mut self, stdin_text: impl Into<String>) -> Self {
31        self.stdin_text = Some(stdin_text.into());
32        self
33    }
34
35    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
36        self.cwd = Some(cwd.into());
37        self
38    }
39
40    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
41        self.env.insert(key.into(), value.into());
42        self
43    }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct CompletedRun {
48    pub argv: Vec<String>,
49    pub exit_code: i32,
50    pub stdout: String,
51    pub stderr: String,
52}
53
54type StreamCallback = Arc<Mutex<dyn FnMut(&str, &str) + Send>>;
55type RunExecutor = dyn Fn(CommandSpec) -> CompletedRun + Send + Sync;
56type StreamExecutor = dyn Fn(CommandSpec, StreamCallback) -> CompletedRun + Send + Sync;
57
58pub struct Runner {
59    executor: Box<RunExecutor>,
60    stream_executor: Box<StreamExecutor>,
61}
62
63impl Runner {
64    pub fn new() -> Self {
65        Self {
66            executor: Box::new(default_run_executor),
67            stream_executor: Box::new(default_stream_executor),
68        }
69    }
70
71    pub fn with_executor(executor: Box<RunExecutor>) -> Self {
72        Self {
73            executor,
74            stream_executor: Box::new(default_stream_executor),
75        }
76    }
77
78    pub fn with_stream_executor(stream_executor: Box<StreamExecutor>) -> Self {
79        Self {
80            executor: Box::new(default_run_executor),
81            stream_executor,
82        }
83    }
84
85    pub fn run(&self, spec: CommandSpec) -> CompletedRun {
86        (self.executor)(spec)
87    }
88
89    pub fn stream<F>(&self, spec: CommandSpec, on_event: F) -> CompletedRun
90    where
91        F: FnMut(&str, &str) + Send + 'static,
92    {
93        (self.stream_executor)(spec, Arc::new(Mutex::new(on_event)))
94    }
95}
96
97impl Default for Runner {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103pub fn build_prompt_spec(prompt: &str) -> Result<CommandSpec, &'static str> {
104    let normalized_prompt = prompt.trim();
105    if normalized_prompt.is_empty() {
106        return Err("prompt must not be empty");
107    }
108    Ok(CommandSpec::new(["opencode", "run", normalized_prompt]))
109}
110
111fn default_run_executor(spec: CommandSpec) -> CompletedRun {
112    let mut command = build_command(&spec);
113    let output = command
114        .output()
115        .unwrap_or_else(|error| failed_output(&spec, error));
116    CompletedRun {
117        argv: spec.argv,
118        exit_code: output.status.code().unwrap_or(1),
119        stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
120        stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
121    }
122}
123
124fn default_stream_executor(spec: CommandSpec, callback: StreamCallback) -> CompletedRun {
125    let argv = spec.argv.clone();
126    let mut command = build_command(&spec);
127    command.stdout(Stdio::piped());
128    command.stderr(Stdio::piped());
129
130    let mut child = match command.spawn() {
131        Ok(child) => child,
132        Err(error) => {
133            let error_msg = format!(
134                "failed to start {}: {}",
135                spec.argv.first().map(|s| s.as_str()).unwrap_or("(unknown)"),
136                error
137            );
138            if let Ok(mut cb) = callback.lock() {
139                cb("stderr", &error_msg);
140            }
141            return CompletedRun {
142                argv,
143                exit_code: 1,
144                stdout: String::new(),
145                stderr: error_msg,
146            };
147        }
148    };
149
150    if let Some(stdin_text) = &spec.stdin_text {
151        if let Some(mut stdin) = child.stdin.take() {
152            let _ = stdin.write_all(stdin_text.as_bytes());
153        }
154    }
155
156    let stdout_pipe = child.stdout.take();
157    let stderr_pipe = child.stderr.take();
158
159    let cb_out = Arc::clone(&callback);
160    let stdout_thread = thread::spawn(move || {
161        let mut buf = String::new();
162        if let Some(pipe) = stdout_pipe {
163            use std::io::BufRead;
164            let reader = std::io::BufReader::new(pipe);
165            for line in reader.lines() {
166                match line {
167                    Ok(text) => {
168                        buf.push_str(&text);
169                        buf.push('\n');
170                        let chunk = format!("{text}\n");
171                        if let Ok(mut cb) = cb_out.lock() {
172                            cb("stdout", &chunk);
173                        }
174                    }
175                    Err(_) => break,
176                }
177            }
178        }
179        buf
180    });
181
182    let cb_err = Arc::clone(&callback);
183    let stderr_thread = thread::spawn(move || {
184        let mut buf = String::new();
185        if let Some(pipe) = stderr_pipe {
186            use std::io::BufRead;
187            let reader = std::io::BufReader::new(pipe);
188            for line in reader.lines() {
189                match line {
190                    Ok(text) => {
191                        buf.push_str(&text);
192                        buf.push('\n');
193                        let chunk = format!("{text}\n");
194                        if let Ok(mut cb) = cb_err.lock() {
195                            cb("stderr", &chunk);
196                        }
197                    }
198                    Err(_) => break,
199                }
200            }
201        }
202        buf
203    });
204
205    let stdout_buf = stdout_thread.join().unwrap_or_default();
206    let stderr_buf = stderr_thread.join().unwrap_or_default();
207
208    let status = child.wait().unwrap_or_else(|error| {
209        exit_status_from_code(failed_output(&spec, error).status.code().unwrap_or(1))
210    });
211
212    CompletedRun {
213        argv,
214        exit_code: status.code().unwrap_or(1),
215        stdout: stdout_buf,
216        stderr: stderr_buf,
217    }
218}
219
220fn build_command(spec: &CommandSpec) -> Command {
221    let mut argv = spec.argv.iter();
222    let program = argv.next().cloned().unwrap_or_default();
223    let mut command = Command::new(program);
224    command.args(argv);
225    if let Some(cwd) = &spec.cwd {
226        command.current_dir(cwd);
227    }
228    command.envs(&spec.env);
229    command.stdin(if spec.stdin_text.is_some() {
230        Stdio::piped()
231    } else {
232        Stdio::null()
233    });
234    command
235}
236
237fn failed_output(spec: &CommandSpec, error: io::Error) -> std::process::Output {
238    let stderr = format!(
239        "failed to start {}: {}",
240        spec.argv.first().map(|s| s.as_str()).unwrap_or("(unknown)"),
241        error
242    )
243    .into_bytes();
244    std::process::Output {
245        status: exit_status_from_code(1),
246        stdout: Vec::new(),
247        stderr,
248    }
249}
250
251#[cfg(unix)]
252fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
253    std::process::ExitStatus::from_raw(code << 8)
254}
255
256#[cfg(windows)]
257fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
258    std::process::ExitStatus::from_raw(code as u32)
259}
260
261#[cfg(unix)]
262use std::os::unix::process::ExitStatusExt;
263
264#[cfg(windows)]
265use std::os::windows::process::ExitStatusExt;