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 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-05 00:37:43 -05:00
parent d0c7ac342e
commit 40b707b077
3 changed files with 29 additions and 38 deletions

View file

@ -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<PermissionLevel>,
output_format: Option<OutputFormat>,
) {
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)]

View file

@ -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<String>,
pub model: Option<String>,
pub permissions: Option<String>,
pub output_format: Option<String>,
pub permissions: Option<PermissionLevel>,
pub output_format: Option<OutputFormat>,
}
#[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<AgentDefaults>,
#[serde(default)]
pub llm: Option<LlmDefaults>,
}
@ -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"));
}

View file

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