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