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

Core config & arguments

The framework configures itself through the same machinery it gives your services: the core owns a config struct, and its values follow the usual precedence — defaults < config files < environment < arguments. This page lists what the core processes, which values are deliberately locked out of config files, and how to remove core features from a binary entirely.

Reserved ids#

Two service ids are reserved — registering under either is a startup error: core, the framework's own configuration, and introspection, the core-registered Introspector. In config files the core's own values nest under its id:

json
json
{
    "core": {
        "disable": ["sqlite"],
        "override": ["sqlite=mysql"]
    }
}

Environment names, as always, derive from the applet id, not from core: for an applet srv the core's --config argument reads SRV_CONFIG.

What the core processes#

ValueArgumentEnvironmentConfig filePurpose
config--config, -cderived (<APPLET>_CONFIG)neverexplicit config file; replaces the location search
help--help, -hneverneverprint the full argument schema with current effective values, exit 0
write-config--write-configneverneverwrite the merged configuration, exit 0
disable--disablederivedyesremove service ids from the closure
enable--enablederivedyesforce service ids into the closure
override--overridederivedyesremap dependency ids (old=new)

disable, enable and override are list-valued: repeat the argument to append (--disable a --disable b), comma-separate the environment value, use arrays in files.

The core also dogfoods its own metadata layer, so tooling can treat these values sensibly: --config declares HintFile, and --disable/--enable declare HintServiceID — completable from the Introspector. --override takes from=to pairs, which no honest hint describes, so it declares none. See Your config struct.

Run-scoped values and why config files refuse them#

help and write-config are argument-only. A config file or an inherited environment variable setting them would be a persistent denial of service — every run printing help, or writing a config and exiting. config is settable by argument or environment (<APPLET>_CONFIG is a legitimate deployment pattern) but never by a config file — a file choosing the next file is redirection, not configuration.

Enforcement is loud in both directions:

  • a config file attempting to set any of the three (help, write-config, config) is a startup error, never a silent skip;
  • all three are excluded from --write-config output, so a generated config can never smuggle them back in.

The same semantics are available to your own services: tag a field dump:"-" and it becomes run-scoped — excluded from generated configs and refused from config files.

`--write-config` and `--config` together#

--write-config short-circuits the run: the pipeline stops right after the merge — the configuration is written and the process exits 0 without Configured, Start or Run. Where it writes depends on --config:

sh
sh
$ mytool --write-config                            # no target: merged config as JSON to stdout
$ mytool --write-config -c /etc/mytool/config.yaml # target: written in the format the extension names
$ mytool --port 9090 --write-config -c local.json  # arguments participate in the merge as usual

The output format follows the target's extension via a format provider — .json natively, .yaml with the provider linked in. If no registered provider claims the target's extension, startup fails and says so.

The target is input and output:

  • An existing target is loaded first as the explicit config, then rewritten — which makes --write-config a one-command normalizer: point it at a hand-edited or differently-formatted file and it comes back canonical (this is also the way to convert JSON ⇄ YAML).
  • A missing target is only created. And here lies the one deliberate exception to --config semantics: in a normal run a non-empty --config replaces the standard location search entirely, but in --write-config mode a missing target does not — the locations are loaded and merged as usual, so the newly written file captures the configuration the binary would actually be running with.

The output is also minimal: empty values — and the sections they would leave empty — are omitted.

Permissions are conservative: a newly created file gets mode 0600; an existing target's permissions are the operator's prior decision and are left untouched — normalizing a file must not silently revoke a deliberately granted group read.

Recomposing services#

Operators can recompose a binary without recompiling:

sh
sh
$ mytool --disable sqlite --enable mysql --override sqlite=mysql
  • disable removes services from the closure even if required. Disabling the dispatched applet itself is a startup error, as is listing the same id in both enable and disable.
  • enable pulls services in — with their transitive dependencies — even if nothing requires them.
  • override remaps ids: wherever a dependency names sqlite, mysql is resolved instead. The substitute must satisfy the dependency's field type, checked at resolve time. An override key may name an unregistered id, and an override matching no dependency is legal — but every unused key is logged as a warning, so a typo never silently does nothing.
  • A dependency marked ;optional tolerates a disabled target, never an unknown one: an id that names no registered service is a startup error even on an optional field.

Limits#

Config files are size-capped: the default cap is 1 MiB, checked with stat before the file is ever opened. The cap is a build-time property of the binary:

go
go
fw.MaxConfigSize(64 << 10) // tighter than the default

Suppressing core features#

A hardened or embedded binary can remove pieces of the core's surface entirely — build-time, from main() or an init() before Main:

go
go
func main() {
    fw.Suppress(fw.FeatureConfigFile, fw.FeatureOverride)
    fw.Main()
}

Suppressible: FeatureConfigFile (--config), FeatureWriteConfig, FeatureDisable, FeatureEnable, FeatureOverride, FeatureHelp. A suppressed feature vanishes: its argument becomes unknown (a strict-parse error), its environment variable is never consulted, and its key appearing in a config file's core section is a loud startup error — operators learn it is not honored instead of wondering why it is ignored.

The counterpart for default-off features is fw.Enable. There is currently one: FeatureSCMDebug unlocks the Windows-only --scm-debug argument, which runs the Windows service code path in a console for testing. It is argument-only by construction, absent from --help, and without the opt-in the token is rejected as an unknown argument — as it is on every non-Windows platform.

Misusing the build-time API is itself a collected startup violation, never silently ignored: suppressing a default-off feature, enabling a default-on one, unknown features, or a non-positive MaxConfigSize all fail startup with every problem reported at once.

Related: the full rules live in the design spec; the API surface is in the reference.