Back to list
Development Update — August 13
Two threads today, both about a program describing itself honestly to whatever reads it. The larger is the foundation for structured CLI output: skywire cli learns to emit --help --json — a machine-readable schema of all 379 commands — and its output helper is replaced by a printer that renders one self-describing value rather than a JSON blob and a hand-written human string that had already drifted apart. In the same pass the transport command’s output shape, which existed as three copies that disagreed, is consolidated into one importable type the e2e suite shares. Alongside it, a release-hygiene fix on the mobile side stops the visor’s version being stamped from the wrong tag.
Skywire: Structured CLI Output
3866 cli: structured output — a printer that renders one value, and --help --json replaces the CLI’s output helper with one that lets a command’s result describe itself. pkg/cliout’s PrintOutput took a JSON value and a pre-rendered human string as two separate arguments, so nothing tied them together — and they drifted: visor info printed DMSG Latency: 0s from the human side while the JSON side carried the same unmeasured zero, because each was written on its own. Now the value renders itself — struct tags are the machine contract, a Human method the text — and one printer decides which to emit, following kubernetes’ ResourcePrinter (PrintObj(obj, io.Writer) error) so a new format is a new printer rather than an edit to 343 runnable commands, with gh’s TTY handling (indented for a terminal, compact when piped, because the second reader is a parser). Four defects of the old helper go with it, each now pinned by a test: an if output != "" that compared an interface{} against a string and printed any non-string value as Go’s %v regardless; a discarded GetBool error that let a command whose flag set lacked json silently emit human text for --json, indistinguishable from the user not asking; a marshal failure that printed to stderr and exited 0 with an empty stdout a parser can’t read (it returns the error now); and a nil value that printed nothing at all (it encodes as null, streams instead of buffering, and writes its own newline). Errors return rather than exit, because PrintFatalError’s os.Exit(1) runs no deferred close, flush or unlock and can’t be tested without a subprocess. The same PR adds --help --json: Cobra has no machine format — there is no JSON anywhere in the library — so the schema is theirs, emitting a command’s name, path, every flag with its type and default, and the whole subtree beneath it, registered through SetHelpFunc (which children inherit, since --help is intercepted before Run and can’t be handled in a Run body) and leaving the non-JSON help rendered exactly as before. One call at the root returns all 379 commands (526 KB), which is how the rest of the CLI audit gets done without running each command and parsing prose written for a person.
Skywire: One Shape for cli tp
3866 also fixes a real drift bug on the way: skywire cli tp emitted a different JSON document depending on which code path produced it. The shape was declared twice in tp.go, both times inside a function body, and the two copies disagreed — one carried an inactive field the other did not — while a third copy lived in internal/integration as stcpTpView, because a function-local type cannot be imported and the e2e suite still had to unmarshal it. Three statements of one contract, none of which the compiler compared. A command’s output is an API — callers pipe it into jq, tests unmarshal it, other programs depend on the field names — so the shape moves to pkg/cliout/clitp where it can be imported; both call sites now alias the canonical type and the e2e suite imports it instead of restating it, so a renamed field breaks the build rather than silently unmarshalling to zero and asserting on the zero — which is exactly what would have happened to the byte counters TestEnv_STCPDataRoute exists to measure. Flags vary the payload, not the type: --bw fills the byte counters, --more the version/country/services, --logs the latency, each omitempty and simply absent otherwise, so a consumer checks whether a field is present and never has to recognise a different document; the emitted JSON is unchanged. One more thing goes with it — a .gitignore rule shipped alongside an earlier artifact removal turned out to match the cmd/skywire-cli/commands directory as readily as a stray binary, and once a directory is excluded nothing beneath it can be re-included, so new source files under any cmd/ subdirectory would silently fail to be added (caught by git add refusing a new file minutes after the rule shipped). It is rewritten to name the two artifacts instead of matching their shape, with a note that a generic “tracked file is an ELF or wasm binary” guard belongs in CI, by content rather than by name.
Skywire: Correct Version Stamping on Mobile
3865 fix(config): derive the visor version from release tags, not the mobile-v* APK tag corrects a wrong version being written into the visor config on mobile releases. The Skywire Mobile build carries its own mobile-v* APK tag, and the version-derivation logic was reading that tag when stamping the embedded visor’s version — so a released build reported the mobile app’s tag as the visor version rather than the actual Skywire release it was built from. The fix teaches the version parse (pkg/visor/visorconfig/parse.go) and the Makefile to derive the visor version from the release tags proper, ignoring the mobile-specific APK tag, so the config and the visor’s self-reported build line agree with the real release across both desktop and mobile packaging — the kind of thing that is invisible until a version string is wrong in the field, and then matters.