sync from monorepo @ 2452e92e

This commit is contained in:
2026-05-08 01:59:04 +02:00
commit b03dc15371
459 changed files with 129586 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
//! Platform-native configuration and data path resolution for Dirigent.
//!
//! Provides `DirigentPaths` for resolving config and data directories
//! across Linux, macOS, and Windows. On Linux/macOS, creates a symlink
//! from config_dir/data to data_dir for discoverability.
mod paths;
pub use paths::ConfigPathError;
pub use paths::DirigentPaths;
+282
View File
@@ -0,0 +1,282 @@
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigPathError {
#[error("Could not determine config directory for this platform")]
NoConfigDir,
#[error("Could not determine data directory for this platform")]
NoDataDir,
#[error("Failed to create directory {path}: {source}")]
CreateDir {
path: PathBuf,
source: std::io::Error,
},
#[error("Failed to create symlink from {from} to {to}: {source}")]
Symlink {
from: PathBuf,
to: PathBuf,
source: std::io::Error,
},
}
/// Platform-native paths for dirigent configuration and data.
///
/// Environment variable overrides (highest priority):
/// - `DIRIGENT_CONFIG_DIR` -- overrides the config directory
/// - `DIRIGENT_DATA_DIR` -- overrides the data directory
///
/// Platform-native defaults (fallback):
///
/// | Platform | Config Dir | Data Dir |
/// |----------|-----------|----------|
/// | Linux | `$XDG_CONFIG_HOME/dirigent/` | `$XDG_DATA_HOME/dirigent/` |
/// | macOS | `~/.config/dirigent/` | `~/Library/Application Support/dirigent/` |
/// | Windows | `%APPDATA%\dirigent\` | `%LOCALAPPDATA%\dirigent\` |
#[derive(Debug, Clone)]
pub struct DirigentPaths {
config_dir: PathBuf,
data_dir: PathBuf,
}
impl DirigentPaths {
/// Resolve platform-native paths.
pub fn resolve() -> Result<Self, ConfigPathError> {
let config_dir = Self::resolve_config_dir()?;
let data_dir = Self::resolve_data_dir()?;
Ok(Self {
config_dir,
data_dir,
})
}
/// Create directories and symlink if they don't exist.
pub fn ensure_dirs(&self) -> Result<(), ConfigPathError> {
std::fs::create_dir_all(&self.config_dir).map_err(|e| ConfigPathError::CreateDir {
path: self.config_dir.clone(),
source: e,
})?;
std::fs::create_dir_all(&self.data_dir).map_err(|e| ConfigPathError::CreateDir {
path: self.data_dir.clone(),
source: e,
})?;
let noproject = self.noproject_home_dir();
std::fs::create_dir_all(&noproject).map_err(|e| ConfigPathError::CreateDir {
path: noproject,
source: e,
})?;
// Create symlink on Linux/macOS: config_dir/data -> data_dir
#[cfg(unix)]
{
let symlink_path = self.config_dir.join("data");
if symlink_path.symlink_metadata().is_err() {
std::os::unix::fs::symlink(&self.data_dir, &symlink_path).map_err(|e| {
ConfigPathError::Symlink {
from: symlink_path,
to: self.data_dir.clone(),
source: e,
}
})?;
}
}
Ok(())
}
/// Where `dirigent.toml` lives.
pub fn config_dir(&self) -> &Path {
&self.config_dir
}
/// Where archives, projects, caches live.
pub fn data_dir(&self) -> &Path {
&self.data_dir
}
/// Archive storage directory.
pub fn archive_dir(&self) -> PathBuf {
self.data_dir.join("archives")
}
/// Project storage directory.
pub fn projects_dir(&self) -> PathBuf {
self.data_dir.join("projects")
}
/// Default working directory for connectors when no project is active.
/// Lives under the data directory alongside other runtime artifacts.
pub fn noproject_home_dir(&self) -> PathBuf {
self.data_dir.join("noproject_home")
}
/// Task output storage directory.
pub fn tasks_dir(&self) -> PathBuf {
self.data_dir.join("tasks")
}
/// Log storage directory.
pub fn logs_dir(&self) -> PathBuf {
self.data_dir.join("logs")
}
/// Main config file path.
pub fn config_file(&self) -> PathBuf {
self.config_dir.join("dirigent.toml")
}
fn resolve_config_dir() -> Result<PathBuf, ConfigPathError> {
// Environment variable override (highest priority)
if let Ok(dir) = std::env::var("DIRIGENT_CONFIG_DIR") {
if !dir.trim().is_empty() {
// Absolutize relative paths against CWD to prevent nested resolution
// when subprocesses run from different working directories.
let path = PathBuf::from(&dir);
if path.is_relative() {
return Ok(std::env::current_dir()
.map_err(|_| ConfigPathError::NoConfigDir)?
.join(path));
}
return Ok(path);
}
}
// Platform-native fallback
#[cfg(target_os = "macos")]
{
dirs::home_dir()
.map(|d| d.join(".config").join("dirigent"))
.ok_or(ConfigPathError::NoConfigDir)
}
#[cfg(not(target_os = "macos"))]
{
dirs::config_dir()
.map(|d| d.join("dirigent"))
.ok_or(ConfigPathError::NoConfigDir)
}
}
fn resolve_data_dir() -> Result<PathBuf, ConfigPathError> {
// Environment variable override (highest priority)
if let Ok(dir) = std::env::var("DIRIGENT_DATA_DIR") {
if !dir.trim().is_empty() {
// Absolutize relative paths against CWD to prevent nested resolution
// when subprocesses run from different working directories.
let path = PathBuf::from(&dir);
if path.is_relative() {
return Ok(std::env::current_dir()
.map_err(|_| ConfigPathError::NoDataDir)?
.join(path));
}
return Ok(path);
}
}
// Platform-native fallback
#[cfg(target_os = "windows")]
{
dirs::data_local_dir()
.map(|d| d.join("dirigent"))
.ok_or(ConfigPathError::NoDataDir)
}
#[cfg(not(target_os = "windows"))]
{
dirs::data_dir()
.map(|d| d.join("dirigent"))
.ok_or(ConfigPathError::NoDataDir)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_returns_paths() {
let paths = DirigentPaths::resolve().expect("should resolve paths");
assert!(paths.config_dir().to_string_lossy().contains("dirigent"));
assert!(paths.data_dir().to_string_lossy().contains("dirigent"));
}
#[test]
fn test_config_file_path() {
let paths = DirigentPaths::resolve().unwrap();
let config = paths.config_file();
assert!(config.to_string_lossy().ends_with("dirigent.toml"));
}
#[test]
fn test_archive_dir() {
let paths = DirigentPaths::resolve().unwrap();
let archive = paths.archive_dir();
assert!(archive.to_string_lossy().ends_with("archives"));
}
#[test]
fn test_projects_dir() {
let paths = DirigentPaths::resolve().unwrap();
let projects = paths.projects_dir();
assert!(projects.to_string_lossy().ends_with("projects"));
}
#[test]
fn test_config_file_under_config_dir() {
let paths = DirigentPaths::resolve().unwrap();
assert!(paths.config_file().starts_with(paths.config_dir()));
}
#[test]
fn test_subdirs_under_data_dir() {
let paths = DirigentPaths::resolve().unwrap();
assert!(paths.archive_dir().starts_with(paths.data_dir()));
assert!(paths.projects_dir().starts_with(paths.data_dir()));
assert!(paths.logs_dir().starts_with(paths.data_dir()));
}
use serial_test::serial;
#[test]
#[serial]
fn test_config_dir_override_from_env() {
let tmp = std::env::temp_dir().join("dirigent_test_config_override");
unsafe { std::env::set_var("DIRIGENT_CONFIG_DIR", &tmp) };
let paths = DirigentPaths::resolve().expect("should resolve with env override");
assert_eq!(paths.config_dir(), tmp.as_path());
unsafe { std::env::remove_var("DIRIGENT_CONFIG_DIR") };
}
#[test]
#[serial]
fn test_data_dir_override_from_env() {
let tmp = std::env::temp_dir().join("dirigent_test_data_override");
unsafe { std::env::set_var("DIRIGENT_DATA_DIR", &tmp) };
let paths = DirigentPaths::resolve().expect("should resolve with env override");
assert_eq!(paths.data_dir(), tmp.as_path());
unsafe { std::env::remove_var("DIRIGENT_DATA_DIR") };
}
#[test]
fn test_noproject_home_under_data_dir() {
let paths = DirigentPaths::resolve().unwrap();
assert!(paths.noproject_home_dir().starts_with(paths.data_dir()));
}
#[test]
#[serial]
fn test_both_dir_overrides_from_env() {
let tmp_config = std::env::temp_dir().join("dirigent_test_both_config");
let tmp_data = std::env::temp_dir().join("dirigent_test_both_data");
unsafe { std::env::set_var("DIRIGENT_CONFIG_DIR", &tmp_config) };
unsafe { std::env::set_var("DIRIGENT_DATA_DIR", &tmp_data) };
let paths = DirigentPaths::resolve().expect("should resolve with both overrides");
assert_eq!(paths.config_dir(), tmp_config.as_path());
assert_eq!(paths.data_dir(), tmp_data.as_path());
assert!(paths.archive_dir().starts_with(&tmp_data));
assert!(paths.projects_dir().starts_with(&tmp_data));
unsafe { std::env::remove_var("DIRIGENT_CONFIG_DIR") };
unsafe { std::env::remove_var("DIRIGENT_DATA_DIR") };
}
}