use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::home::user_home;
pub const VARIABLES: [&str; 3] = [
"OPENCLAW_HOME",
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
];
pub const STATE_DIR_NAME: &str = ".openclaw";
pub const CONFIG_FILE: &str = "openclaw.json";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Location {
pub state_dir: PathBuf,
pub config: PathBuf,
}
impl Location {
pub fn at(state_dir: PathBuf, config: Option<PathBuf>) -> Self {
let config = config.unwrap_or_else(|| state_dir.join(CONFIG_FILE));
Self { state_dir, config }
}
pub fn default_for(home: &Path) -> Self {
Self::at(home.join(STATE_DIR_NAME), None)
}
pub fn config_is_default(&self) -> bool {
self.config == self.state_dir.join(CONFIG_FILE)
}
pub fn from_env() -> Result<Option<Self>> {
let get = |name: &str| std::env::var(name).ok();
if !VARIABLES.iter().any(|name| value(get(name)).is_some()) {
return Ok(None);
}
resolve(get, &user_home()?).map(Some)
}
}
pub fn env_disagrees(set_up: &Location) -> Result<Option<String>> {
Ok(Location::from_env()?
.filter(|env| env != set_up)
.map(|env| {
format!(
"OPENCLAW_HOME, OPENCLAW_STATE_DIR or OPENCLAW_CONFIG_PATH name {} (state {}), \
but `openmoat init` set up {} (state {}), which OpenMoat uses; run `openmoat init` with \
them set to set up that one instead",
env.config.display(),
env.state_dir.display(),
set_up.config.display(),
set_up.state_dir.display()
)
}))
}
pub fn resolve(get: impl Fn(&str) -> Option<String>, os_home: &Path) -> Result<Location> {
let [home_var, state_var, config_var] = VARIABLES;
let home = match value(get(home_var)) {
Some(home) => expand(&home, os_home)?,
None => os_home.to_path_buf(),
};
let state_dir = match value(get(state_var)) {
Some(state) => expand(&state, &home)?,
None => home.join(STATE_DIR_NAME),
};
let config = match value(get(config_var)) {
Some(config) => Some(expand(&config, &home)?),
None => None,
};
Ok(Location::at(state_dir, config))
}
fn value(raw: Option<String>) -> Option<String> {
raw.map(|v| v.trim().to_owned())
.filter(|v| !v.is_empty() && v != "undefined" && v != "null")
}
fn expand(path: &str, home: &Path) -> Result<PathBuf> {
let expanded = match path.strip_prefix('~') {
Some("") => home.to_path_buf(),
Some(rest) if rest.starts_with(['/', '\\']) => home.join(&rest[1..]),
_ => PathBuf::from(path),
};
crate::context::absolute(&expanded)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
fn located(vars: &[(&str, &str)]) -> Location {
let vars: BTreeMap<String, String> = vars
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect();
resolve(|name| vars.get(name).cloned(), Path::new("/os/home")).unwrap()
}
fn abs(path: &str) -> PathBuf {
crate::context::absolute(Path::new(path)).unwrap()
}
#[test]
fn defaults_follow_openclaw_home_and_explicit_paths_win() {
assert_eq!(located(&[]), Location::default_for(Path::new("/os/home")));
let home = located(&[("OPENCLAW_HOME", "~/svc")]);
assert_eq!(home.state_dir, abs("/os/home/svc/.openclaw"));
assert_eq!(home.config, abs("/os/home/svc/.openclaw/openclaw.json"));
assert!(home.config_is_default());
let state = located(&[
("OPENCLAW_HOME", "/srv/oc"),
("OPENCLAW_STATE_DIR", "~/state"),
]);
assert_eq!(state.state_dir, abs("/srv/oc/state"));
assert_eq!(state.config, abs("/srv/oc/state/openclaw.json"));
let config = located(&[("OPENCLAW_CONFIG_PATH", " /etc/oc/main.json5 ")]);
assert_eq!(config.state_dir, PathBuf::from("/os/home/.openclaw"));
assert_eq!(config.config, abs("/etc/oc/main.json5"));
assert!(!config.config_is_default());
}
#[test]
fn blank_undefined_and_null_are_unset() {
for unset in ["", " ", "undefined", "null"] {
assert_eq!(
located(&[("OPENCLAW_HOME", unset), ("OPENCLAW_CONFIG_PATH", unset)]),
Location::default_for(Path::new("/os/home")),
"{unset:?}"
);
}
}
}