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 built in — see below |
What you give up today#
No shell completions. 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 _ "example.com/sxcli-completions" // hypothetically, one day
Shell completions are the concrete 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. A completion service reading that schema
can emit bash/zsh/fish scripts exactly the way cobra's completion
subcommand does — registered as a service, pulled in with a blank
import, enabled like anything else.
And the schema API exists: the core's Introspector (injected like
any service — see Services & injection) exposes
Applets(), Services(), and Arguments(appletID, args) — the
closure-true argument schema an applet would have for a given command
line, each entry carrying the argument names, env var, usage text, type,
and the declared value domain (ArgInfo.Allowed). Everything a
completion generator needs; the service itself just hasn't been written
yet. (Custom value parsers remain an open item.) The pattern stands:
features arrive as services, compose through the closure, and the core
stays small.
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.