use std::path::PathBuf;
pub const NIKA_HOME_ENV: &str = "NIKA_HOME";
pub const NIKA_DIR_NAME: &str = ".nika";
pub const NIKA_PROJECT_DIR: &str = ".nika";
pub const NIKA_MANIFEST: &str = "nika.yaml";
pub const NIKA_LOCKFILE: &str = "nika.lock";
pub const MCP_CONFIG: &str = "mcp.yaml";
pub const GLOBAL_CONFIG: &str = "config.toml";
pub const REGISTRY_INDEX: &str = "registry.yaml";
pub const DAEMON_SOCKET: &str = "nika.sock";
pub const DAEMON_PID: &str = "nika.pid";
pub fn nika_home() -> PathBuf {
if let Ok(custom_home) = std::env::var(NIKA_HOME_ENV) {
return PathBuf::from(custom_home);
}
match dirs::home_dir() {
Some(h) => h.join(NIKA_DIR_NAME),
None => {
let fallback = std::env::temp_dir().join(NIKA_DIR_NAME);
tracing::error!(
path = %fallback.display(),
"Could not determine home directory. Using temporary fallback. Set NIKA_HOME environment variable for a secure persistent location."
);
fallback
}
}
}
pub fn nika_home_opt() -> Option<PathBuf> {
if let Ok(custom_home) = std::env::var(NIKA_HOME_ENV) {
return Some(PathBuf::from(custom_home));
}
dirs::home_dir().map(|h| h.join(NIKA_DIR_NAME))
}
pub fn nika_home_result() -> Result<PathBuf, crate::NikaError> {
nika_home_opt().ok_or(crate::NikaError::HomeDirectoryNotFound)
}
pub fn user_home_result() -> Result<PathBuf, crate::NikaError> {
dirs::home_dir().ok_or(crate::NikaError::HomeDirectoryNotFound)
}
pub fn packages_dir() -> PathBuf {
nika_home().join("packages")
}
pub fn models_dir() -> PathBuf {
nika_home().join("models")
}
pub fn backups_dir() -> PathBuf {
nika_home().join("backups")
}
pub fn cache_dir() -> PathBuf {
nika_home().join("cache")
}
pub fn daemon_dir() -> PathBuf {
nika_home().join("daemon")
}
pub fn global_config_path() -> PathBuf {
nika_home().join(GLOBAL_CONFIG)
}
pub fn global_mcp_config_path() -> PathBuf {
nika_home().join(MCP_CONFIG)
}
pub fn registry_index_path() -> PathBuf {
packages_dir().join(REGISTRY_INDEX)
}
pub fn daemon_socket_path() -> PathBuf {
daemon_dir().join(DAEMON_SOCKET)
}
pub fn daemon_pid_path() -> PathBuf {
daemon_dir().join(DAEMON_PID)
}
pub fn project_nika_dir(project_root: &std::path::Path) -> PathBuf {
project_root.join(NIKA_PROJECT_DIR)
}
pub fn project_mcp_config_path(project_root: &std::path::Path) -> PathBuf {
project_nika_dir(project_root).join(MCP_CONFIG)
}
pub fn project_manifest_path(project_root: &std::path::Path) -> PathBuf {
project_root.join(NIKA_MANIFEST)
}
pub fn project_lockfile_path(project_root: &std::path::Path) -> PathBuf {
project_root.join(NIKA_LOCKFILE)
}
pub fn project_sessions_dir(project_root: &std::path::Path) -> PathBuf {
project_nika_dir(project_root).join("sessions")
}
pub fn package_dir(scope: &str, name: &str, version: &str) -> PathBuf {
packages_dir().join(scope).join(name).join(version)
}
pub fn package_manifest_path(scope: &str, name: &str, version: &str) -> PathBuf {
package_dir(scope, name, version).join("manifest.yaml")
}
pub fn ensure_nika_home() -> std::io::Result<()> {
let home = nika_home();
std::fs::create_dir_all(&home)?;
std::fs::create_dir_all(home.join("packages"))?;
std::fs::create_dir_all(home.join("models"))?;
std::fs::create_dir_all(home.join("backups"))?;
std::fs::create_dir_all(home.join("cache"))?;
std::fs::create_dir_all(home.join("daemon"))?;
Ok(())
}
pub fn ensure_project_nika_dir(project_root: &std::path::Path) -> std::io::Result<()> {
let nika_dir = project_nika_dir(project_root);
std::fs::create_dir_all(&nika_dir)?;
std::fs::create_dir_all(nika_dir.join("sessions"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::env;
fn with_temp_nika_home<F, R>(f: F) -> R
where
F: FnOnce(&std::path::Path) -> R,
{
let temp_dir = tempfile::tempdir().unwrap();
let old_val = env::var(NIKA_HOME_ENV).ok();
env::set_var(NIKA_HOME_ENV, temp_dir.path());
let result = f(temp_dir.path());
if let Some(val) = old_val {
env::set_var(NIKA_HOME_ENV, val);
} else {
env::remove_var(NIKA_HOME_ENV);
}
result
}
#[test]
#[serial]
fn test_nika_home_default() {
let old_val = env::var(NIKA_HOME_ENV).ok();
env::remove_var(NIKA_HOME_ENV);
let home = nika_home();
assert!(home.ends_with(".nika"));
if let Some(val) = old_val {
env::set_var(NIKA_HOME_ENV, val);
}
}
#[test]
#[serial]
fn test_nika_home_env_override() {
with_temp_nika_home(|temp_path| {
let home = nika_home();
assert_eq!(home, temp_path);
});
}
#[test]
#[serial]
fn test_nika_home_opt_returns_some() {
with_temp_nika_home(|temp_path| {
let home = nika_home_opt();
assert_eq!(home, Some(temp_path.to_path_buf()));
});
}
#[test]
#[serial]
fn test_packages_dir() {
with_temp_nika_home(|temp_path| {
let packages = packages_dir();
assert_eq!(packages, temp_path.join("packages"));
});
}
#[test]
#[serial]
fn test_models_dir() {
with_temp_nika_home(|temp_path| {
let models = models_dir();
assert_eq!(models, temp_path.join("models"));
});
}
#[test]
#[serial]
fn test_backups_dir() {
with_temp_nika_home(|temp_path| {
let backups = backups_dir();
assert_eq!(backups, temp_path.join("backups"));
});
}
#[test]
#[serial]
fn test_cache_dir() {
with_temp_nika_home(|temp_path| {
let cache = cache_dir();
assert_eq!(cache, temp_path.join("cache"));
});
}
#[test]
#[serial]
fn test_daemon_dir() {
with_temp_nika_home(|temp_path| {
let daemon = daemon_dir();
assert_eq!(daemon, temp_path.join("daemon"));
});
}
#[test]
#[serial]
fn test_global_config_path() {
with_temp_nika_home(|temp_path| {
let config = global_config_path();
assert_eq!(config, temp_path.join("config.toml"));
});
}
#[test]
#[serial]
fn test_global_mcp_config_path() {
with_temp_nika_home(|temp_path| {
let mcp = global_mcp_config_path();
assert_eq!(mcp, temp_path.join("mcp.yaml"));
});
}
#[test]
#[serial]
fn test_registry_index_path() {
with_temp_nika_home(|temp_path| {
let registry = registry_index_path();
assert_eq!(registry, temp_path.join("packages").join("registry.yaml"));
});
}
#[test]
#[serial]
fn test_daemon_socket_path() {
with_temp_nika_home(|temp_path| {
let socket = daemon_socket_path();
assert_eq!(socket, temp_path.join("daemon").join("nika.sock"));
});
}
#[test]
#[serial]
fn test_daemon_pid_path() {
with_temp_nika_home(|temp_path| {
let pid = daemon_pid_path();
assert_eq!(pid, temp_path.join("daemon").join("nika.pid"));
});
}
#[test]
fn test_project_paths() {
let project_root = std::path::Path::new("/tmp/my-project");
assert_eq!(
project_nika_dir(project_root),
PathBuf::from("/tmp/my-project/.nika")
);
assert_eq!(
project_mcp_config_path(project_root),
PathBuf::from("/tmp/my-project/.nika/mcp.yaml")
);
assert_eq!(
project_manifest_path(project_root),
PathBuf::from("/tmp/my-project/nika.yaml")
);
assert_eq!(
project_lockfile_path(project_root),
PathBuf::from("/tmp/my-project/nika.lock")
);
assert_eq!(
project_sessions_dir(project_root),
PathBuf::from("/tmp/my-project/.nika/sessions")
);
}
#[test]
#[serial]
fn test_package_dir() {
with_temp_nika_home(|temp_path| {
let pkg = package_dir("@nika", "seo-audit", "1.0.0");
assert_eq!(pkg, temp_path.join("packages/@nika/seo-audit/1.0.0"));
});
}
#[test]
#[serial]
fn test_package_manifest_path() {
with_temp_nika_home(|temp_path| {
let manifest = package_manifest_path("@workflows", "code-review", "2.1.0");
assert_eq!(
manifest,
temp_path.join("packages/@workflows/code-review/2.1.0/manifest.yaml")
);
});
}
#[test]
#[serial]
fn test_ensure_nika_home_creates_directories() {
with_temp_nika_home(|temp_path| {
let _ = std::fs::remove_dir_all(temp_path);
ensure_nika_home().unwrap();
assert!(temp_path.exists());
assert!(temp_path.join("packages").exists());
assert!(temp_path.join("models").exists());
assert!(temp_path.join("backups").exists());
assert!(temp_path.join("cache").exists());
assert!(temp_path.join("daemon").exists());
});
}
#[test]
fn test_ensure_project_nika_dir() {
let temp_dir = tempfile::tempdir().unwrap();
let project_root = temp_dir.path();
ensure_project_nika_dir(project_root).unwrap();
assert!(project_root.join(".nika").exists());
assert!(project_root.join(".nika/sessions").exists());
}
#[test]
fn test_constants() {
assert_eq!(NIKA_HOME_ENV, "NIKA_HOME");
assert_eq!(NIKA_DIR_NAME, ".nika");
assert_eq!(NIKA_PROJECT_DIR, ".nika");
assert_eq!(NIKA_MANIFEST, "nika.yaml");
assert_eq!(NIKA_LOCKFILE, "nika.lock");
assert_eq!(MCP_CONFIG, "mcp.yaml");
assert_eq!(GLOBAL_CONFIG, "config.toml");
assert_eq!(REGISTRY_INDEX, "registry.yaml");
assert_eq!(DAEMON_SOCKET, "nika.sock");
assert_eq!(DAEMON_PID, "nika.pid");
}
}