Rename cli.toml to user.toml

This commit is contained in:
Bryan Helmkamp 2026-03-29 21:24:10 -04:00
parent dfc30b6d3a
commit be6e37fa26
58 changed files with 434 additions and 255 deletions

View file

@ -110,9 +110,9 @@ Send the `X-Fabro-Demo: 1` header on any API request to get static mock data wit
## Pointing the CLI at a server
The CLI can delegate commands to a running Fabro server instead of executing locally. Set `mode = "server"` in `~/.fabro/cli.toml`:
The CLI can delegate commands to a running Fabro server instead of executing locally. Set `mode = "server"` in `~/.fabro/user.toml`:
```toml title="cli.toml"
```toml title="user.toml"
mode = "server"
[server]
@ -125,7 +125,7 @@ Or use the `--server-url` flag:
fabro --server-url https://fabro.example.com:3000 model list
```
This applies to commands like `fabro model list`, `fabro llm chat`, and `fabro exec`. See [CLI Configuration](/reference/cli-configuration#mode) for the full options including mTLS setup.
This applies to commands like `fabro model list`, `fabro llm chat`, and `fabro exec`. See [User Configuration](/reference/user-configuration#mode) for the full options including mTLS setup.
## Next steps

View file

@ -119,7 +119,7 @@ Customize the git author identity used for checkpoint commits. When not set, def
| `name` | Git author name | `"fabro"` |
| `email` | Git author email | `"fabro@local"` |
The CLI can also set `[git.author]` in `cli.toml` to override the server default.
The CLI can also set `[git.author]` in `user.toml` to override the server default.
### `[git.webhooks]` section

View file

@ -107,7 +107,7 @@ Hooks are defined as `[[hooks]]` entries in any of these TOML config files:
- **`fabro.toml`** — project-level hooks, apply to all workflows in the project
- **`workflow.toml`** — per-workflow hooks
- **`~/.fabro/cli.toml`** or **`~/.fabro/server.toml`** — global defaults for all runs
- **`~/.fabro/user.toml`** or **`~/.fabro/server.toml`** — global defaults for all runs
See [Merging hook configs](#merging-hook-configs) for how these layers combine.
@ -344,7 +344,7 @@ Command hooks do **not** fail open. A non-zero exit code (other than 0 or 2) pro
Hooks from multiple config files are merged in this order (later layers win on name collisions):
1. **`~/.fabro/cli.toml`** or **`~/.fabro/server.toml`** — global defaults
1. **`~/.fabro/user.toml`** or **`~/.fabro/server.toml`** — global defaults
2. **`fabro.toml`** — project-level overrides
3. **`workflow.toml`** — per-workflow overrides

View file

@ -30,7 +30,7 @@ For example, a server named `filesystem` exposing a `read_file` tool becomes `mc
MCP servers can be configured in two places:
- **`~/.fabro/cli.toml`** — applies to `fabro exec` sessions. See [CLI Configuration](/reference/cli-configuration#mcp_servers-section).
- **`~/.fabro/user.toml`** — applies to `fabro exec` sessions. See [User Configuration](/reference/user-configuration#mcp_servers-section).
- **Run config TOML** — applies to workflow runs (`fabro run`). See [Run Configuration](/execution/run-configuration#mcp_servers).
Each server entry specifies a transport type and optional timeouts. The server name is the TOML table key and is used in qualified tool names.

View file

@ -105,7 +105,7 @@
"pages": [
"reference/dot-language",
"reference/cli",
"reference/cli-configuration",
"reference/user-configuration",
"reference/run-directory",
"reference/sdk",
"reference/architecture",

View file

@ -12,7 +12,7 @@ These flags apply to all subcommands:
| `--debug` | Enable DEBUG-level logging (default is INFO) |
| `--no-upgrade-check` | Skip the automatic background upgrade check |
| `--storage-dir <DIR>` | Storage directory for local run data (default: `~/.fabro`). Implies standalone mode. |
| `--server-url <URL>` | Fabro API server URL (overrides `server.base_url` from `cli.toml`). Implies server mode. |
| `--server-url <URL>` | Fabro API server URL (overrides `server.base_url` from `user.toml`). Implies server mode. |
| `-h, --help` | Print help |
| `-V, --version` | Print version |
@ -20,9 +20,9 @@ Fabro loads environment variables from `~/.fabro/.env`.
## Configuration
CLI defaults can be set in `~/.fabro/cli.toml` so you don't have to pass common flags every time:
CLI defaults can be set in `~/.fabro/user.toml` so you don't have to pass common flags every time:
```toml title="cli.toml"
```toml title="user.toml"
[exec]
provider = "anthropic"
model = "claude-opus-4-6"
@ -33,7 +33,7 @@ output_format = "text"
model = "claude-sonnet-4-5"
```
CLI flags always override `cli.toml` values, which override hardcoded defaults.
CLI flags always override `user.toml` values, which override hardcoded defaults.
---
@ -47,7 +47,7 @@ fabro config show demo
fabro config show run.toml
```
With no argument, Fabro prints the merged ambient defaults from `~/.fabro/cli.toml` and the nearest `fabro.toml`.
With no argument, Fabro prints the merged ambient defaults from `~/.fabro/user.toml` and the nearest `fabro.toml`.
When you pass a workflow name or path:
@ -750,7 +750,7 @@ fabro upgrade --version 0.6.0
| `--force` | Upgrade even if already on the target version |
| `--dry-run` | Preview what would happen without making changes |
Fabro refuses to downgrade unless you specify an explicit `--version`. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in [`cli.toml`](/reference/cli-configuration#upgrade_check) or the `--no-upgrade-check` global flag.
Fabro refuses to downgrade unless you specify an explicit `--version`. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in [`user.toml`](/reference/user-configuration#upgrade_check) or the `--no-upgrade-check` global flag.
## `fabro asset list`

View file

@ -1,25 +1,25 @@
---
title: "CLI Configuration"
description: "Configure default settings for the Fabro CLI with cli.toml"
title: "User Configuration"
description: "Configure default user settings for Fabro with user.toml"
---
Fabro loads CLI defaults from `~/.fabro/cli.toml` so you don't have to pass common flags every time. The file is optional — if it doesn't exist, built-in defaults are used.
Fabro loads user defaults from `~/.fabro/user.toml` so you don't have to pass common flags every time. The file is optional — if it doesn't exist, built-in defaults are used.
## File location
The default path is `~/.fabro/cli.toml`. Fabro silently skips loading if the file is missing.
The default path is `~/.fabro/user.toml`. Fabro silently skips loading if the file is missing.
## Precedence
CLI flags always take the highest priority:
1. **CLI flags** — always win
2. **`cli.toml`** — used when no flag is provided
2. **`user.toml`** — used when no flag is provided
3. **Built-in defaults** — used when neither flag nor config is set
## Full example
```toml title="cli.toml"
```toml title="user.toml"
verbose = true
upgrade_check = true
mode = "server"
@ -184,7 +184,7 @@ Optional mTLS configuration for authenticating with the server. When present, th
Paths support `~/` expansion. Example:
```toml title="cli.toml"
```toml title="user.toml"
[server.tls]
cert = "~/.fabro/tls/client.crt"
key = "~/.fabro/tls/client.key"
@ -195,7 +195,7 @@ ca = "~/.fabro/tls/ca.crt"
Enable auto-PR globally so workflows open a GitHub pull request on successful completion — even when running with a `.fabro` file instead of a `run.toml`.
```toml title="cli.toml"
```toml title="user.toml"
[pull_request]
enabled = true
```
@ -204,7 +204,7 @@ enabled = true
|---|---|---|
| `enabled` | Automatically create a PR after successful runs | `false` |
Precedence: `run.toml` > `fabro.toml` (project config) > `cli.toml` > `server.toml` > built-in default (`false`).
Precedence: `run.toml` > `fabro.toml` (project config) > `user.toml` > `server.toml` > built-in default (`false`).
## `[mcp_servers]` section
@ -214,7 +214,7 @@ Configure [MCP servers](/agents/mcp) to connect to during `fabro exec` sessions.
Spawn a local process and communicate over stdin/stdout:
```toml title="cli.toml"
```toml title="user.toml"
[mcp_servers.filesystem]
type = "stdio"
command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
@ -237,7 +237,7 @@ NODE_ENV = "production"
Connect to a remote MCP server over Streamable HTTP:
```toml title="cli.toml"
```toml title="user.toml"
[mcp_servers.sentry]
type = "http"
url = "https://mcp.sentry.dev/mcp"
@ -256,7 +256,7 @@ Authorization = "Bearer sk-xxx"
### Sandbox transport
Run an MCP server inside the workflow's sandbox and connect via preview URL. Only available with remote sandbox providers ([Daytona](/integrations/daytona)) that support port previews. Typically configured in [run config TOML](/execution/run-configuration#mcp_servers) rather than `cli.toml`.
Run an MCP server inside the workflow's sandbox and connect via preview URL. Only available with remote sandbox providers ([Daytona](/integrations/daytona)) that support port previews. Typically configured in [run config TOML](/execution/run-configuration#mcp_servers) rather than `user.toml`.
```toml title="run.toml"
[mcp_servers.playwright]

View file

@ -68,10 +68,10 @@ struct Cli {
args: AgentArgs,
}
pub use fabro_config::cli::{OutputFormat, PermissionLevel};
pub use fabro_config::user::{OutputFormat, PermissionLevel};
impl AgentArgs {
/// Fill `None` fields from cli.toml values, then hardcoded defaults.
/// Fill `None` fields from user.toml values, then hardcoded defaults.
pub fn apply_cli_defaults(
&mut self,
provider: Option<&str>,

View file

@ -38,7 +38,7 @@ pub(crate) struct GlobalArgs {
pub storage_dir: Option<PathBuf>,
#[cfg(feature = "server")]
/// Server URL (overrides server.base_url from cli.toml)
/// Server URL (overrides server.base_url from user.toml)
#[arg(
long,
global = true,

View file

@ -7,11 +7,11 @@ use fabro_workflows::assets::{AssetEntry, scan_assets};
use fabro_workflows::run_lookup::{resolve_run, runs_base};
use crate::args::{AssetCpArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::split_run_path;
use crate::user_config::load_user_settings_with_globals;
pub(super) fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let (run_id, asset_path) = parse_source(&args.source);
let run = resolve_run(&base, run_id)?;

View file

@ -5,11 +5,11 @@ use fabro_workflows::assets::scan_assets;
use fabro_workflows::run_lookup::{resolve_run, runs_base};
use crate::args::{AssetListArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_size;
use crate::user_config::load_user_settings_with_globals;
pub(super) fn list_command(args: &AssetListArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let run = resolve_run(&base, &args.run_id)?;
let runtime_state = RuntimeState::new(&run.path);

View file

@ -2,7 +2,7 @@ use std::io::Write;
use std::path::Path;
use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs, GlobalArgs};
use crate::cli_config;
use crate::user_config;
use fabro_config::{ConfigLayer, FabroSettings};
pub(crate) fn dispatch(ns: ConfigNamespace, globals: &GlobalArgs) -> anyhow::Result<()> {
@ -17,7 +17,7 @@ fn merged_config(workflow: Option<&Path>, globals: &GlobalArgs) -> anyhow::Resul
Some(path) => ConfigLayer::for_workflow(path, &cwd)?,
None => ConfigLayer::project(&cwd)?,
};
let cli = cli_config::cli_layer_with_globals(globals)?;
let cli = user_config::user_layer_with_globals(globals)?;
base.combine(cli).resolve()
}

View file

@ -21,7 +21,7 @@ use regex::Regex;
#[cfg(feature = "server")]
use semver::Version;
use crate::cli_config::load_cli_settings;
use crate::user_config::load_user_settings;
// ---------------------------------------------------------------------------
// System dependency types and parsers (server mode only)
@ -194,23 +194,46 @@ fn apply_live_result(
}
}
pub(crate) fn check_config(path: Option<PathBuf>) -> CheckResult {
match path {
Some(p) => CheckResult {
pub(crate) fn check_config(
user_path: Option<PathBuf>,
legacy_path: Option<PathBuf>,
) -> CheckResult {
match (user_path, legacy_path) {
(Some(p), None) => CheckResult {
name: "Configuration".to_string(),
status: CheckStatus::Pass,
summary: p.display().to_string(),
details: vec![CheckDetail::new(format!("Loaded from {}", p.display()))],
remediation: None,
},
None => CheckResult {
(Some(p), Some(legacy)) => CheckResult {
name: "Configuration".to_string(),
status: CheckStatus::Warning,
summary: "no config file found".to_string(),
summary: p.display().to_string(),
details: vec![
CheckDetail::new(format!("Loaded from {}", p.display())),
CheckDetail::new(format!("Ignoring legacy config file {}", legacy.display())),
],
remediation: Some(format!("Delete or rename {}", legacy.display())),
},
(None, Some(legacy)) => CheckResult {
name: "Configuration".to_string(),
status: CheckStatus::Warning,
summary: "legacy config file ignored".to_string(),
details: vec![
CheckDetail::new(format!("Found legacy config file {}", legacy.display())),
CheckDetail::new("Rename it to ~/.fabro/user.toml".to_string()),
],
remediation: Some(format!("Rename {} to ~/.fabro/user.toml", legacy.display())),
},
(None, None) => CheckResult {
name: "Configuration".to_string(),
status: CheckStatus::Warning,
summary: "no user config file found".to_string(),
details: vec![CheckDetail::new(
"Create ~/.fabro/cli.toml to configure Arc".to_string(),
"Create ~/.fabro/user.toml to configure Fabro".to_string(),
)],
remediation: Some("Create ~/.fabro/cli.toml".to_string()),
remediation: Some("Create ~/.fabro/user.toml".to_string()),
},
}
}
@ -938,10 +961,12 @@ pub(crate) async fn run_doctor(verbose: bool, live: bool) -> i32 {
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
// Gather state
let cli_settings = load_cli_settings().unwrap_or_default();
let cli_settings = load_user_settings().unwrap_or_default();
let config_path = dirs::home_dir().map(|h| h.join(".fabro").join("cli.toml"));
let config_exists = config_path.as_ref().is_some_and(|p| p.exists());
let user_config_path = fabro_config::user::default_user_config_path();
let user_config_exists = user_config_path.as_ref().is_some_and(|p| p.exists());
let legacy_config_path = fabro_config::user::legacy_user_config_path();
let legacy_config_exists = legacy_config_path.as_ref().is_some_and(|p| p.exists());
let llm_statuses: Vec<(Provider, bool)> = Provider::ALL
.iter()
@ -1142,7 +1167,18 @@ pub(crate) async fn run_doctor(verbose: bool, live: bool) -> i32 {
CheckSection {
title: "Required".into(),
checks: vec![
check_config(if config_exists { config_path } else { None }),
check_config(
if user_config_exists {
user_config_path
} else {
None
},
if legacy_config_exists {
legacy_config_path
} else {
None
},
),
check_llm_providers(&llm_statuses, llm_live_results.as_deref()),
check_github_app(&github_status),
],
@ -1195,18 +1231,31 @@ mod tests {
#[test]
fn check_config_pass_with_path() {
let result = check_config(Some(PathBuf::from("/home/user/.fabro/cli.toml")));
let result = check_config(Some(PathBuf::from("/home/user/.fabro/user.toml")), None);
assert_eq!(result.status, CheckStatus::Pass);
assert!(result.summary.contains(".fabro/cli.toml"));
assert!(result.summary.contains(".fabro/user.toml"));
}
#[test]
fn check_config_warning_without_path() {
let result = check_config(None);
let result = check_config(None, None);
assert_eq!(result.status, CheckStatus::Warning);
assert!(result.remediation.is_some());
}
#[test]
fn check_config_warning_for_legacy_only_path() {
let result = check_config(None, Some(PathBuf::from("/home/user/.fabro/cli.toml")));
assert_eq!(result.status, CheckStatus::Warning);
assert!(result.summary.contains("legacy"));
assert!(
result
.remediation
.as_deref()
.is_some_and(|remediation| remediation.contains(".fabro/user.toml"))
);
}
// -- check_llm_providers --
#[test]

View file

@ -6,10 +6,10 @@ use fabro_config::mcp::McpServerEntry;
use fabro_mcp::config::McpServerConfig;
use crate::args::GlobalArgs;
use crate::cli_config;
use crate::user_config;
pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = cli_config::load_cli_settings_with_globals(globals)?;
let cli_settings = user_config::load_user_settings_with_globals(globals)?;
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled());
let exec_defaults = cli_settings.exec.as_ref();
@ -20,7 +20,7 @@ pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result
exec_defaults.and_then(|a| a.output_format),
);
#[cfg(feature = "server")]
let resolved = cli_config::resolve_mode(
let resolved = user_config::resolve_mode(
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
&cli_settings,
@ -33,9 +33,9 @@ pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result
#[cfg(feature = "server")]
{
match resolved.mode {
cli_config::ExecutionMode::Server => {
user_config::ExecutionMode::Server => {
tracing::info!(mode = "server", "Agent session starting");
let http_client = cli_config::build_server_client(resolved.tls.as_ref())?;
let http_client = user_config::build_server_client(resolved.tls.as_ref())?;
let provider_name = args
.provider
.clone()
@ -53,7 +53,7 @@ pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result
.map_err(|e| anyhow::anyhow!("Failed to register fabro server adapter: {e}"))?;
run_with_args_and_client(args, Some(client), mcp_servers).await?
}
cli_config::ExecutionMode::Standalone => {
user_config::ExecutionMode::Standalone => {
tracing::info!(mode = "standalone", "Agent session starting");
run_with_args(args, mcp_servers).await?
}

View file

@ -20,7 +20,7 @@ static RANKDIR_RE: LazyLock<regex::Regex> =
pub(crate) fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
let settings = ConfigLayer::for_workflow(&args.workflow, &cwd)?
.combine(ConfigLayer::cli()?)
.combine(ConfigLayer::user()?)
.resolve()?;
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
let validated = validate(ValidateInput {

View file

@ -439,28 +439,31 @@ async fn setup_github_app(
.context("missing 'pem' in GitHub response")?
.to_string();
// Write non-secret config to cli.toml
let cli_toml_path = arc_dir.join("cli.toml");
let existing = std::fs::read_to_string(&cli_toml_path).unwrap_or_default();
// Write non-secret config to user.toml
let user_toml_path = arc_dir.join(fabro_config::user::USER_CONFIG_FILENAME);
let existing = std::fs::read_to_string(&user_toml_path).unwrap_or_default();
let mut doc: toml::Value = if existing.is_empty() {
toml::Value::Table(toml::Table::default())
} else {
toml::from_str(&existing).context("failed to parse existing cli.toml")?
toml::from_str(&existing).context("failed to parse existing user.toml")?
};
let table = doc.as_table_mut().context("cli.toml root is not a table")?;
let table = doc
.as_table_mut()
.context("user.toml root is not a table")?;
let git = table
.entry("git")
.or_insert(toml::Value::Table(toml::Table::default()));
let git_table = git
.as_table_mut()
.context("cli.toml [git] is not a table")?;
.context("user.toml [git] is not a table")?;
git_table.insert("app_id".into(), toml::Value::String(app_id));
git_table.insert("slug".into(), toml::Value::String(slug.clone()));
git_table.insert("client_id".into(), toml::Value::String(client_id));
std::fs::write(&cli_toml_path, toml::to_string_pretty(&doc)?)?;
std::fs::write(&user_toml_path, toml::to_string_pretty(&doc)?)?;
eprintln!(
" {}",
s.dim.apply_to(format!("Wrote {}", cli_toml_path.display()))
s.dim
.apply_to(format!("Wrote {}", user_toml_path.display()))
);
eprintln!(
" {}",
@ -649,8 +652,8 @@ pub(crate) async fn run_install(web_url: &str) -> Result<()> {
if setup_github {
let github_env_pairs = setup_github_app(&arc_dir, &s, web_url).await?;
let slug = {
let cli_toml_path = arc_dir.join("cli.toml");
let toml_content = std::fs::read_to_string(&cli_toml_path).unwrap_or_default();
let user_toml_path = arc_dir.join(fabro_config::user::USER_CONFIG_FILENAME);
let toml_content = std::fs::read_to_string(&user_toml_path).unwrap_or_default();
let doc: toml::Value = toml::from_str(&toml_content)
.unwrap_or(toml::Value::Table(toml::Table::default()));
doc.get("git")

View file

@ -18,21 +18,21 @@ pub(super) async fn execute(
#[cfg(feature = "server")]
{
let resolved = crate::cli_config::resolve_mode(
let resolved = crate::user_config::resolve_mode(
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
cli_settings,
);
match resolved.mode {
crate::cli_config::ExecutionMode::Server => {
let client = crate::cli_config::build_server_client(resolved.tls.as_ref())?;
crate::user_config::ExecutionMode::Server => {
let client = crate::user_config::build_server_client(resolved.tls.as_ref())?;
let server = ServerConnection {
client,
base_url: resolved.server_base_url,
};
run_chat_via_server(args, &server).await?;
}
crate::cli_config::ExecutionMode::Standalone => {
crate::user_config::ExecutionMode::Standalone => {
run_chat(args).await?;
}
}

View file

@ -4,10 +4,10 @@ mod prompt;
use anyhow::Result;
use crate::args::{GlobalArgs, LlmCommand, LlmNamespace};
use crate::cli_config::load_cli_settings_with_globals;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn dispatch(ns: LlmNamespace, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
match ns.command {
LlmCommand::Prompt(args) => prompt::execute(args, &cli_settings, globals).await,

View file

@ -18,21 +18,21 @@ pub(super) async fn execute(
#[cfg(feature = "server")]
{
let resolved = crate::cli_config::resolve_mode(
let resolved = crate::user_config::resolve_mode(
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
cli_settings,
);
match resolved.mode {
crate::cli_config::ExecutionMode::Server => {
let client = crate::cli_config::build_server_client(resolved.tls.as_ref())?;
crate::user_config::ExecutionMode::Server => {
let client = crate::user_config::build_server_client(resolved.tls.as_ref())?;
let server = ServerConnection {
client,
base_url: resolved.server_base_url,
};
run_prompt_via_server(args, &server).await?;
}
crate::cli_config::ExecutionMode::Standalone => {
crate::user_config::ExecutionMode::Standalone => {
run_prompt(args).await?;
}
}

View file

@ -5,27 +5,27 @@ use fabro_llm::cli::{ModelsCommand, run_models};
use crate::args::GlobalArgs;
#[cfg(feature = "server")]
use crate::cli_config;
use crate::user_config;
pub(crate) async fn execute(command: Option<ModelsCommand>, globals: &GlobalArgs) -> Result<()> {
let server = {
#[cfg(feature = "server")]
{
let cli_settings = cli_config::load_cli_settings_with_globals(globals)?;
let resolved = cli_config::resolve_mode(
let cli_settings = user_config::load_user_settings_with_globals(globals)?;
let resolved = user_config::resolve_mode(
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
&cli_settings,
);
match resolved.mode {
cli_config::ExecutionMode::Server => {
let client = cli_config::build_server_client(resolved.tls.as_ref())?;
user_config::ExecutionMode::Server => {
let client = user_config::build_server_client(resolved.tls.as_ref())?;
Some(ServerConnection {
client,
base_url: resolved.server_base_url,
})
}
cli_config::ExecutionMode::Standalone => None,
user_config::ExecutionMode::Standalone => None,
}
}
#[cfg(not(feature = "server"))]

View file

@ -6,14 +6,14 @@ use fabro_workflows::run_lookup::runs_base;
use tracing::info;
use crate::args::{GlobalArgs, PrCloseArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn close_command(
args: PrCloseArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
close_from(&base, args, github_app).await
}

View file

@ -13,15 +13,15 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::{GlobalArgs, PrCreateArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn create_command(
args: PrCreateArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
create_from(&base, args, github_app).await
}

View file

@ -8,15 +8,15 @@ use futures::future::join_all;
use tracing::info;
use crate::args::{GlobalArgs, PrListArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn list_command(
args: PrListArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
list_from(store.as_ref(), &base, args, github_app).await

View file

@ -7,14 +7,14 @@ use tracing::info;
use fabro_workflows::run_lookup::runs_base;
use crate::args::{GlobalArgs, PrMergeArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn merge_command(
args: PrMergeArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
merge_from(&base, args, github_app).await
}

View file

@ -12,12 +12,12 @@ use fabro_workflows::pull_request::PullRequestRecord;
use fabro_workflows::run_lookup::resolve_run_combined;
use crate::args::{GlobalArgs, PrCommand, PrNamespace};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::github::build_github_app_credentials;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn dispatch(ns: PrNamespace, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let github_app = build_github_app_credentials(cli_settings.app_id());
match ns.command {

View file

@ -7,14 +7,14 @@ use tracing::info;
use fabro_workflows::run_lookup::runs_base;
use crate::args::{GlobalArgs, PrViewArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn view_command(
args: PrViewArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
view_from(&base, args, github_app).await
}

View file

@ -17,13 +17,13 @@ use fabro_workflows::git::{GitSyncStatus, sync_status};
use fabro_workflows::operations::{ValidateInput, WorkflowInput, validate};
use crate::args::{GlobalArgs, PreflightArgs};
use crate::cli_config::{cli_layer_with_globals, load_cli_settings_with_globals};
use crate::shared::github::build_github_app_credentials;
use crate::user_config::{load_user_settings_with_globals, user_layer_with_globals};
pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli = cli_layer_with_globals(globals)?;
let cli_settings: FabroSettings = load_cli_settings_with_globals(globals)?;
let cli = user_layer_with_globals(globals)?;
let cli_settings: FabroSettings = load_user_settings_with_globals(globals)?;
args.verbose = args.verbose || cli_settings.verbose_enabled();
let github_app = build_github_app_credentials(cli_settings.app_id());

View file

@ -3,8 +3,8 @@ use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use tokio::task::spawn_blocking;
use crate::cli_config::load_cli_settings;
use crate::shared::github::build_github_app_credentials;
use crate::user_config::load_user_settings;
pub(super) fn git_repo_root() -> Result<PathBuf> {
let output = std::process::Command::new("git")
@ -153,7 +153,7 @@ async fn check_github_app_installation() {
};
// Load CLI config to get app_id and slug
let Ok(cli_settings) = load_cli_settings() else {
let Ok(cli_settings) = load_user_settings() else {
return;
};

View file

@ -2,12 +2,12 @@ use anyhow::Result;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, RunArgs};
use crate::cli_config::{self, cli_layer_with_globals};
use crate::user_config::{self, user_layer_with_globals};
pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli_settings = cli_config::load_cli_settings_with_globals(globals)?;
let cli = cli_layer_with_globals(globals)?;
let cli_settings = user_config::load_user_settings_with_globals(globals)?;
let cli = user_layer_with_globals(globals)?;
args.verbose = args.verbose || cli_settings.verbose_enabled();
let quiet = args.detach;

View file

@ -10,8 +10,8 @@ use tokio::fs;
use tracing::{debug, info};
use crate::args::{CpArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::split_run_path;
use crate::user_config::load_user_settings_with_globals;
enum CopyDirection {
Download {
@ -28,7 +28,7 @@ enum CopyDirection {
pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()> {
let direction = parse_direction(&args.src, &args.dst)?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
match direction {

View file

@ -12,9 +12,9 @@ use fabro_workflows::operations::{
};
use fabro_workflows::records::{RunRecord, RunRecordExt};
use crate::cli_config;
use crate::shared;
use crate::store;
use crate::user_config;
pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> Result<()> {
let _ = fabro_proctitle::init();
@ -24,7 +24,7 @@ pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bo
});
let run_record = RunRecord::load(&run_dir)?;
let cli_settings = cli_config::load_cli_settings()?;
let cli_settings = user_config::load_user_settings()?;
let on_node: fabro_workflows::OnNodeCallback = Some({
let short_id = super::short_run_id(&run_record.run_id).to_string();
fabro_proctitle::set(&format!("fabro: {short_id}"));

View file

@ -11,12 +11,12 @@ use fabro_workflows::sandbox_git::GIT_REMOTE;
use tracing::{debug, info};
use crate::args::{DiffArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs) -> Result<()> {
info!(run_id = %args.run, "Showing diff");
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -9,12 +9,12 @@ use fabro_workflows::operations::{
use git2::Repository;
use crate::args::{ForkArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store::{build_store, open_run_reader};
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let durable_store = build_store(&cli_settings.storage_dir())?;
let run_id =
find_run_id_by_prefix_or_store(&repo, durable_store.as_ref(), &args.run_id).await?;

View file

@ -13,11 +13,11 @@ use tokio::time;
use tracing::{debug, info, warn};
use crate::args::{GlobalArgs, LogsArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -4,8 +4,8 @@ use fabro_util::terminal::Styles;
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use crate::args::{GlobalArgs, RunCommands};
use crate::cli_config::{cli_layer_with_globals, load_cli_settings_with_globals};
use crate::store;
use crate::user_config::{load_user_settings_with_globals, user_layer_with_globals};
pub(crate) mod attach;
pub(crate) mod command;
@ -35,13 +35,13 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
RunCommands::Run(args) => command::execute(args, globals).await,
RunCommands::Create(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli = cli_layer_with_globals(globals)?;
let cli = user_layer_with_globals(globals)?;
let (run_id, _run_dir) = create::create_run(&args, cli, styles, true)?;
println!("{run_id}");
Ok(())
}
RunCommands::Start { run } => {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
@ -51,7 +51,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
}
RunCommands::Attach { run } => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
@ -80,7 +80,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled())
};
resume::resume_command(args, styles, globals).await

View file

@ -6,12 +6,12 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::{GlobalArgs, PreviewArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::validate_daytona_provider;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -5,8 +5,8 @@ use fabro_workflows::records::{RunRecord, RunRecordExt};
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use crate::args::{GlobalArgs, ResumeArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
/// Resume an interrupted workflow run.
///
@ -18,7 +18,7 @@ pub(crate) async fn resume_command(
styles: &'static Styles,
globals: &GlobalArgs,
) -> anyhow::Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -12,13 +12,13 @@ use fabro_workflows::operations::{
use git2::Repository;
use crate::args::{GlobalArgs, RewindArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::color_if;
use crate::store::{build_store, open_run_reader};
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let durable_store = build_store(&cli_settings.storage_dir())?;
let run_id =
find_run_id_by_prefix_or_store(&repo, durable_store.as_ref(), &args.run_id).await?;

View file

@ -6,12 +6,12 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::{GlobalArgs, SshArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::validate_daytona_provider;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -9,12 +9,12 @@ use fabro_workflows::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt
use tracing::info;
use crate::args::{GlobalArgs, WaitArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_duration_ms;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -11,8 +11,8 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflows::run_status::RunStatus;
use crate::args::{GlobalArgs, InspectArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
#[derive(Debug, Serialize)]
pub(crate) struct InspectOutput {
@ -27,7 +27,7 @@ pub(crate) struct InspectOutput {
}
pub(crate) async fn run(args: &InspectArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -12,9 +12,9 @@ use fabro_workflows::run_lookup::{StatusFilter, filter_runs, runs_base, scan_run
use fabro_workflows::run_status::RunStatus;
use crate::args::{GlobalArgs, RunsListArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::{color_if, format_duration_ms, tilde_path};
use crate::store;
use crate::user_config::load_user_settings_with_globals;
use super::short_run_id;
@ -23,7 +23,7 @@ pub(crate) async fn list_command(
styles: &Styles,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let runs = scan_runs_combined(store.as_ref(), &base).await?;

View file

@ -11,13 +11,13 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflows::run_status::{RunStatus, RunStatusRecord, write_run_status};
use crate::args::{GlobalArgs, RunsRemoveArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
use super::short_run_id;
pub(crate) async fn remove_command(args: &RunsRemoveArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
remove_from(args, store.as_ref(), &base).await

View file

@ -8,11 +8,11 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use serde::Serialize;
use crate::args::{GlobalArgs, StoreDumpArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -10,12 +10,12 @@ use fabro_workflows::run_lookup::{logs_base, runs_base, scan_runs_combined};
use fabro_workflows::run_status::RunStatus;
use crate::args::{DfArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_size;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let data_dir = cli_settings.storage_dir();
let runs_base_dir = runs_base(&data_dir);
let logs_base_dir = logs_base(&data_dir);

View file

@ -9,12 +9,12 @@ use tracing::{debug, info};
use fabro_workflows::run_lookup::{StatusFilter, filter_runs, runs_base, scan_runs_combined};
use crate::args::{GlobalArgs, RunsPruneArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_size;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
prune_from(args, store.as_ref(), &base).await

View file

@ -11,7 +11,7 @@ use crate::shared::{print_diagnostics, relative_path};
pub(crate) fn run(args: &ValidateArgs, styles: &Styles) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
let settings = ConfigLayer::for_workflow(&args.workflow, &cwd)?
.combine(ConfigLayer::cli()?)
.combine(ConfigLayer::user()?)
.resolve()?;
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
let validated = validate(ValidateInput {

View file

@ -1,13 +1,13 @@
#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)]
mod args;
mod cli_config;
mod commands;
mod logging;
mod shared;
#[cfg(feature = "sleep_inhibitor")]
mod sleep_inhibitor;
mod store;
mod user_config;
use anyhow::Result;
use args::{Commands, GlobalArgs, LONG_VERSION, RunCommands};
@ -115,7 +115,7 @@ async fn main_inner() -> (String, Result<()>) {
Err(err) => return (command_name, Err(err)),
}
} else {
match cli_config::load_cli_settings() {
match user_config::load_user_settings() {
Ok(cli_settings) => (
cli_settings.log.as_ref().and_then(|l| l.level.clone()),
cli_settings.upgrade_check_enabled(),
@ -126,7 +126,7 @@ async fn main_inner() -> (String, Result<()>) {
}
#[cfg(not(feature = "server"))]
{
match cli_config::load_cli_settings() {
match user_config::load_user_settings() {
Ok(cli_settings) => (
cli_settings.log.as_ref().and_then(|l| l.level.clone()),
cli_settings.upgrade_check_enabled(),
@ -189,7 +189,7 @@ async fn main_inner() -> (String, Result<()>) {
.await?;
}
Commands::Doctor { verbose, dry_run } => {
let cli_settings = cli_config::load_cli_settings()?;
let cli_settings = user_config::load_user_settings()?;
let verbose = verbose || cli_settings.verbose_enabled();
let exit_code = commands::doctor::run_doctor(verbose, !dry_run).await;
std::process::exit(exit_code);

View file

@ -2,7 +2,7 @@
use std::path::Path;
#[allow(unused_imports)]
pub(crate) use fabro_config::cli::*;
pub(crate) use fabro_config::user::*;
use fabro_config::ConfigLayer;
use fabro_config::FabroSettings;
@ -12,19 +12,19 @@ use crate::args::GlobalArgs;
#[cfg(feature = "server")]
use tracing::debug;
pub(crate) fn load_cli_settings() -> anyhow::Result<FabroSettings> {
ConfigLayer::cli()?.resolve()
pub(crate) fn load_user_settings() -> anyhow::Result<FabroSettings> {
ConfigLayer::user()?.resolve()
}
pub(crate) fn cli_layer_with_globals(globals: &GlobalArgs) -> anyhow::Result<ConfigLayer> {
let layer = ConfigLayer::cli()?;
pub(crate) fn user_layer_with_globals(globals: &GlobalArgs) -> anyhow::Result<ConfigLayer> {
let layer = ConfigLayer::user()?;
Ok(apply_global_overrides(layer, globals))
}
pub(crate) fn load_cli_settings_with_globals(
pub(crate) fn load_user_settings_with_globals(
globals: &GlobalArgs,
) -> anyhow::Result<FabroSettings> {
cli_layer_with_globals(globals)?.resolve()
user_layer_with_globals(globals)?.resolve()
}
pub(crate) fn apply_global_overrides(mut layer: ConfigLayer, globals: &GlobalArgs) -> ConfigLayer {

View file

@ -6,9 +6,9 @@ use std::time::Duration;
use assert_cmd::Command;
use chrono::TimeZone;
use fabro_config::FabroSettings;
#[cfg(feature = "server")]
use fabro_config::cli::ExecutionMode;
use fabro_config::mcp::McpTransport;
#[cfg(feature = "server")]
use fabro_config::user::ExecutionMode;
use fabro_git_storage::branchstore::BranchStore;
use fabro_git_storage::gitobj::Store as GitStore;
use fabro_store::{NodeVisitRef, RuntimeState, SlateStore, Store as _};
@ -36,7 +36,7 @@ fn setup_config_show_fixture() -> (tempfile::TempDir, tempfile::TempDir) {
let home_fabro = home.path().join(".fabro");
std::fs::create_dir_all(&home_fabro).unwrap();
std::fs::write(
home_fabro.join("cli.toml"),
home_fabro.join("user.toml"),
r#"
verbose = true
@ -160,7 +160,7 @@ fn setup_external_workflow_fixture() -> (tempfile::TempDir, tempfile::TempDir, s
let home_fabro = home.path().join(".fabro");
std::fs::create_dir_all(&home_fabro).unwrap();
std::fs::write(
home_fabro.join("cli.toml"),
home_fabro.join("user.toml"),
format!(
r#"
storage_dir = "{}"
@ -225,7 +225,7 @@ fn init_cli_home(storage_dir: &Path) -> tempfile::TempDir {
std::fs::create_dir_all(&home_fabro).unwrap();
let storage_dir = serde_json::to_string(&storage_dir.to_string_lossy().into_owned()).unwrap();
std::fs::write(
home_fabro.join("cli.toml"),
home_fabro.join("user.toml"),
format!("storage_dir = {storage_dir}\n"),
)
.unwrap();
@ -2020,16 +2020,82 @@ fn config_show_missing_run_config_errors() {
.stderr(predicate::str::contains("Workflow not found"));
}
#[test]
fn config_show_legacy_cli_config_warns_and_ignores_it() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let home_fabro = home.path().join(".fabro");
std::fs::create_dir_all(&home_fabro).unwrap();
std::fs::write(
home_fabro.join("cli.toml"),
r#"
verbose = true
[llm]
model = "legacy-model"
"#,
)
.unwrap();
let assert = arc()
.env("HOME", home.path())
.current_dir(project.path())
.args(["config", "show"])
.assert()
.success()
.stderr(predicate::str::contains("ignoring legacy config file"))
.stderr(predicate::str::contains("Rename it to"));
let cfg = parse_config_show(&assert.get_output().stdout);
assert_eq!(cfg.verbose, None);
assert_eq!(cfg.llm, None);
}
#[test]
fn config_show_user_config_wins_over_legacy_cli_config() {
let (home, project) = setup_config_show_fixture();
std::fs::write(
home.path().join(".fabro").join("cli.toml"),
r#"
[llm]
model = "legacy-model"
[vars]
shared = "legacy"
"#,
)
.unwrap();
let assert = arc()
.env("HOME", home.path())
.current_dir(project.path())
.args(["config", "show"])
.assert()
.success()
.stderr(predicate::str::contains("ignoring legacy config file"));
let cfg = parse_config_show(&assert.get_output().stdout);
let llm = cfg.llm.as_ref().expect("llm config");
assert_eq!(llm.model.as_deref(), Some("project-model"));
assert_eq!(
cfg.vars
.as_ref()
.and_then(|vars| vars.get("shared").map(String::as_str)),
Some("project")
);
}
#[test]
#[cfg(feature = "server")]
fn config_show_server_url_overrides_cli_defaults() {
let (home, project) = setup_config_show_fixture();
let cli_toml = home.path().join(".fabro").join("cli.toml");
let user_toml = home.path().join(".fabro").join("user.toml");
std::fs::write(
&cli_toml,
&user_toml,
format!(
"{}\nmode = \"standalone\"\n[server]\nbase_url = \"https://config.example.com\"\n",
std::fs::read_to_string(&cli_toml).unwrap()
std::fs::read_to_string(&user_toml).unwrap()
),
)
.unwrap();

View file

@ -1,6 +1,6 @@
---
source: lib/crates/fabro-cli/tests/it/cli.rs
assertion_line: 745
assertion_line: 747
expression: stdout
---
Start the HTTP API server
@ -9,29 +9,29 @@ Usage: fabro serve [OPTIONS]
Options:
--debug
Enable DEBUG-level logging (default is INFO)
Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--port <PORT>
Port to listen on [default: 3000]
--host <HOST>
Host address to bind to [default: 127.0.0.1]
--no-upgrade-check
Disable automatic upgrade check
Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=]
--model <MODEL>
Override default LLM model
--quiet
Suppress non-essential output
Suppress non-essential output [env: FABRO_QUIET=]
--provider <PROVIDER>
Override default LLM provider
--verbose
Enable verbose output
Enable verbose output [env: FABRO_VERBOSE=]
--dry-run
Execute with simulated LLM backend
--storage-dir <STORAGE_DIR>
Storage directory (default: ~/.fabro)
Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=]
--sandbox <SANDBOX>
Sandbox for agent tools
--server-url <SERVER_URL>
Server URL (overrides server.base_url from cli.toml)
Server URL (overrides server.base_url from user.toml) [env: FABRO_SERVER_URL=]
--max-concurrent-runs <MAX_CONCURRENT_RUNS>
Maximum number of concurrent run executions
--config <CONFIG>

View file

@ -2,7 +2,10 @@ macro_rules! trycmd_subcommand {
($name:ident, $dir:expr) => {
#[test]
fn $name() {
let home = tempfile::tempdir().unwrap();
let home = home.path().display().to_string();
trycmd::TestCases::new()
.env("HOME", home)
.case(concat!("tests/cmd/", $dir, "/*.trycmd"))
.case(concat!("tests/cmd/", $dir, "/*.toml"));
}

View file

@ -1,77 +0,0 @@
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use crate::config::ConfigLayer;
pub use fabro_types::settings::cli::{
ClientTlsSettings, ExecSettings, ExecutionMode, OutputFormat, PermissionLevel, ServerSettings,
};
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ClientTlsConfig {
pub cert: Option<PathBuf>,
pub key: Option<PathBuf>,
pub ca: Option<PathBuf>,
}
impl TryFrom<ClientTlsConfig> for ClientTlsSettings {
type Error = anyhow::Error;
fn try_from(value: ClientTlsConfig) -> Result<Self, Self::Error> {
Ok(Self {
cert: value.cert.ok_or_else(|| {
anyhow!("server.tls.cert is required when server.tls is configured")
})?,
key: value.key.ok_or_else(|| {
anyhow!("server.tls.key is required when server.tls is configured")
})?,
ca: value.ca.ok_or_else(|| {
anyhow!("server.tls.ca is required when server.tls is configured")
})?,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ServerConfig {
pub base_url: Option<String>,
pub tls: Option<ClientTlsConfig>,
}
impl TryFrom<ServerConfig> for ServerSettings {
type Error = anyhow::Error;
fn try_from(value: ServerConfig) -> Result<Self, Self::Error> {
Ok(Self {
base_url: value.base_url,
tls: value.tls.map(TryInto::try_into).transpose()?,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ExecConfig {
pub provider: Option<String>,
pub model: Option<String>,
pub permissions: Option<PermissionLevel>,
pub output_format: Option<OutputFormat>,
}
impl From<ExecConfig> for ExecSettings {
fn from(value: ExecConfig) -> Self {
Self {
provider: value.provider,
model: value.model,
permissions: value.permissions,
output_format: value.output_format,
}
}
}
/// Load CLI config from an explicit path or `~/.fabro/cli.toml`, returning defaults if the
/// default file doesn't exist. An explicit path that doesn't exist is an error.
pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
crate::load_config_file(path, "cli.toml")
}

View file

@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::cli::{self, ExecConfig, ExecutionMode, ServerConfig};
use crate::combine::Combine;
use crate::hook::{HookConfig, HookDefinition};
use crate::mcp::McpServerEntry;
@ -14,6 +13,7 @@ use crate::run::{
use crate::sandbox::SandboxConfig;
use crate::server::{self, ApiConfig, Features, GitConfig, LogConfig, WebConfig};
use crate::settings::FabroSettings;
use crate::user::{self, ExecConfig, ExecutionMode, ServerConfig};
fn is_default_checkpoint(c: &CheckpointConfig) -> bool {
c.exclude_globs.is_empty()
@ -21,7 +21,7 @@ fn is_default_checkpoint(c: &CheckpointConfig) -> bool {
/// Unified sparse configuration type for all Fabro config sources.
///
/// Loading functions (`load_cli_config`, `load_server_config`, `load_run_config`,
/// Loading functions (`load_user_config`, `load_server_config`, `load_run_config`,
/// `parse_project_config`) all return this type. Fields irrelevant to a
/// particular source are left unset (`None` / empty).
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
@ -76,7 +76,7 @@ pub struct ConfigLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub github: Option<GitHubConfig>,
// --- CLI config fields ---
// --- User config fields ---
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<ExecutionMode>,
@ -222,9 +222,9 @@ impl ConfigLayer {
.unwrap_or_default())
}
/// Load CLI defaults from `~/.fabro/cli.toml`.
pub fn cli() -> anyhow::Result<Self> {
cli::load_cli_config(None)
/// Load user defaults from `~/.fabro/user.toml`.
pub fn user() -> anyhow::Result<Self> {
user::load_user_config(None)
}
/// Load server defaults from `~/.fabro/server.toml`.

View file

@ -1,6 +1,5 @@
extern crate self as fabro_config;
pub mod cli;
pub mod combine;
pub mod config;
pub mod dotenv;
@ -11,6 +10,7 @@ pub mod run;
pub mod sandbox;
pub mod server;
pub mod settings;
pub mod user;
pub use config::ConfigLayer;
pub use fabro_types::Combine;

View file

@ -0,0 +1,135 @@
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use crate::config::ConfigLayer;
pub use fabro_types::settings::user::{
ClientTlsSettings, ExecSettings, ExecutionMode, OutputFormat, PermissionLevel, ServerSettings,
};
pub const USER_CONFIG_FILENAME: &str = "user.toml";
pub const LEGACY_USER_CONFIG_FILENAME: &str = "cli.toml";
static WARNED_LEGACY_USER_CONFIGS: OnceLock<Mutex<HashSet<PathBuf>>> = OnceLock::new();
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ClientTlsConfig {
pub cert: Option<PathBuf>,
pub key: Option<PathBuf>,
pub ca: Option<PathBuf>,
}
impl TryFrom<ClientTlsConfig> for ClientTlsSettings {
type Error = anyhow::Error;
fn try_from(value: ClientTlsConfig) -> Result<Self, Self::Error> {
Ok(Self {
cert: value.cert.ok_or_else(|| {
anyhow!("server.tls.cert is required when server.tls is configured")
})?,
key: value.key.ok_or_else(|| {
anyhow!("server.tls.key is required when server.tls is configured")
})?,
ca: value.ca.ok_or_else(|| {
anyhow!("server.tls.ca is required when server.tls is configured")
})?,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ServerConfig {
pub base_url: Option<String>,
pub tls: Option<ClientTlsConfig>,
}
impl TryFrom<ServerConfig> for ServerSettings {
type Error = anyhow::Error;
fn try_from(value: ServerConfig) -> Result<Self, Self::Error> {
Ok(Self {
base_url: value.base_url,
tls: value.tls.map(TryInto::try_into).transpose()?,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ExecConfig {
pub provider: Option<String>,
pub model: Option<String>,
pub permissions: Option<PermissionLevel>,
pub output_format: Option<OutputFormat>,
}
impl From<ExecConfig> for ExecSettings {
fn from(value: ExecConfig) -> Self {
Self {
provider: value.provider,
model: value.model,
permissions: value.permissions,
output_format: value.output_format,
}
}
}
pub fn default_user_config_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".fabro").join(USER_CONFIG_FILENAME))
}
pub fn legacy_user_config_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".fabro").join(LEGACY_USER_CONFIG_FILENAME))
}
fn warned_legacy_user_configs() -> &'static Mutex<HashSet<PathBuf>> {
WARNED_LEGACY_USER_CONFIGS.get_or_init(|| Mutex::new(HashSet::new()))
}
fn should_warn_about_legacy_user_config(path: &Path) -> bool {
warned_legacy_user_configs()
.lock()
.expect("legacy user config warning lock poisoned")
.insert(path.to_path_buf())
}
/// Load user config from an explicit path or `~/.fabro/user.toml`, returning defaults if the
/// default file doesn't exist. An explicit path that doesn't exist is an error.
pub fn load_user_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
if let Some(explicit) = path {
return crate::load_config_file(Some(explicit), USER_CONFIG_FILENAME);
}
if let Some(legacy_path) = legacy_user_config_path() {
if legacy_path.is_file() && should_warn_about_legacy_user_config(&legacy_path) {
let target = default_user_config_path()
.unwrap_or_else(|| PathBuf::from(format!("~/.fabro/{USER_CONFIG_FILENAME}")));
eprintln!(
"Warning: ignoring legacy config file {}. Rename it to {}.",
legacy_path.display(),
target.display()
);
}
}
crate::load_config_file(None, USER_CONFIG_FILENAME)
}
#[cfg(test)]
mod tests {
use super::should_warn_about_legacy_user_config;
#[test]
fn should_warn_about_legacy_user_config_once_per_path() {
let dir = tempfile::tempdir().unwrap();
let first = dir.path().join("cli.toml");
let second = dir.path().join("other-cli.toml");
assert!(should_warn_about_legacy_user_config(&first));
assert!(!should_warn_about_legacy_user_config(&first));
assert!(should_warn_about_legacy_user_config(&second));
}
}

View file

@ -3,17 +3,14 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
pub mod cli;
pub mod hook;
pub mod mcp;
pub mod project;
pub mod run;
pub mod sandbox;
pub mod server;
pub mod user;
pub use cli::{
ClientTlsSettings, ExecSettings, ExecutionMode, OutputFormat, PermissionLevel, ServerSettings,
};
pub use hook::{HookConfig, HookDefinition, HookEvent, HookType, TlsMode};
pub use mcp::{
McpServerConfig, McpServerEntry, McpTransport, default_startup_timeout_secs,
@ -35,6 +32,9 @@ pub use server::{
GitProvider, GitSettings, LogSettings, TlsSettings, WebSettings, WebhookSettings,
WebhookStrategy,
};
pub use user::{
ClientTlsSettings, ExecSettings, ExecutionMode, OutputFormat, PermissionLevel, ServerSettings,
};
fn is_default_checkpoint(c: &CheckpointSettings) -> bool {
c.exclude_globs.is_empty()