use std::path::{Path, PathBuf};
pub fn resolve_oxi_home(oxi_home: Option<&str>, user_home: Option<&Path>) -> Option<PathBuf> {
if let Some(p) = oxi_home.map(str::trim).filter(|s| !s.is_empty()) {
return Some(PathBuf::from(p));
}
user_home.map(|h| h.join(".oxi"))
}
pub fn resolve_oxicode_home(
oxicode_home: Option<&str>,
oxi_home: Option<&Path>,
) -> Option<PathBuf> {
if let Some(p) = oxicode_home.map(str::trim).filter(|s| !s.is_empty()) {
return Some(PathBuf::from(p));
}
oxi_home.map(|h| h.join("oxicode"))
}
pub fn resolve_legacy_home(user_home: Option<&Path>) -> Option<PathBuf> {
user_home.map(|h| h.join(".oxicode"))
}
pub fn resolve_read_path(canonical_home: &Path, legacy_home: Option<&Path>, rel: &Path) -> PathBuf {
let canonical = canonical_home.join(rel);
if canonical.exists() {
return canonical;
}
if let Some(legacy) = legacy_home {
let legacy_path = legacy.join(rel);
if legacy_path.exists() {
return legacy_path;
}
}
canonical
}
pub fn oxi_home() -> Option<PathBuf> {
resolve_oxi_home(
std::env::var("OXI_HOME").ok().as_deref(),
dirs::home_dir().as_deref(),
)
}
pub fn oxicode_home() -> Option<PathBuf> {
resolve_oxicode_home(
std::env::var("OXICODE_HOME").ok().as_deref(),
oxi_home().as_deref(),
)
}
pub fn legacy_home_dir() -> Option<PathBuf> {
if std::env::var_os("OXICODE_HOME").is_some_and(|v| !v.to_string_lossy().trim().is_empty()) {
return None;
}
let p = resolve_legacy_home(dirs::home_dir().as_deref())?;
p.is_dir().then_some(p)
}
pub fn read_path(rel: impl AsRef<Path>) -> Option<PathBuf> {
Some(resolve_read_path(
&oxicode_home()?,
legacy_home_dir().as_deref(),
rel.as_ref(),
))
}
pub fn migration_journal_path() -> Option<PathBuf> {
oxi_home().map(|h| h.join("oxicode.migration-journal.json"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn home(p: &str) -> Option<PathBuf> {
Some(PathBuf::from(p))
}
#[test]
fn oxi_home_env_wins() {
let got = resolve_oxi_home(Some("/custom/oxi"), Some(Path::new("/home/u")));
assert_eq!(got, home("/custom/oxi"));
}
#[test]
fn oxi_home_empty_env_falls_through() {
let got = resolve_oxi_home(Some(""), Some(Path::new("/home/u")));
assert_eq!(got, home("/home/u/.oxi"));
}
#[test]
fn oxi_home_whitespace_env_falls_through() {
let got = resolve_oxi_home(Some(" "), Some(Path::new("/home/u")));
assert_eq!(got, home("/home/u/.oxi"));
}
#[test]
fn oxi_home_defaults_to_user_home() {
let got = resolve_oxi_home(None, Some(Path::new("/home/u")));
assert_eq!(got, home("/home/u/.oxi"));
}
#[test]
fn oxi_home_none_when_both_absent() {
assert_eq!(resolve_oxi_home(None, None), None);
}
#[test]
fn oxicode_home_env_wins() {
let got = resolve_oxicode_home(Some("/custom/ox"), Some(Path::new("/home/u/.oxi")));
assert_eq!(got, home("/custom/ox"));
}
#[test]
fn oxicode_home_empty_env_falls_through() {
let got = resolve_oxicode_home(Some(""), Some(Path::new("/home/u/.oxi")));
assert_eq!(got, home("/home/u/.oxi/oxicode"));
}
#[test]
fn oxicode_home_defaults_to_oxi_subdir() {
let got = resolve_oxicode_home(None, Some(Path::new("/home/u/.oxi")));
assert_eq!(got, home("/home/u/.oxi/oxicode"));
}
#[test]
fn oxicode_home_none_without_oxi_home() {
assert_eq!(resolve_oxicode_home(None, None), None);
}
#[test]
fn legacy_candidate_is_user_home_dot_oxicode() {
let got = resolve_legacy_home(Some(Path::new("/home/u")));
assert_eq!(got, home("/home/u/.oxicode"));
}
#[test]
fn legacy_candidate_none_without_user_home() {
assert_eq!(resolve_legacy_home(None), None);
}
#[test]
fn read_path_prefers_canonical_when_it_exists() {
let tmp = tempfile::tempdir().unwrap();
let canonical = tmp.path().join("canonical");
let legacy = tmp.path().join("legacy");
std::fs::create_dir_all(canonical.join("skills")).unwrap();
std::fs::create_dir_all(legacy.join("skills")).unwrap();
std::fs::write(legacy.join("skills").join("old.md"), "legacy").unwrap();
let got = resolve_read_path(&canonical, Some(&legacy), Path::new("skills"));
assert_eq!(got, canonical.join("skills"));
}
#[test]
fn read_path_falls_back_to_legacy_when_canonical_lacks_item() {
let tmp = tempfile::tempdir().unwrap();
let canonical = tmp.path().join("canonical");
let legacy = tmp.path().join("legacy");
std::fs::create_dir_all(&canonical).unwrap();
std::fs::create_dir_all(legacy.join("skills")).unwrap();
let got = resolve_read_path(&canonical, Some(&legacy), Path::new("skills"));
assert_eq!(got, legacy.join("skills"));
}
#[test]
fn read_path_defaults_to_canonical_when_neither_exists() {
let tmp = tempfile::tempdir().unwrap();
let canonical = tmp.path().join("canonical");
std::fs::create_dir_all(&canonical).unwrap();
let got = resolve_read_path(&canonical, None, Path::new("auth.json"));
assert_eq!(got, canonical.join("auth.json"));
}
#[test]
fn read_path_ignores_legacy_without_override_gap() {
let tmp = tempfile::tempdir().unwrap();
let canonical = tmp.path().join("canonical");
let legacy = tmp.path().join("legacy");
std::fs::create_dir_all(&canonical).unwrap();
std::fs::create_dir_all(&legacy).unwrap();
std::fs::write(canonical.join("auth.json"), "{}").unwrap();
std::fs::write(legacy.join("auth.json"), "{}").unwrap();
let got = resolve_read_path(&canonical, Some(&legacy), Path::new("auth.json"));
assert_eq!(got, canonical.join("auth.json"));
}
#[test]
fn oxi_home_resolves_in_ci() {
assert!(oxi_home().is_some(), "OXI_HOME or HOME is always set in CI");
}
#[test]
fn oxicode_home_nests_under_oxi_home() {
let oxi = oxi_home().expect("oxi home resolves in CI");
if std::env::var_os("OXICODE_HOME").is_none() {
assert_eq!(oxicode_home(), Some(oxi.join("oxicode")));
}
}
}