check for updates (#1764)
1. Ping https://api.github.com/repos/openai/codex/releases/latest (at most once every 20 hrs) 2. Store the result in ~/.codex/version.jsonl 3. If CARGO_PKG_VERSION < latest_version, print a message at boot. --------- Co-authored-by: easong-openai <easong@openai.com>
This commit is contained in:
@@ -83,6 +83,7 @@ if (wantsNative && process.platform !== 'win32') {
|
|||||||
|
|
||||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
env: { ...process.env, CODEX_MANAGED_BY_NPM: "1" },
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", (err) => {
|
child.on("error", (err) => {
|
||||||
|
|||||||
3
codex-rs/Cargo.lock
generated
3
codex-rs/Cargo.lock
generated
@@ -843,6 +843,7 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"codex-ansi-escape",
|
"codex-ansi-escape",
|
||||||
"codex-arg0",
|
"codex-arg0",
|
||||||
@@ -861,6 +862,8 @@ dependencies = [
|
|||||||
"ratatui",
|
"ratatui",
|
||||||
"ratatui-image",
|
"ratatui-image",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shlex",
|
"shlex",
|
||||||
"strum 0.27.2",
|
"strum 0.27.2",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
codex-ansi-escape = { path = "../ansi-escape" }
|
codex-ansi-escape = { path = "../ansi-escape" }
|
||||||
codex-arg0 = { path = "../arg0" }
|
codex-arg0 = { path = "../arg0" }
|
||||||
@@ -41,6 +42,8 @@ ratatui = { version = "0.29.0", features = [
|
|||||||
] }
|
] }
|
||||||
ratatui-image = "8.0.0"
|
ratatui-image = "8.0.0"
|
||||||
regex-lite = "0.1"
|
regex-lite = "0.1"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { version = "1", features = ["preserve_order"] }
|
serde_json = { version = "1", features = ["preserve_order"] }
|
||||||
shlex = "1.3.0"
|
shlex = "1.3.0"
|
||||||
strum = "0.27.2"
|
strum = "0.27.2"
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ mod text_formatting;
|
|||||||
mod tui;
|
mod tui;
|
||||||
mod user_approval_widget;
|
mod user_approval_widget;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
mod updates;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
use color_eyre::owo_colors::OwoColorize;
|
||||||
|
|
||||||
pub use cli::Cli;
|
pub use cli::Cli;
|
||||||
|
|
||||||
pub async fn run_main(
|
pub async fn run_main(
|
||||||
@@ -139,6 +144,38 @@ pub async fn run_main(
|
|||||||
.with(tui_layer)
|
.with(tui_layer)
|
||||||
.try_init();
|
.try_init();
|
||||||
|
|
||||||
|
#[allow(clippy::print_stderr)]
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
if let Some(latest_version) = updates::get_upgrade_version(&config) {
|
||||||
|
let current_version = env!("CARGO_PKG_VERSION");
|
||||||
|
let exe = std::env::current_exe()?;
|
||||||
|
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{} {current_version} -> {latest_version}.",
|
||||||
|
"✨⬆️ Update available!".bold().cyan()
|
||||||
|
);
|
||||||
|
|
||||||
|
if managed_by_npm {
|
||||||
|
let npm_cmd = "npm install -g @openai/codex@latest";
|
||||||
|
eprintln!("Run {} to update.", npm_cmd.cyan().on_black());
|
||||||
|
} else if cfg!(target_os = "macos")
|
||||||
|
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
|
||||||
|
{
|
||||||
|
let brew_cmd = "brew upgrade codex";
|
||||||
|
eprintln!("Run {} to update.", brew_cmd.cyan().on_black());
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"See {} for the latest releases and installation options.",
|
||||||
|
"https://github.com/openai/codex/releases/latest"
|
||||||
|
.cyan()
|
||||||
|
.on_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("");
|
||||||
|
}
|
||||||
|
|
||||||
let show_login_screen = should_show_login_screen(&config);
|
let show_login_screen = should_show_login_screen(&config);
|
||||||
if show_login_screen {
|
if show_login_screen {
|
||||||
std::io::stdout()
|
std::io::stdout()
|
||||||
|
|||||||
137
codex-rs/tui/src/updates.rs
Normal file
137
codex-rs/tui/src/updates.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#![cfg(any(not(debug_assertions), test))]
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Duration;
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_core::config::Config;
|
||||||
|
|
||||||
|
pub fn get_upgrade_version(config: &Config) -> Option<String> {
|
||||||
|
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)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::error!("Failed to update version: {e}"))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
info.and_then(|info| {
|
||||||
|
let current_version = env!("CARGO_PKG_VERSION");
|
||||||
|
if is_newer(&info.latest_version, current_version).unwrap_or(false) {
|
||||||
|
Some(info.latest_version)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct VersionInfo {
|
||||||
|
latest_version: String,
|
||||||
|
// ISO-8601 timestamp (RFC3339)
|
||||||
|
last_checked_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> {
|
||||||
|
let contents = std::fs::read_to_string(version_file)?;
|
||||||
|
Ok(serde_json::from_str(&contents)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_for_update(version_file: &Path) -> anyhow::Result<()> {
|
||||||
|
let ReleaseInfo {
|
||||||
|
tag_name: latest_tag_name,
|
||||||
|
} = reqwest::Client::new()
|
||||||
|
.get(LATEST_RELEASE_URL)
|
||||||
|
.header(
|
||||||
|
"User-Agent",
|
||||||
|
format!(
|
||||||
|
"codex/{} (+https://github.com/openai/codex)",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<ReleaseInfo>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
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(),
|
||||||
|
last_checked_at: Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_newer(latest: &str, current: &str) -> Option<bool> {
|
||||||
|
match (parse_version(latest), parse_version(current)) {
|
||||||
|
(Some(l), Some(c)) => Some(l > c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
|
||||||
|
let mut iter = v.trim().split('.');
|
||||||
|
let maj = iter.next()?.parse::<u64>().ok()?;
|
||||||
|
let min = iter.next()?.parse::<u64>().ok()?;
|
||||||
|
let pat = iter.next()?.parse::<u64>().ok()?;
|
||||||
|
Some((maj, min, pat))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prerelease_version_is_not_considered_newer() {
|
||||||
|
assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), None);
|
||||||
|
assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_semver_comparisons_work() {
|
||||||
|
assert_eq!(is_newer("0.11.1", "0.11.0"), Some(true));
|
||||||
|
assert_eq!(is_newer("0.11.0", "0.11.1"), Some(false));
|
||||||
|
assert_eq!(is_newer("1.0.0", "0.9.9"), Some(true));
|
||||||
|
assert_eq!(is_newer("0.9.9", "1.0.0"), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whitespace_is_ignored() {
|
||||||
|
assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3)));
|
||||||
|
assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user