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

Advanced CLI

The minimal CLI stopped at struct tags. This demo adds the declarative layer on top: fw.WithMetadata — long-form descriptions for humans and tooling, and value domains the framework enforces on every source. Same file server, grown up.

The applet, annotated#

go
go
package main

import (
    "log/slog"
    "net/http"

    fw "sxcli.dev/fw"
    _ "sxcli.dev/fw/configfmt/yaml"
    _ "sxcli.dev/fw/sink/console"
)

type Config struct {
    Addr   string   `json:"addr"   arg:"addr,a"  usage:"listen address"`
    Dir    string   `json:"dir"    arg:"dir,d"   usage:"directory to serve"`
    Index  string   `json:"index"  arg:"index"   usage:"directory index mode"`
    Codecs []string `json:"codecs" arg:"codec"   usage:"compression codec, repeatable"`
}

type Srv struct{ cfg Config }

func (s *Srv) Configured() error { return nil }

func (s *Srv) Run() int {
    slog.Info("serving", "dir", s.cfg.Dir, "addr", s.cfg.Addr,
        "index", s.cfg.Index, "codecs", s.cfg.Codecs)
    // … same server as the minimal demo …
    _ = http.ListenAndServe
    return 0
}

func main() {
    s := &Srv{cfg: Config{Addr: ":8080", Dir: ".", Index: "list"}}
    fw.Register("srv", s,
        fw.WithConfig(&s.cfg),
        fw.WithMetadata(&fw.Metadata{
            Description: "Static file server with negotiable directory " +
                "index behavior and response compression.",
            Fields: map[string]any{
                "Index": fw.FieldMetadata[string]{
                    Allowed: []string{"list", "none", "spa"},
                    Doc: "How directory URLs respond: 'list' renders a " +
                        "listing, 'none' returns 404, 'spa' serves " +
                        "index.html for client-side routing.",
                },
                "Codecs": fw.FieldMetadata[string]{
                    Allowed: []string{"gzip", "zstd", "br"},
                    Doc: "Compression codecs offered during content " +
                        "negotiation, in preference order.",
                },
            },
        }))
    fw.Main()
}

Two kinds of annotation, per field:

  • Doc — the long-form description. The usage: tag stays the --help one-liner; Doc is what documentation generators and rich completion UIs show.
  • Allowed — a closed value domain. Note the default ("list") is itself inside the domain: a registered default outside its own declared set is caught at registration, with every other violation.

Enforced, not advisory#

A non-empty Allowed is honored by the machinery on every write path — the same rule whether the value arrives by argument, environment or config file:

sh
sh
$ srv --index tree            # startup fails: names the source (argument),
                              # the offending value, and the allowed set
$ SRV_INDEX=tree srv          # same failure, source: environment
$ echo 'srv: {index: tree}' > srv-config.yaml && srv   # same, source: file

Slice fields are checked per element:

sh
sh
$ srv --codec gzip --codec lzma    # fails: 'lzma' not in [gzip zstd br]
$ SRV_CODEC=gzip,zstd srv          # fine — every element in the domain

The applet keeps whatever checks it wants as defense in depth, but by the time Configured() runs, s.cfg.Index is guaranteed to be one of the three declared values — invalid configurations die at startup, loudly, before any code runs.

Where the declarations surface#

Declare once, and the same metadata feeds everything downstream:

  • the framework's own validation, as above;
  • Describe("srv") on the introspection surface returns the long-form description;
  • Arguments("srv", args) returns the schema with ArgInfo.Allowed populated — a completion service can offer exactly list, none, spa after --index, and trust the set because the machinery enforces it.

The full rules live in Your config struct § Metadata and value domains.