Coming from flag/cobra/viper
An honest comparison. sxcli-fw is simpler and has fewer features than the cobra/viper stack — deliberately. What it offers instead is one composition model: when a feature is missing, the path to it is a service, not a bigger core.
The concept map#
| You know | Here it is |
|---|---|
flag.StringVar(&v, "name", "x", "usage") | a config struct field — Name string with json + conf + usage tags |
cobra.Command and subcommands | applets — with symlink dispatch as a bonus |
cobra.Args / manual args[0] handling | declared positionals: pos:"0", pos:"rest", validated and in --help |
PersistentPreRun / PostRun chains | the lifecycle: Configured → Start → Run → Stop, dependency-ordered |
viper.AutomaticEnv + SetEnvPrefix | derived env names (<ALIAS>__<NAME>), per field, no setup |
viper.AddConfigPath / SetConfigFile | the standard location search + --config |
| viper's flag > env > config > default | identical, written low-to-high: defaults < config files < environment < arguments, built in |
| viper's yaml/toml/ini readers | JSON native; format providers for the rest |
| hand-rolled config file versioning | schema migrations: typed steps, --upgrade-config |
wiring components by hand in main() | services & injection |
cobra completion bash | not in core — a separate module, sxcli.dev/completion (see below) |
go vet for your CLI wiring | sxcli-vet: the startup checks, at compile time |
Just the configuration, then?#
If the service model is more than you want, take the config engine on its
own: sxcli.dev/conf is the same
args/env/files/validation/migrations pipeline with no services, no
lifecycle and no dispatch — the direct viper replacement. fw is its first
consumer, so nothing is lost by starting there and growing later.
What you give up today#
No custom value types beyond the supported kinds. No generated man pages. No middleware ecosystem. No dynamic subcommand trees — a binary's applets are fixed at composition. If any of those is a hard requirement today, cobra is the mature choice and this page won't argue.
Positional arguments are no longer on that list: they are declared with
pos: tags, validated, and rendered in --help.
The path back: everything is a service#
The framework's answer to a missing feature is not a bigger core — it is a service someone writes once and every binary can accept:
import "sxcli.dev/completion/bash" fw.Builder().Accept(bash.ID /* , … */).Main() // completions, as a module
Shell completions are the worked example. In cobra they are generated
from the command tree the library already holds; here the equivalent
source of truth is the argument schema the framework builds from every
config struct in the closure, read through the core's Introspector
(injected like any service — see
Services & injection): Applets(),
SingleApplet(), and Arguments(applet, args) return the
closure-true schema an applet would have for a given command line, each
entry carrying the argument names, env var, usage text, type, the
declared value domain (ArgInfo.Allowed) and the advisory
ArgInfo.Hint.
That is not a thought experiment: completions live in a separate module,
sxcli.dev/completion, and the core grew what it needed to support them
from the outside — System applets so a completion endpoint cannot
disturb your command line, and value hints so the generator knows a flag
takes a directory. Any capability gap gets fixed as a core API
improvement, never a backdoor. That is the deal the architecture
makes: features arrive as modules, compose through the closure, and the
core stays small. (Custom value parsers remain an open item.)
The philosophy difference#
flag/cobra/viper are imperative: you call APIs to build up commands, flags and bindings, and the structure lives in that code. Here the structure is declarative: one struct per service is the whole interface, the framework derives arguments, environment and file keys from it, and strictness is the default — unknown arguments, unknown config keys and misconfiguration are startup errors, never silent.
If that trade — fewer features, one model, loud failures — reads like a relief rather than a limitation, Getting started takes ten minutes.