use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
pub fn get_home_dir() -> Result<PathBuf> {
if let Ok(home) = std::env::var("ATM_HOME") {
let trimmed = home.trim();
if !trimmed.is_empty() {
let path = PathBuf::from(trimmed);
return Ok(path);
}
}
dirs::home_dir().context("Could not determine home directory")
}
pub fn get_os_home_dir() -> Result<PathBuf> {
dirs::home_dir().context("Could not determine OS home directory")
}
pub fn claude_root_dir() -> Result<PathBuf> {
Ok(claude_root_dir_for(&get_home_dir()?))
}
pub fn claude_root_dir_for(home: &Path) -> PathBuf {
home.join(".claude")
}
pub fn teams_root_dir() -> Result<PathBuf> {
Ok(teams_root_dir_for(&get_home_dir()?))
}
pub fn teams_root_dir_for(home: &Path) -> PathBuf {
claude_root_dir_for(home).join("teams")
}
pub fn team_dir_for(home: &Path, team: &str) -> PathBuf {
teams_root_dir_for(home).join(team)
}
pub fn team_config_path_for(home: &Path, team: &str) -> PathBuf {
team_dir_for(home, team).join("config.json")
}
pub fn inbox_path_for(home: &Path, team: &str, agent: &str) -> PathBuf {
team_dir_for(home, team)
.join("inboxes")
.join(format!("{agent}.json"))
}
pub fn claude_settings_path_for(home: &Path) -> PathBuf {
claude_root_dir_for(home).join("settings.json")
}
pub fn claude_scripts_dir_for(home: &Path) -> PathBuf {
claude_root_dir_for(home).join("scripts")
}
pub fn claude_agents_dir_for(home: &Path) -> PathBuf {
claude_root_dir_for(home).join("agents")
}
pub fn atm_config_dir_for(home: &Path) -> PathBuf {
home.join(".config").join("atm")
}
pub fn sessions_dir_for(home: &Path) -> PathBuf {
atm_config_dir_for(home).join("agent-sessions")
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::env;
#[test]
#[serial]
fn test_atm_home_set() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", "/custom/home") };
let home = get_home_dir().unwrap();
assert_eq!(home, PathBuf::from("/custom/home"));
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
#[serial]
fn test_atm_home_not_set_uses_platform_default() {
let original = env::var("ATM_HOME").ok();
unsafe { env::remove_var("ATM_HOME") };
let home = get_home_dir().unwrap();
assert_eq!(home, dirs::home_dir().unwrap());
unsafe {
if let Some(v) = original {
env::set_var("ATM_HOME", v);
}
}
}
#[test]
#[serial]
fn test_atm_home_empty_string_uses_platform_default() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", "") };
let home = get_home_dir().unwrap();
assert_eq!(home, dirs::home_dir().unwrap());
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
#[serial]
fn test_atm_home_whitespace_only_uses_platform_default() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", " ") };
let home = get_home_dir().unwrap();
assert_eq!(home, dirs::home_dir().unwrap());
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
#[serial]
fn test_atm_home_with_trailing_slash() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", "/custom/home/") };
let home = get_home_dir().unwrap();
let expected = PathBuf::from("/custom/home/");
assert_eq!(home, expected);
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
#[serial]
fn test_atm_home_with_spaces_in_path() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", "/path with spaces/home") };
let home = get_home_dir().unwrap();
assert_eq!(home, PathBuf::from("/path with spaces/home"));
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
#[serial]
fn test_atm_home_relative_path() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", "relative/path") };
let home = get_home_dir().unwrap();
assert_eq!(home, PathBuf::from("relative/path"));
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
#[serial]
fn test_atm_home_with_leading_trailing_whitespace() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", " /custom/home ") };
let home = get_home_dir().unwrap();
assert_eq!(home, PathBuf::from("/custom/home"));
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
#[serial]
fn test_multiple_calls_consistent() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", "/test/home") };
let home1 = get_home_dir().unwrap();
let home2 = get_home_dir().unwrap();
assert_eq!(home1, home2);
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
#[test]
fn test_path_helpers_build_canonical_paths() {
let home = PathBuf::from("test-home");
assert_eq!(claude_root_dir_for(&home), home.join(".claude"));
assert_eq!(
teams_root_dir_for(&home),
home.join(".claude").join("teams")
);
assert_eq!(
team_dir_for(&home, "atm-dev"),
home.join(".claude").join("teams").join("atm-dev")
);
assert_eq!(
team_config_path_for(&home, "atm-dev"),
home.join(".claude")
.join("teams")
.join("atm-dev")
.join("config.json")
);
assert_eq!(
inbox_path_for(&home, "atm-dev", "arch-ctm"),
home.join(".claude")
.join("teams")
.join("atm-dev")
.join("inboxes")
.join("arch-ctm.json")
);
assert_eq!(
claude_settings_path_for(&home),
home.join(".claude").join("settings.json")
);
assert_eq!(
claude_scripts_dir_for(&home),
home.join(".claude").join("scripts")
);
assert_eq!(
claude_agents_dir_for(&home),
home.join(".claude").join("agents")
);
assert_eq!(atm_config_dir_for(&home), home.join(".config").join("atm"));
assert_eq!(
sessions_dir_for(&home),
home.join(".config").join("atm").join("agent-sessions")
);
}
#[test]
#[serial]
fn test_root_dir_helpers_respect_atm_home() {
let original = env::var("ATM_HOME").ok();
unsafe { env::set_var("ATM_HOME", "test-home") };
assert_eq!(
claude_root_dir().unwrap(),
PathBuf::from("test-home/.claude")
);
assert_eq!(
teams_root_dir().unwrap(),
PathBuf::from("test-home/.claude/teams")
);
unsafe {
match original {
Some(v) => env::set_var("ATM_HOME", v),
None => env::remove_var("ATM_HOME"),
}
}
}
}