Implementation notes β Flag-to-config binding¶
Spec: 2026-06-12-flag-to-config-binding (status: IMPLEMENTED).
What was implemented¶
D1 β WithBoundFlags + convention helper (pkg/cmd/root/options.go)¶
- New
RootOptionfunctional-option type and an extensible constructorNewCmdRootWithOptions(props, opts ...RootOption). The existingNewCmdRootandNewCmdRootWithConfigare now thin wrappers over it, so all existing call sites and the generator's AST injection intoNewCmdRootare unchanged. WithBoundFlags(map[string]*pflag.Flag)β explicit config-key β flag map.WithConventionBoundFlags(*pflag.FlagSet)β derives keys from flag names via the hyphen-to-dot convention.ConventionKey(name)β exportedstrings.ReplaceAll(name, "-", ".")so downstreams can compute the same keys.WithSubcommandsandWithConfigPathswere added so the wrapper constructors compose cleanly through options.- Both flag options register the supplied flags onto the root command's persistent flag set (so cobra parses them) in addition to recording them for binding. This is the key ergonomic decision β see deviations below.
D2 β Bind during config load, changed flags only (pkg/cmd/root/root.go)¶
- Binding happens in
newRootPreRunE, immediately afterprops.Config = cfg(i.e. after the file + env layers are established) via the newbindCommandFlagshelper. Viper'sBindPFlagsits aboveAutomaticEnv, so no custom precedence logic was needed. bindChangedFlagsfilters byflag.Changed, so a flag at its default never clobbers config. Bind errors are logged at debug and skipped (one bad flag never aborts startup).
D3 β Built-ins folded through the same path¶
--ciand--debugare bound to config keysci/debugthrough the samebindChangedFlagspath (builtinBoundFlags).Config.GetBool("ci")now reflects--ci.--debug's immediate log-level effect is preserved:configureLoggingstill readsflags.DebugfromextractFlagsindependently of binding. Only config-visibility changed, exactly as the spec required.
D4 β Docs reconciled¶
docs/concepts/config.mdpreviously listed env > .env > flags > files. Corrected to the canonical flags > env > file > embedded > defaults, with a new "Binding Flags to Config" subsection.docs/components/config.mdalready documented flags-first; added a "Binding CLI flags to config" section, theBindPFlagmethod in both interface listings, and replaced the manualif dbHost == ""example narrative.- New how-to:
docs/how-to/bind-flags-to-config.md(linked fromdocs/how-to/index.md).
D5 β Per-command binding¶
bindCommandFlagsbinds the dispatched command's local flags (cmd.LocalFlags()) by the hyphen-to-dot convention, filtered byChanged. Inherited persistent flags are skipped (the root binds its own). The built-in keys are excluded from convention binding to avoid double-mapping.- The root
PersistentPreRunEruns for the command's own subtree, so per-command binding works on plainmainwithout depending on the unmerged cobra-traverse-hooks change.
Container API (pkg/config/container.go)¶
- Added
BindPFlag(key string, flag *pflag.Flag) errorto theContainableinterface and*Container. It routes throughresolverViper()andqualifyKey(), so binding works correctly onSub()containers (env-aware delegation preserved). Mock regenerated (mocks/pkg/config/Containable.go).
Deviations from the spec (and why)¶
- New constructor instead of changing
NewCmdRoot's signature. The spec's illustrative shape implied options onNewCmdRoot.NewCmdRootalready takes variadic*setup.Command, and the generator injects subcommands into that call by AST. To avoid a breaking signature change and generator churn, options live on a newNewCmdRootWithOptions; the old constructors delegate to it. - Bound-flag options also register the flags on the root command. Binding
needs the same
*pflag.Flagobject cobra parses. Requiring authors to both register a flag and pass its pointer separately is error-prone, so the options callPersistentFlags().AddFlagfor any supplied flag not already present. This makes the one-call ergonomic correct by construction. - Per-command binding uses
LocalFlags()+ convention only (no per-command explicit map option in this pass). The spec's D5 calls for per-command binding filtered byChanged; convention binding of a command's own flags satisfies the stated example (serve --server-port) with zero boilerplate. An explicit per-command map can be layered on later if a downstream needs non-conventional keys (see open questions).
Verification¶
- New tests:
pkg/config/bindflag_test.go(container),pkg/cmd/root/bindflag_test.go(E2E throughPersistentPreRunE: explicit, convention, precedence flag>env>file, built-ins, per-command),pkg/cmd/root/options_internal_test.go(option/helper edge cases).options.gohelpers at 100% coverage. just testgreen;just test-racegreen;golangci-lint runon the changed packages (pkg/cmd/root,pkg/config,mocks/pkg/config) reports 0 issues.- NOTE: a full-module
just lintfails on pre-existing wsl_v5 findings in committed mock files (and leakage from a sibling git worktree in this environment); this is unrelated to and not introduced by this change β the baseline (git stashof this branch) fails identically.
Open questions for review¶
- Built-in key names.
--ci/--debugbind to bare keysci/debug. If a downstream nests these (e.g.runtime.ci), the binding won't reach them. Acceptable? The previous behaviour read barecifrom config, so this is consistent. - Convention mapping of dotted flag names. A flag literally named with a dot
(
--a.b) maps verbatim to keya.b. We map by hyphenβdot only; dots are not themselves transformed. Documented as "avoid dots in flag names". Is an explicit reject/warn warranted instead of silent verbatim mapping? - Per-command explicit binding. Only convention-based per-command binding
shipped (deviation 3). Do we want a
WithCommandBoundFlags(cmd, map)style option for non-conventional per-command keys, or is convention sufficient? - Root local flags via convention. When the root command runs with no
subcommand, its local flags (
--config,--output) are convention-bound when changed (--outputβoutput,--configβconfig). Bindingoutputis useful; bindingconfig(a[]string) is harmless but unused. Leave as-is, or exclude--config/--outputfrom convention binding like the built-ins?