sxcli.devSimple Extensible CLI
You are viewing an archived snapshot (v0.2.0). Switch to the current version →

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 + arg + usage tags
cobra.Command and subcommandsapplets — with symlink dispatch as a bonus
PersistentPreRun / PostRun chainsthe lifecycle: ConfiguredStartRunStop, dependency-ordered
viper.AutomaticEnv + SetEnvPrefixderived env names (<APPLET>_<ARG>), 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
wiring components by hand in main()services & injection
cobra completion bashnot 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:

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