use std::env;
use std::path::{Path, PathBuf};
#[cfg(not(target_os = "windows"))]
use anyhow::Context;
use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallScope {
Project,
Global,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstallPaths {
pub scope: InstallScope,
pub config_root: PathBuf,
pub runtime_root: PathBuf,
pub adapter_detection_root: PathBuf,
pub codex_user_config: Option<PathBuf>,
}
impl InstallPaths {
pub fn project(root: &Path) -> Self {
let root = root.to_path_buf();
#[cfg(test)]
let codex_user_config = None;
#[cfg(not(test))]
let codex_user_config = resolve_codex_user_config_path();
Self {
scope: InstallScope::Project,
config_root: root.clone(),
runtime_root: root.clone(),
adapter_detection_root: root,
codex_user_config,
}
}
pub fn global(store_root: &Path) -> Result<Self> {
let home = resolve_home_dir()?;
#[cfg(test)]
let codex_user_config = None;
#[cfg(not(test))]
let codex_user_config = resolve_codex_user_config_path();
Ok(Self::new(
InstallScope::Global,
store_root.join("global"),
home.clone(),
home,
)
.with_codex_user_config(codex_user_config))
}
pub fn new(
scope: InstallScope,
config_root: PathBuf,
runtime_root: PathBuf,
adapter_detection_root: PathBuf,
) -> Self {
Self {
scope,
config_root,
runtime_root,
adapter_detection_root,
codex_user_config: None,
}
}
pub fn with_codex_user_config(mut self, path: Option<PathBuf>) -> Self {
self.codex_user_config = path;
self
}
pub const fn is_global(&self) -> bool {
matches!(self.scope, InstallScope::Global)
}
}
#[cfg(not(test))]
fn resolve_codex_user_config_path() -> Option<PathBuf> {
if !codex_user_config_enabled(env::var_os("NODUS_ENABLE_CODEX_USER_CONFIG").as_deref()) {
return None;
}
if let Some(home) = env::var_os("CODEX_HOME") {
return Some(PathBuf::from(home).join("config.toml"));
}
resolve_home_dir()
.ok()
.map(|home| home.join(".codex").join("config.toml"))
}
fn codex_user_config_enabled(value: Option<&std::ffi::OsStr>) -> bool {
value.is_some_and(|raw| raw == "1" || raw.eq_ignore_ascii_case("true"))
}
fn resolve_home_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")]
{
if let Some(profile) = env::var_os("USERPROFILE") {
return Ok(PathBuf::from(profile));
}
if let (Some(drive), Some(path)) = (env::var_os("HOMEDRIVE"), env::var_os("HOMEPATH")) {
return Ok(PathBuf::from(drive).join(path));
}
anyhow::bail!("failed to determine the home directory for global installs");
}
#[cfg(not(target_os = "windows"))]
{
env::var_os("HOME")
.map(PathBuf::from)
.context("failed to determine the home directory for global installs")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsStr;
#[test]
fn project_scope_reuses_the_same_root_for_all_paths() {
let root = Path::new("/tmp/project");
let paths = InstallPaths::project(root);
assert_eq!(paths.scope, InstallScope::Project);
assert_eq!(paths.config_root, root);
assert_eq!(paths.runtime_root, root);
assert_eq!(paths.adapter_detection_root, root);
assert_eq!(paths.codex_user_config, None);
}
#[test]
fn codex_user_config_is_disabled_by_default() {
assert!(!codex_user_config_enabled(None));
}
#[test]
fn codex_user_config_enabled_for_truthy_values() {
assert!(codex_user_config_enabled(Some(OsStr::new("1"))));
assert!(codex_user_config_enabled(Some(OsStr::new("true"))));
assert!(codex_user_config_enabled(Some(OsStr::new("TRUE"))));
assert!(codex_user_config_enabled(Some(OsStr::new("True"))));
}
#[test]
fn codex_user_config_disabled_for_other_values() {
assert!(!codex_user_config_enabled(Some(OsStr::new("0"))));
assert!(!codex_user_config_enabled(Some(OsStr::new("false"))));
assert!(!codex_user_config_enabled(Some(OsStr::new(""))));
assert!(!codex_user_config_enabled(Some(OsStr::new("yes"))));
}
#[test]
fn global_scope_uses_store_root_for_config_and_home_for_runtime() {
let store_root = Path::new("/tmp/nodus-store");
let home = PathBuf::from("/tmp/home");
let paths = InstallPaths::new(
InstallScope::Global,
store_root.join("global"),
home.clone(),
home.clone(),
);
assert_eq!(paths.scope, InstallScope::Global);
assert_eq!(paths.config_root, PathBuf::from("/tmp/nodus-store/global"));
assert_eq!(paths.runtime_root, home);
assert_eq!(paths.adapter_detection_root, PathBuf::from("/tmp/home"));
assert_eq!(paths.codex_user_config, None);
}
}