use std::io; use std::path::PathBuf; #[cfg(test)] use std::sync::{Mutex, MutexGuard, OnceLock}; use portable_pty::CommandBuilder; pub(crate) const HERDR_PANE_ID_ENV_VAR: &str = "HERDR_PANE_ID"; pub(crate) const HERDR_TAB_ID_ENV_VAR: &str = "HERDR_TAB_ID"; pub(crate) const HERDR_WORKSPACE_ID_ENV_VAR: &str = "HERDR_WORKSPACE_ID"; pub(crate) const PI_CODING_AGENT_DIR_ENV_VAR: &str = "PI_CODING_AGENT_DIR"; pub(crate) const CLAUDE_CONFIG_DIR_ENV_VAR: &str = "CLAUDE_CONFIG_DIR"; pub(crate) const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME"; pub(crate) const KIMI_CODE_HOME_ENV_VAR: &str = "KIMI_CODE_HOME"; pub(crate) const COPILOT_HOME_ENV_VAR: &str = "COPILOT_HOME"; pub(crate) const QODERCLI_CONFIG_DIR_ENV_VAR: &str = "QODER_CONFIG_DIR"; pub(crate) const CURSOR_CONFIG_DIR_ENV_VAR: &str = "CURSOR_CONFIG_DIR"; pub(crate) fn apply_pane_base_env(cmd: &mut CommandBuilder) { cmd.env(crate::api::SOCKET_PATH_ENV_VAR, crate::api::socket_path()); } pub(crate) fn pi_extension_dir() -> io::Result { Ok( config_dir_from_env_or_home(PI_CODING_AGENT_DIR_ENV_VAR, &[".pi", "agent"])? .join("extensions"), ) } pub(crate) fn omp_extension_dir() -> io::Result { Ok( config_dir_from_env_or_home(PI_CODING_AGENT_DIR_ENV_VAR, &[".omp", "agent"])? .join("extensions"), ) } pub(crate) fn claude_dir() -> io::Result { config_dir_from_env_or_home(CLAUDE_CONFIG_DIR_ENV_VAR, &[".claude"]) } pub(crate) fn codex_dir() -> io::Result { config_dir_from_env_or_home(CODEX_HOME_ENV_VAR, &[".codex"]) } pub(crate) fn kimi_dir() -> io::Result { config_dir_from_env_or_home(KIMI_CODE_HOME_ENV_VAR, &[".kimi-code"]) } pub(crate) fn copilot_dir() -> io::Result { config_dir_from_env_or_home(COPILOT_HOME_ENV_VAR, &[".copilot"]) } pub(crate) fn devin_dir() -> io::Result { if let Some(value) = std::env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty()) { return expand_tilde_path(PathBuf::from(value)).map(|path| path.join("devin")); } Ok(home_dir()?.join(".config").join("devin")) } pub(crate) fn droid_dir() -> io::Result { Ok(home_dir()?.join(".factory")) } pub(crate) fn config_dir_from_env_or_home( env_var: &str, home_relative_segments: &[&str], ) -> io::Result { if let Some(value) = std::env::var_os(env_var).filter(|value| !value.is_empty()) { return expand_tilde_path(PathBuf::from(value)); } let mut path = home_dir()?; for segment in home_relative_segments { path.push(segment); } Ok(path) } pub(crate) fn expand_tilde_path(path: PathBuf) -> io::Result { let Some(raw) = path.to_str() else { return Ok(path); }; if raw == "~" { return home_dir(); } if let Some(rest) = raw .strip_prefix("~/") .or_else(|| raw.strip_prefix("~\\")) .or_else(|| raw.strip_prefix('~')) { return Ok(home_dir()?.join(rest)); } Ok(path) } pub(crate) fn opencode_dir() -> io::Result { Ok(home_dir()?.join(".config/opencode")) } pub(crate) fn kilo_dir() -> io::Result { Ok(home_dir()?.join(".config/kilo")) } pub(crate) fn hermes_dir() -> io::Result { Ok(home_dir()?.join(".hermes")) } pub(crate) fn hermes_plugin_dir() -> io::Result { Ok(hermes_dir()? .join("plugins") .join(super::HERMES_PLUGIN_INSTALL_NAME)) } pub(crate) fn qodercli_dir() -> io::Result { config_dir_from_env_or_home(QODERCLI_CONFIG_DIR_ENV_VAR, &[".qoder"]) } pub(crate) fn cursor_dir() -> io::Result { config_dir_from_env_or_home(CURSOR_CONFIG_DIR_ENV_VAR, &[".cursor"]) } pub(crate) fn home_dir() -> io::Result { if let Some(home) = std::env::var_os("HOME").filter(|value| !value.is_empty()) { return Ok(PathBuf::from(home)); } #[cfg(windows)] { if let Some(profile) = std::env::var_os("USERPROFILE").filter(|value| !value.is_empty()) { return Ok(PathBuf::from(profile)); } if let (Some(drive), Some(path)) = ( std::env::var_os("HOMEDRIVE").filter(|value| !value.is_empty()), std::env::var_os("HOMEPATH").filter(|value| !value.is_empty()), ) { let mut home = PathBuf::from(drive); home.push(path); return Ok(home); } } Err(io::Error::other( "home directory is not set; cannot locate home directory", )) } #[cfg(test)] pub(crate) fn integration_env_lock() -> MutexGuard<'static, ()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() }