use std::path::PathBuf;
use crate::bootstrap::ironclaw_base_dir;
use crate::cli::fmt;
use crate::settings::Settings;
fn load_settings() -> Settings {
load_settings_from(&Settings::default_path(), &Settings::default_toml_path())
}
fn load_settings_from(json_path: &std::path::Path, toml_path: &std::path::Path) -> Settings {
let mut settings = Settings::load_from(json_path);
match Settings::load_toml(toml_path) {
Ok(Some(toml_settings)) => {
settings.merge_from(&toml_settings);
}
Ok(None) => {} Err(e) => {
eprintln!("Warning: failed to parse {}: {}", toml_path.display(), e);
}
}
settings
}
pub async fn run_status_command() -> anyhow::Result<()> {
let settings = load_settings();
println!();
println!(" {}IronClaw Status{}", fmt::bold(), fmt::reset());
println!();
println!(
"{}",
fmt::kv_line(
"Version",
&format!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
12,
)
);
let db_backend = std::env::var("DATABASE_BACKEND")
.ok()
.unwrap_or_else(|| "postgres".to_string());
let db_value = match db_backend.as_str() {
"libsql" | "turso" | "sqlite" => {
let path = std::env::var("LIBSQL_PATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| crate::config::default_libsql_path());
if path.exists() {
let turso = if std::env::var("LIBSQL_URL").is_ok() {
" + Turso sync"
} else {
""
};
format!("libSQL ({}{})", path.display(), turso)
} else {
format!("libSQL (file missing: {})", path.display())
}
}
_ => {
if std::env::var("DATABASE_URL").is_ok() {
match check_database().await {
Ok(()) => "connected (PostgreSQL)".to_string(),
Err(e) => format!("error ({})", e),
}
} else {
"not configured".to_string()
}
}
};
println!("{}", fmt::kv_line("Database", &db_value, 12));
let session_path = crate::config::llm::default_session_path();
let session_value = if session_path.exists() {
format!("found ({})", session_path.display())
} else {
"not found (run `ironclaw onboard`)".to_string()
};
println!("{}", fmt::kv_line("Session", &session_value, 12));
let secrets_value = if std::env::var("SECRETS_MASTER_KEY").is_ok() {
"configured (env)".to_string()
} else {
"env not set (keychain may be configured)".to_string()
};
println!("{}", fmt::kv_line("Secrets", &secrets_value, 12));
let emb_enabled = settings.embeddings.enabled
|| std::env::var("OPENAI_API_KEY").is_ok()
|| std::env::var("EMBEDDING_ENABLED")
.map(|v| v == "true")
.unwrap_or(false);
let emb_value = if emb_enabled {
format!(
"enabled (provider: {}, model: {})",
settings.embeddings.provider, settings.embeddings.model
)
} else {
"disabled".to_string()
};
println!("{}", fmt::kv_line("Embeddings", &emb_value, 12));
let tools_dir = settings
.wasm
.tools_dir
.clone()
.unwrap_or_else(default_tools_dir);
let tools_value = if tools_dir.exists() {
let count = count_wasm_files(&tools_dir);
format!("{} installed ({})", count, tools_dir.display())
} else {
format!("directory not found ({})", tools_dir.display())
};
println!("{}", fmt::kv_line("WASM Tools", &tools_value, 12));
let channels_dir = settings
.channels
.wasm_channels_dir
.clone()
.unwrap_or_else(default_channels_dir);
let mut channel_info = vec!["cli".to_string()];
if settings.channels.http_enabled {
channel_info.push(format!(
"http:{}",
settings.channels.http_port.unwrap_or(3000)
));
}
if channels_dir.exists() {
let wasm_count = count_wasm_files(&channels_dir);
if wasm_count > 0 {
channel_info.push(format!("{} wasm", wasm_count));
}
}
println!("{}", fmt::kv_line("Channels", &channel_info.join(", "), 12));
let hb_enabled = settings.heartbeat.enabled
|| std::env::var("HEARTBEAT_ENABLED")
.map(|v| v == "true")
.unwrap_or(false);
let hb_value = if hb_enabled {
format!("enabled (interval: {}s)", settings.heartbeat.interval_secs)
} else {
"disabled".to_string()
};
println!("{}", fmt::kv_line("Heartbeat", &hb_value, 12));
let mcp_value = match crate::tools::mcp::config::load_mcp_servers().await {
Ok(servers) => {
let enabled = servers.servers.iter().filter(|s| s.enabled).count();
let total = servers.servers.len();
format!("{} enabled / {} configured", enabled, total)
}
Err(_) => "none configured".to_string(),
};
println!("{}", fmt::kv_line("MCP Servers", &mcp_value, 12));
println!();
println!(
"{}",
fmt::kv_line(
"Config",
&crate::bootstrap::ironclaw_env_path().display().to_string(),
12,
)
);
Ok(())
}
#[cfg(feature = "postgres")]
async fn check_database() -> anyhow::Result<()> {
let url = std::env::var("DATABASE_URL").map_err(|_| anyhow::anyhow!("DATABASE_URL not set"))?;
let config: deadpool_postgres::Config = deadpool_postgres::Config {
url: Some(url),
..Default::default()
};
let pool = crate::db::tls::create_pool(&config, crate::config::SslMode::from_env())
.map_err(|e| anyhow::anyhow!("pool error: {}", e))?;
let client = tokio::time::timeout(std::time::Duration::from_secs(5), pool.get())
.await
.map_err(|_| anyhow::anyhow!("timeout"))?
.map_err(|e| anyhow::anyhow!("{}", e))?;
client
.execute("SELECT 1", &[])
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(())
}
#[cfg(not(feature = "postgres"))]
async fn check_database() -> anyhow::Result<()> {
Ok(())
}
fn count_wasm_files(dir: &std::path::Path) -> usize {
std::fs::read_dir(dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "wasm"))
.count()
})
.unwrap_or(0)
}
fn default_tools_dir() -> PathBuf {
ironclaw_base_dir().join("tools")
}
fn default_channels_dir() -> PathBuf {
ironclaw_base_dir().join("channels")
}
#[cfg(test)]
mod tests {
use super::load_settings_from;
#[test]
fn reads_toml_heartbeat_enabled() {
let dir = tempfile::tempdir().expect("tempdir");
let json_path = dir.path().join("settings.json");
let toml_path = dir.path().join("config.toml");
std::fs::write(
&toml_path,
"[heartbeat]\nenabled = true\ninterval_secs = 600",
)
.expect("write toml");
let settings = load_settings_from(&json_path, &toml_path);
assert!(settings.heartbeat.enabled);
assert_eq!(settings.heartbeat.interval_secs, 600);
}
#[test]
fn defaults_without_config_files() {
let dir = tempfile::tempdir().expect("tempdir");
let settings = load_settings_from(
&dir.path().join("nonexistent.json"),
&dir.path().join("nonexistent.toml"),
);
assert!(!settings.heartbeat.enabled);
}
#[test]
fn reads_json_heartbeat_enabled() {
let dir = tempfile::tempdir().expect("tempdir");
let json_path = dir.path().join("settings.json");
let toml_path = dir.path().join("nonexistent.toml");
std::fs::write(
&json_path,
r#"{"heartbeat":{"enabled":true,"interval_secs":900}}"#,
)
.expect("write json");
let settings = load_settings_from(&json_path, &toml_path);
assert!(settings.heartbeat.enabled);
assert_eq!(settings.heartbeat.interval_secs, 900);
}
#[test]
fn toml_overlay_wins_over_json() {
let dir = tempfile::tempdir().expect("tempdir");
let json_path = dir.path().join("settings.json");
let toml_path = dir.path().join("config.toml");
std::fs::write(
&json_path,
r#"{"heartbeat":{"enabled":false,"interval_secs":100}}"#,
)
.expect("write json");
std::fs::write(
&toml_path,
"[heartbeat]\nenabled = true\ninterval_secs = 200",
)
.expect("write toml");
let settings = load_settings_from(&json_path, &toml_path);
assert!(settings.heartbeat.enabled);
assert_eq!(settings.heartbeat.interval_secs, 200);
}
#[test]
fn invalid_toml_falls_back_gracefully() {
let dir = tempfile::tempdir().expect("tempdir");
let json_path = dir.path().join("settings.json");
let toml_path = dir.path().join("config.toml");
std::fs::write(
&json_path,
r#"{"heartbeat":{"enabled":true,"interval_secs":500}}"#,
)
.expect("write json");
std::fs::write(&toml_path, "this is not valid toml [[[").expect("write bad toml");
let settings = load_settings_from(&json_path, &toml_path);
assert!(settings.heartbeat.enabled);
assert_eq!(settings.heartbeat.interval_secs, 500);
}
}