Compare commits

...

7 Commits

Author SHA1 Message Date
Ahmed Ibrahim
abe596976b fix 2025-08-08 18:18:25 -07:00
Ahmed Ibrahim
6967b51065 fix 2025-08-08 18:17:21 -07:00
Ahmed Ibrahim
697f7b1300 fix 2025-08-08 18:15:33 -07:00
Ahmed Ibrahim
14a3bb51b3 refactor 2025-08-08 17:58:14 -07:00
Ahmed Ibrahim
4f590ebf44 refactor 2025-08-08 17:57:18 -07:00
aibrahim-oai
ddabd42236 feat(cli): auto-update command 2025-08-08 17:44:28 -07:00
aibrahim-oai
b3d47cfa11 feat(cli): add update command 2025-08-08 17:18:59 -07:00
9 changed files with 142 additions and 21 deletions

View File

@@ -17,6 +17,7 @@
- [Quickstart](#quickstart)
- [Installing and running Codex CLI](#installing-and-running-codex-cli)
- [Updating](#updating)
- [Using Codex with your ChatGPT plan](#using-codex-with-your-chatgpt-plan)
- [Usage-based billing alternative: Use an OpenAI API key](#usage-based-billing-alternative-use-an-openai-api-key)
- [Choosing Codex's level of autonomy](#choosing-codexs-level-of-autonomy)
@@ -76,6 +77,16 @@ Then simply run `codex` to get started:
codex
```
### Updating
Upgrade an existing installation to the latest release:
```shell
codex update
```
The command checks for a newer version and will attempt to upgrade automatically if the CLI was installed via npm or Homebrew.
<details>
<summary>You can also go to the <a href="https://github.com/openai/codex/releases/latest">latest GitHub Release</a> and download the appropriate binary for your platform.</summary>
@@ -340,11 +351,12 @@ Help us improve by filing issues or submitting PRs (see the section below for ho
## CLI reference
| Command | Purpose | Example |
| ------------------ | ---------------------------------- | ------------------------------- |
| `codex` | Interactive TUI | `codex` |
| `codex "..."` | Initial prompt for interactive TUI | `codex "fix lint errors"` |
| `codex exec "..."` | Non-interactive "automation mode" | `codex exec "explain utils.ts"` |
| Command | Purpose | Example |
| ------------------ | ------------------------------------- | ------------------------------- |
| `codex` | Interactive TUI | `codex` |
| `codex "..."` | Initial prompt for interactive TUI | `codex "fix lint errors"` |
| `codex exec "..."` | Non-interactive "automation mode" | `codex exec "explain utils.ts"` |
| `codex update` | Check for updates and upgrade the CLI | `codex update` |
Key flags: `--model/-m`, `--ask-for-approval/-a`.

7
codex-rs/Cargo.lock generated
View File

@@ -658,10 +658,16 @@ dependencies = [
name = "codex-common"
version = "0.0.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"codex-core",
"reqwest",
"serde",
"serde_json",
"tokio",
"toml 0.9.4",
"tracing",
]
[[package]]
@@ -888,7 +894,6 @@ dependencies = [
"ratatui",
"ratatui-image",
"regex-lite",
"reqwest",
"serde",
"serde_json",
"shlex",

View File

@@ -20,7 +20,7 @@ clap = { version = "4", features = ["derive"] }
clap_complete = "4"
codex-arg0 = { path = "../arg0" }
codex-chatgpt = { path = "../chatgpt" }
codex-common = { path = "../common", features = ["cli"] }
codex-common = { path = "../common", features = ["cli", "updates"] }
codex-core = { path = "../core" }
codex-exec = { path = "../exec" }
codex-login = { path = "../login" }

View File

@@ -13,6 +13,12 @@ use codex_cli::login::run_login_with_chatgpt;
use codex_cli::login::run_logout;
use codex_cli::proto;
use codex_common::CliConfigOverrides;
use codex_common::updates::check_for_update;
use codex_common::updates::get_upgrade_version;
#[cfg(not(debug_assertions))]
use codex_core::config::Config;
#[cfg(not(debug_assertions))]
use codex_core::config::ConfigOverrides;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
use std::path::PathBuf;
@@ -68,6 +74,9 @@ enum Subcommand {
/// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
#[clap(visible_alias = "a")]
Apply(ApplyCommand),
/// Check for a newer Codex release and upgrade automatically when possible.
Update,
}
#[derive(Debug, Parser)]
@@ -190,6 +199,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides);
run_apply_command(apply_cli, None).await?;
}
Some(Subcommand::Update) => {
run_update().await?;
}
}
Ok(())
@@ -211,3 +223,88 @@ fn print_completion(cmd: CompletionCommand) {
let name = "codex";
generate(cmd.shell, &mut app, name, &mut std::io::stdout());
}
#[cfg(not(debug_assertions))]
async fn run_update() -> anyhow::Result<()> {
let overrides = ConfigOverrides {
model: None,
cwd: None,
approval_policy: None,
sandbox_mode: None,
model_provider: None,
config_profile: None,
codex_linux_sandbox_exe: None,
base_instructions: None,
include_plan_tool: None,
disable_response_storage: None,
show_raw_agent_reasoning: None,
};
let config = Config::load_with_cli_overrides(Vec::new(), overrides)?;
let version_file = config.codex_home.join("version.json");
if let Err(e) = check_for_update(&version_file).await {
#[allow(clippy::print_stderr)]
eprintln!("Failed to check for updates: {e}");
}
let current_version = env!("CARGO_PKG_VERSION");
if let Some(latest_version) = get_upgrade_version(&config) {
println!("Current version: {current_version}");
println!("Latest version: {latest_version}");
let exe = std::env::current_exe()?;
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
if managed_by_npm {
println!("Updating via npm...");
match Command::new("npm")
.args(["install", "-g", "@openai/codex@latest"])
.status()
{
Ok(status) if status.success() => {
println!("Codex updated successfully.");
}
Ok(status) => {
println!(
"`npm install` exited with status {status}. Run `npm install -g @openai/codex@latest` manually if needed."
);
}
Err(err) => {
println!(
"Failed to run npm: {err}. Run `npm install -g @openai/codex@latest` manually."
);
}
}
} else if cfg!(target_os = "macos")
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
{
println!("Updating via Homebrew...");
match Command::new("brew").args(["upgrade", "codex"]).status() {
Ok(status) if status.success() => {
println!("Codex updated successfully.");
}
Ok(status) => {
println!(
"`brew upgrade` exited with status {status}. Run `brew upgrade codex` manually if needed."
);
}
Err(err) => {
println!("Failed to run Homebrew: {err}. Run `brew upgrade codex` manually.");
}
}
} else {
println!(
"See https://github.com/openai/codex/releases/latest for the latest releases and installation options."
);
}
} else {
println!("Codex {current_version} is up to date.");
}
Ok(())
}
#[cfg(debug_assertions)]
async fn run_update() -> anyhow::Result<()> {
println!("Update checking is disabled in debug builds.");
Ok(())
}

View File

@@ -7,13 +7,20 @@ version = { workspace = true }
workspace = true
[dependencies]
anyhow = { version = "1", optional = true }
chrono = { version = "0.4", features = ["serde"], optional = true }
clap = { version = "4", features = ["derive", "wrap_help"], optional = true }
codex-core = { path = "../core" }
serde = { version = "1", optional = true }
reqwest = { version = "0.12", features = ["json"], optional = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
tokio = { version = "1", features = ["fs"], optional = true }
toml = { version = "0.9", optional = true }
tracing = "0.1.41"
[features]
# Separate feature so that `clap` is not a mandatory dependency.
cli = ["clap", "serde", "toml"]
elapsed = []
sandbox_summary = []
updates = ["anyhow", "chrono", "reqwest", "serde", "serde_json", "tokio"]

View File

@@ -29,3 +29,6 @@ mod config_summary;
pub use config_summary::create_config_summary_entries;
// Shared fuzzy matcher (used by TUI selection popups and other UI filtering)
pub mod fuzzy_match;
#[cfg(any(test, feature = "updates"))]
pub mod updates;

View File

@@ -1,15 +1,15 @@
#![cfg(any(not(debug_assertions), test))]
use chrono::DateTime;
use chrono::Duration;
use chrono::Utc;
use codex_core::config::Config;
use serde::Deserialize;
use serde::Serialize;
use std::path::Path;
use std::path::PathBuf;
use tracing::error;
use codex_core::config::Config;
/// Returns the latest available version string if it is newer than the current
/// one, otherwise `None`.
pub fn get_upgrade_version(config: &Config) -> Option<String> {
let version_file = version_filepath(config);
let info = read_version_info(&version_file).ok();
@@ -18,13 +18,11 @@ pub fn get_upgrade_version(config: &Config) -> Option<String> {
None => true,
Some(info) => info.last_checked_at < Utc::now() - Duration::hours(20),
} {
// Refresh the cached latest version in the background so TUI startup
// isnt blocked by a network call. The UI reads the previously cached
// value (if any) for this run; the next run shows the banner if needed.
// Refresh in the background; callers can use the cached value for this run.
tokio::spawn(async move {
check_for_update(&version_file)
.await
.inspect_err(|e| tracing::error!("Failed to update version: {e}"))
.inspect_err(|e| error!("Failed to update version: {e}"))
});
}
@@ -62,7 +60,8 @@ fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> {
Ok(serde_json::from_str(&contents)?)
}
async fn check_for_update(version_file: &Path) -> anyhow::Result<()> {
/// Fetches the latest release info and updates the on-disk cache file.
pub async fn check_for_update(version_file: &Path) -> anyhow::Result<()> {
let ReleaseInfo {
tag_name: latest_tag_name,
} = reqwest::Client::new()

View File

@@ -29,6 +29,7 @@ codex-common = { path = "../common", features = [
"cli",
"elapsed",
"sandbox_summary",
"updates",
] }
codex-core = { path = "../core" }
codex-file-search = { path = "../file-search" }
@@ -48,7 +49,6 @@ ratatui = { version = "0.29.0", features = [
] }
ratatui-image = "8.0.0"
regex-lite = "0.1"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
shlex = "1.3.0"

View File

@@ -48,8 +48,6 @@ mod text_formatting;
mod tui;
mod user_approval_widget;
#[cfg(not(debug_assertions))]
mod updates;
#[cfg(not(debug_assertions))]
use color_eyre::owo_colors::OwoColorize;
@@ -211,7 +209,7 @@ pub async fn run_main(
#[allow(clippy::print_stderr)]
#[cfg(not(debug_assertions))]
if let Some(latest_version) = updates::get_upgrade_version(&config) {
if let Some(latest_version) = codex_common::updates::get_upgrade_version(&config) {
let current_version = env!("CARGO_PKG_VERSION");
let exe = std::env::current_exe()?;
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();