Command Middleware System¶
The Command Middleware System provides a powerful way to inject shared behavior across your CLI command tree. Instead of duplicating logic in every command's RunE function, you can define "middlewares" that wrap your commands to handle cross-cutting concerns.
The Chain Pattern¶
GTB uses a functional "Chain" pattern for middleware, similar to those found in modern web frameworks like Gin or Echo. A middleware is a function that receives the "next" handler in the chain and returns a new handler.
type Middleware func(next func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error
This pattern is superior to simple lifecycle hooks (like PreRun) because it allows the middleware to "wrap" the entire execution. A middleware can:
1. Execute logic before the command runs.
2. Execute logic after the command runs.
3. Recover from panics within the command.
4. Decide whether to call the next handler at all (e.g., for auth checks).
5. Transform the error returned by the command.
Global vs. Feature Middleware¶
GTB distinguishes between two scopes of middleware:
- Global Middleware
- Applied to every command in the tool. Ideal for universal concerns like panic recovery, execution timing, and global telemetry.
- Feature Middleware
- Applied only to commands belonging to a specific Feature. Ideal for domain-specific concerns like verifying API keys for AI commands or checking for a valid git repository.
Execution Order¶
Middleware is applied in a deterministic order to ensure predictable behavior:
- Global Middleware executes first, in the order they were registered.
- Feature Middleware executes second, in the order they were registered.
- The Command Handler executes last.
Because it is a wrapping chain, the "before" logic runs in registration order (outermost to innermost), and the "after" logic (and defer blocks) runs in reverse registration order (innermost to outermost).
The Registry Lifecycle¶
To ensure thread safety and architectural consistency, the middleware registry follows a strict lifecycle:
- Registration: Occurs during the
init()phase of your packages. - Sealing: The registry is "sealed" during the root command registration. No further middleware can be added once the command tree is being built.
- Execution: When a parent attaches a child via
parent.Register(child),Chain()wraps the child'sRunEexactly once โ with that child's own feature key. Every command in the tree picks up its own middleware at attach time; nothing is wrapped twice.
How wrapping is wired¶
Each *setup.Command carries a Feature field (props.FeatureCmd) that the wrapping path uses as the middleware-registry key:
When the root constructor (or any parent) calls Register, the framework:
- Iterates each child you pass in.
- Wraps the child's
RunEwithChain(child.Feature, child.RunE)โ applying global middleware then any middleware registered against the child's feature. - Calls the embedded
(*cobra.Command).AddCommandto splice the child into the cobra tree.
Because each command owns its own feature, a parent never needs to know which middleware its descendants need. Siblings can have different feature keys and pick up entirely different middleware stacks.
Manual / late attachment¶
If you build your CLI tree after the root has been initialised (dynamic plugins, conditional commands, late discovery), still use Register:
rootCmd := root.NewCmdRoot(p) // *setup.Command
if pluginEnabled {
rootCmd.Register(plugin.NewCmdPlugin(p))
}
The Register call is idempotent against double-attachment (the underlying cobra parent rejects duplicates) and always wires middleware correctly, regardless of when it fires relative to setup.Seal().
Deprecated: AddCommandWithMiddleware
The legacy setup.AddCommandWithMiddleware(parent, child, feature) helper is still exported but marked // Deprecated:. It now delegates to Command.Register, no longer recurses into descendants (the recursive re-wrap with the parent's feature was always semantically wrong), and will be removed in v1.0. Migrate to parent.Register(child) โ the v0.4-to-v0.5 migration guide has the diff.
Middleware vs. Hooks
Use Hooks (PersistentPreRunE) for environmental setup like loading config files. Use Middleware for operational concerns that need to wrap the execution, like timing, logging, or error recovery.