From b985768dc11446c60e092af510b75e5e8863e514 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 27 Apr 2026 23:33:59 -0700 Subject: [PATCH] Add `codex update` command (#19933) ## Why Addresses #9274 Running `codex update` currently starts an interactive Codex session with `update` as the prompt. That is a rough edge for users who expect a direct self-update command after seeing the existing update notice, and it forces them to copy the suggested package-manager command manually. ## What changed - Added a top-level `codex update` subcommand. - Reused the existing install-channel detection and update command runner that the TUI already uses for update prompts. - Exposed the update-action lookup from `codex-tui` so the CLI can invoke the same behavior. - Added CLI coverage to ensure `codex update` is parsed as a subcommand instead of becoming an interactive prompt. ## Verification - `cargo test -p codex-cli` - `cargo test -p codex-tui update_action::tests` --- codex-rs/cli/src/main.rs | 36 +++++++++++++++++++++++ codex-rs/cli/tests/marketplace_upgrade.rs | 2 +- codex-rs/cli/tests/update.rs | 24 +++++++++++++++ codex-rs/tui/src/lib.rs | 2 ++ codex-rs/tui/src/update_action.rs | 2 +- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 codex-rs/cli/tests/update.rs diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 852dff616..c134bc8a2 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -134,6 +134,9 @@ enum Subcommand { /// Generate shell completion scripts. Completion(CompletionCommand), + /// Update Codex to the latest version. + Update, + /// Run commands within a Codex-provided sandbox. Sandbox(SandboxArgs), @@ -615,6 +618,25 @@ fn run_update_action(action: UpdateAction) -> anyhow::Result<()> { Ok(()) } +fn run_update_command() -> anyhow::Result<()> { + #[cfg(debug_assertions)] + { + anyhow::bail!( + "`codex update` is not available in debug builds. Install a release build of Codex to use this command." + ); + } + + #[cfg(not(debug_assertions))] + { + let Some(action) = codex_tui::get_update_action() else { + anyhow::bail!( + "Could not detect the Codex installation method. Please update manually: https://developers.openai.com/codex/cli/" + ); + }; + run_update_action(action) + } +} + fn run_execpolicycheck(cmd: ExecPolicyCheckCommand) -> anyhow::Result<()> { cmd.run() } @@ -998,6 +1020,14 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { )?; print_completion(completion_cli); } + Some(Subcommand::Update) => { + reject_remote_mode_for_subcommand( + root_remote.as_deref(), + root_remote_auth_token_env.as_deref(), + "update", + )?; + run_update_command()?; + } Some(Subcommand::Cloud(mut cloud_cli)) => { reject_remote_mode_for_subcommand( root_remote.as_deref(), @@ -1890,6 +1920,12 @@ mod tests { assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_)))); } + #[test] + fn update_parses_as_update_subcommand() { + let cli = MultitoolCli::try_parse_from(["codex", "update"]).expect("parse"); + assert!(matches!(cli.subcommand, Some(Subcommand::Update))); + } + #[test] fn plugin_marketplace_remove_parses_under_plugin() { let cli = diff --git a/codex-rs/cli/tests/marketplace_upgrade.rs b/codex-rs/cli/tests/marketplace_upgrade.rs index 081203ebe..268d75358 100644 --- a/codex-rs/cli/tests/marketplace_upgrade.rs +++ b/codex-rs/cli/tests/marketplace_upgrade.rs @@ -30,7 +30,7 @@ async fn marketplace_upgrade_no_longer_runs_at_top_level() -> Result<()> { .args(["marketplace", "upgrade"]) .assert() .failure() - .stderr(contains("unexpected argument 'upgrade' found")); + .stderr(contains("unrecognized subcommand 'upgrade'")); Ok(()) } diff --git a/codex-rs/cli/tests/update.rs b/codex-rs/cli/tests/update.rs new file mode 100644 index 000000000..cf1742cda --- /dev/null +++ b/codex-rs/cli/tests/update.rs @@ -0,0 +1,24 @@ +use anyhow::Result; +use predicates::str::contains; +use std::path::Path; +use tempfile::TempDir; + +fn codex_command(codex_home: &Path) -> Result { + let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?); + cmd.env("CODEX_HOME", codex_home); + Ok(cmd) +} + +#[cfg(debug_assertions)] +#[tokio::test] +async fn update_does_not_start_interactive_prompt() -> Result<()> { + let codex_home = TempDir::new()?; + + codex_command(codex_home.path())? + .arg("update") + .assert() + .failure() + .stderr(contains("`codex update` is not available in debug builds")); + + Ok(()) +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 6dd527c71..654e90a63 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -171,6 +171,8 @@ mod tui; mod ui_consts; pub(crate) mod update_action; pub use update_action::UpdateAction; +#[cfg(not(debug_assertions))] +pub use update_action::get_update_action; mod update_prompt; #[cfg(any(not(debug_assertions), test))] mod update_versions; diff --git a/codex-rs/tui/src/update_action.rs b/codex-rs/tui/src/update_action.rs index 1ac2ff675..aca065440 100644 --- a/codex-rs/tui/src/update_action.rs +++ b/codex-rs/tui/src/update_action.rs @@ -59,7 +59,7 @@ impl UpdateAction { } #[cfg(not(debug_assertions))] -pub(crate) fn get_update_action() -> Option { +pub fn get_update_action() -> Option { UpdateAction::from_install_context(InstallContext::current()) }