Commandment, a CLI framework built around go 1.27 generic methods

I started working on this project about a month before 1.27. Today I’m happy to announce that I’ve released v1.0.0-rc.1 of Commandment, a Go framework for building typed command-line applications. The v1 feature set and public API are now essentially complete; this RC is the point where I want the design exposed to real use and criticism before I freeze it as v1.0.0.

The main reason Commandment exists is that a nontrivial CLI tends to accumulate several overlapping descriptions of itself. As an SRE consultant, this has been a huge thorn in my side for the better part of a decade. I have played with different CLI libraries and design patterns within those libraries, but nothing really solved the problems I had for making production level CLI-based apps without a significant amount of custom code. A lot of my problems have to do with how most common Go CLI libraries are imperative by design.

Commandment instead compiles a typed declaration into one semantic application model. Running the CLI, generating help and completion, inspecting it, and testing its semantics are different operations over that same model rather than separate approximations of it. Its as close to declarative as I could get it, and I think it looks pretty clean.

Let’s check out an example:

type scanConfig struct {
    World string
}

type abductConfig struct {
    Target string
}

var definition = cli.Definition[struct{}]{
    Name: "alienctl",
    Declare: func(root *cli.CommandBuilder[struct{}]) {
        root.Subcommands.Command(cli.Command[scanConfig]{
            Name: "scan",
            Declare: func(scan *cli.CommandBuilder[scanConfig]) {
                scan.Arguments.String(&scan.Config.World, "world")
            },
        })
    
        root.Subcommands.Command(cli.Command[abductConfig]{
            Name: "abduct",
            Declare: func(abduct *cli.CommandBuilder[abductConfig]){
                abduct.Arguments.String(&abduct.Config.Target, "target")
            },
        })
    },
}

The parent is authored through CommandBuilder[struct{}], while scan and abduct each keep their own concrete configuration type. The child type belongs to the Command method invocation rather than becoming another type parameter of the parent.

Generic methods are used throughout the declaration API for similar relationships. Typed destinations, for example, remain typed while being connected to arguments, flags, environment input, defaults, rules, dependencies, facts, and other parts of the model.

Another great feature of commandment, is that once the declaration is compiled, the resulting application remains the authoritative semantic model. Execution, help, completion, inspection, diagnostics, and semantic tests consume that model instead of reconstructing the CLI independently.

Touching on my claim of “feature-completeness,” I mean that I consider the intended v1 capabilities and API design substantially closed. I honestly can’t think of a single extra feature to add to the front end API. I am actually expecting a bit of push back on including a Signals API which I personally think is ultra badass but some purists out there may not appreciate. You be the judge.

Also, I don’t mean that an RC does not contain bugs. The point of rc.1 is to find mistakes before I commit to a definitive v1.0.0 release. If all goes well, I could see cutting an actual v1 sometime next month or early December.

Please review the repo, make your own CLI if you feel up to it, and let me know what you think. I want you to be as brutal as you want. I’m so incredibly tired of looking at this codebase and need fresh pairs of eyes on it before I feel comfortable assuring that commandment is ready for “production” use.

Repo: GitHub - abstractionsinc/commandment: A full-feature CLI Library for Go · GitHub

Examples: commandment/examples at main · abstractionsinc/commandment · GitHub