mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
235 lines
8.6 KiB
Rust
235 lines
8.6 KiB
Rust
//! Diagnoses whether Codex update paths target the running installation.
|
|
//!
|
|
//! Update diagnostics combine cached version metadata, install-channel hints,
|
|
//! and bounded latest-version probes. For npm-managed launches, this module also
|
|
//! verifies that npm install -g would update the package root that launched the
|
|
//! current process, which catches PATH and prefix mismatches before the user runs
|
|
//! an update command.
|
|
|
|
use std::path::Path;
|
|
|
|
use codex_core::config::Config;
|
|
use codex_install_context::InstallContext;
|
|
use codex_install_context::InstallMethod;
|
|
use serde::Deserialize;
|
|
|
|
use super::CheckStatus;
|
|
use super::DoctorCheck;
|
|
use super::NpmRootCheck;
|
|
use super::doctor_install_context;
|
|
use super::doctor_managed_by_npm;
|
|
use super::npm_global_root_check;
|
|
use super::run_command;
|
|
|
|
const VERSION_FILE_NAME: &str = "version.json";
|
|
const GITHUB_LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest";
|
|
const HOMEBREW_CASK_API_URL: &str = "https://formulae.brew.sh/api/cask/codex.json";
|
|
|
|
/// Builds the update-health row for the current installation.
|
|
///
|
|
/// Network failures while fetching latest-version metadata degrade the row to a
|
|
/// warning instead of failing doctor outright; update freshness is useful
|
|
/// support context but should not mask more direct install/config failures.
|
|
pub(super) fn updates_check(config: &Config) -> DoctorCheck {
|
|
let current_exe = std::env::current_exe().ok();
|
|
let install_context = doctor_install_context(current_exe.as_deref());
|
|
let mut details = vec![
|
|
format!(
|
|
"check for update on startup: {}",
|
|
config.check_for_update_on_startup
|
|
),
|
|
format!("update action: {}", update_action_label(&install_context)),
|
|
];
|
|
let version_file = config.codex_home.join(VERSION_FILE_NAME);
|
|
push_cached_version_details(&mut details, &version_file);
|
|
|
|
let mut status = CheckStatus::Ok;
|
|
let mut summary = "update configuration is locally consistent".to_string();
|
|
let mut remediation = None;
|
|
|
|
if doctor_managed_by_npm(current_exe.as_deref()) {
|
|
match npm_global_root_check() {
|
|
NpmRootCheck::Match { package_root } => {
|
|
details.push(format!("npm update target: {}", package_root.display()));
|
|
}
|
|
NpmRootCheck::Mismatch {
|
|
running_package_root,
|
|
npm_package_root,
|
|
} => {
|
|
status = CheckStatus::Fail;
|
|
summary = "update would target a different npm install".to_string();
|
|
details.push(format!(
|
|
"running package root: {}",
|
|
running_package_root.display()
|
|
));
|
|
details.push(format!("npm package root: {}", npm_package_root.display()));
|
|
remediation = Some(format!(
|
|
"Fix PATH or npm prefix so the running package root ({}) matches the npm global package root ({}).",
|
|
running_package_root.display(),
|
|
npm_package_root.display()
|
|
));
|
|
}
|
|
NpmRootCheck::MissingPackageRoot => {
|
|
status = status.max(CheckStatus::Warning);
|
|
summary = "npm update target could not be proven".to_string();
|
|
remediation = Some(
|
|
"Reinstall or update Codex so the JS shim provides CODEX_MANAGED_PACKAGE_ROOT."
|
|
.to_string(),
|
|
);
|
|
}
|
|
NpmRootCheck::NpmUnavailable(error) => {
|
|
status = status.max(CheckStatus::Warning);
|
|
summary = "npm update target could not be inspected".to_string();
|
|
details.push(format!("npm root -g failed: {error}"));
|
|
}
|
|
}
|
|
}
|
|
|
|
match fetch_latest_version(&install_context) {
|
|
Ok(latest_version) => {
|
|
details.push(format!("latest version: {latest_version}"));
|
|
if is_newer(&latest_version, env!("CARGO_PKG_VERSION")) == Some(true) {
|
|
details.push("latest version status: newer version is available".to_string());
|
|
} else {
|
|
details.push("latest version status: current version is not older".to_string());
|
|
}
|
|
}
|
|
Err(err) => {
|
|
status = status.max(CheckStatus::Warning);
|
|
details.push(format!("latest version probe: {err}"));
|
|
}
|
|
}
|
|
|
|
let mut check = DoctorCheck::new("updates.status", "updates", status, summary).details(details);
|
|
if let Some(remediation) = remediation {
|
|
check = check.remediation(remediation);
|
|
}
|
|
check
|
|
}
|
|
|
|
fn push_cached_version_details(details: &mut Vec<String>, version_file: &Path) {
|
|
details.push(format!("version cache: {}", version_file.display()));
|
|
match std::fs::read_to_string(version_file) {
|
|
Ok(contents) => match serde_json::from_str::<VersionInfo>(&contents) {
|
|
Ok(info) => {
|
|
details.push(format!("cached latest version: {}", info.latest_version));
|
|
if let Some(last_checked_at) = info.last_checked_at {
|
|
details.push(format!("last checked at: {last_checked_at}"));
|
|
}
|
|
if let Some(dismissed_version) = info.dismissed_version {
|
|
details.push(format!("dismissed version: {dismissed_version}"));
|
|
}
|
|
}
|
|
Err(err) => details.push(format!("version cache parse: {err}")),
|
|
},
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
details.push("version cache: missing".to_string());
|
|
}
|
|
Err(err) => details.push(format!("version cache read: {err}")),
|
|
}
|
|
}
|
|
|
|
fn update_action_label(context: &InstallContext) -> &'static str {
|
|
match &context.method {
|
|
InstallMethod::Npm => "npm install -g @openai/codex",
|
|
InstallMethod::Bun => "bun install -g @openai/codex",
|
|
InstallMethod::Brew => "brew upgrade --cask codex",
|
|
InstallMethod::Standalone { .. } => "standalone installer",
|
|
InstallMethod::Other => "manual or unknown",
|
|
}
|
|
}
|
|
|
|
fn fetch_latest_version(context: &InstallContext) -> Result<String, String> {
|
|
match &context.method {
|
|
InstallMethod::Brew => fetch_homebrew_cask_version(),
|
|
InstallMethod::Npm
|
|
| InstallMethod::Bun
|
|
| InstallMethod::Standalone { .. }
|
|
| InstallMethod::Other => fetch_latest_github_release_version(),
|
|
}
|
|
}
|
|
|
|
fn fetch_latest_github_release_version() -> Result<String, String> {
|
|
#[derive(Deserialize)]
|
|
struct ReleaseInfo {
|
|
tag_name: String,
|
|
}
|
|
|
|
let info = http_get_json::<ReleaseInfo>(GITHUB_LATEST_RELEASE_URL)?;
|
|
info.tag_name
|
|
.strip_prefix("rust-v")
|
|
.map(str::to_string)
|
|
.ok_or_else(|| format!("failed to parse latest tag {}", info.tag_name))
|
|
}
|
|
|
|
fn fetch_homebrew_cask_version() -> Result<String, String> {
|
|
#[derive(Deserialize)]
|
|
struct HomebrewCaskInfo {
|
|
version: String,
|
|
}
|
|
|
|
http_get_json::<HomebrewCaskInfo>(HOMEBREW_CASK_API_URL).map(|info| info.version)
|
|
}
|
|
|
|
fn http_get_json<T>(url: &str) -> Result<T, String>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
let body = run_command("curl", ["-fsSL", "--max-time", "5", url])?;
|
|
serde_json::from_str::<T>(&body).map_err(|err| err.to_string())
|
|
}
|
|
|
|
fn is_newer(latest: &str, current: &str) -> Option<bool> {
|
|
match (parse_version(latest), parse_version(current)) {
|
|
(Some(latest), Some(current)) => Some(latest > current),
|
|
(Some(_), None) | (None, Some(_)) | (None, None) => None,
|
|
}
|
|
}
|
|
|
|
fn parse_version(value: &str) -> Option<(u64, u64, u64)> {
|
|
let mut parts = value.trim().split('.');
|
|
let major = parts.next()?.parse::<u64>().ok()?;
|
|
let minor = parts.next()?.parse::<u64>().ok()?;
|
|
let patch = parts.next()?.parse::<u64>().ok()?;
|
|
Some((major, minor, patch))
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct VersionInfo {
|
|
latest_version: String,
|
|
#[serde(default)]
|
|
last_checked_at: Option<String>,
|
|
#[serde(default)]
|
|
dismissed_version: Option<String>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn is_newer_compares_plain_semver() {
|
|
assert_eq!(is_newer("1.2.4", "1.2.3"), Some(true));
|
|
assert_eq!(is_newer("1.2.3", "1.2.4"), Some(false));
|
|
assert_eq!(is_newer("1.2.3-beta.1", "1.2.2"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn update_action_labels_install_contexts() {
|
|
assert_eq!(
|
|
update_action_label(&InstallContext {
|
|
method: InstallMethod::Npm,
|
|
package_layout: None,
|
|
}),
|
|
"npm install -g @openai/codex"
|
|
);
|
|
assert_eq!(
|
|
update_action_label(&InstallContext {
|
|
method: InstallMethod::Other,
|
|
package_layout: None,
|
|
}),
|
|
"manual or unknown"
|
|
);
|
|
}
|
|
}
|