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 exit_status_from_code(failed_output(&spec, error).status.code().unwrap_or(1))
238 });
239
240 CompletedRun {
241 argv,
242 exit_code: status.code().unwrap_or(1),
243 stdout: stdout_buf,
244 stderr: stderr_buf,
245 }
246}
247
248fn build_command(spec: &CommandSpec) -> Command {
249 let mut argv = spec.argv.iter();
250 let program = argv.next().cloned().unwrap_or_default();
251 let mut command = Command::new(program);
252 command.args(argv);
253 if let Some(cwd) = &spec.cwd {
254 command.current_dir(cwd);
255 }
256 command.envs(&spec.env);
257 command.stdin(if spec.stdin_text.is_some() {
258 Stdio::piped()
259 } else {
260 Stdio::null()
261 });
262 command
263}
264
265fn failed_output(spec: &CommandSpec, error: io::Error) -> std::process::Output {
266 let stderr = format!(
267 "failed to start {}: {}",
268 spec.argv.first().map(|s| s.as_str()).unwrap_or("(unknown)"),
269 error
270 )
271 .into_bytes();
272 std::process::Output {
273 status: exit_status_from_code(1),
274 stdout: Vec::new(),
275 stderr,
276 }
277}
278
279#[cfg(unix)]
280fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
281 std::process::ExitStatus::from_raw(code << 8)
282}
283
284#[cfg(windows)]
285fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
286 std::process::ExitStatus::from_raw(code as u32)
287}
288
289#[cfg(unix)]
290use std::os::unix::process::ExitStatusExt;
291
292#[cfg(windows)]
293use std::os::windows::process::ExitStatusExt;