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;