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 + arg + usage tags |
cobra.Command and subcommands | applets — with symlink dispatch as a bonus |
PersistentPreRun / PostRun chains | the lifecycle: Configured → Start → Run → Stop, dependency-ordered |
viper.AutomaticEnv + SetEnvPrefix | derived env names (<APPLET>_<ARG>), 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 |
wiring components by hand in main() | services & injection |
cobra completion bash | not in core — a separate module, sxcli.dev/completion (see below) |
What you give up today#
No positional argument routing or validation (positionals are collected, nothing more). No custom value types beyond the supported kinds. No generated man pages. No middleware ecosystem. If any of those is a hard requirement today, cobra is the mature choice and this page won't argue.
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 imports:
import _ "sxcli.dev/completion" // shell 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(appletID, 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.