Skip to main content

call_coding_clis/invoke/
plan.rs

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}