diff --git a/docs/changelog/2026-03-27.mdx b/docs/changelog/2026-03-27.mdx index e833e63dd..ae25d3600 100644 --- a/docs/changelog/2026-03-27.mdx +++ b/docs/changelog/2026-03-27.mdx @@ -1,14 +1,14 @@ --- -title: "fabro config show, preflight subcommand, and storage-dir" +title: "fabro settings, preflight subcommand, and storage-dir" date: "2026-03-27" --- -## `fabro config show` +## `fabro settings` -A new `fabro config show` command displays the fully resolved configuration for a workflow, showing how CLI flags, environment variables, project config, and workflow defaults are layered together. This makes it easy to debug configuration issues without guessing which layer is winning. +A new `fabro settings` command displays the fully resolved configuration for a workflow, showing how CLI flags, environment variables, project config, and workflow defaults are layered together. This makes it easy to debug configuration issues without guessing which layer is winning. ```bash -fabro config show my-workflow +fabro settings my-workflow ``` ## `fabro preflight` diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index f42568e51..e2ffe22b8 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -37,14 +37,14 @@ CLI flags always override `user.toml` values, which override hardcoded defaults. --- -## `fabro config show` +## `fabro settings` Print the merged resolved configuration as YAML. ```bash -fabro config show -fabro config show demo -fabro config show run.toml +fabro settings +fabro settings demo +fabro settings run.toml ``` With no argument, Fabro prints the merged ambient defaults from `~/.fabro/user.toml` and the nearest `fabro.toml`. diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 82ebb3422..0cecf5414 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -558,7 +558,7 @@ pub(crate) struct DfArgs { } #[derive(Args)] -pub(crate) struct ConfigShowArgs { +pub(crate) struct SettingsArgs { /// Optional workflow name, .fabro path, or .toml run config to overlay pub(crate) workflow: Option, } @@ -794,7 +794,7 @@ pub(crate) enum Commands { /// Manage secrets in ~/.fabro/.env Secret(SecretNamespace), /// Inspect merged configuration - Config(ConfigNamespace), + Settings(SettingsArgs), /// Workflow operations Workflow(WorkflowNamespace), /// Open the Discord community in the browser @@ -871,9 +871,7 @@ impl Commands { SecretCommand::Rm(_) => "secret rm", SecretCommand::Set(_) => "secret set", }, - Self::Config(ns) => match &ns.command { - ConfigCommand::Show(_) => "config show", - }, + Self::Settings(_) => "settings", Self::Workflow(ns) => match &ns.command { WorkflowCommand::List(_) => "workflow list", WorkflowCommand::Create(_) => "workflow create", @@ -962,18 +960,6 @@ pub(crate) enum SecretCommand { Set(SecretSetArgs), } -#[derive(Args)] -pub(crate) struct ConfigNamespace { - #[command(subcommand)] - pub(crate) command: ConfigCommand, -} - -#[derive(Subcommand)] -pub(crate) enum ConfigCommand { - /// Print the merged FabroSettings as YAML - Show(ConfigShowArgs), -} - #[derive(Args)] pub(crate) struct SystemNamespace { #[command(subcommand)] diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index 38d54746d..009c74f8b 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -1,16 +1,10 @@ use std::io::Write; use std::path::Path; -use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs, GlobalArgs}; +use crate::args::{GlobalArgs, SettingsArgs}; use crate::user_config; use fabro_config::{ConfigLayer, FabroSettings}; -pub(crate) fn dispatch(ns: ConfigNamespace, globals: &GlobalArgs) -> anyhow::Result<()> { - match ns.command { - ConfigCommand::Show(args) => show_command(&args, globals), - } -} - fn merged_config(workflow: Option<&Path>, globals: &GlobalArgs) -> anyhow::Result { let cwd = std::env::current_dir()?; let base = match workflow { @@ -22,7 +16,7 @@ fn merged_config(workflow: Option<&Path>, globals: &GlobalArgs) -> anyhow::Resul base.combine(cli).resolve() } -pub(crate) fn show_command(args: &ConfigShowArgs, globals: &GlobalArgs) -> anyhow::Result<()> { +pub(crate) fn execute(args: &SettingsArgs, globals: &GlobalArgs) -> anyhow::Result<()> { let config = merged_config(args.workflow.as_deref(), globals)?; let mut yaml = serde_yaml::to_string(&config)?; if !yaml.ends_with('\n') { diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index ee2731d25..30dc7e1af 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -210,7 +210,7 @@ async fn main_inner() -> (String, Result<()>) { } Commands::Pr(ns) => commands::pr::dispatch(ns, &globals).await?, Commands::Secret(ns) => commands::secret::dispatch(ns)?, - Commands::Config(ns) => commands::config::dispatch(ns, &globals)?, + Commands::Settings(args) => commands::config::execute(&args, &globals)?, Commands::Workflow(ns) => commands::workflow::dispatch(ns)?, Commands::Skill(ns) => commands::skill::dispatch(ns)?, Commands::Upgrade(args) => { @@ -245,10 +245,7 @@ async fn main_inner() -> (String, Result<()>) { #[cfg(test)] mod tests { use super::*; - use args::{ - ConfigCommand, ConfigNamespace, ProviderCommand, ProviderNamespace, StoreCommand, - StoreNamespace, - }; + use args::{ProviderCommand, ProviderNamespace, StoreCommand, StoreNamespace}; use clap::Parser; #[test] @@ -456,13 +453,11 @@ mod tests { } #[test] - fn parse_config_show_command() { - let cli = Cli::try_parse_from(["fabro", "config", "show"]).expect("should parse"); - assert_eq!(cli.command.name(), "config show"); + fn parse_settings_command() { + let cli = Cli::try_parse_from(["fabro", "settings"]).expect("should parse"); + assert_eq!(cli.command.name(), "settings"); match *cli.command { - Commands::Config(ConfigNamespace { - command: ConfigCommand::Show(args), - }) => { + Commands::Settings(args) => { assert!(args.workflow.is_none()); } _ => panic!("unexpected command variant"), @@ -470,12 +465,10 @@ mod tests { } #[test] - fn parse_config_show_with_workflow() { - let cli = Cli::try_parse_from(["fabro", "config", "show", "demo"]).expect("should parse"); + fn parse_settings_with_workflow() { + let cli = Cli::try_parse_from(["fabro", "settings", "demo"]).expect("should parse"); match *cli.command { - Commands::Config(ConfigNamespace { - command: ConfigCommand::Show(args), - }) => { + Commands::Settings(args) => { assert_eq!(args.workflow, Some(std::path::PathBuf::from("demo"))); } _ => panic!("unexpected command variant"), @@ -484,23 +477,21 @@ mod tests { #[test] fn parse_quiet_flag() { - let cli = - Cli::try_parse_from(["fabro", "--quiet", "config", "show"]).expect("should parse"); + let cli = Cli::try_parse_from(["fabro", "--quiet", "settings"]).expect("should parse"); assert!(cli.globals.quiet); assert!(!cli.globals.verbose); } #[test] fn parse_verbose_flag() { - let cli = - Cli::try_parse_from(["fabro", "--verbose", "config", "show"]).expect("should parse"); + let cli = Cli::try_parse_from(["fabro", "--verbose", "settings"]).expect("should parse"); assert!(!cli.globals.quiet); assert!(cli.globals.verbose); } #[test] fn quiet_and_verbose_conflict() { - let result = Cli::try_parse_from(["fabro", "--quiet", "--verbose", "config", "show"]); + let result = Cli::try_parse_from(["fabro", "--quiet", "--verbose", "settings"]); assert!( result.is_err(), "should fail when both --quiet and --verbose" diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs index 182f74471..469bed21f 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs @@ -10,7 +10,7 @@ use predicates::prelude::*; #[test] fn help() { let context = test_context!(); - let mut cmd = context.config(); + let mut cmd = context.settings(); cmd.arg("--help"); fabro_snapshot!(context.filters(), cmd, @" success: true @@ -18,35 +18,7 @@ fn help() { ----- stdout ----- Inspect merged configuration - Usage: fabro config [OPTIONS] - - Commands: - show Print the merged FabroSettings as YAML - help Print this message or the help of the given subcommand(s) - - Options: - --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] - --quiet Suppress non-essential output [env: FABRO_QUIET=] - --verbose Enable verbose output [env: FABRO_VERBOSE=] - --storage-dir Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]] - -h, --help Print help - ----- stderr ----- - "); -} - -#[test] -fn show_help() { - let context = test_context!(); - let mut cmd = context.config(); - cmd.args(["show", "--help"]); - fabro_snapshot!(context.filters(), cmd, @" - success: true - exit_code: 0 - ----- stdout ----- - Print the merged FabroSettings as YAML - - Usage: fabro config show [OPTIONS] [WORKFLOW] + Usage: fabro settings [OPTIONS] [WORKFLOW] Arguments: [WORKFLOW] Optional workflow name, .fabro path, or .toml run config to overlay @@ -62,17 +34,35 @@ fn show_help() { "); } +#[test] +fn old_config_show_command_is_rejected() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["config", "show"]); + fabro_snapshot!(context.filters(), cmd, @" + success: false + exit_code: 2 + ----- stdout ----- + ----- stderr ----- + error: unrecognized subcommand 'config' + + Usage: fabro [OPTIONS] + + For more information, try '--help'. + "); +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -fn parse_config_show(stdout: &[u8]) -> FabroSettings { +fn parse_settings(stdout: &[u8]) -> FabroSettings { serde_yaml::from_slice(stdout).expect("stdout should be valid YAML FabroSettings") } -/// Set up home config and project config for config show tests. +/// Set up home config and project config for settings command tests. /// Uses `context.home_dir` for the home directory. Returns project tempdir. -fn setup_config_show_fixture(context: &fabro_test::TestContext) -> tempfile::TempDir { +fn setup_settings_fixture(context: &fabro_test::TestContext) -> tempfile::TempDir { context.write_home( ".fabro/user.toml", r#" @@ -262,21 +252,20 @@ commands = ["workflow-setup"] // --------------------------------------------------------------------------- #[test] -fn config_show_merges_cli_and_project_defaults() { +fn settings_merges_cli_and_project_defaults() { let context = test_context!(); - let project = setup_config_show_fixture(&context); + let project = setup_settings_fixture(&context); let output = context - .command() + .settings() .current_dir(project.path()) - .args(["config", "show"]) .assert() .success() .get_output() .stdout .clone(); - let cfg = parse_config_show(&output); + let cfg = parse_settings(&output); let llm = cfg.llm.as_ref().expect("llm config"); assert_eq!(llm.model.as_deref(), Some("project-model")); assert_eq!(llm.provider.as_deref(), Some("openai")); @@ -299,21 +288,21 @@ fn config_show_merges_cli_and_project_defaults() { } #[test] -fn config_show_workflow_name_applies_run_overlay_and_deep_merges() { +fn settings_workflow_name_applies_run_overlay_and_deep_merges() { let context = test_context!(); - let project = setup_config_show_fixture(&context); + let project = setup_settings_fixture(&context); let output = context - .command() + .settings() .current_dir(project.path()) - .args(["config", "show", "demo"]) + .args(["demo"]) .assert() .success() .get_output() .stdout .clone(); - let cfg = parse_config_show(&output); + let cfg = parse_settings(&output); let llm = cfg.llm.as_ref().expect("llm config"); assert_eq!(cfg.goal.as_deref(), Some("demo goal")); assert_eq!(llm.model.as_deref(), Some("run-model")); @@ -375,7 +364,7 @@ fn config_show_workflow_name_applies_run_overlay_and_deep_merges() { } #[test] -fn config_show_explicit_workflow_path_uses_workflow_project_layers() { +fn settings_explicit_workflow_path_uses_workflow_project_layers() { let context = test_context!(); let (project, _storage_dir) = setup_external_workflow_fixture(&context); let cwd = tempfile::tempdir().unwrap(); @@ -383,17 +372,17 @@ fn config_show_explicit_workflow_path_uses_workflow_project_layers() { // Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from user.toml let output = context - .command() + .settings() .env_remove("FABRO_STORAGE_DIR") .current_dir(cwd.path()) - .args(["config", "show", workflow.to_str().unwrap()]) + .args([workflow.to_str().unwrap()]) .assert() .success() .get_output() .stdout .clone(); - let cfg = parse_config_show(&output); + let cfg = parse_settings(&output); assert_eq!(cfg.auto_approve, Some(true)); assert_eq!( cfg.setup.as_ref().expect("setup config").commands, @@ -474,40 +463,39 @@ fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() { } #[test] -fn config_show_fabro_path_matches_ambient_defaults() { +fn settings_fabro_path_matches_ambient_defaults() { let context = test_context!(); - let project = setup_config_show_fixture(&context); + let project = setup_settings_fixture(&context); let ambient = context - .command() + .settings() .current_dir(project.path()) - .args(["config", "show"]) .assert() .success() .get_output() .stdout .clone(); let graph = context - .command() + .settings() .current_dir(project.path()) - .args(["config", "show", "standalone.fabro"]) + .args(["standalone.fabro"]) .assert() .success() .get_output() .stdout .clone(); - assert_eq!(parse_config_show(&graph), parse_config_show(&ambient)); + assert_eq!(parse_settings(&graph), parse_settings(&ambient)); } #[test] -fn config_show_missing_run_config_errors() { +fn settings_missing_run_config_errors() { let context = test_context!(); - let project = setup_config_show_fixture(&context); + let project = setup_settings_fixture(&context); - let mut cmd = context.command(); + let mut cmd = context.settings(); cmd.current_dir(project.path()); - cmd.args(["config", "show", "missing.toml"]); + cmd.args(["missing.toml"]); fabro_snapshot!(context.filters(), cmd, @" success: false exit_code: 1 @@ -518,7 +506,7 @@ fn config_show_missing_run_config_errors() { } #[test] -fn config_show_legacy_cli_config_warns_and_ignores_it() { +fn settings_legacy_cli_config_warns_and_ignores_it() { let context = test_context!(); let project = tempfile::tempdir().unwrap(); @@ -533,23 +521,22 @@ model = "legacy-model" ); let assert = context - .command() + .settings() .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); + let cfg = parse_settings(&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() { +fn settings_user_config_wins_over_legacy_cli_config() { let context = test_context!(); - let project = setup_config_show_fixture(&context); + let project = setup_settings_fixture(&context); context.write_home( ".fabro/cli.toml", r#" @@ -562,14 +549,13 @@ shared = "legacy" ); let assert = context - .command() + .settings() .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 cfg = parse_settings(&assert.get_output().stdout); let llm = cfg.llm.as_ref().expect("llm config"); assert_eq!(llm.model.as_deref(), Some("project-model")); assert_eq!( @@ -582,9 +568,9 @@ shared = "legacy" #[test] #[cfg(feature = "server")] -fn config_show_server_url_overrides_cli_defaults() { +fn settings_server_url_overrides_cli_defaults() { let context = test_context!(); - let project = setup_config_show_fixture(&context); + let project = setup_settings_fixture(&context); let user_toml_path = context.home_dir.join(".fabro/user.toml"); let existing = std::fs::read_to_string(&user_toml_path).unwrap(); context.write_home( @@ -597,14 +583,14 @@ fn config_show_server_url_overrides_cli_defaults() { let output = context .command() .current_dir(project.path()) - .args(["--server-url", "https://cli.example.com", "config", "show"]) + .args(["--server-url", "https://cli.example.com", "settings"]) .assert() .success() .get_output() .stdout .clone(); - let cfg = parse_config_show(&output); + let cfg = parse_settings(&output); assert_eq!(cfg.mode, Some(ExecutionMode::Server)); assert_eq!( cfg.server diff --git a/lib/crates/fabro-cli/tests/it/cmd/config_show.rs b/lib/crates/fabro-cli/tests/it/cmd/config_show.rs index c4aa2aaea..ae2df16a7 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/config_show.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/config_show.rs @@ -3,15 +3,15 @@ use fabro_test::{fabro_snapshot, test_context}; #[test] fn help() { let context = test_context!(); - let mut cmd = context.command(); - cmd.args(["config", "show", "--help"]); + let mut cmd = context.settings(); + cmd.arg("--help"); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- - Print the merged FabroSettings as YAML + Inspect merged configuration - Usage: fabro config show [OPTIONS] [WORKFLOW] + Usage: fabro settings [OPTIONS] [WORKFLOW] Arguments: [WORKFLOW] Optional workflow name, .fabro path, or .toml run config to overlay diff --git a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs index 1734240b9..92aad0671 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs @@ -36,7 +36,7 @@ fn help() { install Set up the Fabro environment (LLMs, certs, GitHub) pr Pull request operations secret Manage secrets in ~/.fabro/.env - config Inspect merged configuration + settings Inspect merged configuration workflow Workflow operations discord Open the Discord community in the browser docs Open the docs website in the browser diff --git a/lib/crates/fabro-test/src/lib.rs b/lib/crates/fabro-test/src/lib.rs index 4f583c287..7f1f5e57c 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -144,10 +144,10 @@ impl TestContext { cmd } - /// Build a `config` subcommand. - pub fn config(&self) -> Command { + /// Build a `settings` subcommand. + pub fn settings(&self) -> Command { let mut cmd = self.command(); - cmd.arg("config"); + cmd.arg("settings"); cmd }