Skip to content

Migrating to the command-group model

A command that exists only to group its subcommands has nothing to implement. It now gets no Run<Name> at all, and its generated cmd.go wires the framework default instead:

RunE: setup.GroupRunE,

Invoked bare it prints its usage and exits 0. Given a verb it does not have it says so and exits 2:

$ tool alpha
Usage:
  tool alpha [command]
...

$ tool alpha bogus
WARN unknown command "bogus" for "tool alpha": unknown subcommand
$ echo $?
2

Implements spec 0190.

Why this changed

The generator used to write a Run<Name> returning errorhandling.ErrRunSubCommand for every command with children, and wire RunE to call it — so a bare group exited 2.

Two problems followed from cmd.go referring to a function in the developer's own package:

  • Where .gtb/ignore sealed a main.go that was absent, the generator could neither create the callee nor stub it, and the emitted call did not compile (undefined: RunAlpha).
  • The fix for that suppressed the RunE in exactly that case — which meant the exit code of a built binary depended on the ignore file. The same group exited 0 or 2 according to whether a seal happened to cover a file that was not there.

setup.GroupRunE refers to nothing in your package, so neither can happen. A seal now governs what the generator writes and nothing about what your tool does.

What changes for your project

Run gtb regenerate project. The run tells you which commands changed:

1 command changed behaviour
WARN group no longer returns a usage error  command=alpha
     path=pkg/cmd/alpha/cmd.go  was="exit 2 (subcommand required)"
     now="exit 0 (usage), exit 2 on an unknown subcommand"
     keep-old="give RunAlpha a body returning errorhandling.ErrRunSubCommand"

It reports on the run that makes the change, and stays silent afterwards.

A group whose Run<Name> is an untouched stub

Nothing to do. The generator stops calling that stub and wires the default. Your main.go is not edited or deleted — the stub stays exactly as it is, and it is a live seam rather than litter: give it a body and the next run classifies the group as working again.

A group with a hand-written Run<Name>

Nothing changes. A body is intent, and the generator keeps calling it. That includes a parent whose body only prints its own verb list — the generator cannot tell that apart from real work without guessing, so it does not try.

If such a group should take the new behaviour, delete the function body and regenerate. You lose a hand-maintained verb list that drifts from the real subcommand set every time a child is added, and gain the unknown-verb report. regenerate names the candidates for you, once, on the run that changes another group.

A group that wants a bare invocation to stay an error

Give its Run<Name> a body returning the sentinel, which is still available:

func RunAlpha(ctx context.Context, props *props.Props, opts *AlphaOptions, args []string) error {
    return errorhandling.ErrRunSubCommand
}

That makes it a working group, so the generator wires RunE to it as before.

A main.go that now defines nothing

A pure group with no pre-run hooks has nothing to put in main.go, so a newly scaffolded one gets none. Existing files are left alone.

What did not change

  • Leaves. A command with no children still gets a Run<Name> returning errorhandling.ErrNotImplemented, and still exits 2.
  • The root command. Cobra already reports an unknown command there, and exits 1. That inconsistency with a group's 2 is left as it is — the root is cobra's to answer.
  • errorhandling.ErrRunSubCommand. Still exported, still correct for a hand-written command that wants a bare invocation to be a usage error. GTB simply stops generating it.

Version skew

Emitted code references setup.GroupRunE and, through it, errorhandling.ErrUnknownSubCommand — added in go/errorhandling v0.4.0. A project regenerated by a newer gtb while pinned to an older go-tool-base will not compile.

Bump the framework, then regenerate. If go.mod is in your .gtb/ignore, gtb cannot bump the pin for you and the mismatch arrives as a build error naming the missing symbol.

The gtb CLI itself

gtb's own groups adopted the same model, so gtb <group> <unrecognised> was silent help and exit 0, and is now a named error and exit 2gtb generate, remove, regenerate, ignore, attach, detach, template, telemetry and keys. A script relying on the old status was relying on a bug: the command it asked for never ran.

gtb mcp and its children are built by the ophis library and are unchanged. See spec 0191.