Skip to content

Migrating from v0.4 to v0.5

v0.5 introduces a typed command composition model that replaces the previous setup.AddCommandWithMiddleware(parent, child, feature) wiring with a composed *setup.Command type that carries its own feature key and exposes a Register method. Middleware is wired exactly once, at the point a parent attaches a child β€” no more recursive re-wrapping, and no more bogus props.<Name>Cmd references for user-defined commands.

The change is breaking for any package that returns *cobra.Command from a NewCmd<Name> constructor or that passes such a value to pkg/cmd/root.NewCmdRoot / Execute. All affected source files in a GTB-scaffolded project are regenerated by gtb regenerate β€” running the regenerator is the entire migration for end users. The notes below document what changes for hand-written code that calls into the library directly.


Breaking Changes

NewCmd<Name> and NewCmdRoot return *setup.Command

Packages affected: pkg/cmd/root, every built-in pkg/cmd/<name>, and every generated pkg/cmd/<name> in scaffolded projects.

Before:

import (
    "github.com/spf13/cobra"
    "gitlab.com/phpboyscout/go-tool-base/pkg/cmd/root"
    "gitlab.com/phpboyscout/go-tool-base/pkg/props"
    "gitlab.com/phpboyscout/go-tool-base/pkg/setup"
)

func NewCmdServe(p *props.Props) *cobra.Command {
    cmd := &cobra.Command{Use: "serve", RunE: runServe}
    return cmd
}

// in root wiring
rootCmd := root.NewCmdRoot(p)
setup.AddCommandWithMiddleware(rootCmd, serve.NewCmdServe(p), props.ServeCmd)
root.Execute(rootCmd, p)

After:

func NewCmdServe(p *props.Props) *setup.Command {
    cmd := setup.Wrap("serve", &cobra.Command{Use: "serve", RunE: runServe})
    return cmd
}

// in root wiring β€” children attach in the variadic constructor
rootCmd := root.NewCmdRoot(p, serve.NewCmdServe(p))
root.Execute(rootCmd, p)

Migration:

  1. Run gtb regenerate in a scaffolded project β€” the generator emits the new shape automatically.
  2. For hand-written packages built on top of pkg/cmd/root:
    • Change the return type of NewCmd<Name> to *setup.Command.
    • Construct cmd via setup.Wrap("<feature-key>", &cobra.Command{...}) so cmd is the composed type from the start. Existing cmd.Flags().… and cmd.MarkFlags… calls keep working β€” they flow through the embedded *cobra.Command.
    • Pass children to the variadic root.NewCmdRoot(p, ...) or to parent.Register(child). Both wire global and feature-specific middleware in a single pass.
  3. Replace any direct callers that expected a *cobra.Command with .Command (the embedded field): myCmd.Command.SilenceErrors = true.

setup.AddCommandWithMiddleware is deprecated

Package: pkg/setup

AddCommandWithMiddleware(parent, child, feature *cobra.Command) and ApplyMiddlewareRecursively(cmd, feature) are kept as // Deprecated: shims for v0.5 to give downstream callers a window to migrate. The shim no longer recurses into descendants β€” each command in the tree wraps itself with its own feature when its parent registers it. The recursive re-wrap-with-parent's-feature behaviour was always semantically wrong (it imposed the parent's middleware key on children that already had their own).

Removal planned: v1.0.

Migration: Replace each call site with parent.Register(child) β€” see the example above. The child's NewCmd<Name> is now responsible for its own feature key via setup.Wrap("<name>", cmd).


New Features

setup.Command, setup.Wrap, and Command.Register

A composed Command type wraps *cobra.Command with its own FeatureCmd key. setup.Wrap(feature, cmd) produces one; parent.Register(child...) attaches one or more children and wires global plus feature-specific middleware exactly once at attach time.

parent := setup.Wrap("parent", &cobra.Command{Use: "parent"})
parent.Register(
    childA.NewCmdChildA(p),  // returns *setup.Command
    childB.NewCmdChildB(p),
)

Each child carries its own feature, so feature-specific middleware (registered via setup.RegisterMiddleware(feature, …)) applies to that child without affecting siblings. Global middleware (registered via RegisterGlobalMiddleware) always wraps every child.

See pkg/setup for the full API.