diff --git a/lib/crates/arc-cli/src/doctor.rs b/lib/crates/arc-cli/src/doctor.rs index eef0afed2..19b91be2a 100644 --- a/lib/crates/arc-cli/src/doctor.rs +++ b/lib/crates/arc-cli/src/doctor.rs @@ -200,9 +200,9 @@ pub fn check_config(path: Option) -> CheckResult { status: CheckStatus::Warning, summary: "no config file found".to_string(), details: vec![CheckDetail { - text: "Create ~/.arc/server.toml to configure Arc".to_string(), + text: "Create ~/.arc/cli.toml to configure Arc".to_string(), }], - remediation: Some("Create ~/.arc/server.toml".to_string()), + remediation: Some("Create ~/.arc/cli.toml".to_string()), }, } } @@ -989,7 +989,9 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { let styles = Styles::detect_stdout(); // Gather state - let config_path = dirs::home_dir().map(|h| h.join(".arc").join("server.toml")); + let cli_config = arc_config::cli::load_cli_config(None).unwrap_or_default(); + + let config_path = dirs::home_dir().map(|h| h.join(".arc").join("cli.toml")); let config_exists = config_path.as_ref().is_some_and(|p| p.exists()); let llm_statuses: Vec<(Provider, bool)> = Provider::ALL @@ -1001,6 +1003,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { let daytona_configured = std::env::var("DAYTONA_API_KEY").is_ok(); + #[cfg(feature = "server")] let server_config = arc_config::server::load_server_config(None).unwrap_or_default(); #[cfg(feature = "server")] @@ -1016,7 +1019,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { allowed_usernames_count: server_config.web.auth.allowed_usernames.len(), }; - let git_app_id = server_config.git.app_id.clone(); + let git_app_id = cli_config.git.as_ref().and_then(|g| g.app_id.clone()); let private_key_raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok(); let sign_result = match (&git_app_id, &private_key_raw) { (Some(app_id), Some(raw)) => { @@ -1043,7 +1046,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 { }; let github_status = GithubAppStatus { app_id: git_app_id, - slug: server_config.git.slug.clone(), + slug: cli_config.git.as_ref().and_then(|g| g.slug.clone()), private_key_set: private_key_raw.is_some(), sign_result, #[cfg(feature = "server")] @@ -1219,9 +1222,9 @@ mod tests { #[test] fn check_config_pass_with_path() { - let result = check_config(Some(PathBuf::from("/home/user/.arc/server.toml"))); + let result = check_config(Some(PathBuf::from("/home/user/.arc/cli.toml"))); assert_eq!(result.status, CheckStatus::Pass); - assert!(result.summary.contains(".arc/server.toml")); + assert!(result.summary.contains(".arc/cli.toml")); } #[test] diff --git a/lib/crates/arc-cli/src/main.rs b/lib/crates/arc-cli/src/main.rs index 03ba5fde0..5c9df9533 100644 --- a/lib/crates/arc-cli/src/main.rs +++ b/lib/crates/arc-cli/src/main.rs @@ -127,10 +127,8 @@ enum LlmCommand { Chat(arc_llm::cli::ChatArgs), } -fn build_github_app_credentials( - config: &arc_config::server::ServerConfig, -) -> Option { - let app_id = config.git.app_id.as_ref()?; +fn build_github_app_credentials(app_id: Option<&str>) -> Option { + let app_id = app_id?; let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?; let private_key_pem = if raw.starts_with("-----") { raw @@ -140,7 +138,7 @@ fn build_github_app_credentials( String::from_utf8(pem_bytes).ok()? }; Some(arc_github::GitHubAppCredentials { - app_id: app_id.clone(), + app_id: app_id.to_string(), private_key_pem, }) } @@ -231,7 +229,7 @@ async fn main_inner() -> Result<()> { match cli.command { Command::Llm { command } => { let cli_config = cli_config::load_cli_config(None)?; - let llm_defaults = cli_config.llm.as_ref(); + let llm_defaults = cli_config.run_defaults.llm.as_ref(); match command { LlmCommand::Prompt(mut args) => { if args.model.is_none() { @@ -324,29 +322,21 @@ async fn main_inner() -> Result<()> { Command::Run(mut args) => { let styles: &'static arc_util::terminal::Styles = Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr())); - let server_config = arc_config::server::load_server_config(None)?; let cli_config = cli_config::load_cli_config(None)?; args.verbose = args.verbose || cli_config.verbose; - let github_app = build_github_app_credentials(&server_config); + let github_app = build_github_app_credentials( + cli_config.git.as_ref().and_then(|g| g.app_id.as_deref()), + ); let cli_author = cli_config.git.as_ref().map(|g| &g.author); let git_author = arc_workflows::git::GitAuthor::from_options( - cli_author - .and_then(|a| a.name.clone()) - .or_else(|| server_config.git.author.name.clone()), - cli_author - .and_then(|a| a.email.clone()) - .or_else(|| server_config.git.author.email.clone()), + cli_author.and_then(|a| a.name.clone()), + cli_author.and_then(|a| a.email.clone()), ); - let mut run_defaults = server_config.run_defaults; - if cli_config.pull_request.is_some() { - run_defaults.pull_request = cli_config.pull_request; - } - arc_workflows::cli::run::run_command( args, - run_defaults, + cli_config.run_defaults, styles, github_app, git_author, @@ -411,8 +401,10 @@ async fn main_inner() -> Result<()> { } Command::Pr { command } => match command { PrCommand::Create(args) => { - let server_config = arc_config::server::load_server_config(None)?; - let github_app = build_github_app_credentials(&server_config); + let cli_config = cli_config::load_cli_config(None)?; + let github_app = build_github_app_credentials( + cli_config.git.as_ref().and_then(|g| g.app_id.as_deref()), + ); arc_workflows::cli::pr::pr_create_command(args, github_app).await?; } }, diff --git a/lib/crates/arc-config/src/cli.rs b/lib/crates/arc-config/src/cli.rs index 1f13984c5..a4c8a796d 100644 --- a/lib/crates/arc-config/src/cli.rs +++ b/lib/crates/arc-config/src/cli.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use arc_agent::cli::{OutputFormat, PermissionLevel}; use arc_mcp::config::{McpServerConfig, McpTransport}; -use arc_workflows::cli::run_config::PullRequestConfig; +use arc_workflows::cli::run_config::RunDefaults; use serde::Deserialize; use tracing::debug; @@ -36,13 +36,10 @@ pub struct ExecDefaults { pub output_format: Option, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -pub struct LlmDefaults { - pub model: Option, -} - #[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct CliGitConfig { + pub app_id: Option, + pub slug: Option, #[serde(default)] pub author: crate::server::GitAuthorConfig, } @@ -73,13 +70,13 @@ pub struct CliConfig { pub mode: Option, pub server: Option, pub exec: Option, - pub llm: Option, pub git: Option, #[serde(default)] pub verbose: bool, #[serde(default)] pub log: crate::server::LogConfig, - pub pull_request: Option, + #[serde(flatten)] + pub run_defaults: RunDefaults, #[serde(default)] pub mcp_servers: HashMap, } @@ -134,7 +131,7 @@ model = "claude-sonnet-4-5" assert_eq!(exec.model.as_deref(), Some("claude-opus-4-6")); assert_eq!(exec.permissions, Some(PermissionLevel::ReadWrite)); assert_eq!(exec.output_format, Some(OutputFormat::Text)); - let llm = config.llm.unwrap(); + let llm = config.run_defaults.llm.unwrap(); assert_eq!(llm.model.as_deref(), Some("claude-sonnet-4-5")); } @@ -150,7 +147,7 @@ provider = "openai" assert_eq!(exec.model, None); assert_eq!(exec.permissions, None); assert_eq!(exec.output_format, None); - assert_eq!(config.llm, None); + assert_eq!(config.run_defaults.llm, None); } #[test] @@ -272,14 +269,56 @@ email = "me@local" enabled = true "#; let config: CliConfig = toml::from_str(toml).unwrap(); - let pr = config.pull_request.unwrap(); + let pr = config.run_defaults.pull_request.unwrap(); assert!(pr.enabled); } #[test] fn parse_pull_request_absent() { let config: CliConfig = toml::from_str("").unwrap(); - assert_eq!(config.pull_request, None); + assert_eq!(config.run_defaults.pull_request, None); + } + + #[test] + fn parse_git_config_with_app_id() { + let toml = r#" +[git] +app_id = "12345" +slug = "my-app" + +[git.author] +name = "arc-bot" +email = "arc@test.com" +"#; + let config: CliConfig = toml::from_str(toml).unwrap(); + let git = config.git.unwrap(); + assert_eq!(git.app_id.as_deref(), Some("12345")); + assert_eq!(git.slug.as_deref(), Some("my-app")); + assert_eq!(git.author.name.as_deref(), Some("arc-bot")); + } + + #[test] + fn parse_llm_with_provider_and_fallbacks() { + let toml = r#" +[llm] +model = "claude-sonnet-4-5" +provider = "anthropic" +"#; + let config: CliConfig = toml::from_str(toml).unwrap(); + let llm = config.run_defaults.llm.unwrap(); + assert_eq!(llm.model.as_deref(), Some("claude-sonnet-4-5")); + assert_eq!(llm.provider.as_deref(), Some("anthropic")); + } + + #[test] + fn parse_sandbox_config() { + let toml = r#" +[sandbox] +provider = "daytona" +"#; + let config: CliConfig = toml::from_str(toml).unwrap(); + let sandbox = config.run_defaults.sandbox.unwrap(); + assert_eq!(sandbox.provider.as_deref(), Some("daytona")); } #[test]