Skip to main content

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