sxcli.devSimple Extensible CLI

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 knowHere it is
flag.StringVar(&v, "name", "x", "usage")a config struct field — Name string with json + conf + usage tags
cobra.Command and subcommandsapplets — with symlink dispatch as a bonus
cobra.Args / manual args[0] handlingdeclared positionals: pos:"0", pos:"rest", validated and in --help
PersistentPreRun / PostRun chainsthe lifecycle: ConfiguredStartRunStop, dependency-ordered
viper.AutomaticEnv + SetEnvPrefixderived env names (<ALIAS>__<NAME>), per field, no setup
viper.AddConfigPath / SetConfigFilethe standard location search + --config
viper's flag > env > config > defaultidentical, written low-to-high: defaults < config files < environment < arguments, built in
viper's yaml/toml/ini readersJSON native; format providers for the rest
hand-rolled config file versioningschema migrations: typed steps, --upgrade-config
wiring components by hand in main()services & injection
cobra completion bashnot in core — a separate module, sxcli.dev/completion (see below)
go vet for your CLI wiringsxcli-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:

go
go
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.