Skip to main content

codex_wrapper/
types.rs

1//! Domain types shared across commands: enums for CLI options, version parsing,
2//! and structured JSONL events.
3
4#[cfg(feature = "json")]
5use std::collections::HashMap;
6use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10
11/// Sandbox policy for model-generated shell commands.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum SandboxMode {
15    /// Read-only filesystem access.
16    ReadOnly,
17    /// Write access limited to the workspace directory (default).
18    #[default]
19    WorkspaceWrite,
20    /// Full filesystem access -- use with extreme caution.
21    DangerFullAccess,
22}
23
24impl SandboxMode {
25    pub(crate) fn as_arg(self) -> &'static str {
26        match self {
27            Self::ReadOnly => "read-only",
28            Self::WorkspaceWrite => "workspace-write",
29            Self::DangerFullAccess => "danger-full-access",
30        }
31    }
32}
33
34/// When the model should ask for human approval before executing commands.
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "kebab-case")]
37pub enum ApprovalPolicy {
38    /// Only run trusted commands without asking.
39    Untrusted,
40    /// Ask on failure (deprecated -- prefer `OnRequest` or `Never`).
41    OnFailure,
42    /// The model decides when to ask (default).
43    #[default]
44    OnRequest,
45    /// Never ask for approval.
46    Never,
47}
48
49impl ApprovalPolicy {
50    pub(crate) fn as_arg(self) -> &'static str {
51        match self {
52            Self::Untrusted => "untrusted",
53            Self::OnFailure => "on-failure",
54            Self::OnRequest => "on-request",
55            Self::Never => "never",
56        }
57    }
58}
59
60/// Color output mode for exec commands.
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "lowercase")]
63pub enum Color {
64    /// Always emit color codes.
65    Always,
66    /// Never emit color codes.
67    Never,
68    /// Auto-detect terminal support (default).
69    #[default]
70    Auto,
71}
72
73impl Color {
74    pub(crate) fn as_arg(self) -> &'static str {
75        match self {
76            Self::Always => "always",
77            Self::Never => "never",
78            Self::Auto => "auto",
79        }
80    }
81}
82
83/// A single parsed JSONL event from `--json` output.
84///
85/// The `event_type` field corresponds to the `"type"` key in the JSON.
86/// All other fields are captured in `extra`.
87#[cfg(feature = "json")]
88#[derive(Debug, Clone, Deserialize, Serialize)]
89pub struct JsonLineEvent {
90    #[serde(rename = "type", default)]
91    pub event_type: String,
92    #[serde(flatten)]
93    pub extra: HashMap<String, serde_json::Value>,
94}
95
96/// Parsed semantic version of the Codex CLI (`major.minor.patch`).
97///
98/// Supports comparison and ordering for version-gating logic.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct CliVersion {
101    pub major: u32,
102    pub minor: u32,
103    pub patch: u32,
104}
105
106impl CliVersion {
107    #[must_use]
108    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
109        Self {
110            major,
111            minor,
112            patch,
113        }
114    }
115
116    pub fn parse_version_output(output: &str) -> Result<Self, VersionParseError> {
117        output
118            .split_whitespace()
119            .find_map(|token| token.parse().ok())
120            .ok_or_else(|| VersionParseError(output.trim().to_string()))
121    }
122
123    #[must_use]
124    pub fn satisfies_minimum(&self, minimum: &CliVersion) -> bool {
125        self >= minimum
126    }
127}
128
129impl PartialOrd for CliVersion {
130    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
131        Some(self.cmp(other))
132    }
133}
134
135impl Ord for CliVersion {
136    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
137        self.major
138            .cmp(&other.major)
139            .then(self.minor.cmp(&other.minor))
140            .then(self.patch.cmp(&other.patch))
141    }
142}
143
144impl fmt::Display for CliVersion {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
147    }
148}
149
150impl FromStr for CliVersion {
151    type Err = VersionParseError;
152
153    fn from_str(s: &str) -> Result<Self, Self::Err> {
154        let parts: Vec<&str> = s.split('.').collect();
155        if parts.len() != 3 {
156            return Err(VersionParseError(s.to_string()));
157        }
158
159        Ok(Self {
160            major: parts[0]
161                .parse()
162                .map_err(|_| VersionParseError(s.to_string()))?,
163            minor: parts[1]
164                .parse()
165                .map_err(|_| VersionParseError(s.to_string()))?,
166            patch: parts[2]
167                .parse()
168                .map_err(|_| VersionParseError(s.to_string()))?,
169        })
170    }
171}
172
173#[derive(Debug, Clone, thiserror::Error)]
174#[error("invalid version string: {0:?}")]
175pub struct VersionParseError(pub String);
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn parses_codex_version_output() {
183        let version = CliVersion::parse_version_output("codex-cli 0.116.0").unwrap();
184        assert_eq!(version, CliVersion::new(0, 116, 0));
185    }
186
187    #[test]
188    fn parses_plain_version_output() {
189        let version = CliVersion::parse_version_output("0.116.0").unwrap();
190        assert_eq!(version, CliVersion::new(0, 116, 0));
191    }
192}