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    /// Build the command arguments.
50    fn build_args(&self) -> Vec<String> {
51        vec![
52            "app-server".to_string(),
53            "--listen".to_string(),
54            "stdio://".to_string(),
55        ]
56    }
57
58    /// Spawn the app-server process asynchronously.
59    #[cfg(feature = "async-client")]
60    pub async fn spawn(self) -> crate::error::Result<tokio::process::Child> {
61        let args = self.build_args();
62
63        debug!(
64            "[CLI] Spawning async app-server: {} {}",
65            self.command.display(),
66            args.join(" ")
67        );
68
69        let mut cmd = tokio::process::Command::new(&self.command);
70        cmd.args(&args)
71            .stdin(Stdio::piped())
72            .stdout(Stdio::piped())
73            .stderr(Stdio::piped());
74
75        if let Some(ref dir) = self.working_directory {
76            cmd.current_dir(dir);
77        }
78
79        cmd.spawn().map_err(crate::error::Error::Io)
80    }
81
82    /// Spawn the app-server process synchronously.
83    pub fn spawn_sync(self) -> std::io::Result<std::process::Child> {
84        let args = self.build_args();
85
86        debug!(
87            "[CLI] Spawning sync app-server: {} {}",
88            self.command.display(),
89            args.join(" ")
90        );
91
92        let mut cmd = std::process::Command::new(&self.command);
93        cmd.args(&args)
94            .stdin(Stdio::piped())
95            .stdout(Stdio::piped())
96            .stderr(Stdio::piped());
97
98        if let Some(ref dir) = self.working_directory {
99            cmd.current_dir(dir);
100        }
101
102        cmd.spawn()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_default_args() {
112        let builder = AppServerBuilder::new();
113        let args = builder.build_args();
114
115        assert_eq!(args, vec!["app-server", "--listen", "stdio://"]);
116    }
117
118    #[test]
119    fn test_custom_command() {
120        let builder = AppServerBuilder::new().command("/usr/local/bin/codex");
121        assert_eq!(builder.command, PathBuf::from("/usr/local/bin/codex"));
122    }
123
124    #[test]
125    fn test_working_directory() {
126        let builder = AppServerBuilder::new().working_directory("/tmp/work");
127        assert_eq!(builder.working_directory, Some(PathBuf::from("/tmp/work")));
128    }
129}