Codex Handbook
tui/src/updates.rs 144 lines
#![cfg(not(debug_assertions))]use crate::legacy_core::config::Config;use crate::npm_registry;use crate::npm_registry::NpmPackageInfo;use crate::update_action;use crate::update_action::UpdateAction;use crate::update_versions::extract_version_from_latest_tag;use crate::update_versions::is_newer;use crate::update_versions::is_source_build_version;use crate::updates_cache::VersionInfo;use crate::updates_cache::read_version_info;use crate::updates_cache::version_filepath;use chrono::Duration;use chrono::Utc;use codex_login::default_client::create_client;use serde::Deserialize;use std::path::Path;use crate::version::CODEX_CLI_VERSION;pub(crate) use crate::updates_cache::dismiss_version;pub fn get_upgrade_version(config: &Config) -> Option<String> {    if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) {        return None;    }    let action = update_action::get_update_action();    let version_file = version_filepath(config);    let info = read_version_info(&version_file).ok();    if match &info {        None => true,        Some(info) => info.last_checked_at < Utc::now() - Duration::hours(20),    } {        // Refresh the cached latest version in the background so TUI startup        // isn’t 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.        tokio::spawn(async move {            check_for_update(&version_file, action)                .await                .inspect_err(|e| tracing::error!("Failed to update version: {e}"))        });    }    info.and_then(|info| {        if is_newer(&info.latest_version, CODEX_CLI_VERSION).unwrap_or(false) {            Some(info.latest_version)        } else {            None        }    })}// 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_API_URL: &str = "https://formulae.brew.sh/api/cask/codex.json";const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest";#[derive(Deserialize, Debug, Clone)]struct ReleaseInfo {    tag_name: String,}#[derive(Deserialize, Debug, Clone)]struct HomebrewCaskInfo {    version: String,}async fn check_for_update(version_file: &Path, action: Option<UpdateAction>) -> anyhow::Result<()> {    let latest_version = match action {        Some(UpdateAction::BrewUpgrade) => {            let HomebrewCaskInfo { version } = create_client()                .get(HOMEBREW_CASK_API_URL)                .send()                .await?                .error_for_status()?                .json::<HomebrewCaskInfo>()                .await?;            version        }        Some(UpdateAction::NpmGlobalLatest) | Some(UpdateAction::BunGlobalLatest) => {            let latest_version = fetch_latest_github_release_version().await?;            let package_info = create_client()                .get(npm_registry::PACKAGE_URL)                .send()                .await?                .error_for_status()?                .json::<NpmPackageInfo>()                .await?;            npm_registry::ensure_version_ready(&package_info, &latest_version)?;            latest_version        }        Some(UpdateAction::StandaloneUnix) | Some(UpdateAction::StandaloneWindows) | None => {            fetch_latest_github_release_version().await?        }    };    // Preserve any previously dismissed version if present.    let prev_info = read_version_info(version_file).ok();    let info = VersionInfo {        latest_version,        last_checked_at: Utc::now(),        dismissed_version: prev_info.and_then(|p| p.dismissed_version),    };    let json_line = format!("{}\n", serde_json::to_string(&info)?);    if let Some(parent) = version_file.parent() {        tokio::fs::create_dir_all(parent).await?;    }    tokio::fs::write(version_file, json_line).await?;    Ok(())}async fn fetch_latest_github_release_version() -> anyhow::Result<String> {    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)}/// 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> {    if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) {        return None;    }    let version_file = version_filepath(config);    let latest = get_upgrade_version(config)?;    // If the user dismissed this exact version previously, do not show the popup.    if let Ok(info) = read_version_info(&version_file)        && info.dismissed_version.as_deref() == Some(latest.as_str())    {        return None;    }    Some(latest)}