1use semver::Version;
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5use thiserror::Error;
6
7use super::{BinaryFingerprint, CapabilityCacheKey, CapabilityProbePlan};
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
18pub struct CodexCapabilities {
19 pub cache_key: CapabilityCacheKey,
21 pub fingerprint: Option<BinaryFingerprint>,
23 pub version: Option<CodexVersionInfo>,
25 pub features: CodexFeatureFlags,
27 pub probe_plan: CapabilityProbePlan,
29 pub collected_at: SystemTime,
31}
32
33#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
35pub struct CodexVersionInfo {
36 pub raw: String,
38 pub semantic: Option<(u64, u64, u64)>,
40 pub commit: Option<String>,
42 pub channel: CodexReleaseChannel,
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
48pub enum CodexReleaseChannel {
49 Stable,
50 Beta,
51 Nightly,
52 Custom,
54}
55
56impl std::fmt::Display for CodexReleaseChannel {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 let label = match self {
59 CodexReleaseChannel::Stable => "stable",
60 CodexReleaseChannel::Beta => "beta",
61 CodexReleaseChannel::Nightly => "nightly",
62 CodexReleaseChannel::Custom => "custom",
63 };
64 write!(f, "{label}")
65 }
66}
67
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
70pub struct CodexRelease {
71 pub channel: CodexReleaseChannel,
73 pub version: Version,
75}
76
77#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
84pub struct CodexLatestReleases {
85 pub stable: Option<Version>,
87 pub beta: Option<Version>,
89 pub nightly: Option<Version>,
91}
92
93impl CodexLatestReleases {
94 pub fn select_for_channel(
98 &self,
99 channel: CodexReleaseChannel,
100 ) -> (Option<CodexRelease>, CodexReleaseChannel, bool) {
101 if let Some(release) = self.release_for_channel(channel) {
102 return (Some(release), channel, false);
103 }
104
105 let fallback = self
106 .stable
107 .as_ref()
108 .map(|version| CodexRelease {
109 channel: CodexReleaseChannel::Stable,
110 version: version.clone(),
111 })
112 .or_else(|| {
113 self.beta.as_ref().map(|version| CodexRelease {
114 channel: CodexReleaseChannel::Beta,
115 version: version.clone(),
116 })
117 })
118 .or_else(|| {
119 self.nightly.as_ref().map(|version| CodexRelease {
120 channel: CodexReleaseChannel::Nightly,
121 version: version.clone(),
122 })
123 });
124
125 let fallback_channel = fallback
126 .as_ref()
127 .map(|release| release.channel)
128 .unwrap_or(channel);
129 let fell_back = fallback_channel != channel;
130 (fallback, fallback_channel, fell_back)
131 }
132
133 fn release_for_channel(&self, channel: CodexReleaseChannel) -> Option<CodexRelease> {
134 match channel {
135 CodexReleaseChannel::Stable => self.stable.as_ref().map(|version| CodexRelease {
136 channel,
137 version: version.clone(),
138 }),
139 CodexReleaseChannel::Beta => self.beta.as_ref().map(|version| CodexRelease {
140 channel,
141 version: version.clone(),
142 }),
143 CodexReleaseChannel::Nightly => self.nightly.as_ref().map(|version| CodexRelease {
144 channel,
145 version: version.clone(),
146 }),
147 CodexReleaseChannel::Custom => None,
148 }
149 }
150}
151
152#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
154pub struct CodexUpdateAdvisory {
155 pub local_release: Option<CodexRelease>,
157 pub latest_release: Option<CodexRelease>,
159 pub comparison_channel: CodexReleaseChannel,
161 pub status: CodexUpdateStatus,
163 pub notes: Vec<String>,
165}
166
167impl CodexUpdateAdvisory {
168 pub fn is_update_recommended(&self) -> bool {
170 matches!(
171 self.status,
172 CodexUpdateStatus::UpdateRecommended | CodexUpdateStatus::UnknownLocalVersion
173 )
174 }
175}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
179pub enum CodexUpdateStatus {
180 UpToDate,
182 UpdateRecommended,
184 LocalNewerThanKnown,
186 UnknownLocalVersion,
188 UnknownLatestVersion,
190}
191
192#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
197pub struct CodexFeatureFlags {
198 pub supports_features_list: bool,
200 pub supports_output_schema: bool,
202 pub supports_add_dir: bool,
204 pub supports_mcp_login: bool,
206}
207
208#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
210pub struct CapabilityFeatureOverrides {
211 pub supports_features_list: Option<bool>,
213 pub supports_output_schema: Option<bool>,
215 pub supports_add_dir: Option<bool>,
217 pub supports_mcp_login: Option<bool>,
219}
220
221impl CapabilityFeatureOverrides {
222 pub fn is_empty(&self) -> bool {
224 self.supports_features_list.is_none()
225 && self.supports_output_schema.is_none()
226 && self.supports_add_dir.is_none()
227 && self.supports_mcp_login.is_none()
228 }
229
230 pub fn from_flags(flags: CodexFeatureFlags) -> Self {
232 CapabilityFeatureOverrides {
233 supports_features_list: Some(flags.supports_features_list),
234 supports_output_schema: Some(flags.supports_output_schema),
235 supports_add_dir: Some(flags.supports_add_dir),
236 supports_mcp_login: Some(flags.supports_mcp_login),
237 }
238 }
239
240 pub fn enabling(flags: CodexFeatureFlags) -> Self {
242 CapabilityFeatureOverrides {
243 supports_features_list: flags.supports_features_list.then_some(true),
244 supports_output_schema: flags.supports_output_schema.then_some(true),
245 supports_add_dir: flags.supports_add_dir.then_some(true),
246 supports_mcp_login: flags.supports_mcp_login.then_some(true),
247 }
248 }
249}
250
251#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
255pub struct CapabilityOverrides {
256 pub snapshot: Option<CodexCapabilities>,
258 pub version: Option<CodexVersionInfo>,
260 pub features: CapabilityFeatureOverrides,
262}
263
264impl CapabilityOverrides {
265 pub fn is_empty(&self) -> bool {
267 self.snapshot.is_none() && self.version.is_none() && self.features.is_empty()
268 }
269}
270
271#[derive(Clone, Copy, Debug, Eq, PartialEq)]
273pub enum CapabilitySnapshotFormat {
274 Json,
275 Toml,
276}
277
278impl CapabilitySnapshotFormat {
279 pub(crate) fn from_path(path: &Path) -> Option<Self> {
280 let ext = path.extension()?.to_string_lossy().to_lowercase();
281 match ext.as_str() {
282 "json" => Some(Self::Json),
283 "toml" => Some(Self::Toml),
284 _ => None,
285 }
286 }
287}
288
289#[derive(Debug, Error)]
291pub enum CapabilitySnapshotError {
292 #[error("failed to read capability snapshot from `{path}`: {source}")]
293 ReadSnapshot {
294 path: PathBuf,
295 #[source]
296 source: std::io::Error,
297 },
298 #[error("failed to write capability snapshot to `{path}`: {source}")]
299 WriteSnapshot {
300 path: PathBuf,
301 #[source]
302 source: std::io::Error,
303 },
304 #[error("failed to decode capability snapshot from JSON: {source}")]
305 JsonDecode {
306 #[source]
307 source: serde_json::Error,
308 },
309 #[error("failed to encode capability snapshot to JSON: {source}")]
310 JsonEncode {
311 #[source]
312 source: serde_json::Error,
313 },
314 #[error("failed to decode capability snapshot from TOML: {source}")]
315 TomlDecode {
316 #[source]
317 source: toml::de::Error,
318 },
319 #[error("failed to encode capability snapshot to TOML: {source}")]
320 TomlEncode {
321 #[source]
322 source: toml::ser::Error,
323 },
324 #[error("unsupported capability snapshot format for `{path}`; use .json/.toml or supply a format explicitly")]
325 UnsupportedFormat { path: PathBuf },
326}