use semver::Version;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use thiserror::Error;
use super::{BinaryFingerprint, CapabilityCacheKey, CapabilityProbePlan};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CodexCapabilities {
pub cache_key: CapabilityCacheKey,
pub fingerprint: Option<BinaryFingerprint>,
pub version: Option<CodexVersionInfo>,
pub features: CodexFeatureFlags,
pub probe_plan: CapabilityProbePlan,
pub collected_at: SystemTime,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CodexVersionInfo {
pub raw: String,
pub semantic: Option<(u64, u64, u64)>,
pub commit: Option<String>,
pub channel: CodexReleaseChannel,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CodexReleaseChannel {
Stable,
Beta,
Nightly,
Custom,
}
impl std::fmt::Display for CodexReleaseChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
CodexReleaseChannel::Stable => "stable",
CodexReleaseChannel::Beta => "beta",
CodexReleaseChannel::Nightly => "nightly",
CodexReleaseChannel::Custom => "custom",
};
write!(f, "{label}")
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CodexRelease {
pub channel: CodexReleaseChannel,
pub version: Version,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CodexLatestReleases {
pub stable: Option<Version>,
pub beta: Option<Version>,
pub nightly: Option<Version>,
}
impl CodexLatestReleases {
pub fn select_for_channel(
&self,
channel: CodexReleaseChannel,
) -> (Option<CodexRelease>, CodexReleaseChannel, bool) {
if let Some(release) = self.release_for_channel(channel) {
return (Some(release), channel, false);
}
let fallback = self
.stable
.as_ref()
.map(|version| CodexRelease {
channel: CodexReleaseChannel::Stable,
version: version.clone(),
})
.or_else(|| {
self.beta.as_ref().map(|version| CodexRelease {
channel: CodexReleaseChannel::Beta,
version: version.clone(),
})
})
.or_else(|| {
self.nightly.as_ref().map(|version| CodexRelease {
channel: CodexReleaseChannel::Nightly,
version: version.clone(),
})
});
let fallback_channel = fallback
.as_ref()
.map(|release| release.channel)
.unwrap_or(channel);
let fell_back = fallback_channel != channel;
(fallback, fallback_channel, fell_back)
}
fn release_for_channel(&self, channel: CodexReleaseChannel) -> Option<CodexRelease> {
match channel {
CodexReleaseChannel::Stable => self.stable.as_ref().map(|version| CodexRelease {
channel,
version: version.clone(),
}),
CodexReleaseChannel::Beta => self.beta.as_ref().map(|version| CodexRelease {
channel,
version: version.clone(),
}),
CodexReleaseChannel::Nightly => self.nightly.as_ref().map(|version| CodexRelease {
channel,
version: version.clone(),
}),
CodexReleaseChannel::Custom => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CodexUpdateAdvisory {
pub local_release: Option<CodexRelease>,
pub latest_release: Option<CodexRelease>,
pub comparison_channel: CodexReleaseChannel,
pub status: CodexUpdateStatus,
pub notes: Vec<String>,
}
impl CodexUpdateAdvisory {
pub fn is_update_recommended(&self) -> bool {
matches!(
self.status,
CodexUpdateStatus::UpdateRecommended | CodexUpdateStatus::UnknownLocalVersion
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CodexUpdateStatus {
UpToDate,
UpdateRecommended,
LocalNewerThanKnown,
UnknownLocalVersion,
UnknownLatestVersion,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CodexFeatureFlags {
pub supports_features_list: bool,
pub supports_output_schema: bool,
pub supports_add_dir: bool,
pub supports_mcp_login: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CapabilityFeatureOverrides {
pub supports_features_list: Option<bool>,
pub supports_output_schema: Option<bool>,
pub supports_add_dir: Option<bool>,
pub supports_mcp_login: Option<bool>,
}
impl CapabilityFeatureOverrides {
pub fn is_empty(&self) -> bool {
self.supports_features_list.is_none()
&& self.supports_output_schema.is_none()
&& self.supports_add_dir.is_none()
&& self.supports_mcp_login.is_none()
}
pub fn from_flags(flags: CodexFeatureFlags) -> Self {
CapabilityFeatureOverrides {
supports_features_list: Some(flags.supports_features_list),
supports_output_schema: Some(flags.supports_output_schema),
supports_add_dir: Some(flags.supports_add_dir),
supports_mcp_login: Some(flags.supports_mcp_login),
}
}
pub fn enabling(flags: CodexFeatureFlags) -> Self {
CapabilityFeatureOverrides {
supports_features_list: flags.supports_features_list.then_some(true),
supports_output_schema: flags.supports_output_schema.then_some(true),
supports_add_dir: flags.supports_add_dir.then_some(true),
supports_mcp_login: flags.supports_mcp_login.then_some(true),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CapabilityOverrides {
pub snapshot: Option<CodexCapabilities>,
pub version: Option<CodexVersionInfo>,
pub features: CapabilityFeatureOverrides,
}
impl CapabilityOverrides {
pub fn is_empty(&self) -> bool {
self.snapshot.is_none() && self.version.is_none() && self.features.is_empty()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CapabilitySnapshotFormat {
Json,
Toml,
}
impl CapabilitySnapshotFormat {
pub(crate) fn from_path(path: &Path) -> Option<Self> {
let ext = path.extension()?.to_string_lossy().to_lowercase();
match ext.as_str() {
"json" => Some(Self::Json),
"toml" => Some(Self::Toml),
_ => None,
}
}
}
#[derive(Debug, Error)]
pub enum CapabilitySnapshotError {
#[error("failed to read capability snapshot from `{path}`: {source}")]
ReadSnapshot {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to write capability snapshot to `{path}`: {source}")]
WriteSnapshot {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to decode capability snapshot from JSON: {source}")]
JsonDecode {
#[source]
source: serde_json::Error,
},
#[error("failed to encode capability snapshot to JSON: {source}")]
JsonEncode {
#[source]
source: serde_json::Error,
},
#[error("failed to decode capability snapshot from TOML: {source}")]
TomlDecode {
#[source]
source: toml::de::Error,
},
#[error("failed to encode capability snapshot to TOML: {source}")]
TomlEncode {
#[source]
source: toml::ser::Error,
},
#[error("unsupported capability snapshot format for `{path}`; use .json/.toml or supply a format explicitly")]
UnsupportedFormat { path: PathBuf },
}