cli/docs/pkg/cli/commands.md
Virgil d84d8cc838
All checks were successful
Security Scan / security (push) Successful in 17s
feat: add persistent CLI flag helpers
2026-04-01 09:43:59 +00:00

4.9 KiB

title description
Command Builders Creating commands, flag helpers, args validation, and the config struct pattern.

Command Builders

The framework provides three command constructors and a full set of flag helpers. All wrap cobra but remove the need to import it directly.

Command Types

NewCommand -- Standard command (returns error)

The most common form. The handler returns an error which Main() handles:

cmd := cli.NewCommand("build", "Build the project", "", func(cmd *cli.Command, args []string) error {
    if err := compile(); err != nil {
        return cli.WrapVerb(err, "compile", "project")
    }
    cli.Success("Build complete")
    return nil
})

The third parameter is the long description (shown in --help). Pass "" to omit it.

NewGroup -- Parent command (subcommands only)

Creates a command with no handler, used to group subcommands:

scoreCmd := cli.NewGroup("score", "Scoring commands", "")
scoreCmd.AddCommand(grammarCmd, attentionCmd, tierCmd)
root.AddCommand(scoreCmd)

NewRun -- Simple command (no error return)

For commands that cannot fail:

cmd := cli.NewRun("version", "Show version", "", func(cmd *cli.Command, args []string) {
    cli.Println("v1.0.0")
})

Re-exports

The framework re-exports cobra types so you never need to import cobra directly:

cli.Command        // = cobra.Command
cli.PositionalArgs // = cobra.PositionalArgs

Flag Helpers

All flag helpers follow the same signature: (cmd, ptr, name, short, default, usage). Pass "" for the short name to omit the short flag.

var cfg struct {
    Model    string
    Verbose  bool
    Count    int
    Score    float64
    Seed     int64
    Timeout  time.Duration
    Tags     []string
}

cli.StringFlag(cmd, &cfg.Model, "model", "m", "", "Model path")
cli.BoolFlag(cmd, &cfg.Verbose, "verbose", "v", false, "Verbose output")
cli.IntFlag(cmd, &cfg.Count, "count", "n", 10, "Item count")
cli.Float64Flag(cmd, &cfg.Score, "score", "s", 0.0, "Min score")
cli.Int64Flag(cmd, &cfg.Seed, "seed", "", 0, "Random seed")
cli.DurationFlag(cmd, &cfg.Timeout, "timeout", "t", 30*time.Second, "Timeout")
cli.StringSliceFlag(cmd, &cfg.Tags, "tag", "", nil, "Tags")

Persistent Flags

Persistent flags are inherited by all subcommands:

cli.PersistentStringFlag(parentCmd, &dbPath, "db", "d", "", "Database path")
cli.PersistentBoolFlag(parentCmd, &debug, "debug", "", false, "Debug mode")
cli.PersistentIntFlag(parentCmd, &retries, "retries", "r", 3, "Retry count")
cli.PersistentInt64Flag(parentCmd, &seed, "seed", "", 0, "Seed value")
cli.PersistentFloat64Flag(parentCmd, &ratio, "ratio", "", 1.0, "Scaling ratio")
cli.PersistentDurationFlag(parentCmd, &timeout, "timeout", "t", 30*time.Second, "Timeout")
cli.PersistentStringSliceFlag(parentCmd, &tags, "tag", "", nil, "Tags")

Args Validation

Constrain the number of positional arguments:

cmd := cli.NewCommand("deploy", "Deploy to env", "", deployFn)
cli.WithArgs(cmd, cli.ExactArgs(1))    // Exactly 1 arg
cli.WithArgs(cmd, cli.MinimumNArgs(1)) // At least 1
cli.WithArgs(cmd, cli.MaximumNArgs(3)) // At most 3
cli.WithArgs(cmd, cli.RangeArgs(1, 3)) // Between 1 and 3
cli.WithArgs(cmd, cli.NoArgs())        // No args allowed
cli.WithArgs(cmd, cli.ArbitraryArgs()) // Any number of args

Command Configuration

Add examples to help text:

cli.WithExample(cmd, `  core build --targets linux/amd64
  core build --ci`)

Pattern: Config Struct + Flags

The idiomatic pattern for commands with many flags is to define a config struct, bind flags to its fields, then pass the struct to the business logic:

type DistillOpts struct {
    Model    string
    Probes   string
    Runs     int
    DryRun   bool
}

func addDistillCommand(parent *cli.Command) {
    var cfg DistillOpts

    cmd := cli.NewCommand("distill", "Run distillation", "", func(cmd *cli.Command, args []string) error {
        return RunDistill(cfg)
    })

    cli.StringFlag(cmd, &cfg.Model, "model", "m", "", "Model config path")
    cli.StringFlag(cmd, &cfg.Probes, "probes", "p", "", "Probe set name")
    cli.IntFlag(cmd, &cfg.Runs, "runs", "r", 3, "Runs per probe")
    cli.BoolFlag(cmd, &cfg.DryRun, "dry-run", "", false, "Preview without executing")

    parent.AddCommand(cmd)
}

Registration Function Pattern

Commands are organised in packages under cmd/. Each package exports an Add*Commands function:

// cmd/score/commands.go
package score

import "forge.lthn.ai/core/cli/pkg/cli"

func AddScoreCommands(root *cli.Command) {
    scoreCmd := cli.NewGroup("score", "Scoring commands", "")

    grammarCmd := cli.NewCommand("grammar", "Grammar analysis", "", runGrammar)
    cli.StringFlag(grammarCmd, &inputPath, "input", "i", "", "Input file")
    scoreCmd.AddCommand(grammarCmd)

    root.AddCommand(scoreCmd)
}

Then in main.go:

cli.Main(
    cli.WithCommands("score", score.AddScoreCommands),
)