1use std::{
2 ffi::OsString,
3 fs as std_fs,
4 path::{Path, PathBuf},
5};
6
7use thiserror::Error;
8use tokio::process::Command;
9
10use crate::defaults::{default_rust_log_value, CODEX_BINARY_ENV, CODEX_HOME_ENV, RUST_LOG_ENV};
11use crate::CodexError;
12
13#[derive(Clone, Debug)]
14pub(super) struct CommandEnvironment {
15 binary: PathBuf,
16 codex_home: Option<CodexHomeLayout>,
17 create_home_dirs: bool,
18}
19
20impl CommandEnvironment {
21 pub(super) fn new(
22 binary: PathBuf,
23 codex_home: Option<PathBuf>,
24 create_home_dirs: bool,
25 ) -> Self {
26 Self {
27 binary,
28 codex_home: codex_home.map(CodexHomeLayout::new),
29 create_home_dirs,
30 }
31 }
32
33 pub(super) fn binary_path(&self) -> &Path {
34 &self.binary
35 }
36
37 pub(super) fn codex_home_layout(&self) -> Option<CodexHomeLayout> {
38 self.codex_home.clone()
39 }
40
41 pub(super) fn environment_overrides(&self) -> Result<Vec<(OsString, OsString)>, CodexError> {
42 if let Some(home) = &self.codex_home {
43 home.materialize(self.create_home_dirs)?;
44 }
45
46 let mut envs = Vec::new();
47 envs.push((
48 OsString::from(CODEX_BINARY_ENV),
49 self.binary.as_os_str().to_os_string(),
50 ));
51
52 if let Some(home) = &self.codex_home {
53 envs.push((
54 OsString::from(CODEX_HOME_ENV),
55 home.root().as_os_str().to_os_string(),
56 ));
57 }
58
59 if let Some(value) = default_rust_log_value() {
60 envs.push((OsString::from(RUST_LOG_ENV), OsString::from(value)));
61 }
62
63 Ok(envs)
64 }
65
66 pub(super) fn apply(&self, command: &mut Command) -> Result<(), CodexError> {
67 for (key, value) in self.environment_overrides()? {
68 command.env(key, value);
69 }
70 Ok(())
71 }
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct CodexHomeLayout {
82 root: PathBuf,
83}
84
85impl CodexHomeLayout {
86 pub fn new(root: impl Into<PathBuf>) -> Self {
88 Self { root: root.into() }
89 }
90
91 pub fn root(&self) -> &Path {
93 self.root.as_path()
94 }
95
96 pub fn config_path(&self) -> PathBuf {
98 self.root.join("config.toml")
99 }
100
101 pub fn auth_path(&self) -> PathBuf {
103 self.root.join("auth.json")
104 }
105
106 pub fn credentials_path(&self) -> PathBuf {
108 self.root.join(".credentials.json")
109 }
110
111 pub fn history_path(&self) -> PathBuf {
113 self.root.join("history.jsonl")
114 }
115
116 pub fn conversations_dir(&self) -> PathBuf {
118 self.root.join("conversations")
119 }
120
121 pub fn logs_dir(&self) -> PathBuf {
123 self.root.join("logs")
124 }
125
126 pub fn materialize(&self, create_home_dirs: bool) -> Result<(), CodexError> {
129 if !create_home_dirs {
130 return Ok(());
131 }
132
133 let conversations = self.conversations_dir();
134 let logs = self.logs_dir();
135 for path in [self.root(), conversations.as_path(), logs.as_path()] {
136 std_fs::create_dir_all(path).map_err(|source| CodexError::PrepareCodexHome {
137 path: path.to_path_buf(),
138 source,
139 })?;
140 }
141 Ok(())
142 }
143
144 pub fn seed_auth_from(
151 &self,
152 seed_home: impl AsRef<Path>,
153 options: AuthSeedOptions,
154 ) -> Result<AuthSeedOutcome, AuthSeedError> {
155 let seed_home = seed_home.as_ref();
156 let seed_meta =
157 std_fs::metadata(seed_home).map_err(|source| AuthSeedError::SeedHomeUnreadable {
158 seed_home: seed_home.to_path_buf(),
159 source,
160 })?;
161 if !seed_meta.is_dir() {
162 return Err(AuthSeedError::SeedHomeNotDirectory {
163 seed_home: seed_home.to_path_buf(),
164 });
165 }
166
167 let mut outcome = AuthSeedOutcome::default();
168 let targets = [
169 (
170 "auth.json",
171 options.require_auth,
172 &mut outcome.copied_auth,
173 self.auth_path(),
174 ),
175 (
176 ".credentials.json",
177 options.require_credentials,
178 &mut outcome.copied_credentials,
179 self.credentials_path(),
180 ),
181 ];
182
183 for (name, required, copied, destination) in targets {
184 let source = seed_home.join(name);
185 match std_fs::metadata(&source) {
186 Ok(metadata) => {
187 if !metadata.is_file() {
188 return Err(AuthSeedError::SeedFileNotFile { path: source });
189 }
190
191 if options.create_target_dirs {
192 if let Some(parent) = destination.parent() {
193 std_fs::create_dir_all(parent).map_err(|source_err| {
194 AuthSeedError::CreateTargetDir {
195 path: parent.to_path_buf(),
196 source: source_err,
197 }
198 })?;
199 }
200 }
201
202 std_fs::copy(&source, &destination).map_err(|error| AuthSeedError::Copy {
203 source: source.clone(),
204 destination: destination.to_path_buf(),
205 error,
206 })?;
207 *copied = true;
208 }
209 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
210 if required {
211 return Err(AuthSeedError::SeedFileMissing { path: source });
212 }
213 }
214 Err(err) => {
215 return Err(AuthSeedError::SeedFileUnreadable {
216 path: source,
217 source: err,
218 })
219 }
220 }
221 }
222
223 Ok(outcome)
224 }
225}
226
227#[derive(Clone, Debug, Eq, PartialEq)]
229pub struct AuthSeedOptions {
230 pub require_auth: bool,
232 pub require_credentials: bool,
234 pub create_target_dirs: bool,
236}
237
238impl Default for AuthSeedOptions {
239 fn default() -> Self {
240 Self {
241 require_auth: false,
242 require_credentials: false,
243 create_target_dirs: true,
244 }
245 }
246}
247
248#[derive(Clone, Debug, Default, Eq, PartialEq)]
250pub struct AuthSeedOutcome {
251 pub copied_auth: bool,
253 pub copied_credentials: bool,
255}
256
257#[derive(Debug, Error)]
259pub enum AuthSeedError {
260 #[error("seed CODEX_HOME `{seed_home}` does not exist or is unreadable")]
261 SeedHomeUnreadable {
262 seed_home: PathBuf,
263 #[source]
264 source: std::io::Error,
265 },
266 #[error("seed CODEX_HOME `{seed_home}` is not a directory")]
267 SeedHomeNotDirectory { seed_home: PathBuf },
268 #[error("seed file `{path}` is missing")]
269 SeedFileMissing { path: PathBuf },
270 #[error("seed file `{path}` is not a file")]
271 SeedFileNotFile { path: PathBuf },
272 #[error("seed file `{path}` is unreadable")]
273 SeedFileUnreadable {
274 path: PathBuf,
275 #[source]
276 source: std::io::Error,
277 },
278 #[error("failed to create target directory `{path}`")]
279 CreateTargetDir {
280 path: PathBuf,
281 #[source]
282 source: std::io::Error,
283 },
284 #[error("failed to copy `{source}` to `{destination}`")]
285 Copy {
286 source: PathBuf,
287 destination: PathBuf,
288 #[source]
289 error: std::io::Error,
290 },
291}