mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
Refactor config resolution around ConfigLayer
This commit is contained in:
parent
392add851a
commit
ed651dcd57
48 changed files with 238 additions and 193 deletions
|
|
@ -37,7 +37,7 @@ CLI flags always override `cli.toml` values, which override hardcoded defaults.
|
|||
|
||||
## `fabro config show`
|
||||
|
||||
Print the merged `FabroConfig` as YAML.
|
||||
Print the merged resolved configuration as YAML.
|
||||
|
||||
```bash
|
||||
fabro config show
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to
|
|||
|
||||
| File | Format | When written | Description |
|
||||
|---|---|---|---|
|
||||
| `run.json` | JSON | Run create | Run metadata — `run_id`, `created_at`, `config` (FabroConfig), `graph` (Graph), `workflow_slug`, `working_directory`, `host_repo_path`, `base_branch`, `labels` |
|
||||
| `run.json` | JSON | Run create | Run metadata — `run_id`, `created_at`, `config` (resolved configuration), `graph` (Graph), `workflow_slug`, `working_directory`, `host_repo_path`, `base_branch`, `labels` |
|
||||
| `start.json` | JSON | Run start | Start metadata — `run_id`, `start_time`, `run_branch`, `base_sha` |
|
||||
| `workflow.fabro` | Graphviz | Run create | Copy of the original workflow graph when the raw DOT source is available |
|
||||
| `run.pid` | Text | Legacy only | Legacy process ID file from older runs. Current detached launches use launcher records instead, and current attach/resume no longer read `run.pid`. |
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
#[allow(unused_imports)]
|
||||
pub(crate) use fabro_config::cli::*;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use fabro_config::ConfigLayer;
|
||||
use fabro_config::FabroSettings;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use tracing::debug;
|
||||
|
||||
pub(crate) fn load_cli_settings(path: Option<&Path>) -> anyhow::Result<FabroSettings> {
|
||||
load_cli_config(path)?.try_into()
|
||||
pub(crate) fn load_cli_settings() -> anyhow::Result<FabroSettings> {
|
||||
ConfigLayer::cli()?.resolve()
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::split_run_path;
|
||||
|
||||
pub(super) fn cp_command(args: &AssetCpArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let (run_id, asset_path) = parse_source(&args.source);
|
||||
let run = resolve_run(&base, run_id)?;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::format_size;
|
||||
|
||||
pub(super) fn list_command(args: &AssetListArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let run = resolve_run(&base, &args.run_id)?;
|
||||
let runtime_state = RuntimeState::new(&run.path);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ use std::io::Write;
|
|||
use std::path::Path;
|
||||
|
||||
use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs};
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{ResolveSettingsInput, discover_project_config, resolve_settings};
|
||||
use fabro_config::{FabroConfig, FabroSettings};
|
||||
use fabro_config::{ConfigLayer, FabroSettings};
|
||||
|
||||
pub(crate) fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> {
|
||||
match ns.command {
|
||||
|
|
@ -13,24 +11,13 @@ pub(crate) fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
fn merged_config(workflow: Option<&Path>) -> anyhow::Result<FabroSettings> {
|
||||
if let Some(workflow) = workflow {
|
||||
let cli_config = load_cli_config(None)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
return resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: workflow.to_path_buf(),
|
||||
cwd,
|
||||
defaults: cli_config,
|
||||
overrides: FabroConfig::default(),
|
||||
apply_project_config: true,
|
||||
});
|
||||
}
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
let project_config = discover_project_config(&cwd)?
|
||||
.map(|(_, config)| config)
|
||||
.unwrap_or_default();
|
||||
let cli_config = load_cli_config(None)?;
|
||||
FabroConfig::combine(project_config, cli_config).try_into()
|
||||
let base = match workflow {
|
||||
Some(path) => ConfigLayer::for_workflow(path, &cwd)?,
|
||||
None => ConfigLayer::project(&cwd)?,
|
||||
};
|
||||
|
||||
base.combine(ConfigLayer::cli()?).resolve()
|
||||
}
|
||||
|
||||
pub(crate) fn show_command(args: &ConfigShowArgs) -> anyhow::Result<()> {
|
||||
|
|
|
|||
|
|
@ -938,7 +938,7 @@ 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(None).unwrap_or_default();
|
||||
let cli_settings = load_cli_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());
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::args::GlobalArgs;
|
|||
use crate::cli_config;
|
||||
|
||||
pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = cli_config::load_cli_settings(None)?;
|
||||
let cli_settings = cli_config::load_cli_settings()?;
|
||||
#[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();
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use std::io::Write;
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::bail;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{ResolveSettingsInput, resolve_settings, resolve_workflow_path};
|
||||
use fabro_config::ConfigLayer;
|
||||
use fabro_config::project::resolve_workflow_path;
|
||||
use fabro_graphviz::render::render_dot;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_validate::Severity;
|
||||
|
|
@ -19,14 +19,9 @@ static RANKDIR_RE: LazyLock<regex::Regex> =
|
|||
|
||||
pub(crate) fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let settings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: args.workflow.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
overrides: fabro_config::FabroConfig::default(),
|
||||
apply_project_config: true,
|
||||
})?;
|
||||
let settings = ConfigLayer::for_workflow(&args.workflow, &cwd)?
|
||||
.combine(ConfigLayer::cli()?)
|
||||
.resolve()?;
|
||||
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let validated = validate(ValidateInput {
|
||||
workflow: WorkflowInput::Path(args.workflow.clone()),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::args::{GlobalArgs, LlmCommand, LlmNamespace};
|
|||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub(crate) async fn dispatch(ns: LlmNamespace, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
|
||||
match ns.command {
|
||||
LlmCommand::Prompt(args) => prompt::execute(args, &cli_settings, globals).await,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ pub(crate) async fn execute(command: Option<ModelsCommand>, globals: &GlobalArgs
|
|||
let server = {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
let cli_settings = cli_config::load_cli_settings(None)?;
|
||||
let cli_settings = cli_config::load_cli_settings()?;
|
||||
let resolved = cli_config::resolve_mode(
|
||||
globals.mode.clone(),
|
||||
globals.server_url.as_deref(),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub(super) async fn close_command(
|
|||
args: PrCloseArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
close_from(&base, args, github_app).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub(super) async fn create_command(
|
|||
args: PrCreateArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
create_from(&base, args, github_app).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub(super) async fn list_command(
|
|||
args: PrListArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
list_from(store.as_ref(), &base, args, github_app).await
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub(super) async fn merge_command(
|
|||
args: PrMergeArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
merge_from(&base, args, github_app).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::github::build_github_app_credentials;
|
||||
|
||||
pub(crate) async fn dispatch(ns: PrNamespace) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let github_app = build_github_app_credentials(cli_settings.app_id());
|
||||
|
||||
match ns.command {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub(super) async fn view_command(
|
|||
args: PrViewArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
view_from(&base, args, github_app).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@ use std::path::Path;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{
|
||||
ResolveSettingsInput, resolve_settings, resolve_workflow_path, resolve_working_directory,
|
||||
};
|
||||
use fabro_config::{FabroConfig, FabroSettings};
|
||||
use fabro_config::project::{resolve_workflow_path, resolve_working_directory};
|
||||
use fabro_config::{ConfigLayer, FabroSettings};
|
||||
use fabro_graphviz::graph::{Graph, is_llm_handler_type};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_model::{Catalog, Provider};
|
||||
|
|
@ -24,20 +21,17 @@ use crate::shared::github::build_github_app_credentials;
|
|||
|
||||
pub(crate) async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let cli_settings: FabroSettings = cli_defaults.clone().try_into()?;
|
||||
let cli = ConfigLayer::cli()?;
|
||||
let cli_settings: FabroSettings = cli.clone().resolve()?;
|
||||
args.verbose = args.verbose || cli_settings.verbose_enabled();
|
||||
|
||||
let github_app = build_github_app_credentials(cli_settings.app_id());
|
||||
let cli_args_config = FabroConfig::try_from(&args)?;
|
||||
let cli_args_config = ConfigLayer::try_from(&args)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let settings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: args.workflow.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
overrides: cli_args_config,
|
||||
apply_project_config: true,
|
||||
})?;
|
||||
let settings = cli_args_config
|
||||
.combine(ConfigLayer::for_workflow(&args.workflow, &cwd)?)
|
||||
.combine(cli)
|
||||
.resolve()?;
|
||||
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let working_directory = resolve_working_directory(&settings, &cwd);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(None) else {
|
||||
let Ok(cli_settings) = load_cli_settings() else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::ConfigLayer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::{GlobalArgs, RunArgs};
|
||||
|
||||
pub(crate) async fn execute(mut args: RunArgs, _globals: &GlobalArgs) -> Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let cli_settings: fabro_config::FabroSettings = cli_defaults.clone().try_into()?;
|
||||
let cli_settings = ConfigLayer::cli()?.resolve()?;
|
||||
args.verbose = args.verbose || cli_settings.verbose_enabled();
|
||||
|
||||
let quiet = args.detach;
|
||||
let prevent_idle_sleep = cli_settings.prevent_idle_sleep_enabled();
|
||||
let (run_id, run_dir) = super::create::create_run(&args, cli_defaults, styles, quiet)?;
|
||||
let (run_id, run_dir) = super::create::create_run(&args, styles, quiet)?;
|
||||
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = crate::sleep_inhibitor::guard(prevent_idle_sleep);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ enum CopyDirection {
|
|||
|
||||
pub(crate) async fn cp_command(args: CpArgs) -> Result<()> {
|
||||
let direction = parse_direction(&args.src, &args.dst)?;
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
|
||||
match direction {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::args::RunArgs;
|
||||
use fabro_config::project::{ResolveSettingsInput, resolve_settings};
|
||||
use fabro_config::{FabroConfig, FabroSettings};
|
||||
use fabro_config::{ConfigLayer, FabroSettings};
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::error::FabroError;
|
||||
use fabro_workflows::operations::{CreateRunInput, WorkflowInput, create};
|
||||
|
|
@ -14,7 +13,6 @@ use super::output::{print_diagnostics_from_error, print_workflow_report_from_per
|
|||
/// This does NOT execute the workflow — it only prepares the run directory.
|
||||
pub(crate) fn create_run(
|
||||
args: &RunArgs,
|
||||
cli_defaults: FabroConfig,
|
||||
styles: &Styles,
|
||||
quiet: bool,
|
||||
) -> anyhow::Result<(String, PathBuf)> {
|
||||
|
|
@ -22,15 +20,12 @@ pub(crate) fn create_run(
|
|||
.workflow
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("--workflow is required"))?;
|
||||
let cli_args_config = FabroConfig::try_from(args)?;
|
||||
let cli_args_config = ConfigLayer::try_from(args)?;
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let settings: FabroSettings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: workflow_path.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
overrides: cli_args_config,
|
||||
apply_project_config: true,
|
||||
})?;
|
||||
let settings: FabroSettings = cli_args_config
|
||||
.combine(ConfigLayer::for_workflow(workflow_path, &cwd)?)
|
||||
.combine(ConfigLayer::cli()?)
|
||||
.resolve()?;
|
||||
|
||||
let created = match create(CreateRunInput {
|
||||
workflow: WorkflowInput::Path(workflow_path.clone()),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,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(None)?;
|
||||
let cli_settings = cli_config::load_cli_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}"));
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::cli_config::load_cli_settings;
|
|||
|
||||
pub(crate) async fn run(args: DiffArgs) -> Result<()> {
|
||||
info!(run_id = %args.run, "Showing diff");
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use crate::store::{build_store, open_run_reader};
|
|||
|
||||
pub(crate) async fn run(args: &ForkArgs, styles: &Styles) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
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?;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::args::LogsArgs;
|
|||
use crate::cli_config::load_cli_settings;
|
||||
|
||||
pub(crate) async fn run(args: &LogsArgs, styles: &Styles) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use fabro_config::FabroSettingsExt;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
|
||||
|
||||
|
|
@ -35,13 +34,12 @@ 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_defaults = load_cli_config(None)?;
|
||||
let (run_id, _run_dir) = create::create_run(&args, cli_defaults, styles, true)?;
|
||||
let (run_id, _run_dir) = create::create_run(&args, styles, true)?;
|
||||
println!("{run_id}");
|
||||
Ok(())
|
||||
}
|
||||
RunCommands::Start { run } => {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
|
||||
|
|
@ -51,7 +49,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(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
|
||||
|
|
@ -80,7 +78,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(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled())
|
||||
};
|
||||
resume::resume_command(args, styles).await
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||
|
||||
use anyhow::Result;
|
||||
use fabro_config::run::LlmConfig;
|
||||
use fabro_config::{FabroConfig, sandbox as sandbox_config};
|
||||
use fabro_config::{ConfigLayer, sandbox as sandbox_config};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
|
||||
use crate::args::{PreflightArgs, RunArgs};
|
||||
|
|
@ -19,7 +19,7 @@ pub(crate) fn parse_labels(labels: &[String]) -> HashMap<String, String> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
impl TryFrom<&RunArgs> for FabroConfig {
|
||||
impl TryFrom<&RunArgs> for ConfigLayer {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(args: &RunArgs) -> Result<Self, Self::Error> {
|
||||
|
|
@ -61,7 +61,7 @@ impl TryFrom<&RunArgs> for FabroConfig {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&PreflightArgs> for FabroConfig {
|
||||
impl TryFrom<&PreflightArgs> for ConfigLayer {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(args: &PreflightArgs) -> Result<Self, Self::Error> {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::validate_daytona_provider;
|
||||
|
||||
pub(crate) async fn run(args: PreviewArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ pub(crate) async fn resume_command(
|
|||
args: ResumeArgs,
|
||||
styles: &'static Styles,
|
||||
) -> anyhow::Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::store::{build_store, open_run_reader};
|
|||
|
||||
pub(crate) async fn run(args: &RewindArgs, styles: &Styles) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
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?;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::validate_daytona_provider;
|
||||
|
||||
pub(crate) async fn run(args: SshArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::format_duration_ms;
|
||||
|
||||
pub(crate) async fn run(args: &WaitArgs, styles: &Styles) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run_info = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub(crate) struct InspectOutput {
|
|||
}
|
||||
|
||||
pub(crate) async fn run(args: &InspectArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::shared::{color_if, format_duration_ms, tilde_path};
|
|||
use super::short_run_id;
|
||||
|
||||
pub(crate) async fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
let runs = scan_runs_combined(store.as_ref(), &base).await?;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use super::short_run_id;
|
||||
|
||||
pub(crate) async fn remove_command(args: &RunsRemoveArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
remove_from(args, store.as_ref(), &base).await
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::store;
|
||||
|
||||
pub(crate) async fn dump_command(args: &StoreDumpArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
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?;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::format_size;
|
||||
|
||||
pub(super) async fn df_command(args: &DfArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let data_dir = cli_settings.storage_dir();
|
||||
let runs_base_dir = runs_base(&data_dir);
|
||||
let logs_base_dir = logs_base(&data_dir);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::cli_config::load_cli_settings;
|
|||
use crate::shared::format_size;
|
||||
|
||||
pub(super) async fn prune_command(args: &RunsPruneArgs) -> Result<()> {
|
||||
let cli_settings = load_cli_settings(None)?;
|
||||
let cli_settings = load_cli_settings()?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let store = crate::store::build_store(&cli_settings.storage_dir())?;
|
||||
prune_from(args, store.as_ref(), &base).await
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::FabroConfig;
|
||||
use fabro_config::cli::load_cli_config;
|
||||
use fabro_config::project::{ResolveSettingsInput, resolve_settings, resolve_workflow_path};
|
||||
use fabro_config::ConfigLayer;
|
||||
use fabro_config::project::resolve_workflow_path;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_validate::Severity;
|
||||
use fabro_workflows::operations::{ValidateInput, WorkflowInput, validate};
|
||||
|
|
@ -11,14 +10,9 @@ use crate::shared::{print_diagnostics, relative_path};
|
|||
|
||||
pub(crate) fn run(args: &ValidateArgs, styles: &Styles) -> anyhow::Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let cli_defaults = load_cli_config(None)?;
|
||||
let settings = resolve_settings(ResolveSettingsInput {
|
||||
workflow_path: args.workflow.clone(),
|
||||
cwd: cwd.clone(),
|
||||
defaults: cli_defaults,
|
||||
overrides: FabroConfig::default(),
|
||||
apply_project_config: true,
|
||||
})?;
|
||||
let settings = ConfigLayer::for_workflow(&args.workflow, &cwd)?
|
||||
.combine(ConfigLayer::cli()?)
|
||||
.resolve()?;
|
||||
let resolution = resolve_workflow_path(&args.workflow, &cwd)?;
|
||||
let validated = validate(ValidateInput {
|
||||
workflow: WorkflowInput::Path(args.workflow.clone()),
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Err(err) => return (command_name, Err(err)),
|
||||
}
|
||||
} else {
|
||||
match cli_config::load_cli_settings(None) {
|
||||
match cli_config::load_cli_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(None) {
|
||||
match cli_config::load_cli_settings() {
|
||||
Ok(cli_settings) => (
|
||||
cli_settings.log.as_ref().and_then(|l| l.level.clone()),
|
||||
cli_settings.upgrade_check_enabled(),
|
||||
|
|
@ -188,7 +188,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
fabro_server::serve::serve_command(args, styles).await?;
|
||||
}
|
||||
Commands::Doctor { verbose, dry_run } => {
|
||||
let cli_settings = cli_config::load_cli_settings(None)?;
|
||||
let cli_settings = cli_config::load_cli_settings()?;
|
||||
let verbose = verbose || cli_settings.verbose_enabled();
|
||||
let exit_code = commands::doctor::run_doctor(verbose, !dry_run).await;
|
||||
std::process::exit(exit_code);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::FabroConfig;
|
||||
use crate::config::ConfigLayer;
|
||||
|
||||
pub use fabro_types::settings::cli::{
|
||||
ClientTlsSettings, ExecSettings, ExecutionMode, OutputFormat, PermissionLevel, ServerSettings,
|
||||
|
|
@ -72,6 +72,6 @@ impl From<ExecConfig> for ExecSettings {
|
|||
|
||||
/// 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<FabroConfig> {
|
||||
pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
|
||||
crate::load_config_file(path, "cli.toml")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::cli::{ExecConfig, ExecutionMode, ServerConfig};
|
||||
use crate::cli::{self, ExecConfig, ExecutionMode, ServerConfig};
|
||||
use crate::combine::Combine;
|
||||
use crate::hook::{HookConfig, HookDefinition};
|
||||
use crate::mcp::McpServerEntry;
|
||||
use crate::project::ProjectFabroConfig;
|
||||
use crate::project::{self, ProjectFabroConfig};
|
||||
use crate::run::{
|
||||
AssetsConfig, CheckpointConfig, GitHubConfig, LlmConfig, PullRequestConfig, SetupConfig,
|
||||
};
|
||||
use crate::sandbox::SandboxConfig;
|
||||
use crate::server::{ApiConfig, Features, GitConfig, LogConfig, WebConfig};
|
||||
use crate::server::{self, ApiConfig, Features, GitConfig, LogConfig, WebConfig};
|
||||
use crate::settings::FabroSettings;
|
||||
|
||||
fn is_default_checkpoint(c: &CheckpointConfig) -> bool {
|
||||
|
|
@ -25,7 +25,7 @@ fn is_default_checkpoint(c: &CheckpointConfig) -> bool {
|
|||
/// `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)]
|
||||
pub struct FabroConfig {
|
||||
pub struct ConfigLayer {
|
||||
// --- Workflow run config fields ---
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<u32>,
|
||||
|
|
@ -132,7 +132,7 @@ pub struct FabroConfig {
|
|||
pub fabro: Option<ProjectFabroConfig>,
|
||||
}
|
||||
|
||||
impl Combine for FabroConfig {
|
||||
impl Combine for ConfigLayer {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
let hooks = if self.hooks.is_empty() {
|
||||
other.hooks
|
||||
|
|
@ -182,13 +182,58 @@ impl Combine for FabroConfig {
|
|||
}
|
||||
}
|
||||
|
||||
impl FabroConfig {
|
||||
impl ConfigLayer {
|
||||
#[must_use]
|
||||
pub fn combine(self, other: Self) -> Self {
|
||||
Combine::combine(self, other)
|
||||
}
|
||||
|
||||
pub fn try_into_settings(self) -> anyhow::Result<FabroSettings> {
|
||||
/// Load workflow config + project config for a workflow path.
|
||||
///
|
||||
/// Resolves the workflow path, loads its config, discovers project config
|
||||
/// (`fabro.toml`) from the resolved workflow's parent directory, and combines
|
||||
/// them (workflow takes precedence over project).
|
||||
pub fn for_workflow(path: &Path, cwd: &Path) -> anyhow::Result<Self> {
|
||||
let resolution = project::resolve_workflow_path(path, cwd)?;
|
||||
if resolution.workflow_config.is_none() && !resolution.resolved_workflow_path.is_file() {
|
||||
anyhow::bail!(
|
||||
"Workflow not found: {}",
|
||||
resolution.resolved_workflow_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let workflow_config = resolution.workflow_config.unwrap_or_default();
|
||||
let project_config = project::discover_project_config(
|
||||
resolution
|
||||
.resolved_workflow_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new(".")),
|
||||
)?
|
||||
.map(|(_, config)| config)
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(workflow_config.combine(project_config))
|
||||
}
|
||||
|
||||
/// Discover project config (`fabro.toml`) by walking ancestors from `start`.
|
||||
pub fn project(start: &Path) -> anyhow::Result<Self> {
|
||||
Ok(project::discover_project_config(start)?
|
||||
.map(|(_, config)| config)
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Load CLI defaults from `~/.fabro/cli.toml`.
|
||||
pub fn cli() -> anyhow::Result<Self> {
|
||||
cli::load_cli_config(None)
|
||||
}
|
||||
|
||||
/// Load server defaults from `~/.fabro/server.toml`.
|
||||
pub fn server() -> anyhow::Result<Self> {
|
||||
server::load_server_config(None)
|
||||
}
|
||||
|
||||
/// Convert this combined config layer into final resolved settings.
|
||||
pub fn resolve(self) -> anyhow::Result<FabroSettings> {
|
||||
self.try_into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub mod sandbox;
|
|||
pub mod server;
|
||||
pub mod settings;
|
||||
|
||||
pub use config::FabroConfig;
|
||||
pub use config::ConfigLayer;
|
||||
pub use fabro_types::Combine;
|
||||
pub use fabro_util::path::expand_tilde;
|
||||
pub use settings::{FabroSettings, FabroSettingsExt};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use anyhow::{Context, bail};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::FabroSettings;
|
||||
use crate::config::FabroConfig;
|
||||
use crate::config::ConfigLayer;
|
||||
use crate::run;
|
||||
pub use fabro_types::settings::project::ProjectFabroSettings;
|
||||
|
||||
|
|
@ -23,20 +23,11 @@ pub struct ProjectFabroConfig {
|
|||
pub struct WorkflowPathResolution {
|
||||
pub resolved_workflow_path: PathBuf,
|
||||
pub dot_path: PathBuf,
|
||||
pub workflow_config: Option<FabroConfig>,
|
||||
pub workflow_config: Option<ConfigLayer>,
|
||||
pub workflow_toml_path: Option<PathBuf>,
|
||||
pub workflow_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ResolveSettingsInput {
|
||||
pub workflow_path: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
pub defaults: FabroConfig,
|
||||
pub overrides: FabroConfig,
|
||||
pub apply_project_config: bool,
|
||||
}
|
||||
|
||||
fn default_root() -> String {
|
||||
".".to_string()
|
||||
}
|
||||
|
|
@ -50,8 +41,8 @@ impl From<ProjectFabroConfig> for ProjectFabroSettings {
|
|||
}
|
||||
|
||||
/// Parse a project config from a TOML string.
|
||||
pub fn parse_project_config(content: &str) -> anyhow::Result<FabroConfig> {
|
||||
let config: FabroConfig = toml::from_str(content).context("Failed to parse project config")?;
|
||||
pub fn parse_project_config(content: &str) -> anyhow::Result<ConfigLayer> {
|
||||
let config: ConfigLayer = toml::from_str(content).context("Failed to parse project config")?;
|
||||
let version = config.version.unwrap_or(0);
|
||||
if version != SUPPORTED_VERSION {
|
||||
bail!(
|
||||
|
|
@ -62,7 +53,7 @@ pub fn parse_project_config(content: &str) -> anyhow::Result<FabroConfig> {
|
|||
}
|
||||
|
||||
/// Load a project config from a file path.
|
||||
pub fn load_project_config(path: &Path) -> anyhow::Result<FabroConfig> {
|
||||
pub fn load_project_config(path: &Path) -> anyhow::Result<ConfigLayer> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
let config = parse_project_config(&content)?;
|
||||
|
|
@ -77,7 +68,7 @@ pub fn load_project_config(path: &Path) -> anyhow::Result<FabroConfig> {
|
|||
|
||||
/// Walk ancestor directories from `start` looking for `fabro.toml`.
|
||||
/// Returns the config file path and parsed config, or `None` if not found.
|
||||
pub fn discover_project_config(start: &Path) -> anyhow::Result<Option<(PathBuf, FabroConfig)>> {
|
||||
pub fn discover_project_config(start: &Path) -> anyhow::Result<Option<(PathBuf, ConfigLayer)>> {
|
||||
for ancestor in start.ancestors() {
|
||||
let candidate = ancestor.join(CONFIG_FILENAME);
|
||||
if candidate.is_file() {
|
||||
|
|
@ -192,36 +183,6 @@ pub fn resolve_working_directory(settings: &FabroSettings, caller_cwd: &Path) ->
|
|||
}
|
||||
}
|
||||
|
||||
pub fn resolve_settings(input: ResolveSettingsInput) -> anyhow::Result<FabroSettings> {
|
||||
let resolution = resolve_workflow_path(&input.workflow_path, &input.cwd)?;
|
||||
if resolution.workflow_config.is_none() && !resolution.resolved_workflow_path.is_file() {
|
||||
anyhow::bail!(
|
||||
"Workflow not found: {}",
|
||||
resolution.resolved_workflow_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let project_config = if input.apply_project_config {
|
||||
discover_project_config(
|
||||
resolution
|
||||
.resolved_workflow_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new(".")),
|
||||
)?
|
||||
.map(|(_, config)| config)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
FabroConfig::default()
|
||||
};
|
||||
|
||||
input
|
||||
.overrides
|
||||
.combine(resolution.workflow_config.unwrap_or_default())
|
||||
.combine(project_config)
|
||||
.combine(input.defaults)
|
||||
.try_into()
|
||||
}
|
||||
|
||||
fn resolve_workflow_arg_from(arg: &Path, start_dir: &Path) -> anyhow::Result<PathBuf> {
|
||||
resolve_workflow_arg_impl(arg, start_dir, user_workflows_dir().as_deref())
|
||||
}
|
||||
|
|
@ -414,7 +375,7 @@ fn find_closest_match(input: &str, candidates: &[String]) -> Option<String> {
|
|||
///
|
||||
/// Calls `resolve_workflow_arg` first, then if the result is a `.toml` file,
|
||||
/// loads the run config and resolves the graph path within it.
|
||||
pub fn resolve_workflow(arg: &Path) -> anyhow::Result<(PathBuf, Option<FabroConfig>)> {
|
||||
pub fn resolve_workflow(arg: &Path) -> anyhow::Result<(PathBuf, Option<ConfigLayer>)> {
|
||||
let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let resolution = resolve_workflow_path(arg, &start)?;
|
||||
Ok((resolution.dot_path, resolution.workflow_config))
|
||||
|
|
@ -437,7 +398,7 @@ pub fn is_retro_enabled() -> bool {
|
|||
|
||||
/// Resolve the fabro root directory from a config file path and its config.
|
||||
/// The returned path is the directory containing `fabro.toml` joined with the `root` value.
|
||||
pub fn resolve_fabro_root(config_path: &Path, config: &FabroConfig) -> PathBuf {
|
||||
pub fn resolve_fabro_root(config_path: &Path, config: &ConfigLayer) -> PathBuf {
|
||||
let project_dir = config_path
|
||||
.parent()
|
||||
.expect("config_path should have a parent directory");
|
||||
|
|
@ -596,7 +557,7 @@ model = "claude-sonnet-4-6"
|
|||
#[test]
|
||||
fn resolve_fabro_root_with_subdirectory() {
|
||||
let config_path = Path::new("/repo/fabro.toml");
|
||||
let config = FabroConfig {
|
||||
let config = ConfigLayer {
|
||||
version: Some(1),
|
||||
fabro: Some(ProjectFabroConfig {
|
||||
root: Some("fabro/".to_string()),
|
||||
|
|
@ -613,7 +574,7 @@ model = "claude-sonnet-4-6"
|
|||
#[test]
|
||||
fn resolve_fabro_root_with_dot() {
|
||||
let config_path = Path::new("/repo/fabro.toml");
|
||||
let config = FabroConfig {
|
||||
let config = ConfigLayer {
|
||||
version: Some(1),
|
||||
fabro: Some(ProjectFabroConfig {
|
||||
root: Some(".".to_string()),
|
||||
|
|
@ -630,13 +591,92 @@ model = "claude-sonnet-4-6"
|
|||
#[test]
|
||||
fn resolve_fabro_root_without_fabro_section() {
|
||||
let config_path = Path::new("/repo/fabro.toml");
|
||||
let config = FabroConfig::default();
|
||||
let config = ConfigLayer::default();
|
||||
assert_eq!(
|
||||
resolve_fabro_root(config_path, &config),
|
||||
Path::new("/repo/.")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_workflow_discovers_project_from_workflow_location() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let project_dir = tmp.path().join("project");
|
||||
let other_dir = tmp.path().join("other");
|
||||
let workflow_dir = project_dir.join("workflows").join("demo");
|
||||
fs::create_dir_all(&workflow_dir).unwrap();
|
||||
fs::create_dir_all(&other_dir).unwrap();
|
||||
|
||||
fs::write(
|
||||
project_dir.join("fabro.toml"),
|
||||
"version = 1\nverbose = true\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
other_dir.join("fabro.toml"),
|
||||
"version = 1\nverbose = false\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(workflow_dir.join("workflow.toml"), "version = 1\n").unwrap();
|
||||
|
||||
let layer =
|
||||
ConfigLayer::for_workflow(&workflow_dir.join("workflow.toml"), &other_dir).unwrap();
|
||||
|
||||
assert_eq!(layer.verbose, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chained_resolve_preserves_precedence_order() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let project_dir = tmp.path().join("project");
|
||||
let workflow_dir = project_dir.join("workflows").join("demo");
|
||||
fs::create_dir_all(&workflow_dir).unwrap();
|
||||
|
||||
fs::write(
|
||||
project_dir.join("fabro.toml"),
|
||||
"version = 1\nverbose = true\n[llm]\nmodel = \"project-model\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
workflow_dir.join("workflow.toml"),
|
||||
"version = 1\ndry_run = true\n[llm]\nmodel = \"workflow-model\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let cli_defaults = ConfigLayer {
|
||||
verbose: Some(false),
|
||||
llm: Some(crate::run::LlmConfig {
|
||||
model: Some("cli-model".to_string()),
|
||||
provider: None,
|
||||
fallbacks: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let overrides = ConfigLayer {
|
||||
dry_run: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let settings = overrides
|
||||
.combine(
|
||||
ConfigLayer::for_workflow(
|
||||
&workflow_dir.join("workflow.toml"),
|
||||
project_dir.as_path(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.combine(cli_defaults)
|
||||
.resolve()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
settings.llm.as_ref().and_then(|llm| llm.model.as_deref()),
|
||||
Some("workflow-model")
|
||||
);
|
||||
assert_eq!(settings.dry_run, Some(false));
|
||||
assert_eq!(settings.verbose, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_workflow_arg_toml_extension_returned_as_is() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||
use tracing::debug;
|
||||
|
||||
use crate::combine::Combine;
|
||||
use crate::config::FabroConfig;
|
||||
use crate::config::ConfigLayer;
|
||||
use crate::sandbox::DockerfileSource;
|
||||
pub use fabro_types::settings::run::{
|
||||
AssetsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy,
|
||||
|
|
@ -129,7 +129,7 @@ impl From<SetupConfig> for SetupSettings {
|
|||
/// `${env.VARNAME}` references in `[sandbox.env]` are NOT resolved here —
|
||||
/// call [`resolve_sandbox_env`] separately after snapshotting, so that
|
||||
/// plaintext secrets are never written to disk.
|
||||
pub fn load_run_config(path: &Path) -> anyhow::Result<FabroConfig> {
|
||||
pub fn load_run_config(path: &Path) -> anyhow::Result<ConfigLayer> {
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
let mut config = parse_run_config(&contents)?;
|
||||
|
|
@ -144,7 +144,7 @@ pub fn load_run_config(path: &Path) -> anyhow::Result<FabroConfig> {
|
|||
///
|
||||
/// Only whole-value references are supported (no partial interpolation).
|
||||
/// Missing host env vars produce a hard error.
|
||||
pub fn resolve_sandbox_env(config: &mut FabroConfig) -> anyhow::Result<()> {
|
||||
pub fn resolve_sandbox_env(config: &mut ConfigLayer) -> anyhow::Result<()> {
|
||||
if let Some(env) = config.sandbox.as_mut().and_then(|s| s.env.as_mut()) {
|
||||
resolve_env_refs(env)?;
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ pub fn resolve_env_refs(env: &mut HashMap<String, String>) -> anyhow::Result<()>
|
|||
|
||||
/// If the config contains a `dockerfile = { path = "..." }`, read the file
|
||||
/// and replace it with `DockerfileSource::Inline(contents)`.
|
||||
fn resolve_dockerfile(config: &mut FabroConfig, config_dir: &Path) -> anyhow::Result<()> {
|
||||
fn resolve_dockerfile(config: &mut ConfigLayer, config_dir: &Path) -> anyhow::Result<()> {
|
||||
let source = config
|
||||
.sandbox
|
||||
.as_mut()
|
||||
|
|
@ -204,8 +204,8 @@ pub fn resolve_graph_path(toml_path: &Path, graph: &str) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn parse_run_config(contents: &str) -> anyhow::Result<FabroConfig> {
|
||||
let mut config: FabroConfig =
|
||||
pub fn parse_run_config(contents: &str) -> anyhow::Result<ConfigLayer> {
|
||||
let mut config: ConfigLayer =
|
||||
toml::from_str(contents).context("Failed to parse run config TOML")?;
|
||||
|
||||
if config.graph.is_none() {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::FabroConfig;
|
||||
use crate::config::ConfigLayer;
|
||||
use crate::settings::{FabroSettings, FabroSettingsExt};
|
||||
pub use fabro_types::settings::server::{
|
||||
ApiAuthStrategy, ApiSettings, AuthProvider, AuthSettings, FeaturesSettings, GitAuthorSettings,
|
||||
|
|
@ -181,7 +181,7 @@ impl From<LogConfig> for LogSettings {
|
|||
|
||||
/// Load server config from an explicit path or `~/.fabro/server.toml`, returning defaults if the
|
||||
/// default file doesn't exist. An explicit path that doesn't exist is an error.
|
||||
pub fn load_server_config(path: Option<&Path>) -> anyhow::Result<FabroConfig> {
|
||||
pub fn load_server_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
|
||||
crate::load_config_file(path, "server.toml")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||
|
||||
pub use fabro_types::settings::FabroSettings;
|
||||
|
||||
use crate::config::FabroConfig;
|
||||
use crate::config::ConfigLayer;
|
||||
|
||||
pub trait FabroSettingsExt {
|
||||
fn storage_dir(&self) -> PathBuf;
|
||||
|
|
@ -18,10 +18,10 @@ impl FabroSettingsExt for FabroSettings {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FabroConfig> for FabroSettings {
|
||||
impl TryFrom<ConfigLayer> for FabroSettings {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: FabroConfig) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: ConfigLayer) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
version: value.version,
|
||||
goal: value.goal,
|
||||
|
|
@ -60,10 +60,10 @@ impl TryFrom<FabroConfig> for FabroSettings {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&FabroConfig> for FabroSettings {
|
||||
impl TryFrom<&ConfigLayer> for FabroSettings {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &FabroConfig) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: &ConfigLayer) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue