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()) }