From ba90d1e90ee8ff5ed1f94e5fa7b03360513fa0b1 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 4 Mar 2026 23:36:32 -0500 Subject: [PATCH] Add --config CLI arg to override server config path load_server_config() now accepts an optional explicit path. When provided, it reads from that path (erroring if missing) instead of the default ~/.arc/server.toml. The --config flag is wired through ServeArgs and the hot-reload polling loop. Co-Authored-By: Claude Opus 4.6 --- crates/arc-api/src/serve.rs | 11 +++++++-- crates/arc-api/src/server_config.rs | 37 +++++++++++++++++++++++++---- crates/arc-cli/src/doctor.rs | 2 +- crates/arc-cli/src/main.rs | 2 +- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/crates/arc-api/src/serve.rs b/crates/arc-api/src/serve.rs index b82c28d1b..185f4b167 100644 --- a/crates/arc-api/src/serve.rs +++ b/crates/arc-api/src/serve.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -50,6 +51,10 @@ pub struct ServeArgs { /// Maximum number of concurrent run executions #[arg(long)] pub max_concurrent_runs: Option, + + /// Path to server config file (default: ~/.arc/server.toml) + #[arg(long)] + pub config: Option, } /// Start the HTTP API server. @@ -82,7 +87,8 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: }; // Initialize data directory and SQLite database - let server_config = crate::server_config::load_server_config()?; + let config_path = args.config; + let server_config = crate::server_config::load_server_config(config_path.as_deref())?; let data_dir = crate::server_config::resolve_data_dir(&server_config); // Shared config for live reloading @@ -160,12 +166,13 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: // Spawn config polling task (skip in demo mode) if !args.demo { let config_for_poll = Arc::clone(&shared_config); + let config_path_for_poll = config_path.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(5)); interval.tick().await; // skip first immediate tick loop { interval.tick().await; - match crate::server_config::load_server_config() { + match crate::server_config::load_server_config(config_path_for_poll.as_deref()) { Ok(new_config) => { let changed = { let cfg = config_for_poll.read().expect("config lock poisoned"); diff --git a/crates/arc-api/src/server_config.rs b/crates/arc-api/src/server_config.rs index 785a0229c..5b672a174 100644 --- a/crates/arc-api/src/server_config.rs +++ b/crates/arc-api/src/server_config.rs @@ -1,7 +1,8 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use arc_workflows::cli::run_config::RunDefaults; use serde::Deserialize; +use tracing::debug; #[derive(Debug, Clone, Default, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] @@ -106,13 +107,22 @@ pub struct ServerConfig { pub run_defaults: RunDefaults, } -/// Load server config from `~/.arc/server.toml`, returning defaults if the file doesn't exist. -pub fn load_server_config() -> anyhow::Result { +/// Load server config from an explicit path or `~/.arc/server.toml`, returning defaults if the +/// default file doesn't exist. An explicit path that doesn't exist is an error. +pub fn load_server_config(path: Option<&Path>) -> anyhow::Result { + if let Some(explicit) = path { + debug!(path = %explicit.display(), "Loading server config from explicit path"); + let contents = std::fs::read_to_string(explicit)?; + return Ok(toml::from_str(&contents)?); + } + let Some(home) = dirs::home_dir() else { + debug!("No home directory found, using default server config"); return Ok(ServerConfig::default()); }; - let path = home.join(".arc").join("server.toml"); - match std::fs::read_to_string(&path) { + let default_path = home.join(".arc").join("server.toml"); + debug!(path = %default_path.display(), "Loading server config"); + match std::fs::read_to_string(&default_path) { Ok(contents) => Ok(toml::from_str(&contents)?), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(ServerConfig::default()), Err(e) => Err(e.into()), @@ -354,4 +364,21 @@ authentication_strategies = ["jwt"] ); assert!(config.api.tls.is_none()); } + + #[test] + fn load_server_config_from_explicit_path() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("custom.toml"); + std::fs::write(&path, r#"max_concurrent_runs = 42"#).unwrap(); + let config = load_server_config(Some(&path)).unwrap(); + assert_eq!(config.max_concurrent_runs, Some(42)); + } + + #[test] + fn load_server_config_explicit_path_missing_is_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("nonexistent.toml"); + let result = load_server_config(Some(&path)); + assert!(result.is_err()); + } } diff --git a/crates/arc-cli/src/doctor.rs b/crates/arc-cli/src/doctor.rs index 38fe45f5c..9216cd5f9 100644 --- a/crates/arc-cli/src/doctor.rs +++ b/crates/arc-cli/src/doctor.rs @@ -890,7 +890,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { let brave_key_set = std::env::var("BRAVE_SEARCH_API_KEY").is_ok(); - let server_config = arc_api::server_config::load_server_config().unwrap_or_default(); + let server_config = arc_api::server_config::load_server_config(None).unwrap_or_default(); let api_status = ApiStatus { base_url: server_config.api.base_url.clone(), diff --git a/crates/arc-cli/src/main.rs b/crates/arc-cli/src/main.rs index 301337ad1..89c02bf59 100644 --- a/crates/arc-cli/src/main.rs +++ b/crates/arc-cli/src/main.rs @@ -114,7 +114,7 @@ async fn main() -> Result<()> { RunCommand::Start(args) => { let styles: &'static arc_util::terminal::Styles = Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr())); - let server_config = arc_api::server_config::load_server_config()?; + let server_config = arc_api::server_config::load_server_config(None)?; arc_workflows::cli::run::run_command(args, server_config.run_defaults, styles) .await?; }