Skip to main content

codex/commands/
app_server.rs

1use std::fs as std_fs;
2
3use tokio::{process::Command, time};
4
5use crate::{
6    builder::{apply_cli_overrides, resolve_cli_overrides},
7    process::{spawn_with_retry, tee_stream, ConsoleTarget},
8    AppServerCodegenOutput, AppServerCodegenRequest, AppServerProxyRequest, AppServerRequest,
9    CodexClient, CodexError,
10};
11
12impl CodexClient {
13    /// Spawns `codex app-server` with piped stdio for direct app-server integration.
14    pub fn start_app_server(
15        &self,
16        request: AppServerRequest,
17    ) -> Result<tokio::process::Child, CodexError> {
18        let AppServerRequest {
19            listen,
20            ws_audience,
21            ws_auth,
22            ws_issuer,
23            ws_max_clock_skew_seconds,
24            ws_shared_secret_file,
25            ws_token_file,
26            ws_token_sha256,
27            working_dir,
28            overrides,
29        } = request;
30
31        let resolved_overrides =
32            resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
33
34        let mut command = Command::new(self.command_env.binary_path());
35        command
36            .stdin(std::process::Stdio::piped())
37            .stdout(std::process::Stdio::piped())
38            .stderr(std::process::Stdio::piped())
39            .kill_on_drop(true)
40            .current_dir(self.sandbox_working_dir(working_dir)?);
41
42        apply_cli_overrides(&mut command, &resolved_overrides, true);
43        command.arg("app-server");
44
45        if let Some(listen) = listen {
46            command.arg("--listen").arg(listen);
47        }
48        if let Some(ws_audience) = ws_audience {
49            command.arg("--ws-audience").arg(ws_audience);
50        }
51        if let Some(ws_auth) = ws_auth {
52            command.arg("--ws-auth").arg(ws_auth);
53        }
54        if let Some(ws_issuer) = ws_issuer {
55            command.arg("--ws-issuer").arg(ws_issuer);
56        }
57        if let Some(ws_max_clock_skew_seconds) = ws_max_clock_skew_seconds {
58            command
59                .arg("--ws-max-clock-skew-seconds")
60                .arg(ws_max_clock_skew_seconds.to_string());
61        }
62        if let Some(ws_shared_secret_file) = ws_shared_secret_file {
63            command
64                .arg("--ws-shared-secret-file")
65                .arg(ws_shared_secret_file);
66        }
67        if let Some(ws_token_file) = ws_token_file {
68            command.arg("--ws-token-file").arg(ws_token_file);
69        }
70        if let Some(ws_token_sha256) = ws_token_sha256 {
71            command.arg("--ws-token-sha256").arg(ws_token_sha256);
72        }
73
74        self.command_env.apply(&mut command)?;
75
76        spawn_with_retry(&mut command, self.command_env.binary_path())
77    }
78
79    /// Spawns `codex app-server proxy` with piped stdio for proxy/server integration.
80    pub fn start_app_server_proxy(
81        &self,
82        request: AppServerProxyRequest,
83    ) -> Result<tokio::process::Child, CodexError> {
84        let AppServerProxyRequest {
85            socket_path,
86            working_dir,
87            overrides,
88        } = request;
89
90        if socket_path
91            .as_ref()
92            .is_some_and(|path| path.as_os_str().is_empty())
93        {
94            return Err(CodexError::EmptySocketPath);
95        }
96
97        let resolved_overrides =
98            resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
99
100        let mut command = Command::new(self.command_env.binary_path());
101        command
102            .stdin(std::process::Stdio::piped())
103            .stdout(std::process::Stdio::piped())
104            .stderr(std::process::Stdio::piped())
105            .kill_on_drop(true)
106            .current_dir(self.sandbox_working_dir(working_dir)?);
107
108        apply_cli_overrides(&mut command, &resolved_overrides, true);
109        command.arg("app-server").arg("proxy");
110
111        if let Some(socket_path) = socket_path.as_ref() {
112            command.arg("--sock").arg(socket_path);
113        }
114
115        self.command_env.apply(&mut command)?;
116
117        spawn_with_retry(&mut command, self.command_env.binary_path())
118    }
119
120    /// Generates app-server bindings via `codex app-server generate-ts` or `generate-json-schema`.
121    ///
122    /// Ensures the output directory exists, mirrors stdout/stderr according to the builder
123    /// (`mirror_stdout` / `quiet`), and returns captured output plus the exit status. Non-zero
124    /// exits bubble up as [`CodexError::NonZeroExit`] with stderr attached. Use
125    /// [`AppServerCodegenRequest::prettier`] to format TypeScript output with a specific
126    /// Prettier binary and request-level overrides for config/profile toggles.
127    pub async fn generate_app_server_bindings(
128        &self,
129        request: AppServerCodegenRequest,
130    ) -> Result<AppServerCodegenOutput, CodexError> {
131        let AppServerCodegenRequest {
132            target,
133            out_dir,
134            experimental,
135            overrides,
136        } = request;
137
138        std_fs::create_dir_all(&out_dir).map_err(|source| CodexError::PrepareOutputDirectory {
139            path: out_dir.clone(),
140            source,
141        })?;
142
143        let dir_ctx = self.directory_context()?;
144        let resolved_overrides =
145            resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
146
147        let mut command = Command::new(self.command_env.binary_path());
148        command
149            .stdout(std::process::Stdio::piped())
150            .stderr(std::process::Stdio::piped())
151            .kill_on_drop(true)
152            .current_dir(dir_ctx.path());
153
154        apply_cli_overrides(&mut command, &resolved_overrides, true);
155        command
156            .arg("app-server")
157            .arg(target.subcommand())
158            .arg("--out")
159            .arg(&out_dir);
160
161        if experimental {
162            command.arg("--experimental");
163        }
164
165        if let Some(prettier) = target.prettier() {
166            command.arg("--prettier").arg(prettier);
167        }
168
169        self.command_env.apply(&mut command)?;
170
171        let mut child = spawn_with_retry(&mut command, self.command_env.binary_path())?;
172
173        let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
174        let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
175
176        let stdout_task = tokio::spawn(tee_stream(
177            stdout,
178            ConsoleTarget::Stdout,
179            self.mirror_stdout,
180        ));
181        let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
182
183        let wait_task = async move {
184            let status = child
185                .wait()
186                .await
187                .map_err(|source| CodexError::Wait { source })?;
188            let stdout_bytes = stdout_task
189                .await
190                .map_err(CodexError::Join)?
191                .map_err(CodexError::CaptureIo)?;
192            let stderr_bytes = stderr_task
193                .await
194                .map_err(CodexError::Join)?
195                .map_err(CodexError::CaptureIo)?;
196            Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
197        };
198
199        let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
200            wait_task.await?
201        } else {
202            match time::timeout(self.timeout, wait_task).await {
203                Ok(result) => result?,
204                Err(_) => {
205                    return Err(CodexError::Timeout {
206                        timeout: self.timeout,
207                    });
208                }
209            }
210        };
211
212        if !status.success() {
213            return Err(CodexError::NonZeroExit {
214                status,
215                stderr: String::from_utf8(stderr_bytes)?,
216            });
217        }
218
219        Ok(AppServerCodegenOutput {
220            status,
221            stdout: String::from_utf8(stdout_bytes)?,
222            stderr: String::from_utf8(stderr_bytes)?,
223            out_dir,
224        })
225    }
226}