diff --git a/codex-rs/app-server-daemon/README.md b/codex-rs/app-server-daemon/README.md index ac512a8037bd..bf201c28ed61 100644 --- a/codex-rs/app-server-daemon/README.md +++ b/codex-rs/app-server-daemon/README.md @@ -18,6 +18,7 @@ support Windows lifecycle management. ```sh codex app-server daemon start +codex app-server daemon start --codex-bin /absolute/path/to/codex --analytics-default-enabled codex app-server daemon restart codex app-server daemon enable-remote-control codex app-server daemon disable-remote-control @@ -28,8 +29,9 @@ codex app-server daemon bootstrap --remote-control On success, every command writes exactly one JSON object to stdout. Consumers should parse that JSON rather than relying on human-readable text. Lifecycle -responses report the resolved backend, socket path, local CLI version, and -running app-server version when applicable. +responses report the resolved backend, socket path, local CLI version, running +app-server version when applicable, and persisted launch settings such as +`managedCodexPath` and `analyticsDefaultEnabled`. ## Bootstrap flow @@ -44,15 +46,22 @@ $HOME/.codex/packages/standalone/current/codex app-server daemon bootstrap --rem settings under `CODEX_HOME/app-server-daemon/`, starts app-server as a pidfile-backed detached process, and launches a detached updater loop. +`start` and `restart` can persist an explicit `--codex-bin` for clients that +own the executable lifecycle, such as a desktop app bundle. The path must be +absolute and point to an existing file. `--analytics-default-enabled` persists +the matching app-server launch flag for first-party clients. Those overrides do +not enable the standalone updater loop. + ## Installation and update cases -The daemon assumes Codex is installed through `install.sh` and always launches -the standalone managed binary under `CODEX_HOME`. +Without launch overrides, the daemon assumes Codex is installed through +`install.sh` and launches the standalone managed binary under `CODEX_HOME`. | Situation | What starts | Does this daemon fetch new binaries? | Does a running app-server eventually move to a newer binary on its own? | | --- | --- | --- | --- | | `install.sh` has run, but only `start` is used | `start` uses `CODEX_HOME/packages/standalone/current/codex` | No | No. The managed path is used when starting or restarting, but no updater is installed. | | `install.sh` has run, then `bootstrap` is used | The pidfile backend uses `CODEX_HOME/packages/standalone/current/codex` | Yes. Bootstrap launches a detached updater loop that runs `install.sh` hourly. | Yes, while that updater process is alive and app-server is already running. After a successful fetch, the updater restarts app-server with the refreshed binary and only then replaces its own process image. | +| `start --codex-bin /absolute/path/to/codex` is used | `start` and future lifecycle operations use the explicit executable path | No | No. The owner of that executable decides when to restart the daemon with a replacement binary. | | Some other tool updates the managed binary path | The next fresh start or restart uses the updated file at that path | Only if `bootstrap` is active, because the updater still runs `install.sh` on its normal cadence. | Without `bootstrap`, no. With `bootstrap`, the next successful updater pass compares the managed binary contents after `install.sh` runs; if app-server is running and they differ from the updater's current image, it refreshes app-server first and then itself. | ### Standalone installs diff --git a/codex-rs/app-server-daemon/src/backend/mod.rs b/codex-rs/app-server-daemon/src/backend/mod.rs index d40df07a0e98..2322fc8ef36e 100644 --- a/codex-rs/app-server-daemon/src/backend/mod.rs +++ b/codex-rs/app-server-daemon/src/backend/mod.rs @@ -19,6 +19,7 @@ pub(crate) struct BackendPaths { pub(crate) pid_file: PathBuf, pub(crate) update_pid_file: PathBuf, pub(crate) remote_control_enabled: bool, + pub(crate) analytics_default_enabled: bool, } pub(crate) fn pid_backend(paths: BackendPaths) -> PidBackend { @@ -26,6 +27,7 @@ pub(crate) fn pid_backend(paths: BackendPaths) -> PidBackend { paths.codex_bin, paths.pid_file, paths.remote_control_enabled, + paths.analytics_default_enabled, ) } diff --git a/codex-rs/app-server-daemon/src/backend/pid.rs b/codex-rs/app-server-daemon/src/backend/pid.rs index b525e61254b0..bc70f5ceaca7 100644 --- a/codex-rs/app-server-daemon/src/backend/pid.rs +++ b/codex-rs/app-server-daemon/src/backend/pid.rs @@ -70,12 +70,20 @@ enum PidFileState { #[derive(Debug, Clone, Copy)] #[cfg_attr(not(unix), allow(dead_code))] enum PidCommandKind { - AppServer { remote_control_enabled: bool }, + AppServer { + remote_control_enabled: bool, + analytics_default_enabled: bool, + }, UpdateLoop, } impl PidBackend { - pub(crate) fn new(codex_bin: PathBuf, pid_file: PathBuf, remote_control_enabled: bool) -> Self { + pub(crate) fn new( + codex_bin: PathBuf, + pid_file: PathBuf, + remote_control_enabled: bool, + analytics_default_enabled: bool, + ) -> Self { let lock_file = pid_file.with_extension("pid.lock"); Self { codex_bin, @@ -83,6 +91,7 @@ impl PidBackend { lock_file, command_kind: PidCommandKind::AppServer { remote_control_enabled, + analytics_default_enabled, }, } } @@ -403,11 +412,19 @@ impl PidBackend { fn command_args(&self) -> Vec<&'static str> { match self.command_kind { PidCommandKind::AppServer { - remote_control_enabled: true, - } => vec!["app-server", "--remote-control", "--listen", "unix://"], - PidCommandKind::AppServer { - remote_control_enabled: false, - } => vec!["app-server", "--listen", "unix://"], + remote_control_enabled, + analytics_default_enabled, + } => { + let mut args = vec!["app-server"]; + if remote_control_enabled { + args.push("--remote-control"); + } + if analytics_default_enabled { + args.push("--analytics-default-enabled"); + } + args.extend(["--listen", "unix://"]); + args + } PidCommandKind::UpdateLoop => vec!["app-server", "daemon", "pid-update-loop"], } } @@ -417,9 +434,11 @@ impl PidBackend { match self.command_kind { PidCommandKind::AppServer { remote_control_enabled: false, + .. } => Some((REMOTE_CONTROL_DISABLED_ENV_VAR, "1")), PidCommandKind::AppServer { remote_control_enabled: true, + .. } | PidCommandKind::UpdateLoop => None, } diff --git a/codex-rs/app-server-daemon/src/backend/pid_tests.rs b/codex-rs/app-server-daemon/src/backend/pid_tests.rs index 57b3d227e903..1571a2548fe5 100644 --- a/codex-rs/app-server-daemon/src/backend/pid_tests.rs +++ b/codex-rs/app-server-daemon/src/backend/pid_tests.rs @@ -25,6 +25,7 @@ async fn locked_empty_pid_file_is_treated_as_active_reservation() { temp_dir.path().join("codex"), pid_file.clone(), /*remote_control_enabled*/ false, + /*analytics_default_enabled*/ false, ); let reservation = tokio::fs::OpenOptions::new() .create(true) @@ -53,6 +54,7 @@ async fn unlocked_empty_pid_file_is_treated_as_stale_reservation() { temp_dir.path().join("codex"), pid_file.clone(), /*remote_control_enabled*/ false, + /*analytics_default_enabled*/ false, ); assert_eq!( @@ -73,6 +75,7 @@ async fn stop_waits_for_live_reservation_to_resolve() { temp_dir.path().join("codex"), pid_file.clone(), /*remote_control_enabled*/ false, + /*analytics_default_enabled*/ false, ); let reservation = tokio::fs::OpenOptions::new() .create(true) @@ -105,6 +108,7 @@ async fn start_retries_stale_empty_pid_file_under_its_own_lock() { temp_dir.path().join("missing-codex"), pid_file, /*remote_control_enabled*/ false, + /*analytics_default_enabled*/ false, ); let err = backend.start().await.expect_err("start"); @@ -122,6 +126,7 @@ async fn stale_record_cleanup_preserves_replacement_record() { temp_dir.path().join("codex"), pid_file.clone(), /*remote_control_enabled*/ false, + /*analytics_default_enabled*/ false, ); let stale = PidRecord { pid: 1, @@ -168,6 +173,7 @@ fn app_server_remote_control_uses_runtime_flag() { "codex".into(), "app-server.pid".into(), /*remote_control_enabled*/ true, + /*analytics_default_enabled*/ false, ); assert_eq!( @@ -182,6 +188,7 @@ fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env() { "codex".into(), "app-server.pid".into(), /*remote_control_enabled*/ false, + /*analytics_default_enabled*/ false, ); assert_eq!( @@ -194,6 +201,26 @@ fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env() { ); } +#[test] +fn app_server_analytics_default_uses_runtime_flag() { + let backend = PidBackend::new( + "codex".into(), + "app-server.pid".into(), + /*remote_control_enabled*/ false, + /*analytics_default_enabled*/ true, + ); + + assert_eq!( + backend.command_args(), + vec![ + "app-server", + "--analytics-default-enabled", + "--listen", + "unix://" + ] + ); +} + #[tokio::test] async fn read_stderr_log_tail_returns_recent_complete_lines() { let temp_dir = TempDir::new().expect("temp dir"); diff --git a/codex-rs/app-server-daemon/src/lib.rs b/codex-rs/app-server-daemon/src/lib.rs index 5870075fea3c..1361d7306651 100644 --- a/codex-rs/app-server-daemon/src/lib.rs +++ b/codex-rs/app-server-daemon/src/lib.rs @@ -34,10 +34,16 @@ const OPERATION_LOCK_FILE_NAME: &str = "daemon.lock"; const SETTINGS_FILE_NAME: &str = "settings.json"; const STATE_DIR_NAME: &str = "app-server-daemon"; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum LifecycleCommand { - Start, - Restart, + Start { + codex_bin: Option, + analytics_default_enabled: bool, + }, + Restart { + codex_bin: Option, + analytics_default_enabled: bool, + }, Stop, Version, } @@ -62,6 +68,7 @@ pub struct LifecycleOutput { #[serde(skip_serializing_if = "Option::is_none")] pub pid: Option, pub managed_codex_path: PathBuf, + pub analytics_default_enabled: bool, pub managed_codex_version: Option, pub socket_path: PathBuf, #[serde(skip_serializing_if = "Option::is_none")] @@ -283,12 +290,22 @@ impl Daemon { async fn run(&self, command: LifecycleCommand) -> Result { match command { - LifecycleCommand::Start => { + LifecycleCommand::Start { + codex_bin, + analytics_default_enabled, + } => { let _operation_lock = self.acquire_operation_lock().await?; + self.persist_launch_overrides(codex_bin, analytics_default_enabled) + .await?; self.start().await } - LifecycleCommand::Restart => { + LifecycleCommand::Restart { + codex_bin, + analytics_default_enabled, + } => { let _operation_lock = self.acquire_operation_lock().await?; + self.persist_launch_overrides(codex_bin, analytics_default_enabled) + .await?; self.restart().await } LifecycleCommand::Stop => { @@ -304,6 +321,7 @@ impl Daemon { if let Ok(info) = client::probe(&self.socket_path).await { return Ok(self .output( + &settings, LifecycleStatus::AlreadyRunning, self.running_backend(&settings).await?, /*pid*/ None, @@ -316,6 +334,7 @@ impl Daemon { let info = self.wait_until_ready().await?; return Ok(self .output( + &settings, LifecycleStatus::AlreadyRunning, Some(BackendKind::Pid), /*pid*/ None, @@ -324,11 +343,12 @@ impl Daemon { .await); } - self.ensure_managed_codex_bin()?; - let pid = self.start_managed_backend(&settings).await?; + self.ensure_codex_bin(&settings)?; + let pid = self.start_backend(&settings).await?; let info = self.wait_until_ready().await?; Ok(self .output( + &settings, LifecycleStatus::Started, Some(BackendKind::Pid), pid, @@ -347,15 +367,16 @@ impl Daemon { )); } - self.ensure_managed_codex_bin()?; + self.ensure_codex_bin(&settings)?; if let Some(backend) = self.running_backend_instance(&settings).await? { backend.stop().await?; } - let pid = self.start_managed_backend(&settings).await?; + let pid = self.start_backend(&settings).await?; let info = self.wait_until_ready().await?; Ok(self .output( + &settings, LifecycleStatus::Restarted, Some(BackendKind::Pid), pid, @@ -389,7 +410,7 @@ impl Daemon { RestartDecision::Restart => { backend.stop().await?; let _ = self - .start_managed_backend_with_bin(&settings, managed_codex_bin) + .start_backend_with_bin(&settings, managed_codex_bin) .await?; self.wait_until_ready().await?; RestartIfRunningOutcome::Restarted @@ -416,6 +437,7 @@ impl Daemon { backend.stop().await?; return Ok(self .output( + &settings, LifecycleStatus::Stopped, Some(BackendKind::Pid), /*pid*/ None, @@ -432,6 +454,7 @@ impl Daemon { Ok(self .output( + &settings, LifecycleStatus::NotRunning, /*backend*/ None, /*pid*/ None, @@ -445,6 +468,7 @@ impl Daemon { let info = client::probe(&self.socket_path).await?; Ok(self .output( + &settings, LifecycleStatus::Running, self.running_backend(&settings).await?, /*pid*/ None, @@ -481,13 +505,14 @@ impl Daemon { } async fn append_daemon_app_server_context(&self, context: &mut String) { + let settings = self.load_settings().await.unwrap_or_default(); let managed_codex_version = self - .managed_codex_version_best_effort() + .codex_version_best_effort(&settings) .await .unwrap_or_else(|| "unknown".to_string()); context.push_str(&format!( "\n\nDaemon used app-server:\n path: {}\n version: {managed_codex_version}", - self.managed_codex_bin.display() + self.codex_bin(&settings).display() )); } @@ -573,9 +598,9 @@ impl Daemon { settings.save(&self.settings_file).await?; let app_server_version = if let Some(backend) = backend { - self.ensure_managed_codex_bin()?; + self.ensure_codex_bin(&settings)?; backend.stop().await?; - let _ = self.start_managed_backend(&settings).await?; + let _ = self.start_backend(&settings).await?; Some(self.wait_until_ready().await?.app_server_version) } else { None @@ -590,11 +615,13 @@ impl Daemon { } async fn bootstrap_locked(&self, options: BootstrapOptions) -> Result { - self.ensure_managed_codex_bin()?; - let settings = DaemonSettings { remote_control_enabled: options.remote_control_enabled, + codex_bin: None, + analytics_default_enabled: false, }; + self.ensure_codex_bin(&settings)?; + if client::probe(&self.socket_path).await.is_ok() && self.running_backend(&settings).await?.is_none() { @@ -617,7 +644,7 @@ impl Daemon { updater.start().await?; let info = self.wait_until_ready().await?; - let managed_codex_version = self.managed_codex_version_best_effort().await; + let managed_codex_version = self.codex_version_best_effort(&settings).await; Ok(BootstrapOutput { status: BootstrapStatus::Bootstrapped, backend: BackendKind::Pid, @@ -649,12 +676,12 @@ impl Daemon { Ok(None) } - async fn start_managed_backend(&self, settings: &DaemonSettings) -> Result> { - self.start_managed_backend_with_bin(settings, &self.managed_codex_bin) + async fn start_backend(&self, settings: &DaemonSettings) -> Result> { + self.start_backend_with_bin(settings, self.codex_bin(settings)) .await } - async fn start_managed_backend_with_bin( + async fn start_backend_with_bin( &self, settings: &DaemonSettings, managed_codex_bin: &Path, @@ -669,14 +696,20 @@ impl Daemon { updater.is_starting_or_running().await } - fn ensure_managed_codex_bin(&self) -> Result<()> { - if self.managed_codex_bin.is_file() { + fn ensure_codex_bin(&self, settings: &DaemonSettings) -> Result<()> { + let codex_bin = self.codex_bin(settings); + if codex_bin.is_file() { return Ok(()); } - let managed_codex_path = self.managed_codex_bin.display(); + let codex_path = codex_bin.display(); + if settings.codex_bin.is_some() { + return Err(anyhow!( + "configured Codex executable not found at {codex_path}" + )); + } Err(anyhow!( - "managed standalone Codex install not found at {managed_codex_path}\n\n\ + "managed standalone Codex install not found at {codex_path}\n\n\ This command requires the standalone install managed by the Codex installer, because \ the daemon starts and updates app-server from that fixed path.\n\n\ Install it with:\n curl -fsSL https://chatgpt.com/codex/install.sh | sh\n\n\ @@ -685,17 +718,17 @@ impl Daemon { } #[cfg(unix)] - async fn managed_codex_version_best_effort(&self) -> Option { - managed_codex_version(&self.managed_codex_bin).await.ok() + async fn codex_version_best_effort(&self, settings: &DaemonSettings) -> Option { + managed_codex_version(self.codex_bin(settings)).await.ok() } #[cfg(not(unix))] - async fn managed_codex_version_best_effort(&self) -> Option { + async fn codex_version_best_effort(&self, _settings: &DaemonSettings) -> Option { None } fn backend_paths(&self, settings: &DaemonSettings) -> BackendPaths { - self.backend_paths_with_bin(settings, &self.managed_codex_bin) + self.backend_paths_with_bin(settings, self.codex_bin(settings)) } fn backend_paths_with_bin( @@ -708,6 +741,7 @@ impl Daemon { pid_file: self.pid_file.clone(), update_pid_file: self.update_pid_file.clone(), remote_control_enabled: settings.remote_control_enabled, + analytics_default_enabled: settings.analytics_default_enabled, } } @@ -715,6 +749,40 @@ impl Daemon { DaemonSettings::load(&self.settings_file).await } + async fn persist_launch_overrides( + &self, + codex_bin: Option, + analytics_default_enabled: bool, + ) -> Result<()> { + if codex_bin.is_none() && !analytics_default_enabled { + return Ok(()); + } + let mut settings = self.load_settings().await?; + if let Some(codex_bin) = codex_bin { + if !codex_bin.is_absolute() { + return Err(anyhow!("Codex executable path must be absolute")); + } + if !codex_bin.is_file() { + return Err(anyhow!( + "Codex executable not found at {}", + codex_bin.display() + )); + } + settings.codex_bin = Some(codex_bin); + } + if analytics_default_enabled { + settings.analytics_default_enabled = true; + } + settings.save(&self.settings_file).await + } + + fn codex_bin<'a>(&'a self, settings: &'a DaemonSettings) -> &'a Path { + settings + .codex_bin + .as_deref() + .unwrap_or(&self.managed_codex_bin) + } + async fn acquire_operation_lock(&self) -> Result { let operation_lock = self.open_operation_lock_file().await?; let deadline = tokio::time::Instant::now() + OPERATION_LOCK_TIMEOUT; @@ -755,17 +823,19 @@ impl Daemon { async fn output( &self, + settings: &DaemonSettings, status: LifecycleStatus, backend: Option, pid: Option, app_server_version: Option, ) -> LifecycleOutput { - let managed_codex_version = self.managed_codex_version_best_effort().await; + let managed_codex_version = self.codex_version_best_effort(settings).await; LifecycleOutput { status, backend, pid, - managed_codex_path: self.managed_codex_bin.clone(), + managed_codex_path: self.codex_bin(settings).to_path_buf(), + analytics_default_enabled: settings.analytics_default_enabled, managed_codex_version, socket_path: self.socket_path.clone(), cli_version: Some(env!("CARGO_PKG_VERSION").to_string()), @@ -954,6 +1024,7 @@ mod tests { backend: Some(BackendKind::Pid), pid: None, managed_codex_path: "codex".into(), + analytics_default_enabled: true, managed_codex_version: Some("1.2.3".to_string()), socket_path: "codex.sock".into(), cli_version: Some("1.2.3".to_string()), @@ -967,6 +1038,7 @@ mod tests { "status": "alreadyRunning", "backend": "pid", "managedCodexPath": "codex", + "analyticsDefaultEnabled": true, "managedCodexVersion": "1.2.3", "socketPath": "codex.sock", "cliVersion": "1.2.3", diff --git a/codex-rs/app-server-daemon/src/settings.rs b/codex-rs/app-server-daemon/src/settings.rs index 18b9c9faf722..cbcc82e93cff 100644 --- a/codex-rs/app-server-daemon/src/settings.rs +++ b/codex-rs/app-server-daemon/src/settings.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::path::PathBuf; use anyhow::Context; use anyhow::Result; @@ -10,6 +11,9 @@ use tokio::fs; #[serde(rename_all = "camelCase")] pub(crate) struct DaemonSettings { pub(crate) remote_control_enabled: bool, + pub(crate) codex_bin: Option, + #[serde(default)] + pub(crate) analytics_default_enabled: bool, } impl DaemonSettings { @@ -55,9 +59,24 @@ mod tests { assert_eq!( serde_json::to_string(&DaemonSettings { remote_control_enabled: true, + codex_bin: Some("/Applications/Codex.app/Contents/Resources/codex".into()), + analytics_default_enabled: true, }) .expect("serialize"), - r#"{"remoteControlEnabled":true}"# + r#"{"remoteControlEnabled":true,"codexBin":"/Applications/Codex.app/Contents/Resources/codex","analyticsDefaultEnabled":true}"# + ); + } + + #[test] + fn daemon_settings_load_without_launch_overrides() { + assert_eq!( + serde_json::from_str::(r#"{"remoteControlEnabled":true}"#) + .expect("deserialize"), + DaemonSettings { + remote_control_enabled: true, + codex_bin: None, + analytics_default_enabled: false, + } ); } } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index c766baa04881..62fbd10abbda 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -619,10 +619,10 @@ enum AppServerDaemonSubcommand { Bootstrap(AppServerBootstrapCommand), /// Start the local app server daemon if it is not already running. - Start, + Start(AppServerDaemonStartCommand), /// Restart the local app server daemon. - Restart, + Restart(AppServerDaemonStartCommand), /// Enable remote control for future starts and a currently running managed daemon. EnableRemoteControl, @@ -655,6 +655,17 @@ struct AppServerBootstrapCommand { remote_control: bool, } +#[derive(Debug, Args)] +struct AppServerDaemonStartCommand { + /// Codex executable to use for the managed app-server process. + #[arg(long = "codex-bin", value_name = "PATH")] + codex_bin: Option, + + /// Enable analytics by default for the managed app-server process. + #[arg(long = "analytics-default-enabled")] + analytics_default_enabled: bool, +} + #[derive(Debug, Args)] struct GenerateTsCommand { /// Output directory where .ts files will be written @@ -1151,8 +1162,12 @@ async fn cli_main( .await?; } Some(AppServerSubcommand::Daemon(daemon_cli)) => match daemon_cli.subcommand { - AppServerDaemonSubcommand::Start => { - print_app_server_daemon_output(AppServerLifecycleCommand::Start).await?; + AppServerDaemonSubcommand::Start(start_cli) => { + print_app_server_daemon_output(AppServerLifecycleCommand::Start { + codex_bin: start_cli.codex_bin, + analytics_default_enabled: start_cli.analytics_default_enabled, + }) + .await?; } AppServerDaemonSubcommand::Bootstrap(bootstrap_cli) => { let output = @@ -1162,8 +1177,12 @@ async fn cli_main( .await?; println!("{}", serde_json::to_string(&output)?); } - AppServerDaemonSubcommand::Restart => { - print_app_server_daemon_output(AppServerLifecycleCommand::Restart).await?; + AppServerDaemonSubcommand::Restart(restart_cli) => { + print_app_server_daemon_output(AppServerLifecycleCommand::Restart { + codex_bin: restart_cli.codex_bin, + analytics_default_enabled: restart_cli.analytics_default_enabled, + }) + .await?; } AppServerDaemonSubcommand::EnableRemoteControl => { print_app_server_remote_control_output(AppServerRemoteControlMode::Enabled) @@ -2175,8 +2194,8 @@ fn app_server_subcommand_name(subcommand: Option<&AppServerSubcommand>) -> &'sta None => "app-server", Some(AppServerSubcommand::Daemon(daemon)) => match daemon.subcommand { AppServerDaemonSubcommand::Bootstrap(_) => "app-server daemon bootstrap", - AppServerDaemonSubcommand::Start => "app-server daemon start", - AppServerDaemonSubcommand::Restart => "app-server daemon restart", + AppServerDaemonSubcommand::Start(_) => "app-server daemon start", + AppServerDaemonSubcommand::Restart(_) => "app-server daemon restart", AppServerDaemonSubcommand::EnableRemoteControl => { "app-server daemon enable-remote-control" } @@ -3820,13 +3839,58 @@ mod tests { assert!(matches!( app_server_from_args(["codex", "app-server", "daemon", "start"].as_ref()).subcommand, Some(AppServerSubcommand::Daemon(AppServerDaemonCommand { - subcommand: AppServerDaemonSubcommand::Start + subcommand: AppServerDaemonSubcommand::Start(AppServerDaemonStartCommand { + codex_bin: None, + analytics_default_enabled: false + }) })) )); assert!(matches!( app_server_from_args(["codex", "app-server", "daemon", "restart"].as_ref()).subcommand, Some(AppServerSubcommand::Daemon(AppServerDaemonCommand { - subcommand: AppServerDaemonSubcommand::Restart + subcommand: AppServerDaemonSubcommand::Restart(AppServerDaemonStartCommand { + codex_bin: None, + analytics_default_enabled: false + }) + })) + )); + assert!(matches!( + app_server_from_args( + [ + "codex", + "app-server", + "daemon", + "start", + "--codex-bin", + "/tmp/codex" + ] + .as_ref() + ) + .subcommand, + Some(AppServerSubcommand::Daemon(AppServerDaemonCommand { + subcommand: AppServerDaemonSubcommand::Start(AppServerDaemonStartCommand { + codex_bin: Some(codex_bin), + analytics_default_enabled: false + }) + })) if codex_bin == std::path::Path::new("/tmp/codex") + )); + assert!(matches!( + app_server_from_args( + [ + "codex", + "app-server", + "daemon", + "start", + "--analytics-default-enabled" + ] + .as_ref() + ) + .subcommand, + Some(AppServerSubcommand::Daemon(AppServerDaemonCommand { + subcommand: AppServerDaemonSubcommand::Start(AppServerDaemonStartCommand { + codex_bin: None, + analytics_default_enabled: true + }) })) )); assert!(matches!( diff --git a/codex-rs/cli/src/remote_control_cmd.rs b/codex-rs/cli/src/remote_control_cmd.rs index 20f4f4500db9..4e2e07e625c8 100644 --- a/codex-rs/cli/src/remote_control_cmd.rs +++ b/codex-rs/cli/src/remote_control_cmd.rs @@ -527,6 +527,7 @@ mod tests { backend: None, pid: Some(42), managed_codex_path: PathBuf::from("/opt/codex/bin/codex"), + analytics_default_enabled: false, managed_codex_version: Some("1.0.0".to_string()), socket_path: PathBuf::from("/tmp/app-server-control.sock"), cli_version: Some("1.0.0".to_string()), @@ -648,6 +649,7 @@ mod tests { "status": "started", "pid": 42, "managedCodexPath": "/opt/codex/bin/codex", + "analyticsDefaultEnabled": false, "managedCodexVersion": "1.0.0", "socketPath": "/tmp/app-server-control.sock", "cliVersion": "1.0.0",