//! Optional multi-user workspace helpers for loading per-user agent config. //! //! When optional workspace root and user env vars are set and the resolved //! directory exists, that path can contribute AGENTS.md / rules / skills //! discovery. Unset env vars are a no-op (typical for standalone installs). use std::path::PathBuf; /// If optional workspace env vars are set, returns the user's config directory /// when the resolved path exists on disk. Unset or missing paths yield `None`. pub fn optional_workspace_user_dir() -> Option { let root = std::env::var("XAI_ROOT").ok()?; let user = std::env::var("XAI_USER").ok()?; resolve_workspace_user_dir(&root, &workspace_user_relpath(&user)) } /// Map `$XAI_USER` to a path relative to the workspace root. /// /// A bare username is nested one level under `x/` so it cannot collide with an /// unrelated same-named directory at the workspace root. Values that already /// contain a path separator are used as-is (explicit relative path). fn workspace_user_relpath(user: &str) -> String { if user.contains('/') || user.contains('\\') { user.to_string() } else { format!("x/{user}") } } /// Pure logic: join `root` with a relative `user` path and return it if the /// directory exists on disk. /// /// Returns `None` if either argument is empty or the resulting path is not /// a directory. /// /// Example: `resolve_workspace_user_dir("/workspace", "users/alice")` /// → `Some("/workspace/users/alice")` if that directory exists. pub fn resolve_workspace_user_dir(root: &str, user: &str) -> Option { if root.is_empty() || user.is_empty() { return None; } let path = PathBuf::from(root).join(user); path.is_dir().then_some(path) } #[cfg(test)] mod tests { use super::*; use std::fs; // resolve_workspace_user_dir (pure, no env vars) #[test] fn resolve_returns_none_for_empty_root() { assert!(resolve_workspace_user_dir("", "users/someone").is_none()); } #[test] fn resolve_returns_none_for_empty_user() { let tmp = tempfile::tempdir().unwrap(); assert!(resolve_workspace_user_dir(tmp.path().to_str().unwrap(), "").is_none()); } #[test] fn resolve_returns_none_for_both_empty() { assert!(resolve_workspace_user_dir("", "").is_none()); } #[test] fn resolve_returns_none_when_dir_does_not_exist() { let tmp = tempfile::tempdir().unwrap(); assert!( resolve_workspace_user_dir(tmp.path().to_str().unwrap(), "users/nonexistent").is_none() ); } #[test] fn resolve_returns_path_when_dir_exists() { let tmp = tempfile::tempdir().unwrap(); let user_dir = tmp.path().join("users").join("testuser"); fs::create_dir_all(&user_dir).unwrap(); let result = resolve_workspace_user_dir(tmp.path().to_str().unwrap(), "users/testuser"); assert_eq!(result, Some(user_dir)); } #[test] fn resolve_handles_single_component_user() { let tmp = tempfile::tempdir().unwrap(); let user_dir = tmp.path().join("alice"); fs::create_dir_all(&user_dir).unwrap(); let result = resolve_workspace_user_dir(tmp.path().to_str().unwrap(), "alice"); assert_eq!(result, Some(user_dir)); } #[test] fn resolve_handles_deeply_nested_user() { let tmp = tempfile::tempdir().unwrap(); let user_dir = tmp.path().join("org").join("team").join("user"); fs::create_dir_all(&user_dir).unwrap(); let result = resolve_workspace_user_dir(tmp.path().to_str().unwrap(), "org/team/user"); assert_eq!(result, Some(user_dir)); } #[test] fn resolve_returns_none_when_path_is_file_not_dir() { let tmp = tempfile::tempdir().unwrap(); let file_path = tmp.path().join("users").join("testuser"); fs::create_dir_all(file_path.parent().unwrap()).unwrap(); fs::write(&file_path, "not a directory").unwrap(); assert!( resolve_workspace_user_dir(tmp.path().to_str().unwrap(), "users/testuser").is_none() ); } #[test] fn resolve_supports_nested_user_layout_path() { let tmp = tempfile::tempdir().unwrap(); let user_dir = tmp.path().join("x").join("testuser"); fs::create_dir_all(&user_dir).unwrap(); let result = resolve_workspace_user_dir(tmp.path().to_str().unwrap(), "x/testuser"); assert_eq!(result, Some(user_dir)); } // workspace_user_relpath #[test] fn bare_username_is_nested_under_x() { assert_eq!(workspace_user_relpath("alice"), "x/alice"); assert_eq!(workspace_user_relpath("bob"), "x/bob"); } #[test] fn multi_segment_user_is_explicit_relative_path() { assert_eq!(workspace_user_relpath("users/alice"), "users/alice"); assert_eq!(workspace_user_relpath(r"users\alice"), r"users\alice"); } #[test] fn bare_username_does_not_resolve_to_same_named_root_dir() { // Prefer the nested layout even when a same-named directory exists at // the workspace root. let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); fs::create_dir_all(root.join("alice")).unwrap(); let user_dir = root.join("x").join("alice"); fs::create_dir_all(&user_dir).unwrap(); let rel = workspace_user_relpath("alice"); let resolved = resolve_workspace_user_dir(root.to_str().unwrap(), &rel); assert_eq!(resolved, Some(user_dir)); assert_ne!( resolved.as_deref(), Some(root.join("alice").as_path()), "must not resolve to a same-named directory at the workspace root" ); } }