1use crate::exec::{CommandSpec, Runner};
2use crate::output::{parse_transcript_for_runner, schema_name_for_runner, Transcript};
3use crate::parser::{parse_args, resolve_command, resolve_output_mode, CccConfig};
4use std::collections::BTreeMap;
5use std::error::Error as StdError;
6use std::fmt;
7use std::path::Path;
8
9use super::request::{OutputMode, Request, RunnerKind};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct Plan {
13 command_spec: CommandSpec,
14 runner: RunnerKind,
15 output_mode: OutputMode,
16 warnings: Vec<String>,
17}
18
19impl Plan {
20 pub fn command_spec(&self) -> &CommandSpec {
21 &self.command_spec
22 }
23
24 pub fn runner(&self) -> RunnerKind {
25 self.runner
26 }
27
28 pub fn output_mode(&self) -> OutputMode {
29 self.output_mode
30 }
31
32 pub fn warnings(&self) -> &[String] {
33 &self.warnings
34 }
35}
36
37#[derive(Debug)]
38pub enum Error {
39 InvalidRequest(String),
40 Config(String),
41 Spawn(std::io::Error),
42 ToolFailed { exit_code: i32, stderr: String },
43 OutputParse(String),
44}
45
46impl fmt::Display for Error {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Error::InvalidRequest(message) => write!(f, "invalid request: {message}"),
50 Error::Config(message) => write!(f, "configuration error: {message}"),
51 Error::Spawn(error) => write!(f, "spawn error: {error}"),
52 Error::ToolFailed { exit_code, stderr } => {
53 write!(f, "tool failed with exit code {exit_code}: {stderr}")
54 }
55 Error::OutputParse(message) => write!(f, "output parse error: {message}"),
56 }
57 }
58}
59
60impl StdError for Error {}
61
62impl From<std::io::Error> for Error {
63 fn from(error: std::io::Error) -> Self {
64 Self::Spawn(error)
65 }
66}
67
68#[derive(Clone, Debug, PartialEq)]
69pub struct Run {
70 plan: Plan,
71 exit_code: i32,
72 stdout: String,
73 stderr: String,
74 parsed_output: Option<Transcript>,
75}
76
77impl Run {
78 pub fn plan(&self) -> &Plan {
79 &self.plan
80 }
81
82 pub fn exit_code(&self) -> i32 {
83 self.exit_code
84 }
85
86 pub fn stdout(&self) -> &str {
87 &self.stdout
88 }
89
90 pub fn stderr(&self) -> &str {
91 &self.stderr
92 }
93
94 pub fn parsed_output(&self) -> Option<&Transcript> {
95 self.parsed_output.as_ref()
96 }
97
98 pub fn final_text(&self) -> &str {
99 self.parsed_output
100 .as_ref()
101 .map(|output| output.final_text.as_str())
102 .unwrap_or(self.stdout.as_str())
103 }
104}
105
106pub struct Client {
107 config: Option<CccConfig>,
108 binary_overrides: BTreeMap<RunnerKind, String>,
109 runner: Runner,
110}
111
112impl Client {
113 pub fn new() -> Self {
114 Self {
115 config: None,
116 binary_overrides: BTreeMap::new(),
117 runner: Runner::new(),
118 }
119 }
120
121 pub fn with_config(mut self, config: CccConfig) -> Self {
122 self.config = Some(config);
123 self
124 }
125
126 pub fn with_runtime_runner(mut self, runner: Runner) -> Self {
127 self.runner = runner;
128 self
129 }
130
131 pub fn with_binary_override(
132 mut self,
133 runner_kind: RunnerKind,
134 binary: impl Into<String>,
135 ) -> Self {
136 self.binary_overrides.insert(runner_kind, binary.into());
137 self
138 }
139
140 pub fn plan(&self, request: &Request) -> Result<Plan, Error> {
141 let argv = request.to_cli_tokens();
142 let parsed = parse_args(&argv);
143 let config = self.config.clone().unwrap_or_default();
144 let (argv, env, warnings) = resolve_command(&parsed, Some(&config))
145 .map_err(|message| Error::Config(message.to_string()))?;
146 let output_mode = resolve_output_mode(&parsed, Some(&config))
147 .map_err(|message| Error::Config(message.to_string()))
148 .and_then(|mode| {
149 OutputMode::from_cli_value(&mode)
150 .ok_or_else(|| Error::Config("resolved output mode was not recognized".into()))
151 })?;
152 let runner = runner_kind_from_argv(&argv)
153 .or_else(|| request.runner_kind())
154 .unwrap_or(RunnerKind::OpenCode);
155 let mut command_spec = CommandSpec {
156 argv,
157 stdin_text: None,
158 cwd: None,
159 env,
160 };
161 if let Some(provider) = request.provider() {
162 command_spec
163 .env
164 .insert("CCC_PROVIDER".to_string(), provider.to_string());
165 }
166 if let Some(binary_override) = self.binary_overrides.get(&runner) {
167 if let Some(program) = command_spec.argv.first_mut() {
168 *program = binary_override.clone();
169 }
170 }
171 Ok(Plan {
172 command_spec,
173 runner,
174 output_mode,
175 warnings,
176 })
177 }
178
179 pub fn run(&self, request: &Request) -> Result<Run, Error> {
180 let run = self.run_unchecked(request)?;
181 if run.exit_code != 0 {
182 return Err(Error::ToolFailed {
183 exit_code: run.exit_code,
184 stderr: run.stderr.clone(),
185 });
186 }
187 Ok(run)
188 }
189
190 pub fn run_unchecked(&self, request: &Request) -> Result<Run, Error> {
191 let plan = self.plan(request)?;
192 Ok(self.run_preplanned(plan))
193 }
194
195 pub fn stream<F>(&self, request: &Request, on_event: F) -> Result<Run, Error>
196 where
197 F: FnMut(&str, &str) + Send + 'static,
198 {
199 let run = self.stream_unchecked(request, on_event)?;
200 if run.exit_code != 0 {
201 return Err(Error::ToolFailed {
202 exit_code: run.exit_code,
203 stderr: run.stderr.clone(),
204 });
205 }
206 Ok(run)
207 }
208
209 pub fn stream_unchecked<F>(&self, request: &Request, on_event: F) -> Result<Run, Error>
210 where
211 F: FnMut(&str, &str) + Send + 'static,
212 {
213 let plan = self.plan(request)?;
214 Ok(self.stream_preplanned(plan, on_event))
215 }
216
217 fn run_preplanned(&self, plan: Plan) -> Run {
218 let completed = self.runner.run(plan.command_spec().clone());
219 let parsed_output = if should_parse_output_mode(plan.output_mode()) {
220 if schema_name_for_runner(plan.runner()).is_some() {
221 parse_transcript_for_runner(&completed.stdout, plan.runner())
222 } else {
223 None
224 }
225 } else {
226 None
227 };
228
229 Run {
230 plan,
231 exit_code: completed.exit_code,
232 stdout: completed.stdout,
233 stderr: completed.stderr,
234 parsed_output,
235 }
236 }
237
238 fn stream_preplanned<F>(&self, plan: Plan, on_event: F) -> Run
239 where
240 F: FnMut(&str, &str) + Send + 'static,
241 {
242 let completed = self.runner.stream(plan.command_spec().clone(), on_event);
243 let parsed_output = if should_parse_output_mode(plan.output_mode()) {
244 if schema_name_for_runner(plan.runner()).is_some() {
245 parse_transcript_for_runner(&completed.stdout, plan.runner())
246 } else {
247 None
248 }
249 } else {
250 None
251 };
252
253 Run {
254 plan,
255 exit_code: completed.exit_code,
256 stdout: completed.stdout,
257 stderr: completed.stderr,
258 parsed_output,
259 }
260 }
261}
262
263impl Default for Client {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269fn should_parse_output_mode(output_mode: OutputMode) -> bool {
270 matches!(
271 output_mode,
272 OutputMode::Json
273 | OutputMode::StreamJson
274 | OutputMode::Formatted
275 | OutputMode::StreamFormatted
276 )
277}
278
279fn runner_kind_from_argv(argv: &[String]) -> Option<RunnerKind> {
280 let binary = argv.first()?;
281 let name = Path::new(binary)
282 .file_name()
283 .and_then(|value| value.to_str())
284 .unwrap_or(binary.as_str());
285 match name {
286 "opencode" => Some(RunnerKind::OpenCode),
287 "claude" => Some(RunnerKind::Claude),
288 "codex" => Some(RunnerKind::Codex),
289 "kimi" => Some(RunnerKind::Kimi),
290 "cursor-agent" => Some(RunnerKind::Cursor),
291 "gemini" => Some(RunnerKind::Gemini),
292 "roocode" => Some(RunnerKind::RooCode),
293 "crush" => Some(RunnerKind::Crush),
294 _ => None,
295 }
296}