1use std::collections::HashMap;
2use std::fmt;
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "kebab-case")]
9pub enum SandboxMode {
10 ReadOnly,
11 #[default]
12 WorkspaceWrite,
13 DangerFullAccess,
14}
15
16impl SandboxMode {
17 pub(crate) fn as_arg(self) -> &'static str {
18 match self {
19 Self::ReadOnly => "read-only",
20 Self::WorkspaceWrite => "workspace-write",
21 Self::DangerFullAccess => "danger-full-access",
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "kebab-case")]
28pub enum ApprovalPolicy {
29 Untrusted,
30 OnFailure,
31 #[default]
32 OnRequest,
33 Never,
34}
35
36impl ApprovalPolicy {
37 pub(crate) fn as_arg(self) -> &'static str {
38 match self {
39 Self::Untrusted => "untrusted",
40 Self::OnFailure => "on-failure",
41 Self::OnRequest => "on-request",
42 Self::Never => "never",
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum Color {
50 Always,
51 Never,
52 #[default]
53 Auto,
54}
55
56impl Color {
57 pub(crate) fn as_arg(self) -> &'static str {
58 match self {
59 Self::Always => "always",
60 Self::Never => "never",
61 Self::Auto => "auto",
62 }
63 }
64}
65
66#[cfg(feature = "json")]
67#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct JsonLineEvent {
69 #[serde(rename = "type", default)]
70 pub event_type: String,
71 #[serde(flatten)]
72 pub extra: HashMap<String, serde_json::Value>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct CliVersion {
77 pub major: u32,
78 pub minor: u32,
79 pub patch: u32,
80}
81
82impl CliVersion {
83 #[must_use]
84 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
85 Self {
86 major,
87 minor,
88 patch,
89 }
90 }
91
92 pub fn parse_version_output(output: &str) -> Result<Self, VersionParseError> {
93 output
94 .split_whitespace()
95 .find_map(|token| token.parse().ok())
96 .ok_or_else(|| VersionParseError(output.trim().to_string()))
97 }
98
99 #[must_use]
100 pub fn satisfies_minimum(&self, minimum: &CliVersion) -> bool {
101 self >= minimum
102 }
103}
104
105impl PartialOrd for CliVersion {
106 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
107 Some(self.cmp(other))
108 }
109}
110
111impl Ord for CliVersion {
112 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
113 self.major
114 .cmp(&other.major)
115 .then(self.minor.cmp(&other.minor))
116 .then(self.patch.cmp(&other.patch))
117 }
118}
119
120impl fmt::Display for CliVersion {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
123 }
124}
125
126impl FromStr for CliVersion {
127 type Err = VersionParseError;
128
129 fn from_str(s: &str) -> Result<Self, Self::Err> {
130 let parts: Vec<&str> = s.split('.').collect();
131 if parts.len() != 3 {
132 return Err(VersionParseError(s.to_string()));
133 }
134
135 Ok(Self {
136 major: parts[0]
137 .parse()
138 .map_err(|_| VersionParseError(s.to_string()))?,
139 minor: parts[1]
140 .parse()
141 .map_err(|_| VersionParseError(s.to_string()))?,
142 patch: parts[2]
143 .parse()
144 .map_err(|_| VersionParseError(s.to_string()))?,
145 })
146 }
147}
148
149#[derive(Debug, Clone, thiserror::Error)]
150#[error("invalid version string: {0:?}")]
151pub struct VersionParseError(pub String);
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn parses_codex_version_output() {
159 let version = CliVersion::parse_version_output("codex-cli 0.116.0").unwrap();
160 assert_eq!(version, CliVersion::new(0, 116, 0));
161 }
162
163 #[test]
164 fn parses_plain_version_output() {
165 let version = CliVersion::parse_version_output("0.116.0").unwrap();
166 assert_eq!(version, CliVersion::new(0, 116, 0));
167 }
168}