From 40b707b077cefed93276ef922b8e3b4c3be7ff92 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 5 Mar 2026 00:37:43 -0500 Subject: [PATCH] Use typed enums in cli.toml config and simplify defaults - Deserialize permissions/output_format as typed enums instead of strings so invalid values in cli.toml fail at parse time - Use Option::or/or_else combinators instead of if-is_none pattern - Load cli.toml only for agent/llm commands, not all CLI invocations - Standalone arc-agent binary calls apply_cli_defaults for single source of hardcoded defaults - Remove redundant #[serde(default)] on Option fields Co-Authored-By: Claude Opus 4.6 --- crates/arc-agent/src/cli.rs | 48 +++++++++++++------------------- crates/arc-cli/src/cli_config.rs | 11 ++++---- crates/arc-cli/src/main.rs | 8 +++--- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/crates/arc-agent/src/cli.rs b/crates/arc-agent/src/cli.rs index 2ddddfac6..3758e8fb8 100644 --- a/crates/arc-agent/src/cli.rs +++ b/crates/arc-agent/src/cli.rs @@ -57,13 +57,15 @@ struct Cli { args: AgentArgs, } -#[derive(Clone, Copy, Debug, ValueEnum)] +#[derive(Clone, Copy, Debug, PartialEq, ValueEnum, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum OutputFormat { Text, Json, } -#[derive(Clone, Copy, Debug, ValueEnum)] +#[derive(Clone, Copy, Debug, PartialEq, ValueEnum, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum PermissionLevel { ReadOnly, ReadWrite, @@ -76,32 +78,20 @@ impl AgentArgs { &mut self, provider: Option<&str>, model: Option<&str>, - permissions: Option<&str>, - output_format: Option<&str>, + permissions: Option, + output_format: Option, ) { - if self.provider.is_none() { - self.provider = Some( - provider - .map(String::from) - .unwrap_or_else(|| "anthropic".to_string()), - ); - } - if self.model.is_none() { - self.model = model.map(String::from); - } - if self.permissions.is_none() { - self.permissions = Some(match permissions { - Some("read-only") => PermissionLevel::ReadOnly, - Some("full") => PermissionLevel::Full, - _ => PermissionLevel::ReadWrite, - }); - } - if self.output_format.is_none() { - self.output_format = Some(match output_format { - Some("json") => OutputFormat::Json, - _ => OutputFormat::Text, - }); - } + self.provider = self + .provider + .take() + .or_else(|| provider.map(String::from)) + .or_else(|| Some("anthropic".to_string())); + self.model = self.model.take().or_else(|| model.map(String::from)); + self.permissions = self.permissions.or(permissions).or(Some(PermissionLevel::ReadWrite)); + self.output_format = self + .output_format + .or(output_format) + .or(Some(OutputFormat::Text)); } } @@ -627,7 +617,9 @@ pub async fn run_with_args(args: AgentArgs) -> anyhow::Result<()> { pub async fn run() -> anyhow::Result<()> { let _ = dotenvy::dotenv(); let cli = Cli::parse(); - run_with_args(cli.args).await + let mut args = cli.args; + args.apply_cli_defaults(None, None, None, None); + run_with_args(args).await } #[cfg(test)] diff --git a/crates/arc-cli/src/cli_config.rs b/crates/arc-cli/src/cli_config.rs index 5cee5392d..20338ba85 100644 --- a/crates/arc-cli/src/cli_config.rs +++ b/crates/arc-cli/src/cli_config.rs @@ -1,5 +1,6 @@ use std::path::Path; +use arc_agent::cli::{OutputFormat, PermissionLevel}; use serde::Deserialize; use tracing::debug; @@ -7,8 +8,8 @@ use tracing::debug; pub struct AgentDefaults { pub provider: Option, pub model: Option, - pub permissions: Option, - pub output_format: Option, + pub permissions: Option, + pub output_format: Option, } #[derive(Clone, Debug, Default, Deserialize, PartialEq)] @@ -18,9 +19,7 @@ pub struct LlmDefaults { #[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct CliConfig { - #[serde(default)] pub agent: Option, - #[serde(default)] pub llm: Option, } @@ -72,8 +71,8 @@ model = "claude-sonnet-4-5" let agent = config.agent.unwrap(); assert_eq!(agent.provider.as_deref(), Some("anthropic")); assert_eq!(agent.model.as_deref(), Some("claude-opus-4-6")); - assert_eq!(agent.permissions.as_deref(), Some("read-write")); - assert_eq!(agent.output_format.as_deref(), Some("text")); + assert_eq!(agent.permissions, Some(PermissionLevel::ReadWrite)); + assert_eq!(agent.output_format, Some(OutputFormat::Text)); let llm = config.llm.unwrap(); assert_eq!(llm.model.as_deref(), Some("claude-sonnet-4-5")); } diff --git a/crates/arc-cli/src/main.rs b/crates/arc-cli/src/main.rs index 59a9e0660..06c316356 100644 --- a/crates/arc-cli/src/main.rs +++ b/crates/arc-cli/src/main.rs @@ -93,8 +93,6 @@ async fn main() -> Result<()> { eprintln!("Warning: failed to initialize logging: {err:#}"); } - let cli_config = cli_config::load_cli_config(None)?; - let command_name = match &cli.command { Command::Llm { .. } => "llm", Command::Agent(_) => "agent", @@ -109,6 +107,7 @@ async fn main() -> Result<()> { match cli.command { Command::Llm { command } => { + let cli_config = cli_config::load_cli_config(None)?; let llm_defaults = cli_config.llm.as_ref(); match command { LlmCommand::Prompt(mut args) => { @@ -126,12 +125,13 @@ async fn main() -> Result<()> { } } Command::Agent(mut args) => { + let cli_config = cli_config::load_cli_config(None)?; let agent_defaults = cli_config.agent.as_ref(); args.apply_cli_defaults( agent_defaults.and_then(|a| a.provider.as_deref()), agent_defaults.and_then(|a| a.model.as_deref()), - agent_defaults.and_then(|a| a.permissions.as_deref()), - agent_defaults.and_then(|a| a.output_format.as_deref()), + agent_defaults.and_then(|a| a.permissions), + agent_defaults.and_then(|a| a.output_format), ); arc_agent::cli::run_with_args(args).await? }