1use crate::error::{CopilotError, Result};
9use crate::transport::StdioTransport;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::process::Stdio;
13use tokio::process::{Child, Command};
14
15#[derive(Debug, Clone)]
21pub struct ProcessOptions {
22 pub working_directory: Option<PathBuf>,
24
25 pub environment: HashMap<String, String>,
27
28 pub inherit_environment: bool,
30
31 pub redirect_stdin: bool,
33
34 pub redirect_stdout: bool,
36
37 pub redirect_stderr: bool,
39}
40
41impl Default for ProcessOptions {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl ProcessOptions {
48 pub fn new() -> Self {
50 Self {
51 working_directory: None,
52 environment: HashMap::new(),
53 inherit_environment: true,
54 redirect_stdin: true,
55 redirect_stdout: true,
56 redirect_stderr: false,
57 }
58 }
59
60 pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
62 self.working_directory = Some(dir.into());
63 self
64 }
65
66 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
68 self.environment.insert(key.into(), value.into());
69 self
70 }
71
72 pub fn inherit_env(mut self, inherit: bool) -> Self {
74 self.inherit_environment = inherit;
75 self
76 }
77
78 pub fn stdin(mut self, redirect: bool) -> Self {
80 self.redirect_stdin = redirect;
81 self
82 }
83
84 pub fn stdout(mut self, redirect: bool) -> Self {
86 self.redirect_stdout = redirect;
87 self
88 }
89
90 pub fn stderr(mut self, redirect: bool) -> Self {
92 self.redirect_stderr = redirect;
93 self
94 }
95}
96
97pub struct CopilotProcess {
103 child: Child,
104 transport: Option<StdioTransport>,
105 stdout: Option<tokio::process::ChildStdout>,
106 stderr: Option<tokio::process::ChildStderr>,
107}
108
109impl CopilotProcess {
110 pub fn spawn(
112 executable: impl AsRef<Path>,
113 args: &[&str],
114 options: ProcessOptions,
115 ) -> Result<Self> {
116 let executable = executable.as_ref();
117
118 let mut cmd = Command::new(executable);
120 cmd.args(args);
121
122 if let Some(dir) = &options.working_directory {
124 cmd.current_dir(dir);
125 }
126
127 if !options.inherit_environment {
129 cmd.env_clear();
130 }
131 for (key, value) in &options.environment {
132 cmd.env(key, value);
133 }
134
135 cmd.stdin(if options.redirect_stdin {
137 Stdio::piped()
138 } else {
139 Stdio::null()
140 });
141 cmd.stdout(if options.redirect_stdout {
142 Stdio::piped()
143 } else {
144 Stdio::null()
145 });
146 cmd.stderr(if options.redirect_stderr {
147 Stdio::piped()
148 } else {
149 Stdio::null()
150 });
151
152 let mut child = cmd.spawn().map_err(CopilotError::ProcessStart)?;
154
155 let transport = if options.redirect_stdin && options.redirect_stdout {
157 let stdin = child
158 .stdin
159 .take()
160 .ok_or_else(|| CopilotError::InvalidConfig("Failed to capture stdin".into()))?;
161 let stdout = child
162 .stdout
163 .take()
164 .ok_or_else(|| CopilotError::InvalidConfig("Failed to capture stdout".into()))?;
165 Some(StdioTransport::new(stdin, stdout))
166 } else {
167 None
168 };
169
170 let stdout = if transport.is_none() && options.redirect_stdout {
172 child.stdout.take()
173 } else {
174 None
175 };
176
177 let stderr = if options.redirect_stderr {
179 child.stderr.take()
180 } else {
181 None
182 };
183
184 Ok(Self {
185 child,
186 transport,
187 stdout,
188 stderr,
189 })
190 }
191
192 pub fn spawn_stdio(cli_path: impl AsRef<Path>) -> Result<Self> {
194 let options = ProcessOptions::new().stdin(true).stdout(true).stderr(false);
195
196 Self::spawn(cli_path, &["--stdio"], options)
197 }
198
199 pub fn take_transport(&mut self) -> Option<StdioTransport> {
203 self.transport.take()
204 }
205
206 pub fn take_stdout(&mut self) -> Option<tokio::process::ChildStdout> {
208 self.stdout.take()
209 }
210
211 pub fn id(&self) -> Option<u32> {
213 self.child.id()
214 }
215
216 pub async fn is_running(&mut self) -> bool {
218 self.child.try_wait().ok().flatten().is_none()
219 }
220
221 pub async fn try_wait(&mut self) -> Result<Option<i32>> {
223 match self.child.try_wait() {
224 Ok(Some(status)) => Ok(Some(status.code().unwrap_or(-1))),
225 Ok(None) => Ok(None),
226 Err(e) => Err(CopilotError::Transport(e)),
227 }
228 }
229
230 pub async fn wait(&mut self) -> Result<i32> {
232 let status = self.child.wait().await.map_err(CopilotError::Transport)?;
233 Ok(status.code().unwrap_or(-1))
234 }
235
236 pub fn terminate(&mut self) -> Result<()> {
240 self.kill()
243 }
244
245 pub fn kill(&mut self) -> Result<()> {
247 self.child.start_kill().map_err(CopilotError::Transport)
248 }
249
250 pub fn take_stderr(&mut self) -> Option<tokio::process::ChildStderr> {
252 self.stderr.take()
253 }
254}
255
256pub fn find_executable(name: &str) -> Option<PathBuf> {
264 which::which(name).ok()
265}
266
267pub fn is_node_script(path: &Path) -> bool {
269 path.extension()
270 .is_some_and(|ext| ext == "js" || ext == "mjs")
271}
272
273pub fn find_node() -> Option<PathBuf> {
275 find_executable("node")
276}
277
278pub fn find_copilot_cli() -> Option<PathBuf> {
282 if let Ok(cli_path) = std::env::var("COPILOT_CLI_PATH") {
284 let cli_path = cli_path.trim();
285 if !cli_path.is_empty() {
286 let path = PathBuf::from(cli_path);
287 if path.exists() {
288 return Some(path);
289 }
290 }
291 }
292
293 if let Some(path) = find_executable("copilot") {
295 return Some(path);
296 }
297
298 #[cfg(windows)]
300 {
301 if let Some(path) = find_executable("copilot.cmd") {
302 return Some(path);
303 }
304 if let Some(path) = find_executable("copilot.exe") {
305 return Some(path);
306 }
307 }
308
309 None
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn test_process_options_builder() {
318 let options = ProcessOptions::new()
319 .working_dir("/tmp")
320 .env("FOO", "bar")
321 .inherit_env(false)
322 .stdin(true)
323 .stdout(true)
324 .stderr(true);
325
326 assert_eq!(options.working_directory, Some(PathBuf::from("/tmp")));
327 assert_eq!(options.environment.get("FOO"), Some(&"bar".to_string()));
328 assert!(!options.inherit_environment);
329 assert!(options.redirect_stdin);
330 assert!(options.redirect_stdout);
331 assert!(options.redirect_stderr);
332 }
333
334 #[test]
335 fn test_process_options_default() {
336 let options = ProcessOptions::default();
337
338 assert!(options.working_directory.is_none());
339 assert!(options.environment.is_empty());
340 assert!(options.inherit_environment);
341 assert!(options.redirect_stdin);
342 assert!(options.redirect_stdout);
343 assert!(!options.redirect_stderr);
344 }
345
346 #[test]
347 fn test_is_node_script() {
348 assert!(is_node_script(Path::new("script.js")));
349 assert!(is_node_script(Path::new("script.mjs")));
350 assert!(is_node_script(Path::new("/path/to/script.js")));
351 assert!(!is_node_script(Path::new("script.ts")));
352 assert!(!is_node_script(Path::new("script")));
353 assert!(!is_node_script(Path::new("script.py")));
354 }
355
356 #[test]
357 fn test_find_node() {
358 let _ = find_node();
361 }
362
363 #[test]
364 fn test_find_copilot_cli() {
365 let _ = find_copilot_cli();
368 }
369}