Files
codex/codex-rs/tui/src/updates.rs
T
Charlie Marsh d141438342 Detect Vite+-managed Codex installs (#42071)
## Why

Vite+ launches Codex through the same JavaScript shim as npm, so Codex could
misidentify Vite+-owned installations and recommend npm for updates.

## What changed

- Detect Vite+ global installs from their package ownership metadata and pass a
  dedicated `CODEX_MANAGED_BY_VITE_PLUS` marker to the native binary.
- Represent Vite+ in install context and `codex doctor` output.
- Recommend `vp install -g @openai/codex` in update and repair flows for
  Vite+-managed installs.

See https://github.com/openai/codex/pull/30880.

GitOrigin-RevId: 8add724eefd31170a078086392609dbbd7f20b26
2026-09-01 15:12:53 +00:00

166 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![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_http_client::ClientRouteClass;
use codex_http_client::HttpClientFactory;
use codex_http_client::RouteAwareClientPool;
use codex_login::default_client::default_headers;
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),
} {
let http_client_factory = config.http_client_factory();
// 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, http_client_factory)
.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>,
http_client_factory: HttpClientFactory,
) -> anyhow::Result<()> {
let client_pool = RouteAwareClientPool::with_chatgpt_cloudflare_cookies(
http_client_factory,
ClientRouteClass::Other,
)
.with_legacy_custom_ca_fallback();
let latest_version = match action {
Some(UpdateAction::BrewUpgrade) => {
let HomebrewCaskInfo { version } = client_pool
.get(HOMEBREW_CASK_API_URL)
.headers(default_headers())
.send()
.await?
.error_for_status()?
.json::<HomebrewCaskInfo>()
.await?;
version
}
Some(UpdateAction::NpmGlobalLatest)
| Some(UpdateAction::BunGlobalLatest)
| Some(UpdateAction::VitePlusGlobalLatest)
| Some(UpdateAction::PnpmGlobalLatest) => {
let latest_version = fetch_latest_github_release_version(&client_pool).await?;
let package_info = client_pool
.get(npm_registry::PACKAGE_URL)
.headers(default_headers())
.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(&client_pool).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(
client_pool: &RouteAwareClientPool,
) -> anyhow::Result<String> {
let ReleaseInfo {
tag_name: latest_tag_name,
} = client_pool
.get(LATEST_RELEASE_URL)
.headers(default_headers())
.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)
}