mod binary;
mod claude_version;
mod dirs;
mod hook_file;
pub mod location;
pub mod openclaw;
pub mod owned;
pub mod self_paths;
pub mod trust_note;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail};
use openmoat_hosts::{ExtraFile, Host, InstallSpec, Locate, Located};
pub use binary::{alias_of, hook_binary, other_name, stale_hint};
pub use dirs::Recorded;
pub use hook_file::{HookState, Outcome, read_or_empty};
pub use openmoat_hosts::{GUARD_BUDGET_S, HookLayout, HookSpec};
use crate::home::{Home, user_home};
pub const HOOK_BACKUP: &str = "moat-backup";
pub const SANDBOX_BACKUP: &str = "moat-sandbox-backup";
pub fn backup_path(path: &Path, suffix: &str) -> PathBuf {
let mut name = path.as_os_str().to_owned();
name.push(".");
name.push(suffix);
PathBuf::from(name)
}
pub fn back_up(home: &Home, path: &Path, suffix: &str) -> Result<()> {
let backup = backup_path(path, suffix);
if !path.exists() || fs::symlink_metadata(&backup).is_ok() {
return Ok(());
}
fs::copy(path, &backup).with_context(|| format!("backing up {}", path.display()))?;
let mut recorded = Recorded::load(home)?;
recorded.backups.push(backup);
recorded.save(home)
}
pub fn backups(path: &Path) -> Vec<PathBuf> {
[HOOK_BACKUP, SANDBOX_BACKUP]
.into_iter()
.map(|suffix| backup_path(path, suffix))
.filter(|p| p.is_file())
.collect()
}
#[derive(Debug, Clone)]
pub struct HostConfig {
pub host: Host,
pub settings_path: PathBuf,
pub dir: PathBuf,
pub hooks: &'static [HookSpec],
pub layout: HookLayout,
pub blocks_on_failure: fn() -> bool,
}
pub fn dir_variable(host: Host) -> Result<(&'static str, &'static str)> {
let install = install_spec(host)?;
Ok((install.dir_variable, install.dir_name))
}
fn install_spec(host: Host) -> Result<&'static InstallSpec> {
match &host.spec().install {
Some(install) => Ok(install),
None => bail!(
"{} runs another agent's hook; it has none of its own",
host.display_name()
),
}
}
pub fn env_config_dir(host: Host) -> Result<Option<PathBuf>> {
if let Some(locate) = location::custom(host) {
return Ok(location::from_env(locate)?.map(|l| l.dir));
}
let install = install_spec(host)?;
let dir = env_dir(install.dir_variable);
Ok(if install.variable_names_home() {
dir.map(|home| home.join(install.dir_name))
} else {
dir
})
}
impl HostConfig {
pub fn for_host(home: &Home, host: Host) -> Result<Self> {
let recorded = Recorded::load(home)?;
if let Some(locate) = location::custom(host) {
let located = match recorded.dir(host) {
Some(dir) => locate.at(dir, recorded.file(host)),
None => match location::from_env(locate)? {
Some(located) => located,
None => locate.default_for(&user_home()?),
},
};
return Self::with_spec(host, located.config, located.dir);
}
Self::at(host, recorded.dir(host).or(env_config_dir(host)?))
}
pub fn for_init(home: &Home, host: Host) -> Result<Self> {
if let Some(locate) = location::custom(host)
&& let Some(located) = location::from_env(locate)?
{
return Self::with_spec(host, located.config, located.dir);
}
match env_config_dir(host)? {
Some(dir) => Self::at(host, Some(dir)),
None => Self::for_host(home, host),
}
}
pub fn custom_location(&self) -> Option<(&'static dyn Locate, Located)> {
let locate = location::custom(self.host)?;
Some((
locate,
locate.at(self.dir.clone(), Some(self.settings_path.clone())),
))
}
fn at(host: Host, dir: Option<PathBuf>) -> Result<Self> {
let install = install_spec(host)?;
let dir = match dir {
Some(dir) => dir,
None => user_home()?.join(install.dir_name),
};
Self::with_spec(host, dir.join(install.config_file), dir)
}
fn with_spec(host: Host, settings_path: PathBuf, dir: PathBuf) -> Result<Self> {
let install = install_spec(host)?;
let blocks_on_failure: fn() -> bool =
if install.hooks.iter().any(|spec| spec.block_on_failure) {
claude_version::blocks_on_failure
} else {
|| false
};
Ok(Self {
host,
settings_path,
dir,
hooks: install.hooks,
layout: install.hook_file,
blocks_on_failure,
})
}
pub fn dir(&self) -> &Path {
&self.dir
}
pub fn extra_files(&self) -> Vec<PathBuf> {
let files = self
.host
.spec()
.install
.as_ref()
.map_or(&[][..], |i| i.extra_files);
files
.iter()
.map(|file| match file {
ExtraFile::InConfigDir(parts) => parts
.iter()
.fold(self.dir.clone(), |path, part| path.join(part)),
})
.collect()
}
pub fn host_present(&self) -> bool {
self.dir.is_dir()
}
pub fn install(&self, home: &Home, binary: &Path, dry_run: bool) -> Result<Outcome> {
hook_file::install(home, self, binary, dry_run)
}
pub fn render(&self, binary: &Path, current: &str) -> Result<Option<String>> {
hook_file::render(self, binary, current)
}
pub fn state(&self, binary: &std::path::Path) -> HookState {
hook_file::state(self, binary)
}
pub fn set_up(&self, binary: &std::path::Path) -> bool {
self.state(binary) == HookState::Installed
}
pub fn runs_alias(&self, binary: &Path) -> bool {
hook_file::runs_alias(self, binary)
}
pub fn remove(&self, root: &mut serde_json::Value) -> bool {
hook_file::remove(root, self)
}
pub fn runs_calls_when_hook_fails(&self) -> bool {
hook_file::lacks_on_failure(self)
}
}
pub fn detected() -> Result<Vec<(Host, PathBuf)>> {
let mut found = Vec::new();
for host in Host::EVERY {
let Some(detect) = &host.spec().detect else {
continue;
};
let dir = match env_dir(detect.dir_variable) {
Some(dir) => dir,
None => user_home()?.join(detect.dir_name),
};
if dir.is_dir() {
found.push((host, dir));
continue;
}
let path: Vec<PathBuf> = std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).collect())
.unwrap_or_default();
if let Some(binary) = crate::environment::find_in(&path, &[], detect.binary) {
found.push((host, binary));
}
}
Ok(found)
}
fn env_dir(name: &str) -> Option<PathBuf> {
std::env::var_os(name)
.filter(|v| !v.is_empty())
.map(PathBuf::from)
}