Overview
sxcli-fw — Simple Extensible CLI — is a Go framework for building command-line tools and services around one idea: your configuration struct is your entire interface. Declare a struct once and every field is simultaneously a command-line argument, an environment variable, and a config-file key — merged with clear precedence, validated strictly, and handed to your code filled in. Around that sit services, dependency injection, an ordered lifecycle — and, when you want it, busybox-style multi-applet binaries.
The model#
Everything is a service: a struct registered under an id. An applet is
a service implementing the Applet interface — Configured() error plus
Run() int — the dispatchable entry point.
Services declare dependencies with struct tags, and the framework resolves
the applet's dependency closure, configures and starts its members in
dependency order, runs the applet, and stops everything in reverse.
type Backup struct { Store *BoltStore `inject:""` // by concrete type Log slog.Handler `inject:";optional"` // by interface, optional }
A registration carries two names, because code and operators need
different things from a name. The id is import-path-shaped and unique
by construction — what inject tags and compositions refer to. The
alias is the short operator name — what appears in config sections,
env prefixes and --disable.
fw.NewRegistration("example.com/mytool/serve", newServe, configOf). Alias("serve"). Register()
Composition is explicit#
init() only catalogs a service. The binary then names what it takes,
and an import earns its place by being referenced:
fw.Builder(). Accept(serve.ID, grep.ID, console.ID). Main()
fw.Solo(...) is the single-applet front door and fw.Main() takes
everything catalogued. Nothing is wired by blank import, and nothing is
resolved by luck: two unranked candidates for one dependency slot are a
startup error naming both, not a coin flip.
What you get#
- Dispatch by
argv[0](symlink style) or by first subcommand argument — or neither, in single-applet binaries. - Configuration merged from defaults, config files, environment and arguments, driven by one struct per service.
- Versioned config schemas: structs carry a
Version, evolve through typed migration chains, and deployed files keep working. - Dependency injection between services, resolved before anything runs.
- Lifecycle:
Configured→Start→Run→Stop, dependency-ordered, sequential by design. - Logging on
log/slogwith pluggable sinks: console, file, syslog/journald. - Introspection: a read-only composition view — applets, services, closure-true argument schemas — for building completions and doc generators as services.
- Windows service support: the same applet runs as a console process or under the SCM.
Design principles#
The framework is deliberately boring where it counts: lifecycle calls are sequential (no concurrency in the core), startup failures report all problems at once instead of one at a time, and misconfiguration is a loud startup error, never a silent skip.
The strongest version of that rule is what shaped v0.3.0. Services used to
be wired by blank import, which meant registration order was import order —
so reordering an import block could change which service satisfied an
interface. A framework whose promise is "never silent" could not keep a
rule that let goimports rewire a program. Composition became explicit,
and ties became errors.
The family#
fw is the whole framework, but its parts stand alone:
sxcli.dev/conf— the configuration engine on its own: args, environment, files, validation and migrations, with no service model attached. Reach for it when you want the config handling and nothing else.sxcli.dev/vet—sxcli-vet, a static analyser that runs the framework's startup checks at compile time: bad ids, tag mistakes, broken migration chains, ambiguous compositions.sxcli.dev/completion— shell completion driven by the live composition, so candidates match the binary you actually built.
Status#
v0 — the API is settling and may still move. Module path sxcli.dev/fw,
Go 1.26+, licensed under Apache-2.0.
Continue with Getting started.