feat: add IRONCLAW_BASE_DIR env var with LazyLock caching (#397)

This commit is contained in:
ibhagwan
2026-02-27 17:43:43 +04:00
committed by GitHub
parent a7c0be7f1b
commit c592a8f2de
26 changed files with 255 additions and 127 deletions
+181 -7
View File
@@ -7,13 +7,75 @@
//! File: `~/.ironclaw/.env` (standard dotenvy format)
use std::path::PathBuf;
use std::sync::LazyLock;
const IRONCLAW_BASE_DIR_ENV: &str = "IRONCLAW_BASE_DIR";
/// Lazily computed IronClaw base directory, cached for the lifetime of the process.
static IRONCLAW_BASE_DIR: LazyLock<PathBuf> = LazyLock::new(compute_ironclaw_base_dir);
/// Compute the IronClaw base directory from environment.
///
/// This is the underlying implementation used by both the public
/// `ironclaw_base_dir()` function (which caches the result) and tests
/// (which need to verify different configurations).
pub fn compute_ironclaw_base_dir() -> PathBuf {
std::env::var(IRONCLAW_BASE_DIR_ENV)
.map(PathBuf::from)
.map(|path| {
if path.as_os_str().is_empty() {
default_base_dir()
} else if !path.is_absolute() {
eprintln!(
"Warning: IRONCLAW_BASE_DIR is a relative path '{}', resolved against current directory",
path.display()
);
path
} else {
path
}
})
.unwrap_or_else(|_| default_base_dir())
}
/// Get the default IronClaw base directory (~/.ironclaw).
///
/// Logs a warning if the home directory cannot be determined and falls back to
/// the current directory.
fn default_base_dir() -> PathBuf {
if let Some(home) = dirs::home_dir() {
home.join(".ironclaw")
} else {
eprintln!("Warning: Could not determine home directory, using current directory");
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("/tmp"))
.join(".ironclaw")
}
}
/// Get the IronClaw base directory.
///
/// Override with `IRONCLAW_BASE_DIR` environment variable.
/// Defaults to `~/.ironclaw` (or `./.ironclaw` if home directory cannot be determined).
///
/// Thread-safe: the value is computed once and cached in a `LazyLock`.
///
/// # Environment Variable Behavior
/// - If `IRONCLAW_BASE_DIR` is set to a non-empty path, that path is used.
/// - If `IRONCLAW_BASE_DIR` is set to an empty string, it is treated as unset.
/// - If `IRONCLAW_BASE_DIR` contains null bytes, a warning is printed and the default is used.
/// - If the home directory cannot be determined, a warning is printed and the current directory is used.
///
/// # Returns
/// A `PathBuf` pointing to the base directory. The path is not validated
/// for existence.
pub fn ironclaw_base_dir() -> PathBuf {
IRONCLAW_BASE_DIR.clone()
}
/// Path to the IronClaw-specific `.env` file: `~/.ironclaw/.env`.
pub fn ironclaw_env_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join(".env")
ironclaw_base_dir().join(".env")
}
/// Load env vars from `~/.ironclaw/.env` (in addition to the standard `.env`).
@@ -200,9 +262,7 @@ pub async fn migrate_disk_to_db(
store: &dyn crate::db::Database,
user_id: &str,
) -> Result<(), MigrationError> {
let ironclaw_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw");
let ironclaw_dir = ironclaw_base_dir();
let legacy_settings_path = ironclaw_dir.join("settings.json");
if !legacy_settings_path.exists() {
@@ -336,8 +396,11 @@ pub enum MigrationError {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use tempfile::tempdir;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_save_and_load_database_url() {
let dir = tempdir().unwrap();
@@ -727,4 +790,115 @@ INJECTED="pwned"#;
assert_eq!(&found.unwrap().1, value, "{key} value mismatch");
}
}
#[test]
fn test_ironclaw_base_dir_default() {
// This test must run first (or in isolation) before the LazyLock is initialized.
// It verifies that when IRONCLAW_BASE_DIR is not set, the default path is used.
let _guard = ENV_MUTEX.lock().unwrap();
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
// Force re-evaluation by calling the computation function directly
let path = compute_ironclaw_base_dir();
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
assert_eq!(path, home.join(".ironclaw"));
if let Some(val) = old_val {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
}
}
#[test]
fn test_ironclaw_base_dir_env_override() {
// This test verifies that when IRONCLAW_BASE_DIR is set,
// the custom path is used. Must run before LazyLock is initialized.
let _guard = ENV_MUTEX.lock().unwrap();
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/custom/ironclaw/path") };
// Force re-evaluation by calling the computation function directly
let path = compute_ironclaw_base_dir();
assert_eq!(path, std::path::PathBuf::from("/custom/ironclaw/path"));
if let Some(val) = old_val {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
} else {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
}
}
#[test]
fn test_compute_base_dir_env_path_join() {
// Verifies that ironclaw_env_path correctly joins .env to the base dir.
// Uses compute_ironclaw_base_dir directly to avoid LazyLock caching.
let _guard = ENV_MUTEX.lock().unwrap();
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/my/custom/dir") };
// Test the path construction logic directly
let base_path = compute_ironclaw_base_dir();
let env_path = base_path.join(".env");
assert_eq!(env_path, std::path::PathBuf::from("/my/custom/dir/.env"));
if let Some(val) = old_val {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
} else {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
}
}
#[test]
fn test_ironclaw_base_dir_empty_env() {
// Verifies that empty IRONCLAW_BASE_DIR falls back to default.
let _guard = ENV_MUTEX.lock().unwrap();
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "") };
// Force re-evaluation by calling the computation function directly
let path = compute_ironclaw_base_dir();
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
assert_eq!(path, home.join(".ironclaw"));
if let Some(val) = old_val {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
} else {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
}
}
#[test]
fn test_ironclaw_base_dir_special_chars() {
// Verifies that paths with special characters are handled correctly.
let _guard = ENV_MUTEX.lock().unwrap();
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/tmp/test_with-special.chars") };
// Force re-evaluation by calling the computation function directly
let path = compute_ironclaw_base_dir();
assert_eq!(
path,
std::path::PathBuf::from("/tmp/test_with-special.chars")
);
if let Some(val) = old_val {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
} else {
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
}
}
}
+2 -4
View File
@@ -38,6 +38,7 @@ use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use crate::agent::truncate_for_preview;
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
use crate::error::ChannelError;
@@ -279,10 +280,7 @@ fn print_help() {
/// Get the history file path (~/.ironclaw/history).
fn history_path() -> std::path::PathBuf {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw")
.join("history")
ironclaw_base_dir().join("history")
}
#[async_trait]
+3 -6
View File
@@ -17,6 +17,7 @@ use serde::Deserialize;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
use crate::config::SignalConfig;
use crate::error::ChannelError;
@@ -557,9 +558,7 @@ impl SignalChannel {
/// - All paths are within ~/.ironclaw/ sandbox
fn validate_attachment_paths(paths: &[String]) -> Result<(), ChannelError> {
// Get the sandbox base directory (same as MessageTool uses)
let base_dir = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw");
let base_dir = ironclaw_base_dir();
for path in paths {
crate::tools::builtin::path_utils::validate_path(path, Some(&base_dir)).map_err(
@@ -2671,9 +2670,7 @@ mod tests {
use std::fs;
// Create test files in sandbox
let base_dir = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw");
let base_dir = crate::bootstrap::ironclaw_base_dir();
// Create sandbox directory if it doesn't exist (needed for CI)
let _ = fs::create_dir_all(&base_dir);
+2 -4
View File
@@ -11,6 +11,7 @@ use std::sync::Arc;
use tokio::fs;
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::wasm::capabilities::ChannelCapabilities;
use crate::channels::wasm::error::WasmChannelError;
use crate::channels::wasm::runtime::WasmChannelRuntime;
@@ -356,10 +357,7 @@ pub struct DiscoveredChannel {
/// Returns ~/.ironclaw/channels/
#[allow(dead_code)]
pub fn default_channels_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("channels")
ironclaw_base_dir().join("channels")
}
#[cfg(test)]
+2 -5
View File
@@ -6,6 +6,7 @@ use axum::{
response::{Html, IntoResponse},
};
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::web::types::*;
// --- Static file handlers ---
@@ -71,11 +72,7 @@ async fn serve_project_file(project_id: &str, path: &str) -> axum::response::Res
return (StatusCode::BAD_REQUEST, "Invalid project ID").into_response();
}
let base = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw")
.join("projects")
.join(project_id);
let base = ironclaw_base_dir().join("projects").join(project_id);
let file_path = base.join(path);
+2 -5
View File
@@ -26,6 +26,7 @@ use tower_http::set_header::SetResponseHeaderLayer;
use uuid::Uuid;
use crate::agent::SessionManager;
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::IncomingMessage;
use crate::channels::web::auth::{AuthState, auth_middleware};
use crate::channels::web::handlers::skills::{
@@ -1921,11 +1922,7 @@ async fn serve_project_file(project_id: &str, path: &str) -> axum::response::Res
return (StatusCode::BAD_REQUEST, "Invalid project ID").into_response();
}
let base = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw")
.join("projects")
.join(project_id);
let base = ironclaw_base_dir().join("projects").join(project_id);
let file_path = base.join(path);
+3 -3
View File
@@ -6,6 +6,8 @@
use std::path::PathBuf;
use crate::bootstrap::ironclaw_base_dir;
/// Run all diagnostic checks and print results.
pub async fn run_doctor_command() -> anyhow::Result<()> {
println!("IronClaw Doctor");
@@ -195,9 +197,7 @@ async fn try_pg_connect() -> Result<(), String> {
}
fn check_workspace_dir() -> CheckResult {
let dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw");
let dir = ironclaw_base_dir();
if dir.exists() {
if dir.is_dir() {
+3 -8
View File
@@ -5,6 +5,7 @@
use std::path::PathBuf;
use crate::bootstrap::ironclaw_base_dir;
use crate::settings::Settings;
/// Run the status command, printing system health info.
@@ -206,15 +207,9 @@ fn count_wasm_files(dir: &std::path::Path) -> usize {
}
fn default_tools_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("tools")
ironclaw_base_dir().join("tools")
}
fn default_channels_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("channels")
ironclaw_base_dir().join("channels")
}
+2 -3
View File
@@ -9,6 +9,7 @@ use std::sync::Arc;
use clap::Subcommand;
use tokio::fs;
use crate::bootstrap::ironclaw_base_dir;
use crate::config::Config;
#[allow(unused_imports)]
use crate::db::Database;
@@ -19,9 +20,7 @@ use crate::tools::wasm::{CapabilitiesFile, compute_binary_hash};
/// Default tools directory.
fn default_tools_dir() -> PathBuf {
dirs::home_dir()
.map(|h| h.join(".ironclaw").join("tools"))
.unwrap_or_else(|| PathBuf::from(".ironclaw/tools"))
ironclaw_base_dir().join("tools")
}
#[derive(Subcommand, Debug, Clone)]
+2 -4
View File
@@ -2,6 +2,7 @@ use std::path::PathBuf;
use secrecy::SecretString;
use crate::bootstrap::ironclaw_base_dir;
use crate::config::helpers::{optional_env, parse_bool_env, parse_optional_env};
use crate::error::ConfigError;
use crate::settings::Settings;
@@ -193,8 +194,5 @@ impl ChannelsConfig {
/// Get the default channels directory (~/.ironclaw/channels/).
fn default_channels_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("channels")
ironclaw_base_dir().join("channels")
}
+2 -4
View File
@@ -2,6 +2,7 @@ use std::path::PathBuf;
use secrecy::{ExposeSecret, SecretString};
use crate::bootstrap::ironclaw_base_dir;
use crate::config::helpers::{optional_env, parse_optional_env};
use crate::error::ConfigError;
@@ -123,8 +124,5 @@ impl DatabaseConfig {
/// Default libSQL database path (~/.ironclaw/ironclaw.db).
pub fn default_libsql_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("ironclaw.db")
ironclaw_base_dir().join("ironclaw.db")
}
+2 -3
View File
@@ -1,3 +1,4 @@
use crate::bootstrap::ironclaw_base_dir;
use crate::config::helpers::{parse_bool_env, parse_optional_env};
use crate::error::ConfigError;
@@ -41,9 +42,7 @@ impl HygieneConfig {
enabled: self.enabled,
retention_days: self.retention_days,
cadence_hours: self.cadence_hours,
state_dir: dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw"),
state_dir: ironclaw_base_dir(),
}
}
}
+2 -4
View File
@@ -2,6 +2,7 @@ use std::path::PathBuf;
use secrecy::SecretString;
use crate::bootstrap::ironclaw_base_dir;
use crate::config::helpers::{optional_env, parse_optional_env};
use crate::error::ConfigError;
use crate::settings::Settings;
@@ -373,10 +374,7 @@ fn parse_extra_headers(val: &str) -> Result<Vec<(String, String)>, ConfigError>
/// Get the default session file path (~/.ironclaw/session.json).
fn default_session_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("session.json")
ironclaw_base_dir().join("session.json")
}
#[cfg(test)]
+3 -8
View File
@@ -1,5 +1,6 @@
use std::path::PathBuf;
use crate::bootstrap::ironclaw_base_dir;
use crate::config::helpers::{optional_env, parse_bool_env, parse_optional_env};
use crate::error::ConfigError;
@@ -34,18 +35,12 @@ impl Default for SkillsConfig {
/// Get the default user skills directory (~/.ironclaw/skills/).
fn default_skills_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("skills")
ironclaw_base_dir().join("skills")
}
/// Get the default installed skills directory (~/.ironclaw/installed_skills/).
fn default_installed_skills_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("installed_skills")
ironclaw_base_dir().join("installed_skills")
}
impl SkillsConfig {
+2 -4
View File
@@ -1,6 +1,7 @@
use std::path::PathBuf;
use std::time::Duration;
use crate::bootstrap::ironclaw_base_dir;
use crate::config::helpers::{optional_env, parse_bool_env, parse_optional_env};
use crate::error::ConfigError;
@@ -39,10 +40,7 @@ impl Default for WasmConfig {
/// Get the default tools directory (~/.ironclaw/tools/).
fn default_tools_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("tools")
ironclaw_base_dir().join("tools")
}
impl WasmConfig {
+2 -4
View File
@@ -7,6 +7,7 @@
use std::path::PathBuf;
use std::sync::Arc;
use crate::bootstrap::ironclaw_base_dir;
use crate::cli::oauth_defaults::OAUTH_CALLBACK_PORT;
use chrono::{DateTime, Utc};
@@ -46,10 +47,7 @@ impl Default for SessionConfig {
/// Get the default session file path (~/.ironclaw/session.json).
pub fn default_session_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("session.json")
ironclaw_base_dir().join("session.json")
}
/// Manages NEAR AI session tokens with persistence and automatic renewal.
+10 -6
View File
@@ -11,6 +11,7 @@ use chrono::{DateTime, Utc};
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::bootstrap::ironclaw_base_dir;
use crate::error::OrchestratorError;
use crate::orchestrator::auth::{CredentialGrant, TokenStore};
use crate::sandbox::connect_docker;
@@ -158,11 +159,14 @@ fn validate_bind_mount_path(
),
})?;
let home = dirs::home_dir().ok_or_else(|| OrchestratorError::ContainerCreationFailed {
job_id,
reason: "could not determine home directory for path validation".to_string(),
})?;
let projects_base = home.join(".ironclaw").join("projects");
let projects_base = ironclaw_base_dir().join("projects");
if !projects_base.is_absolute() {
return Err(OrchestratorError::ContainerCreationFailed {
job_id,
reason: "base directory is not absolute; cannot safely validate bind mounts".into(),
});
}
// Ensure the base exists so canonicalize always succeeds.
std::fs::create_dir_all(&projects_base).map_err(|e| {
@@ -617,7 +621,7 @@ mod tests {
#[test]
fn test_validate_bind_mount_valid_path() {
let base = dirs::home_dir().unwrap().join(".ironclaw").join("projects");
let base = crate::bootstrap::compute_ironclaw_base_dir().join("projects");
std::fs::create_dir_all(&base).unwrap();
let test_dir = base.join("test_validate_bind");
+3 -3
View File
@@ -12,6 +12,8 @@ use fs4::FileExt;
use rand::Rng;
use serde::{Deserialize, Serialize};
use crate::bootstrap::ironclaw_base_dir;
const PAIRING_CODE_LENGTH: usize = 8;
const PAIRING_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
/// TTL for pending pairing requests (minutes, not hours — reduces brute-force window).
@@ -70,9 +72,7 @@ struct AllowFromStoreFile {
}
fn default_pairing_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
ironclaw_base_dir()
}
fn safe_channel_key(channel: &str) -> Result<String, PairingStoreError> {
+4 -3
View File
@@ -5,6 +5,7 @@ use std::path::{Component, Path, PathBuf};
use tokio::fs;
use crate::bootstrap::ironclaw_base_dir;
use crate::registry::catalog::RegistryError;
use crate::registry::manifest::{BundleDefinition, ExtensionManifest, ManifestKind};
@@ -179,11 +180,11 @@ impl RegistryInstaller {
/// Default installer using standard paths.
pub fn with_defaults(repo_root: PathBuf) -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let base_dir = ironclaw_base_dir();
Self {
repo_root,
tools_dir: home.join(".ironclaw").join("tools"),
channels_dir: home.join(".ironclaw").join("channels"),
tools_dir: base_dir.join("tools"),
channels_dir: base_dir.join("channels"),
}
}
+6 -5
View File
@@ -12,6 +12,8 @@ use std::process::Command;
use anyhow::{Context, Result, bail};
use crate::bootstrap::ironclaw_base_dir;
const SERVICE_LABEL: &str = "com.ironclaw.daemon";
const SYSTEMD_UNIT: &str = "ironclaw.service";
@@ -57,7 +59,7 @@ fn install_macos() -> Result<()> {
}
let exe = std::env::current_exe().context("failed to resolve current executable")?;
let logs_dir = ironclaw_logs_dir()?;
let logs_dir = ironclaw_logs_dir();
std::fs::create_dir_all(&logs_dir)?;
let stdout = logs_dir.join("daemon.stdout.log");
@@ -250,9 +252,8 @@ fn linux_unit_path() -> Result<PathBuf> {
.join(SYSTEMD_UNIT))
}
fn ironclaw_logs_dir() -> Result<PathBuf> {
let home = dirs::home_dir().context("could not find home directory")?;
Ok(home.join(".ironclaw").join("logs"))
fn ironclaw_logs_dir() -> PathBuf {
ironclaw_base_dir().join("logs")
}
// ── Shell helpers ───────────────────────────────────────────────
@@ -350,7 +351,7 @@ mod tests {
#[test]
fn logs_dir_under_ironclaw() {
let path = ironclaw_logs_dir().unwrap();
let path = ironclaw_logs_dir();
let s = path.to_string_lossy();
assert!(s.ends_with(".ironclaw/logs"), "unexpected path: {s}");
}
+4 -8
View File
@@ -7,6 +7,8 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::bootstrap::ironclaw_base_dir;
/// User settings persisted to disk.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Settings {
@@ -656,10 +658,7 @@ impl Settings {
/// Get the default settings file path (~/.ironclaw/settings.json).
pub fn default_path() -> std::path::PathBuf {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw")
.join("settings.json")
ironclaw_base_dir().join("settings.json")
}
/// Load settings from disk, returning default if not found.
@@ -677,10 +676,7 @@ impl Settings {
/// Default TOML config file path (~/.ironclaw/config.toml).
pub fn default_toml_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("config.toml")
ironclaw_base_dir().join("config.toml")
}
/// Load settings from a TOML file.
+5 -10
View File
@@ -20,6 +20,7 @@ use secrecy::{ExposeSecret, SecretString};
#[cfg(feature = "postgres")]
use tokio_postgres::NoTls;
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::wasm::{
ChannelCapabilitiesFile, available_channel_names, install_bundled_channel,
};
@@ -1465,9 +1466,7 @@ impl SetupWizard {
println!();
// Discover available WASM channels
let channels_dir = dirs::home_dir()
.ok_or_else(|| SetupError::Config("Could not determine home directory".into()))?
.join(".ironclaw/channels");
let channels_dir = ironclaw_base_dir().join("channels");
let mut discovered_channels = discover_wasm_channels(&channels_dir).await;
let installed_names: HashSet<String> = discovered_channels
@@ -1697,9 +1696,7 @@ impl SetupWizard {
println!();
// Check which tools are already installed
let tools_dir = dirs::home_dir()
.ok_or_else(|| SetupError::Config("Could not determine home directory".into()))?
.join(".ironclaw/tools");
let tools_dir = ironclaw_base_dir().join("tools");
let installed_tools = discover_installed_tools(&tools_dir).await;
@@ -1739,9 +1736,7 @@ impl SetupWizard {
let installer = crate::registry::installer::RegistryInstaller::new(
repo_root.to_path_buf(),
tools_dir.clone(),
dirs::home_dir()
.unwrap_or_default()
.join(".ironclaw/channels"),
ironclaw_base_dir().join("channels"),
);
let mut installed_count = 0;
@@ -2795,7 +2790,7 @@ async fn install_selected_registry_channels(
let installer = crate::registry::installer::RegistryInstaller::new(
repo_root.clone(),
dirs::home_dir().unwrap_or_default().join(".ironclaw/tools"),
ironclaw_base_dir().join("tools"),
channels_dir.to_path_buf(),
);
+2 -4
View File
@@ -14,6 +14,7 @@ use async_trait::async_trait;
use chrono::Utc;
use uuid::Uuid;
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::IncomingMessage;
use crate::channels::web::types::SseEvent;
use crate::context::{ContextManager, JobContext, JobState};
@@ -554,10 +555,7 @@ fn validate_env_var_name(name: &str) -> Result<(), ToolError> {
}
fn projects_base() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("projects")
ironclaw_base_dir().join("projects")
}
/// Resolve the project directory, creating it if it doesn't exist.
+2 -3
View File
@@ -8,6 +8,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::RwLock;
use crate::bootstrap::ironclaw_base_dir;
use crate::channels::{ChannelManager, OutgoingResponse};
use crate::context::JobContext;
use crate::tools::tool::{
@@ -27,9 +28,7 @@ pub struct MessageTool {
impl MessageTool {
pub fn new(channel_manager: Arc<ChannelManager>) -> Self {
let base_dir = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ironclaw");
let base_dir = ironclaw_base_dir();
Self {
channel_manager,
+2 -4
View File
@@ -9,6 +9,7 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::bootstrap::ironclaw_base_dir;
use crate::tools::tool::ToolError;
/// Configuration for connecting to a remote MCP server.
@@ -251,10 +252,7 @@ impl From<ConfigError> for ToolError {
/// Get the default MCP servers configuration path.
pub fn default_config_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw")
.join("mcp-servers.json")
ironclaw_base_dir().join("mcp-servers.json")
}
/// Load MCP server configurations from the default location.
+2 -5
View File
@@ -20,6 +20,7 @@ use std::path::PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::bootstrap::ironclaw_base_dir;
use crate::workspace::Workspace;
/// Configuration for workspace hygiene.
@@ -37,15 +38,11 @@ pub struct HygieneConfig {
impl Default for HygieneConfig {
fn default() -> Self {
let state_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ironclaw");
Self {
enabled: true,
retention_days: 30,
cadence_hours: 12,
state_dir,
state_dir: ironclaw_base_dir(),
}
}
}