mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-09 22:33:37 +00:00
Make standalone CLI read only cli.toml, never server.toml
Move GitHub App credentials (app_id, slug) and run defaults (llm, sandbox, pull_request) into cli.toml so standalone commands (run, pr create, llm) no longer require server.toml. The server.toml loading is now limited to `arc serve` and server-mode doctor checks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
20b446d1e5
commit
c32e74fbb4
3 changed files with 75 additions and 41 deletions
|
|
@ -200,9 +200,9 @@ pub fn check_config(path: Option<PathBuf>) -> 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]
|
||||
|
|
|
|||
|
|
@ -127,10 +127,8 @@ enum LlmCommand {
|
|||
Chat(arc_llm::cli::ChatArgs),
|
||||
}
|
||||
|
||||
fn build_github_app_credentials(
|
||||
config: &arc_config::server::ServerConfig,
|
||||
) -> Option<arc_github::GitHubAppCredentials> {
|
||||
let app_id = config.git.app_id.as_ref()?;
|
||||
fn build_github_app_credentials(app_id: Option<&str>) -> Option<arc_github::GitHubAppCredentials> {
|
||||
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?;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<OutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
pub struct LlmDefaults {
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
pub struct CliGitConfig {
|
||||
pub app_id: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
#[serde(default)]
|
||||
pub author: crate::server::GitAuthorConfig,
|
||||
}
|
||||
|
|
@ -73,13 +70,13 @@ pub struct CliConfig {
|
|||
pub mode: Option<ExecutionMode>,
|
||||
pub server: Option<ServerDefaults>,
|
||||
pub exec: Option<ExecDefaults>,
|
||||
pub llm: Option<LlmDefaults>,
|
||||
pub git: Option<CliGitConfig>,
|
||||
#[serde(default)]
|
||||
pub verbose: bool,
|
||||
#[serde(default)]
|
||||
pub log: crate::server::LogConfig,
|
||||
pub pull_request: Option<PullRequestConfig>,
|
||||
#[serde(flatten)]
|
||||
pub run_defaults: RunDefaults,
|
||||
#[serde(default)]
|
||||
pub mcp_servers: HashMap<String, McpServerEntry>,
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue