1use log::debug;
7use std::path::PathBuf;
8use std::process::Stdio;
9
10#[derive(Debug, Clone)]
17pub struct AppServerBuilder {
18 command: PathBuf,
19 working_directory: Option<PathBuf>,
20}
21
22impl Default for AppServerBuilder {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl AppServerBuilder {
29 pub fn new() -> Self {
31 Self {
32 command: PathBuf::from("codex"),
33 working_directory: None,
34 }
35 }
36
37 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
39 self.command = path.into();
40 self
41 }
42
43 pub fn working_directory<P: Into<PathBuf>>(mut self, dir: P) -> Self {
45 self.working_directory = Some(dir.into());
46 self
47 }
48
49 fn resolve_command(&self) -> crate::error::Result<PathBuf> {
51 if self.command.is_absolute() {
52 return Ok(self.command.clone());
53 }
54 which::which(&self.command).map_err(|_| crate::error::Error::BinaryNotFound {
55 name: self.command.display().to_string(),
56 })
57 }
58
59 fn build_args(&self) -> Vec<String> {
61 vec![
62 "app-server".to_string(),
63 "--listen".to_string(),
64 "stdio://".to_string(),
65 ]
66 }
67
68 #[cfg(feature = "async-client")]
70 pub async fn spawn(self) -> crate::error::Result<tokio::process::Child> {
71 let resolved = self.resolve_command()?;
72 let args = self.build_args();
73
74 debug!(
75 "[CLI] Spawning async app-server: {} {}",
76 resolved.display(),
77 args.join(" ")
78 );
79
80 let mut cmd = tokio::process::Command::new(&resolved);
81 cmd.args(&args)
82 .stdin(Stdio::piped())
83 .stdout(Stdio::piped())
84 .stderr(Stdio::piped());
85
86 if let Some(ref dir) = self.working_directory {
87 cmd.current_dir(dir);
88 }
89
90 cmd.spawn().map_err(crate::error::Error::Io)
91 }
92
93 pub fn spawn_sync(self) -> crate::error::Result<std::process::Child> {
95 let resolved = self.resolve_command()?;
96 let args = self.build_args();
97
98 debug!(
99 "[CLI] Spawning sync app-server: {} {}",
100 resolved.display(),
101 args.join(" ")
102 );
103
104 let mut cmd = std::process::Command::new(&resolved);
105 cmd.args(&args)
106 .stdin(Stdio::piped())
107 .stdout(Stdio::piped())
108 .stderr(Stdio::piped());
109
110 if let Some(ref dir) = self.working_directory {
111 cmd.current_dir(dir);
112 }
113
114 cmd.spawn().map_err(crate::error::Error::Io)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_default_args() {
124 let builder = AppServerBuilder::new();
125 let args = builder.build_args();
126
127 assert_eq!(args, vec!["app-server", "--listen", "stdio://"]);
128 }
129
130 #[test]
131 fn test_custom_command() {
132 let builder = AppServerBuilder::new().command("/usr/local/bin/codex");
133 assert_eq!(builder.command, PathBuf::from("/usr/local/bin/codex"));
134 }
135
136 #[test]
137 fn test_working_directory() {
138 let builder = AppServerBuilder::new().working_directory("/tmp/work");
139 assert_eq!(builder.working_directory, Some(PathBuf::from("/tmp/work")));
140 }
141}