Skip to main content

codex/commands/
apply_diff.rs

1use std::{env, 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, CliOverridesPatch, CodexClient, CodexError,
9};
10
11impl CodexClient {
12    /// Applies a Codex diff by invoking `codex apply <TASK_ID>`.
13    ///
14    /// Stdout mirrors to the console when `mirror_stdout` is enabled; stderr mirrors unless `quiet`
15    /// is set. Output and exit status are always captured and returned, and `RUST_LOG=error` is
16    /// injected for the child process when the environment variable is unset.
17    ///
18    /// Convenience behavior: if `CODEX_TASK_ID` is set, it is appended as `<TASK_ID>`. When the
19    /// environment variable is missing, the subprocess is still spawned and will typically exit
20    /// non-zero with a "missing TASK_ID" error from the CLI.
21    pub async fn apply(&self) -> Result<ApplyDiffArtifacts, CodexError> {
22        let task_id = env::var_os("CODEX_TASK_ID")
23            .and_then(|v| crate::normalize_non_empty(&v.to_string_lossy()).map(OsString::from));
24        self.apply_task_inner(task_id).await
25    }
26
27    /// Applies a Codex diff by task id via `codex apply <TASK_ID>`.
28    pub async fn apply_task(
29        &self,
30        task_id: impl AsRef<str>,
31    ) -> Result<ApplyDiffArtifacts, CodexError> {
32        let task_id = task_id.as_ref().trim();
33        if task_id.is_empty() {
34            return Err(CodexError::EmptyTaskId);
35        }
36        self.apply_task_inner(Some(OsString::from(task_id))).await
37    }
38
39    /// Shows a Codex Cloud task diff by invoking `codex cloud diff <TASK_ID>`.
40    ///
41    /// Mirrors stdout/stderr using the same `mirror_stdout`/`quiet` defaults as `apply`, but always
42    /// returns the captured output alongside the child exit status. Applies the same `RUST_LOG`
43    /// defaulting behavior when the variable is unset.
44    ///
45    /// Convenience behavior: if `CODEX_TASK_ID` is set, it is appended as `<TASK_ID>`. When the
46    /// environment variable is missing, the subprocess is still spawned and will typically exit
47    /// non-zero with a "missing TASK_ID" error from the CLI.
48    pub async fn diff(&self) -> Result<ApplyDiffArtifacts, CodexError> {
49        let task_id = env::var_os("CODEX_TASK_ID")
50            .and_then(|v| crate::normalize_non_empty(&v.to_string_lossy()).map(OsString::from));
51        self.cloud_diff_task_inner(task_id).await
52    }
53
54    /// Shows a Codex Cloud task diff by task id via `codex cloud diff <TASK_ID>`.
55    pub async fn cloud_diff_task(
56        &self,
57        task_id: impl AsRef<str>,
58    ) -> Result<ApplyDiffArtifacts, CodexError> {
59        let task_id = task_id.as_ref().trim();
60        if task_id.is_empty() {
61            return Err(CodexError::EmptyTaskId);
62        }
63        self.cloud_diff_task_inner(Some(OsString::from(task_id)))
64            .await
65    }
66
67    async fn apply_task_inner(
68        &self,
69        task_id: Option<OsString>,
70    ) -> Result<ApplyDiffArtifacts, CodexError> {
71        let mut args = vec![OsString::from("apply")];
72        if let Some(task_id) = task_id {
73            args.push(task_id);
74        }
75        self.capture_codex_command(args, false).await
76    }
77
78    async fn cloud_diff_task_inner(
79        &self,
80        task_id: Option<OsString>,
81    ) -> Result<ApplyDiffArtifacts, CodexError> {
82        let mut args = vec![OsString::from("cloud"), OsString::from("diff")];
83        if let Some(task_id) = task_id {
84            args.push(task_id);
85        }
86        self.capture_codex_command(args, false).await
87    }
88
89    async fn capture_codex_command(
90        &self,
91        args: Vec<OsString>,
92        include_search: bool,
93    ) -> Result<ApplyDiffArtifacts, CodexError> {
94        let dir_ctx = self.directory_context()?;
95        let resolved_overrides = resolve_cli_overrides(
96            &self.cli_overrides,
97            &CliOverridesPatch::default(),
98            self.model.as_deref(),
99        );
100
101        let mut command = Command::new(self.command_env.binary_path());
102        command
103            .args(&args)
104            .stdout(std::process::Stdio::piped())
105            .stderr(std::process::Stdio::piped())
106            .kill_on_drop(true)
107            .current_dir(dir_ctx.path());
108
109        apply_cli_overrides(&mut command, &resolved_overrides, include_search);
110        self.command_env.apply(&mut command)?;
111
112        let mut child = spawn_with_retry(&mut command, self.command_env.binary_path())?;
113
114        let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
115        let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
116
117        let stdout_task = tokio::spawn(tee_stream(
118            stdout,
119            ConsoleTarget::Stdout,
120            self.mirror_stdout,
121        ));
122        let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
123
124        let wait_task = async move {
125            let status = child
126                .wait()
127                .await
128                .map_err(|source| CodexError::Wait { source })?;
129            let stdout_bytes = stdout_task
130                .await
131                .map_err(CodexError::Join)?
132                .map_err(CodexError::CaptureIo)?;
133            let stderr_bytes = stderr_task
134                .await
135                .map_err(CodexError::Join)?
136                .map_err(CodexError::CaptureIo)?;
137            Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
138        };
139
140        let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
141            wait_task.await?
142        } else {
143            match time::timeout(self.timeout, wait_task).await {
144                Ok(result) => result?,
145                Err(_) => {
146                    return Err(CodexError::Timeout {
147                        timeout: self.timeout,
148                    });
149                }
150            }
151        };
152
153        Ok(ApplyDiffArtifacts {
154            status,
155            stdout: String::from_utf8(stdout_bytes)?,
156            stderr: String::from_utf8(stderr_bytes)?,
157        })
158    }
159}