mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
error
This commit is contained in:
@@ -82,6 +82,8 @@ use crate::context_manager::ContextManager;
|
||||
use crate::environment_context::EnvironmentContext;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::error::error_event_with_update_nudge;
|
||||
use crate::error::stream_error_event_with_update_nudge;
|
||||
#[cfg(test)]
|
||||
use crate::exec::StreamOutput;
|
||||
use crate::exec_policy::ExecPolicyUpdateError;
|
||||
@@ -108,7 +110,6 @@ use crate::protocol::SessionConfiguredEvent;
|
||||
use crate::protocol::SkillErrorInfo;
|
||||
use crate::protocol::SkillInfo;
|
||||
use crate::protocol::SkillLoadOutcomeInfo;
|
||||
use crate::protocol::StreamErrorEvent;
|
||||
use crate::protocol::Submission;
|
||||
use crate::protocol::TokenCountEvent;
|
||||
use crate::protocol::TokenUsage;
|
||||
@@ -201,6 +202,13 @@ fn maybe_push_chat_wire_api_deprecation(
|
||||
});
|
||||
}
|
||||
|
||||
fn compute_is_up_to_date(codex_home: &std::path::Path) -> bool {
|
||||
let version_file = codex_home.join(crate::version::VERSION_FILENAME);
|
||||
crate::version::read_latest_version(&version_file)
|
||||
.and_then(|latest| crate::version::is_up_to_date(&latest, env!("CARGO_PKG_VERSION")))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
impl Codex {
|
||||
/// Spawn a new [`Codex`] and initialize the session.
|
||||
pub async fn spawn(
|
||||
@@ -251,6 +259,7 @@ impl Codex {
|
||||
error!("failed to refresh available models: {err:?}");
|
||||
}
|
||||
let model = models_manager.get_model(&config.model, &config).await;
|
||||
let is_up_to_date = compute_is_up_to_date(&config.codex_home);
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
model: model.clone(),
|
||||
@@ -266,6 +275,7 @@ impl Codex {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy,
|
||||
session_source,
|
||||
is_up_to_date,
|
||||
};
|
||||
|
||||
// Generate a unique ID for the lifetime of this Codex session.
|
||||
@@ -430,6 +440,9 @@ pub(crate) struct SessionConfiguration {
|
||||
original_config_do_not_use: Arc<Config>,
|
||||
/// Source of the session (cli, vscode, exec, mcp, ...)
|
||||
session_source: SessionSource,
|
||||
/// Whether the CLI is up to date with the latest known version at session start.
|
||||
#[allow(dead_code)]
|
||||
is_up_to_date: bool,
|
||||
}
|
||||
|
||||
impl SessionConfiguration {
|
||||
@@ -762,6 +775,11 @@ impl Session {
|
||||
state.get_total_token_usage()
|
||||
}
|
||||
|
||||
pub(crate) async fn is_up_to_date(&self) -> bool {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.is_up_to_date
|
||||
}
|
||||
|
||||
async fn record_initial_history(&self, conversation_history: InitialHistory) {
|
||||
let turn_context = self.new_turn(SessionSettingsUpdate::default()).await;
|
||||
match conversation_history {
|
||||
@@ -791,15 +809,11 @@ impl Session {
|
||||
warn!(
|
||||
"resuming session with different model: previous={prev}, current={curr}"
|
||||
);
|
||||
self.send_event(
|
||||
&turn_context,
|
||||
EventMsg::Warning(WarningEvent {
|
||||
message: format!(
|
||||
"This session was recorded with model `{prev}` but is resuming with `{curr}`. \
|
||||
let message = format!(
|
||||
"This session was recorded with model `{prev}` but is resuming with `{curr}`. \
|
||||
Consider switching back to `{prev}` as it may affect Codex performance."
|
||||
),
|
||||
}),
|
||||
)
|
||||
);
|
||||
self.send_event(&turn_context, EventMsg::Warning(WarningEvent { message }))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -1380,10 +1394,11 @@ impl Session {
|
||||
let codex_error_info = CodexErrorInfo::ResponseStreamDisconnected {
|
||||
http_status_code: codex_error.http_status_code_value(),
|
||||
};
|
||||
let event = EventMsg::StreamError(StreamErrorEvent {
|
||||
message: message.into(),
|
||||
codex_error_info: Some(codex_error_info),
|
||||
});
|
||||
let event = EventMsg::StreamError(stream_error_event_with_update_nudge(
|
||||
message.into(),
|
||||
Some(codex_error_info),
|
||||
self.is_up_to_date().await,
|
||||
));
|
||||
self.send_event(turn_context, event).await;
|
||||
}
|
||||
|
||||
@@ -1629,6 +1644,7 @@ mod handlers {
|
||||
|
||||
use crate::codex::spawn_review_thread;
|
||||
use crate::config::Config;
|
||||
use crate::error::error_event_with_update_nudge;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp::collect_mcp_snapshot_from_manager;
|
||||
use crate::review_prompts::resolve_review_request;
|
||||
@@ -1638,7 +1654,6 @@ mod handlers {
|
||||
use crate::tasks::UserShellCommandTask;
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ListCustomPromptsResponseEvent;
|
||||
@@ -1920,12 +1935,14 @@ mod handlers {
|
||||
&& let Err(e) = rec.shutdown().await
|
||||
{
|
||||
warn!("failed to shutdown rollout recorder: {e}");
|
||||
let is_up_to_date = sess.is_up_to_date().await;
|
||||
let event = Event {
|
||||
id: sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "Failed to shutdown rollout recorder".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
msg: EventMsg::Error(error_event_with_update_nudge(
|
||||
"Failed to shutdown rollout recorder".to_string(),
|
||||
Some(CodexErrorInfo::Other),
|
||||
is_up_to_date,
|
||||
)),
|
||||
};
|
||||
sess.send_event_raw(event).await;
|
||||
}
|
||||
@@ -1959,12 +1976,14 @@ mod handlers {
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
let is_up_to_date = sess.is_up_to_date().await;
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: err.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
msg: EventMsg::Error(error_event_with_update_nudge(
|
||||
err.to_string(),
|
||||
Some(CodexErrorInfo::Other),
|
||||
is_up_to_date,
|
||||
)),
|
||||
};
|
||||
sess.send_event(&turn_context, event.msg).await;
|
||||
}
|
||||
@@ -2229,7 +2248,13 @@ pub(crate) async fn run_task(
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Turn error: {e:#}");
|
||||
let event = EventMsg::Error(e.to_error_event(None));
|
||||
let is_up_to_date = sess.is_up_to_date().await;
|
||||
let error_event = e.to_error_event(None);
|
||||
let event = EventMsg::Error(error_event_with_update_nudge(
|
||||
error_event.message,
|
||||
error_event.codex_error_info,
|
||||
is_up_to_date,
|
||||
));
|
||||
sess.send_event(&turn_context, event).await;
|
||||
// let the user continue the conversation
|
||||
break;
|
||||
@@ -2723,6 +2748,7 @@ mod tests {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
|
||||
session_source: SessionSource::Exec,
|
||||
is_up_to_date: true,
|
||||
};
|
||||
|
||||
let mut state = SessionState::new(session_configuration);
|
||||
@@ -2795,6 +2821,7 @@ mod tests {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
|
||||
session_source: SessionSource::Exec,
|
||||
is_up_to_date: true,
|
||||
};
|
||||
|
||||
let mut state = SessionState::new(session_configuration);
|
||||
@@ -2999,6 +3026,7 @@ mod tests {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
|
||||
session_source: SessionSource::Exec,
|
||||
is_up_to_date: true,
|
||||
};
|
||||
let per_turn_config = Session::build_per_turn_config(&session_configuration);
|
||||
let model_family = ModelsManager::construct_model_family_offline(
|
||||
@@ -3089,6 +3117,7 @@ mod tests {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
|
||||
session_source: SessionSource::Exec,
|
||||
is_up_to_date: true,
|
||||
};
|
||||
let per_turn_config = Session::build_per_turn_config(&session_configuration);
|
||||
let model_family = ModelsManager::construct_model_family_offline(
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::codex::TurnContext;
|
||||
use crate::codex::get_last_assistant_message_from_turn;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::error::error_event_with_update_nudge;
|
||||
use crate::features::Feature;
|
||||
use crate::protocol::CompactedItem;
|
||||
use crate::protocol::ContextCompactedEvent;
|
||||
@@ -67,6 +68,7 @@ async fn run_compact_task_inner(
|
||||
input: Vec<UserInput>,
|
||||
) {
|
||||
let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input);
|
||||
let is_up_to_date = sess.is_up_to_date().await;
|
||||
|
||||
let mut history = sess.clone_history().await;
|
||||
history.record_items(
|
||||
@@ -125,7 +127,12 @@ async fn run_compact_task_inner(
|
||||
continue;
|
||||
}
|
||||
sess.set_total_tokens_full(turn_context.as_ref()).await;
|
||||
let event = EventMsg::Error(e.to_error_event(None));
|
||||
let error_event = e.to_error_event(None);
|
||||
let event = EventMsg::Error(error_event_with_update_nudge(
|
||||
error_event.message,
|
||||
error_event.codex_error_info,
|
||||
is_up_to_date,
|
||||
));
|
||||
sess.send_event(&turn_context, event).await;
|
||||
return;
|
||||
}
|
||||
@@ -142,7 +149,12 @@ async fn run_compact_task_inner(
|
||||
tokio::time::sleep(delay).await;
|
||||
continue;
|
||||
} else {
|
||||
let event = EventMsg::Error(e.to_error_event(None));
|
||||
let error_event = e.to_error_event(None);
|
||||
let event = EventMsg::Error(error_event_with_update_nudge(
|
||||
error_event.message,
|
||||
error_event.codex_error_info,
|
||||
is_up_to_date,
|
||||
));
|
||||
sess.send_event(&turn_context, event).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::Prompt;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::error::error_event_with_update_nudge;
|
||||
use crate::protocol::CompactedItem;
|
||||
use crate::protocol::ContextCompactedEvent;
|
||||
use crate::protocol::EventMsg;
|
||||
@@ -29,9 +30,13 @@ pub(crate) async fn run_remote_compact_task(sess: Arc<Session>, turn_context: Ar
|
||||
|
||||
async fn run_remote_compact_task_inner(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) {
|
||||
if let Err(err) = run_remote_compact_task_inner_impl(sess, turn_context).await {
|
||||
let event = EventMsg::Error(
|
||||
err.to_error_event(Some("Error running remote compact task".to_string())),
|
||||
);
|
||||
let is_up_to_date = sess.is_up_to_date().await;
|
||||
let error_event = err.to_error_event(Some("Error running remote compact task".to_string()));
|
||||
let event = EventMsg::Error(error_event_with_update_nudge(
|
||||
error_event.message,
|
||||
error_event.codex_error_info,
|
||||
is_up_to_date,
|
||||
));
|
||||
sess.send_event(turn_context, event).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::ConversationId;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::StreamErrorEvent;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json;
|
||||
use std::io;
|
||||
@@ -23,6 +24,41 @@ pub type Result<T> = std::result::Result<T, CodexErr>;
|
||||
|
||||
/// Limit UI error messages to a reasonable size while keeping useful context.
|
||||
const ERROR_MESSAGE_UI_MAX_BYTES: usize = 2 * 1024; // 4 KiB
|
||||
const UPDATE_AVAILABLE_NUDGE: &str = "Update available. Run `codex update`.";
|
||||
|
||||
pub(crate) fn error_event_with_update_nudge(
|
||||
message: String,
|
||||
codex_error_info: Option<CodexErrorInfo>,
|
||||
is_up_to_date: bool,
|
||||
) -> ErrorEvent {
|
||||
let message = maybe_append_update_nudge(message, is_up_to_date);
|
||||
ErrorEvent {
|
||||
message,
|
||||
codex_error_info,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stream_error_event_with_update_nudge(
|
||||
message: String,
|
||||
codex_error_info: Option<CodexErrorInfo>,
|
||||
is_up_to_date: bool,
|
||||
) -> StreamErrorEvent {
|
||||
StreamErrorEvent {
|
||||
message: maybe_append_update_nudge(message, is_up_to_date),
|
||||
codex_error_info,
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_append_update_nudge(message: String, is_up_to_date: bool) -> String {
|
||||
if is_up_to_date {
|
||||
return message;
|
||||
}
|
||||
if message.is_empty() {
|
||||
UPDATE_AVAILABLE_NUDGE.to_string()
|
||||
} else {
|
||||
format!("{message}\n{UPDATE_AVAILABLE_NUDGE}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SandboxErr {
|
||||
|
||||
@@ -80,6 +80,7 @@ pub mod spawn;
|
||||
pub mod terminal;
|
||||
mod tools;
|
||||
pub mod turn_diff_tracker;
|
||||
pub mod version;
|
||||
pub use rollout::ARCHIVED_SESSIONS_SUBDIR;
|
||||
pub use rollout::INTERACTIVE_SESSION_SOURCES;
|
||||
pub use rollout::RolloutRecorder;
|
||||
|
||||
@@ -54,11 +54,12 @@ impl SessionTask for GhostSnapshotTask {
|
||||
tokio::task::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(SNAPSHOT_WARNING_THRESHOLD) => {
|
||||
session_for_warning.session
|
||||
session_for_warning
|
||||
.session
|
||||
.send_event(
|
||||
&ctx_for_warning,
|
||||
EventMsg::Warning(WarningEvent {
|
||||
message: "Repository snapshot is taking longer than expected. Large untracked or ignored files can slow snapshots; consider adding large files or directories to .gitignore or disabling `undo` in your config.".to_string()
|
||||
message: "Repository snapshot is taking longer than expected. Large untracked or ignored files can slow snapshots; consider adding large files or directories to .gitignore or disabling `undo` in your config.".to_string(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
247
codex-rs/core/src/version.rs
Normal file
247
codex-rs/core/src/version.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const VERSION_FILENAME: &str = "version.json";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VersionInfo {
|
||||
pub latest_version: String,
|
||||
// ISO-8601 timestamp (RFC3339)
|
||||
pub last_checked_at: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub dismissed_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Version {
|
||||
major: u64,
|
||||
minor: u64,
|
||||
patch: u64,
|
||||
pre: Option<Vec<PrereleaseIdent>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum PrereleaseIdent {
|
||||
Numeric(u64),
|
||||
Alpha(String),
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn parse(input: &str) -> Option<Self> {
|
||||
let mut input = input.trim();
|
||||
if let Some(stripped) = input.strip_prefix("rust-v") {
|
||||
input = stripped;
|
||||
}
|
||||
if let Some(stripped) = input.strip_prefix('v') {
|
||||
input = stripped;
|
||||
}
|
||||
let input = input.splitn(2, '+').next().unwrap_or(input);
|
||||
let mut parts = input.splitn(2, '-');
|
||||
let core = parts.next()?;
|
||||
let pre = parts.next();
|
||||
let mut nums = core.split('.');
|
||||
let major = nums.next()?.parse::<u64>().ok()?;
|
||||
let minor = nums.next()?.parse::<u64>().ok()?;
|
||||
let patch = nums.next()?.parse::<u64>().ok()?;
|
||||
if nums.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
let pre = match pre {
|
||||
None => None,
|
||||
Some(value) if value.is_empty() => None,
|
||||
Some(value) => {
|
||||
let mut idents = Vec::new();
|
||||
for ident in value.split('.') {
|
||||
if ident.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let parsed = if ident.chars().all(|c| c.is_ascii_digit()) {
|
||||
ident.parse::<u64>().ok().map(PrereleaseIdent::Numeric)
|
||||
} else {
|
||||
Some(PrereleaseIdent::Alpha(ident.to_string()))
|
||||
};
|
||||
idents.push(parsed?);
|
||||
}
|
||||
Some(idents)
|
||||
}
|
||||
};
|
||||
Some(Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
pre,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Version {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.major.cmp(&other.major) {
|
||||
std::cmp::Ordering::Equal => {}
|
||||
ordering => return ordering,
|
||||
}
|
||||
match self.minor.cmp(&other.minor) {
|
||||
std::cmp::Ordering::Equal => {}
|
||||
ordering => return ordering,
|
||||
}
|
||||
match self.patch.cmp(&other.patch) {
|
||||
std::cmp::Ordering::Equal => {}
|
||||
ordering => return ordering,
|
||||
}
|
||||
match (&self.pre, &other.pre) {
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(Some(left), Some(right)) => compare_prerelease_idents(left, right),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Version {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_newer(latest: &str, current: &str) -> Option<bool> {
|
||||
let latest = Version::parse(latest)?;
|
||||
if latest.pre.is_some() {
|
||||
return Some(false);
|
||||
}
|
||||
let current = Version::parse(current)?;
|
||||
let current = Version {
|
||||
pre: None,
|
||||
..current
|
||||
};
|
||||
Some(latest > current)
|
||||
}
|
||||
|
||||
pub fn is_up_to_date(latest: &str, current: &str) -> Option<bool> {
|
||||
let latest = Version::parse(latest)?;
|
||||
if latest.pre.is_some() {
|
||||
return Some(true);
|
||||
}
|
||||
let current = Version::parse(current)?;
|
||||
let current = Version {
|
||||
pre: None,
|
||||
..current
|
||||
};
|
||||
Some(current >= latest)
|
||||
}
|
||||
|
||||
pub 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)?)
|
||||
}
|
||||
|
||||
pub fn read_latest_version(version_file: &Path) -> Option<String> {
|
||||
read_version_info(version_file)
|
||||
.ok()
|
||||
.map(|info| info.latest_version)
|
||||
}
|
||||
|
||||
pub 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"))
|
||||
}
|
||||
|
||||
pub 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}'"))
|
||||
}
|
||||
|
||||
fn compare_prerelease_idents(
|
||||
left: &[PrereleaseIdent],
|
||||
right: &[PrereleaseIdent],
|
||||
) -> std::cmp::Ordering {
|
||||
for (l, r) in left.iter().zip(right.iter()) {
|
||||
let ordering = match (l, r) {
|
||||
(PrereleaseIdent::Numeric(a), PrereleaseIdent::Numeric(b)) => a.cmp(b),
|
||||
(PrereleaseIdent::Alpha(a), PrereleaseIdent::Alpha(b)) => a.cmp(b),
|
||||
(PrereleaseIdent::Numeric(_), PrereleaseIdent::Alpha(_)) => std::cmp::Ordering::Less,
|
||||
(PrereleaseIdent::Alpha(_), PrereleaseIdent::Numeric(_)) => std::cmp::Ordering::Greater,
|
||||
};
|
||||
if ordering != std::cmp::Ordering::Equal {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
left.len().cmp(&right.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prerelease_current_is_ignored() {
|
||||
assert_eq!(is_newer("1.2.3", "1.2.3-alpha.1"), Some(false));
|
||||
assert_eq!(is_up_to_date("1.2.3", "1.2.3-alpha.1"), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prerelease_latest_is_ignored() {
|
||||
assert_eq!(is_newer("1.2.4-alpha.1", "1.2.3"), Some(false));
|
||||
assert_eq!(is_up_to_date("1.2.4-alpha.1", "1.2.3"), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prerelease_latest_is_not_considered_newer() {
|
||||
assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), Some(false));
|
||||
assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), Some(false));
|
||||
}
|
||||
|
||||
#[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!(Version::parse(" 1.2.3 \n").is_some(), true);
|
||||
assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true));
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
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 codex_core::version::VERSION_FILENAME;
|
||||
use codex_core::version::VersionInfo;
|
||||
use codex_core::version::extract_version_from_cask;
|
||||
use codex_core::version::extract_version_from_latest_tag;
|
||||
use codex_core::version::is_newer;
|
||||
use codex_core::version::read_version_info;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -45,16 +49,6 @@ pub fn get_upgrade_version(config: &Config) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct VersionInfo {
|
||||
latest_version: String,
|
||||
// ISO-8601 timestamp (RFC3339)
|
||||
last_checked_at: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
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";
|
||||
@@ -69,11 +63,6 @@ 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 latest_version = match update_action::get_update_action() {
|
||||
Some(UpdateAction::BrewUpgrade) => {
|
||||
@@ -116,32 +105,6 @@ async fn check_for_update(version_file: &Path) -> anyhow::Result<()> {
|
||||
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 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> {
|
||||
@@ -177,48 +140,14 @@ pub async fn dismiss_version(config: &Config, version: &str) -> anyhow::Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 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);
|
||||
assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), None);
|
||||
assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), Some(false));
|
||||
assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -231,7 +160,6 @@ mod tests {
|
||||
|
||||
#[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