Skip to main content

codex_codes/
cli.rs

1//! Builder for launching the Codex app-server process.
2//!
3//! The [`AppServerBuilder`] configures and spawns `codex app-server --listen stdio://`,
4//! a long-lived process that speaks JSON-RPC over newline-delimited stdio.
5
6use log::debug;
7use std::path::PathBuf;
8use std::process::Stdio;
9
10/// Builder for launching a Codex app-server process.
11///
12/// Produces commands of the form: `codex app-server --listen stdio://`
13///
14/// All model, sandbox, and approval configuration is done via JSON-RPC
15/// requests after connecting, not via CLI flags.
16#[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    /// Create a new builder with default settings.
30    pub fn new() -> Self {
31        Self {
32            command: PathBuf::from("codex"),
33            working_directory: None,
34        }
35    }
36
37    /// Set custom path to the codex binary.
38    pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
39        self.command = path.into();
40        self
41    }
42
43    /// Set the working directory for the app-server process.
44    pub fn working_directory<P: Into<PathBuf>>(mut self, dir: P) -> Self {
45        self.working_directory = Some(dir.into());
46        self
47    }
48
49    /// Resolve the command path, using `which` for non-absolute paths.
50    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    /// Build the command arguments.
60    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    /// Spawn the app-server process asynchronously.
69    #[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    /// Spawn the app-server process synchronously.
94    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}