refactor(home): move Home to fabro-util so all crates can share it

Home lived in fabro-config, which meant fabro-types (a dependency of
fabro-config) could not use it — forcing Settings::storage_dir() to
duplicate the FABRO_HOME / dirs::home_dir() fallback logic. Moving Home
to the leaf crate fabro-util breaks this layering constraint and lets
Settings::storage_dir() delegate to Home::from_env().storage_dir().

Also adds stable accessors: storage_dir, socket_path, workflows_dir,
logs_dir, tmp_dir. fabro-config re-exports Home for API compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-06 13:08:35 -04:00
parent cd392b29c7
commit c8af7e0638
No known key found for this signature in database
6 changed files with 128 additions and 89 deletions

1
Cargo.lock generated
View file

@ -2014,6 +2014,7 @@ dependencies = [
"clap",
"dirs",
"fabro-macros",
"fabro-util",
"hex",
"serde",
"serde_json",

View file

@ -1,77 +1 @@
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Home {
root: PathBuf,
}
impl Home {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
#[must_use]
pub fn from_env() -> Self {
if let Some(root) = std::env::var_os("FABRO_HOME") {
return Self::new(root);
}
let root =
dirs::home_dir().map_or_else(|| PathBuf::from(".fabro"), |home| home.join(".fabro"));
Self::new(root)
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn user_config(&self) -> PathBuf {
self.root.join("settings.toml")
}
#[must_use]
pub fn server_config(&self) -> PathBuf {
self.root.join("settings.toml")
}
#[must_use]
pub fn certs_dir(&self) -> PathBuf {
self.root.join("certs")
}
#[must_use]
pub fn skills_dir(&self) -> PathBuf {
self.root.join("skills")
}
}
#[cfg(test)]
mod tests {
use super::Home;
#[test]
fn accessors_are_relative_to_root() {
let home = Home::new("/tmp/fabro-home");
assert_eq!(home.root(), std::path::Path::new("/tmp/fabro-home"));
assert_eq!(
home.user_config(),
std::path::Path::new("/tmp/fabro-home/settings.toml")
);
assert_eq!(
home.server_config(),
std::path::Path::new("/tmp/fabro-home/settings.toml")
);
assert_eq!(
home.certs_dir(),
std::path::Path::new("/tmp/fabro-home/certs")
);
assert_eq!(
home.skills_dir(),
std::path::Path::new("/tmp/fabro-home/skills")
);
}
}
pub use fabro_util::Home;

View file

@ -21,6 +21,7 @@ chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, optional = true }
dirs.workspace = true
fabro-macros = { path = "../fabro-macros" }
fabro-util = { path = "../fabro-util" }
hex.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -181,15 +181,9 @@ impl Settings {
}
pub fn storage_dir(&self) -> PathBuf {
self.storage_dir.clone().unwrap_or_else(|| {
std::env::var_os("FABRO_HOME")
.map(|root| PathBuf::from(root).join("storage"))
.or_else(|| dirs::home_dir().map(|home| home.join(".fabro")))
.map_or_else(
|| PathBuf::from(".fabro/storage"),
|root| root.join("storage"),
)
})
self.storage_dir
.clone()
.unwrap_or_else(|| fabro_util::Home::from_env().storage_dir())
}
}
@ -199,11 +193,9 @@ mod tests {
#[test]
fn storage_dir_defaults_to_home_storage_subdir() {
let home = dirs::home_dir().expect("home directory should be available for tests");
assert_eq!(
Settings::default().storage_dir(),
home.join(".fabro/storage")
fabro_util::Home::from_env().storage_dir()
);
}
}

View file

@ -0,0 +1,119 @@
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Home {
root: PathBuf,
}
impl Home {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
#[must_use]
pub fn from_env() -> Self {
if let Some(root) = std::env::var_os("FABRO_HOME") {
return Self::new(root);
}
let root =
dirs::home_dir().map_or_else(|| PathBuf::from(".fabro"), |home| home.join(".fabro"));
Self::new(root)
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn user_config(&self) -> PathBuf {
self.root.join("settings.toml")
}
#[must_use]
pub fn server_config(&self) -> PathBuf {
self.root.join("settings.toml")
}
#[must_use]
pub fn certs_dir(&self) -> PathBuf {
self.root.join("certs")
}
#[must_use]
pub fn skills_dir(&self) -> PathBuf {
self.root.join("skills")
}
#[must_use]
pub fn storage_dir(&self) -> PathBuf {
self.root.join("storage")
}
#[must_use]
pub fn socket_path(&self) -> PathBuf {
self.root.join("fabro.sock")
}
#[must_use]
pub fn workflows_dir(&self) -> PathBuf {
self.root.join("workflows")
}
#[must_use]
pub fn logs_dir(&self) -> PathBuf {
self.root.join("logs")
}
#[must_use]
pub fn tmp_dir(&self) -> PathBuf {
self.root.join("tmp")
}
}
#[cfg(test)]
mod tests {
use super::Home;
#[test]
fn accessors_are_relative_to_root() {
let home = Home::new("/tmp/fabro-home");
assert_eq!(home.root(), std::path::Path::new("/tmp/fabro-home"));
assert_eq!(
home.user_config(),
std::path::Path::new("/tmp/fabro-home/settings.toml")
);
assert_eq!(
home.server_config(),
std::path::Path::new("/tmp/fabro-home/settings.toml")
);
assert_eq!(
home.certs_dir(),
std::path::Path::new("/tmp/fabro-home/certs")
);
assert_eq!(
home.skills_dir(),
std::path::Path::new("/tmp/fabro-home/skills")
);
assert_eq!(
home.storage_dir(),
std::path::Path::new("/tmp/fabro-home/storage")
);
assert_eq!(
home.socket_path(),
std::path::Path::new("/tmp/fabro-home/fabro.sock")
);
assert_eq!(
home.workflows_dir(),
std::path::Path::new("/tmp/fabro-home/workflows")
);
assert_eq!(
home.logs_dir(),
std::path::Path::new("/tmp/fabro-home/logs")
);
assert_eq!(home.tmp_dir(), std::path::Path::new("/tmp/fabro-home/tmp"));
}
}

View file

@ -1,6 +1,7 @@
pub mod backoff;
pub mod check_report;
pub mod env;
pub mod home;
pub mod json;
pub mod path;
pub mod printer;
@ -13,4 +14,5 @@ pub mod warnings;
#[doc(hidden)]
pub use console;
pub use home::Home;
pub use warnings::WARNINGS;