fix: update brew auto update version check (#6238)
### Summary * Use `https://github.com/Homebrew/homebrew-cask/blob/main/Casks/c/codex.rb` to get the latest available version for brew usage.
This commit is contained in:
@@ -21,7 +21,7 @@ use codex_exec::Cli as ExecCli;
|
||||
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
|
||||
use codex_tui::AppExitInfo;
|
||||
use codex_tui::Cli as TuiCli;
|
||||
use codex_tui::updates::UpdateAction;
|
||||
use codex_tui::update_action::UpdateAction;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::path::PathBuf;
|
||||
use supports_color::Stream;
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::render::renderable::Renderable;
|
||||
use crate::resume_picker::ResumeSelection;
|
||||
use crate::tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use crate::updates::UpdateAction;
|
||||
use crate::update_action::UpdateAction;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::ConversationManager;
|
||||
@@ -203,7 +203,7 @@ impl App {
|
||||
tui,
|
||||
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
|
||||
latest_version,
|
||||
crate::updates::get_update_action(),
|
||||
crate::update_action::get_update_action(),
|
||||
))),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::style::user_message_style;
|
||||
use crate::text_formatting::format_and_truncate_tool_result;
|
||||
use crate::text_formatting::truncate_text;
|
||||
use crate::ui_consts::LIVE_PREFIX_COLS;
|
||||
use crate::updates::UpdateAction;
|
||||
use crate::update_action::UpdateAction;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_line;
|
||||
|
||||
@@ -71,8 +71,9 @@ mod terminal_palette;
|
||||
mod text_formatting;
|
||||
mod tui;
|
||||
mod ui_consts;
|
||||
pub mod update_action;
|
||||
mod update_prompt;
|
||||
pub mod updates;
|
||||
mod updates;
|
||||
mod version;
|
||||
|
||||
mod wrapping;
|
||||
|
||||
101
codex-rs/tui/src/update_action.rs
Normal file
101
codex-rs/tui/src/update_action.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
/// Update action the CLI should perform after the TUI exits.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UpdateAction {
|
||||
/// Update via `npm install -g @openai/codex@latest`.
|
||||
NpmGlobalLatest,
|
||||
/// Update via `bun install -g @openai/codex@latest`.
|
||||
BunGlobalLatest,
|
||||
/// Update via `brew upgrade codex`.
|
||||
BrewUpgrade,
|
||||
}
|
||||
|
||||
impl UpdateAction {
|
||||
/// Returns the list of command-line arguments for invoking the update.
|
||||
pub fn command_args(self) -> (&'static str, &'static [&'static str]) {
|
||||
match self {
|
||||
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex"]),
|
||||
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex"]),
|
||||
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "codex"]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns string representation of the command-line arguments for invoking the update.
|
||||
pub fn command_str(self) -> String {
|
||||
let (command, args) = self.command_args();
|
||||
shlex::try_join(std::iter::once(command).chain(args.iter().copied()))
|
||||
.unwrap_or_else(|_| format!("{command} {}", args.join(" ")))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) fn get_update_action() -> Option<UpdateAction> {
|
||||
let exe = std::env::current_exe().unwrap_or_default();
|
||||
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
|
||||
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
|
||||
|
||||
detect_update_action(
|
||||
cfg!(target_os = "macos"),
|
||||
&exe,
|
||||
managed_by_npm,
|
||||
managed_by_bun,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(not(debug_assertions), test))]
|
||||
fn detect_update_action(
|
||||
is_macos: bool,
|
||||
current_exe: &std::path::Path,
|
||||
managed_by_npm: bool,
|
||||
managed_by_bun: bool,
|
||||
) -> Option<UpdateAction> {
|
||||
if managed_by_npm {
|
||||
Some(UpdateAction::NpmGlobalLatest)
|
||||
} else if managed_by_bun {
|
||||
Some(UpdateAction::BunGlobalLatest)
|
||||
} else if is_macos
|
||||
&& (current_exe.starts_with("/opt/homebrew") || current_exe.starts_with("/usr/local"))
|
||||
{
|
||||
Some(UpdateAction::BrewUpgrade)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn detects_update_action_without_env_mutation() {
|
||||
assert_eq!(
|
||||
detect_update_action(false, std::path::Path::new("/any/path"), false, false),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
detect_update_action(false, std::path::Path::new("/any/path"), true, false),
|
||||
Some(UpdateAction::NpmGlobalLatest)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_update_action(false, std::path::Path::new("/any/path"), false, true),
|
||||
Some(UpdateAction::BunGlobalLatest)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_update_action(
|
||||
true,
|
||||
std::path::Path::new("/opt/homebrew/bin/codex"),
|
||||
false,
|
||||
false
|
||||
),
|
||||
Some(UpdateAction::BrewUpgrade)
|
||||
);
|
||||
assert_eq!(
|
||||
detect_update_action(
|
||||
true,
|
||||
std::path::Path::new("/usr/local/bin/codex"),
|
||||
false,
|
||||
false
|
||||
),
|
||||
Some(UpdateAction::BrewUpgrade)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ use crate::selection_list::selection_option_row;
|
||||
use crate::tui::FrameRequester;
|
||||
use crate::tui::Tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use crate::update_action::UpdateAction;
|
||||
use crate::updates;
|
||||
use crate::updates::UpdateAction;
|
||||
use codex_core::config::Config;
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -39,7 +39,7 @@ pub(crate) async fn run_update_prompt_if_needed(
|
||||
let Some(latest_version) = updates::get_upgrade_version_for_popup(config) else {
|
||||
return Ok(UpdatePromptOutcome::Continue);
|
||||
};
|
||||
let Some(update_action) = crate::updates::get_update_action() else {
|
||||
let Some(update_action) = crate::update_action::get_update_action() else {
|
||||
return Ok(UpdatePromptOutcome::Continue);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#![cfg(not(debug_assertions))]
|
||||
|
||||
use crate::update_action;
|
||||
use crate::update_action::UpdateAction;
|
||||
use chrono::DateTime;
|
||||
use chrono::Duration;
|
||||
use chrono::Utc;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::default_client::create_client;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::default_client::create_client;
|
||||
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
|
||||
pub fn get_upgrade_version(config: &Config) -> Option<String> {
|
||||
@@ -47,14 +50,17 @@ struct VersionInfo {
|
||||
dismissed_version: Option<String>,
|
||||
}
|
||||
|
||||
const VERSION_FILENAME: &str = "version.json";
|
||||
// We use the latest version from the cask if installation is via homebrew - homebrew does not immediately pick up the latest release and can lag behind.
|
||||
const HOMEBREW_CASK_URL: &str =
|
||||
"https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/c/codex.rb";
|
||||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest";
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct ReleaseInfo {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
const VERSION_FILENAME: &str = "version.json";
|
||||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest";
|
||||
|
||||
fn version_filepath(config: &Config) -> PathBuf {
|
||||
config.codex_home.join(VERSION_FILENAME)
|
||||
}
|
||||
@@ -65,23 +71,35 @@ fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> {
|
||||
}
|
||||
|
||||
async fn check_for_update(version_file: &Path) -> anyhow::Result<()> {
|
||||
let ReleaseInfo {
|
||||
tag_name: latest_tag_name,
|
||||
} = create_client()
|
||||
.get(LATEST_RELEASE_URL)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<ReleaseInfo>()
|
||||
.await?;
|
||||
let latest_version = match update_action::get_update_action() {
|
||||
Some(UpdateAction::BrewUpgrade) => {
|
||||
let cask_contents = create_client()
|
||||
.get(HOMEBREW_CASK_URL)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?;
|
||||
extract_version_from_cask(&cask_contents)?
|
||||
}
|
||||
_ => {
|
||||
let ReleaseInfo {
|
||||
tag_name: latest_tag_name,
|
||||
} = create_client()
|
||||
.get(LATEST_RELEASE_URL)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<ReleaseInfo>()
|
||||
.await?;
|
||||
extract_version_from_latest_tag(&latest_tag_name)?
|
||||
}
|
||||
};
|
||||
|
||||
// Preserve any previously dismissed version if present.
|
||||
let prev_info = read_version_info(version_file).ok();
|
||||
let info = VersionInfo {
|
||||
latest_version: latest_tag_name
|
||||
.strip_prefix("rust-v")
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'"))?
|
||||
.into(),
|
||||
latest_version,
|
||||
last_checked_at: Utc::now(),
|
||||
dismissed_version: prev_info.and_then(|p| p.dismissed_version),
|
||||
};
|
||||
@@ -101,6 +119,25 @@ fn is_newer(latest: &str, current: &str) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_version_from_cask(cask_contents: &str) -> anyhow::Result<String> {
|
||||
cask_contents
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
let line = line.trim();
|
||||
line.strip_prefix("version \"")
|
||||
.and_then(|rest| rest.strip_suffix('"'))
|
||||
.map(ToString::to_string)
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to find version in Homebrew cask file"))
|
||||
}
|
||||
|
||||
fn extract_version_from_latest_tag(latest_tag_name: &str) -> anyhow::Result<String> {
|
||||
latest_tag_name
|
||||
.strip_prefix("rust-v")
|
||||
.map(str::to_owned)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'"))
|
||||
}
|
||||
|
||||
/// Returns the latest version to show in a popup, if it should be shown.
|
||||
/// This respects the user's dismissal choice for the current latest version.
|
||||
pub fn get_upgrade_version_for_popup(config: &Config) -> Option<String> {
|
||||
@@ -140,57 +177,36 @@ fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
|
||||
Some((maj, min, pat))
|
||||
}
|
||||
|
||||
/// Update action the CLI should perform after the TUI exits.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UpdateAction {
|
||||
/// Update via `npm install -g @openai/codex@latest`.
|
||||
NpmGlobalLatest,
|
||||
/// Update via `bun install -g @openai/codex@latest`.
|
||||
BunGlobalLatest,
|
||||
/// Update via `brew upgrade codex`.
|
||||
BrewUpgrade,
|
||||
}
|
||||
|
||||
#[cfg(any(not(debug_assertions), test))]
|
||||
pub(crate) fn get_update_action() -> Option<UpdateAction> {
|
||||
let exe = std::env::current_exe().unwrap_or_default();
|
||||
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
|
||||
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
|
||||
if managed_by_npm {
|
||||
Some(UpdateAction::NpmGlobalLatest)
|
||||
} else if managed_by_bun {
|
||||
Some(UpdateAction::BunGlobalLatest)
|
||||
} else if cfg!(target_os = "macos")
|
||||
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
|
||||
{
|
||||
Some(UpdateAction::BrewUpgrade)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateAction {
|
||||
/// Returns the list of command-line arguments for invoking the update.
|
||||
pub fn command_args(self) -> (&'static str, &'static [&'static str]) {
|
||||
match self {
|
||||
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex@latest"]),
|
||||
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex@latest"]),
|
||||
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "--cask", "codex"]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns string representation of the command-line arguments for invoking the update.
|
||||
pub fn command_str(self) -> String {
|
||||
let (command, args) = self.command_args();
|
||||
let args_str = args.join(" ");
|
||||
format!("{command} {args_str}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_version_from_cask_contents() {
|
||||
let cask = r#"
|
||||
cask "codex" do
|
||||
version "0.55.0"
|
||||
end
|
||||
"#;
|
||||
assert_eq!(
|
||||
extract_version_from_cask(cask).expect("failed to parse version"),
|
||||
"0.55.0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_version_from_latest_tag() {
|
||||
assert_eq!(
|
||||
extract_version_from_latest_tag("rust-v1.5.0").expect("failed to parse version"),
|
||||
"1.5.0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_tag_without_prefix_is_invalid() {
|
||||
assert!(extract_version_from_latest_tag("v1.5.0").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prerelease_version_is_not_considered_newer() {
|
||||
assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), None);
|
||||
@@ -210,24 +226,4 @@ mod tests {
|
||||
assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3)));
|
||||
assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_update_action() {
|
||||
let prev = std::env::var_os("CODEX_MANAGED_BY_NPM");
|
||||
|
||||
// First: no npm var -> expect None (we do not run from brew in CI)
|
||||
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
|
||||
assert_eq!(get_update_action(), None);
|
||||
|
||||
// Then: with npm var -> expect NpmGlobalLatest
|
||||
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", "1") };
|
||||
assert_eq!(get_update_action(), Some(UpdateAction::NpmGlobalLatest));
|
||||
|
||||
// Restore prior value to avoid leaking state
|
||||
if let Some(v) = prev {
|
||||
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", v) };
|
||||
} else {
|
||||
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user