use std::{
ffi::OsString,
fs as std_fs,
path::{Path, PathBuf},
};
use thiserror::Error;
use tokio::process::Command;
use crate::defaults::{default_rust_log_value, CODEX_BINARY_ENV, CODEX_HOME_ENV, RUST_LOG_ENV};
use crate::CodexError;
#[derive(Clone, Debug)]
pub(super) struct CommandEnvironment {
binary: PathBuf,
codex_home: Option<CodexHomeLayout>,
create_home_dirs: bool,
}
impl CommandEnvironment {
pub(super) fn new(
binary: PathBuf,
codex_home: Option<PathBuf>,
create_home_dirs: bool,
) -> Self {
Self {
binary,
codex_home: codex_home.map(CodexHomeLayout::new),
create_home_dirs,
}
}
pub(super) fn binary_path(&self) -> &Path {
&self.binary
}
pub(super) fn codex_home_layout(&self) -> Option<CodexHomeLayout> {
self.codex_home.clone()
}
pub(super) fn environment_overrides(&self) -> Result<Vec<(OsString, OsString)>, CodexError> {
if let Some(home) = &self.codex_home {
home.materialize(self.create_home_dirs)?;
}
let mut envs = Vec::new();
envs.push((
OsString::from(CODEX_BINARY_ENV),
self.binary.as_os_str().to_os_string(),
));
if let Some(home) = &self.codex_home {
envs.push((
OsString::from(CODEX_HOME_ENV),
home.root().as_os_str().to_os_string(),
));
}
if let Some(value) = default_rust_log_value() {
envs.push((OsString::from(RUST_LOG_ENV), OsString::from(value)));
}
Ok(envs)
}
pub(super) fn apply(&self, command: &mut Command) -> Result<(), CodexError> {
for (key, value) in self.environment_overrides()? {
command.env(key, value);
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CodexHomeLayout {
root: PathBuf,
}
impl CodexHomeLayout {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn root(&self) -> &Path {
self.root.as_path()
}
pub fn config_path(&self) -> PathBuf {
self.root.join("config.toml")
}
pub fn auth_path(&self) -> PathBuf {
self.root.join("auth.json")
}
pub fn credentials_path(&self) -> PathBuf {
self.root.join(".credentials.json")
}
pub fn history_path(&self) -> PathBuf {
self.root.join("history.jsonl")
}
pub fn conversations_dir(&self) -> PathBuf {
self.root.join("conversations")
}
pub fn logs_dir(&self) -> PathBuf {
self.root.join("logs")
}
pub fn materialize(&self, create_home_dirs: bool) -> Result<(), CodexError> {
if !create_home_dirs {
return Ok(());
}
let conversations = self.conversations_dir();
let logs = self.logs_dir();
for path in [self.root(), conversations.as_path(), logs.as_path()] {
std_fs::create_dir_all(path).map_err(|source| CodexError::PrepareCodexHome {
path: path.to_path_buf(),
source,
})?;
}
Ok(())
}
pub fn seed_auth_from(
&self,
seed_home: impl AsRef<Path>,
options: AuthSeedOptions,
) -> Result<AuthSeedOutcome, AuthSeedError> {
let seed_home = seed_home.as_ref();
let seed_meta =
std_fs::metadata(seed_home).map_err(|source| AuthSeedError::SeedHomeUnreadable {
seed_home: seed_home.to_path_buf(),
source,
})?;
if !seed_meta.is_dir() {
return Err(AuthSeedError::SeedHomeNotDirectory {
seed_home: seed_home.to_path_buf(),
});
}
let mut outcome = AuthSeedOutcome::default();
let targets = [
(
"auth.json",
options.require_auth,
&mut outcome.copied_auth,
self.auth_path(),
),
(
".credentials.json",
options.require_credentials,
&mut outcome.copied_credentials,
self.credentials_path(),
),
];
for (name, required, copied, destination) in targets {
let source = seed_home.join(name);
match std_fs::metadata(&source) {
Ok(metadata) => {
if !metadata.is_file() {
return Err(AuthSeedError::SeedFileNotFile { path: source });
}
if options.create_target_dirs {
if let Some(parent) = destination.parent() {
std_fs::create_dir_all(parent).map_err(|source_err| {
AuthSeedError::CreateTargetDir {
path: parent.to_path_buf(),
source: source_err,
}
})?;
}
}
std_fs::copy(&source, &destination).map_err(|error| AuthSeedError::Copy {
source: source.clone(),
destination: destination.to_path_buf(),
error,
})?;
*copied = true;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
if required {
return Err(AuthSeedError::SeedFileMissing { path: source });
}
}
Err(err) => {
return Err(AuthSeedError::SeedFileUnreadable {
path: source,
source: err,
})
}
}
}
Ok(outcome)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuthSeedOptions {
pub require_auth: bool,
pub require_credentials: bool,
pub create_target_dirs: bool,
}
impl Default for AuthSeedOptions {
fn default() -> Self {
Self {
require_auth: false,
require_credentials: false,
create_target_dirs: true,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AuthSeedOutcome {
pub copied_auth: bool,
pub copied_credentials: bool,
}
#[derive(Debug, Error)]
pub enum AuthSeedError {
#[error("seed CODEX_HOME `{seed_home}` does not exist or is unreadable")]
SeedHomeUnreadable {
seed_home: PathBuf,
#[source]
source: std::io::Error,
},
#[error("seed CODEX_HOME `{seed_home}` is not a directory")]
SeedHomeNotDirectory { seed_home: PathBuf },
#[error("seed file `{path}` is missing")]
SeedFileMissing { path: PathBuf },
#[error("seed file `{path}` is not a file")]
SeedFileNotFile { path: PathBuf },
#[error("seed file `{path}` is unreadable")]
SeedFileUnreadable {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to create target directory `{path}`")]
CreateTargetDir {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to copy `{source}` to `{destination}`")]
Copy {
source: PathBuf,
destination: PathBuf,
#[source]
error: std::io::Error,
},
}