Skip to main content

codex/commands/
features.rs

1use std::ffi::OsString;
2
3use tokio::{process::Command, time};
4
5use crate::{
6    builder::{apply_cli_overrides, resolve_cli_overrides},
7    process::{spawn_with_retry, tee_stream, ConsoleTarget},
8    ApplyDiffArtifacts, CodexClient, CodexError, FeaturesCommandRequest, FeaturesDisableRequest,
9    FeaturesEnableRequest, FeaturesListOutput, FeaturesListRequest,
10};
11
12impl CodexClient {
13    /// Runs `codex features` and returns captured output.
14    pub async fn features(
15        &self,
16        request: FeaturesCommandRequest,
17    ) -> Result<ApplyDiffArtifacts, CodexError> {
18        self.run_simple_command_with_overrides(vec![OsString::from("features")], request.overrides)
19            .await
20    }
21
22    /// Enables a CLI feature via `codex features enable <FEATURE>`.
23    pub async fn features_enable(
24        &self,
25        request: FeaturesEnableRequest,
26    ) -> Result<ApplyDiffArtifacts, CodexError> {
27        let FeaturesEnableRequest { feature, overrides } = request;
28        self.run_simple_command_with_overrides(
29            vec![
30                OsString::from("features"),
31                OsString::from("enable"),
32                OsString::from(feature),
33            ],
34            overrides,
35        )
36        .await
37    }
38
39    /// Disables a CLI feature via `codex features disable <FEATURE>`.
40    pub async fn features_disable(
41        &self,
42        request: FeaturesDisableRequest,
43    ) -> Result<ApplyDiffArtifacts, CodexError> {
44        let FeaturesDisableRequest { feature, overrides } = request;
45        self.run_simple_command_with_overrides(
46            vec![
47                OsString::from("features"),
48                OsString::from("disable"),
49                OsString::from(feature),
50            ],
51            overrides,
52        )
53        .await
54    }
55
56    /// Lists CLI features via `codex features list`.
57    ///
58    /// Requests JSON output when `json(true)` is set and falls back to parsing the text table when
59    /// JSON is unavailable. Shared config/profile/search/approval overrides flow through via the
60    /// request/builder, stdout/stderr are mirrored according to the builder, and non-zero exits
61    /// surface as [`CodexError::NonZeroExit`].
62    pub async fn list_features(
63        &self,
64        request: FeaturesListRequest,
65    ) -> Result<FeaturesListOutput, CodexError> {
66        let FeaturesListRequest { json, overrides } = request;
67
68        let dir_ctx = self.directory_context()?;
69        let resolved_overrides =
70            resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
71
72        let mut command = Command::new(self.command_env.binary_path());
73        command
74            .arg("features")
75            .arg("list")
76            .stdout(std::process::Stdio::piped())
77            .stderr(std::process::Stdio::piped())
78            .kill_on_drop(true)
79            .current_dir(dir_ctx.path());
80
81        apply_cli_overrides(&mut command, &resolved_overrides, true);
82
83        if json {
84            command.arg("--json");
85        }
86
87        self.command_env.apply(&mut command)?;
88
89        let mut child = spawn_with_retry(&mut command, self.command_env.binary_path())?;
90
91        let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
92        let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
93
94        let stdout_task = tokio::spawn(tee_stream(
95            stdout,
96            ConsoleTarget::Stdout,
97            self.mirror_stdout,
98        ));
99        let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
100
101        let wait_task = async move {
102            let status = child
103                .wait()
104                .await
105                .map_err(|source| CodexError::Wait { source })?;
106            let stdout_bytes = stdout_task
107                .await
108                .map_err(CodexError::Join)?
109                .map_err(CodexError::CaptureIo)?;
110            let stderr_bytes = stderr_task
111                .await
112                .map_err(CodexError::Join)?
113                .map_err(CodexError::CaptureIo)?;
114            Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
115        };
116
117        let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
118            wait_task.await?
119        } else {
120            match time::timeout(self.timeout, wait_task).await {
121                Ok(result) => result?,
122                Err(_) => {
123                    return Err(CodexError::Timeout {
124                        timeout: self.timeout,
125                    });
126                }
127            }
128        };
129
130        if !status.success() {
131            return Err(CodexError::NonZeroExit {
132                status,
133                stderr: String::from_utf8(stderr_bytes)?,
134            });
135        }
136
137        let stdout_string = String::from_utf8(stdout_bytes)?;
138        let stderr_string = String::from_utf8(stderr_bytes)?;
139        let (features, format) = crate::version::parse_feature_list_output(&stdout_string, json)
140            .map_err(|reason| CodexError::FeatureListParse {
141                reason,
142                stdout: stdout_string.clone(),
143            })?;
144
145        Ok(FeaturesListOutput {
146            status,
147            stdout: stdout_string,
148            stderr: stderr_string,
149            features,
150            format,
151        })
152    }
153}