Compare commits

...

2 Commits

Author SHA1 Message Date
Fouad Matin
504b6e238e Refine update detection for clippy 2025-09-10 14:12:51 -07:00
Fouad Matin
7d27e0e7e7 Add CLI update command 2025-09-10 14:00:08 -07:00
8 changed files with 195 additions and 13 deletions

View File

@@ -32,6 +32,14 @@ Then simply run `codex` to get started:
codex
```
To update Codex after installing it, run:
```shell
codex update
```
You can also run `codex upgrade`. The CLI will reuse the installation method you originally used (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>

View File

@@ -132,6 +132,14 @@ Run interactively:
codex
```
To upgrade Codex after installing it, run:
```shell
codex update
```
(`codex upgrade` works too.) The CLI reuses the installation method you originally used (npm or Homebrew).
Or, run with a prompt as input (and optionally in `Full Auto` mode):
```shell

View File

@@ -38,3 +38,6 @@ tokio = { version = "1", features = [
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
codex-protocol-ts = { path = "../protocol-ts" }
[dev-dependencies]
pretty_assertions = "1"

View File

@@ -2,6 +2,7 @@ pub mod debug_sandbox;
mod exit_status;
pub mod login;
pub mod proto;
pub mod update;
use clap::Parser;
use codex_common::CliConfigOverrides;

View File

@@ -12,6 +12,7 @@ use codex_cli::login::run_login_with_api_key;
use codex_cli::login::run_login_with_chatgpt;
use codex_cli::login::run_logout;
use codex_cli::proto;
use codex_cli::update::run_update_command;
use codex_common::CliConfigOverrides;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
@@ -73,6 +74,10 @@ enum Subcommand {
#[clap(visible_alias = "a")]
Apply(ApplyCommand),
/// Update Codex using the original installation method.
#[clap(visible_alias = "upgrade")]
Update,
/// Internal: generate TypeScript protocol bindings.
#[clap(hide = true)]
GenerateTs(GenerateTsCommand),
@@ -212,6 +217,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
Some(Subcommand::GenerateTs(gen_cli)) => {
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
}
Some(Subcommand::Update) => {
run_update_command().await;
}
}
Ok(())

157
codex-rs/cli/src/update.rs Normal file
View File

@@ -0,0 +1,157 @@
use std::path::Path;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::process::Command;
use crate::exit_status::handle_exit_status;
const RELEASE_URL: &str = "https://github.com/openai/codex/releases/latest";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstallMethod {
Npm,
Brew,
}
#[derive(Debug)]
struct InstallEnvironment {
managed_by_npm: bool,
current_exe: Option<PathBuf>,
is_macos: bool,
}
impl InstallEnvironment {
fn from_system() -> Self {
Self {
managed_by_npm: std::env::var_os("CODEX_MANAGED_BY_NPM").is_some(),
current_exe: std::env::current_exe().ok(),
is_macos: cfg!(target_os = "macos"),
}
}
}
pub async fn run_update_command() -> ! {
let env = InstallEnvironment::from_system();
let Some(method) = detect_install_method(&env) else {
eprintln!("Unable to determine how Codex was installed.");
eprintln!("If you installed Codex with npm, run `npm install -g @openai/codex@latest`.",);
eprintln!("If you installed Codex with Homebrew, run `brew upgrade codex`.");
eprintln!("For other installation methods, see {RELEASE_URL}.");
std::process::exit(1);
};
let (program, args) = match method {
InstallMethod::Npm => ("npm", ["install", "-g", "@openai/codex@latest"]),
InstallMethod::Brew => ("brew", ["upgrade", "codex"]),
};
run_external_command(program, &args).await;
}
fn detect_install_method(env: &InstallEnvironment) -> Option<InstallMethod> {
if env.managed_by_npm {
return Some(InstallMethod::Npm);
}
if env.is_macos
&& env
.current_exe
.as_deref()
.is_some_and(is_homebrew_executable)
{
return Some(InstallMethod::Brew);
}
None
}
fn is_homebrew_executable(exe: &Path) -> bool {
const HOMEBREW_PREFIXES: &[&str] = &["/opt/homebrew", "/usr/local"];
HOMEBREW_PREFIXES
.iter()
.any(|prefix| exe.starts_with(prefix))
}
async fn run_external_command(program: &str, args: &[&str]) -> ! {
let command_display = format_command(program, args);
eprintln!("Running `{command_display}` to update Codex...");
let status = Command::new(program)
.args(args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.await;
match status {
Ok(status) => handle_exit_status(status),
Err(err) => {
eprintln!("Failed to execute `{command_display}`: {err}");
std::process::exit(1);
}
}
}
fn format_command(program: &str, args: &[&str]) -> String {
let mut command = String::from(program);
for arg in args {
command.push(' ');
command.push_str(arg);
}
command
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn detects_npm_when_env_var_is_present() {
let env = InstallEnvironment {
managed_by_npm: true,
current_exe: Some(PathBuf::from("/opt/homebrew/bin/codex")),
is_macos: true,
};
assert_eq!(detect_install_method(&env), Some(InstallMethod::Npm));
}
#[test]
fn detects_homebrew_install_on_macos() {
let env = InstallEnvironment {
managed_by_npm: false,
current_exe: Some(PathBuf::from("/opt/homebrew/bin/codex")),
is_macos: true,
};
assert_eq!(detect_install_method(&env), Some(InstallMethod::Brew));
}
#[test]
fn returns_none_when_install_method_is_unknown() {
let env = InstallEnvironment {
managed_by_npm: false,
current_exe: Some(PathBuf::from("/tmp/codex")),
is_macos: false,
};
assert_eq!(detect_install_method(&env), None);
}
#[test]
fn homebrew_prefixes_are_detected() {
assert!(is_homebrew_executable(Path::new("/opt/homebrew/bin/codex")));
assert!(is_homebrew_executable(Path::new("/usr/local/bin/codex")));
assert!(!is_homebrew_executable(Path::new(
"/home/user/.local/bin/codex"
)));
}
#[test]
fn command_formatting_is_readable() {
assert_eq!(
format_command("npm", &["install", "-g", "@openai/codex@latest"]),
"npm install -g @openai/codex@latest"
);
}
}

View File

@@ -269,21 +269,17 @@ async fn run_ratatui_app(
format!("{current_version} -> {latest_version}.").into(),
]));
if managed_by_npm {
let npm_cmd = "npm install -g @openai/codex@latest";
let knows_update_command = managed_by_npm
|| (cfg!(target_os = "macos")
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local")));
if knows_update_command {
lines.push(Line::from(vec![
"Run ".into(),
npm_cmd.cyan(),
" to update.".into(),
]));
} else if cfg!(target_os = "macos")
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
{
let brew_cmd = "brew upgrade codex";
lines.push(Line::from(vec![
"Run ".into(),
brew_cmd.cyan(),
" to update.".into(),
"codex update".cyan(),
" (or ".into(),
"codex upgrade".cyan(),
") to update automatically.".into(),
]));
} else {
lines.push(Line::from(vec![

View File

@@ -109,6 +109,7 @@ fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn prerelease_version_is_not_considered_newer() {