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 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-04 23:36:32 -05:00
parent 15da1c04b9
commit ba90d1e90e
4 changed files with 43 additions and 9 deletions

View file

@ -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<usize>,
/// Path to server config file (default: ~/.arc/server.toml)
#[arg(long)]
pub config: Option<PathBuf>,
}
/// 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");

View file

@ -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<ServerConfig> {
/// 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<ServerConfig> {
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());
}
}

View file

@ -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(),

View file

@ -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?;
}