Rename config show command to settings

This commit is contained in:
Bryan Helmkamp 2026-03-30 11:28:02 -04:00
parent 9da45b319e
commit 74b2c5c889
9 changed files with 90 additions and 133 deletions

View file

@ -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`

View file

@ -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`.

View file

@ -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<PathBuf>,
}
@ -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)]

View file

@ -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<FabroSettings> {
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') {

View file

@ -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"

View file

@ -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] <COMMAND>
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_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] <COMMAND>
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

View file

@ -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

View file

@ -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

View file

@ -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
}