How the Aqua CLI Runner File Registers Commands: A Deep Dive into the Constructor Pattern

The aqua CLI runner registers commands by passing a variadic list of constructor functions to a commands helper that instantiates each sub-command with shared util.Param and cliargs.GlobalArgs parameters, wiring them into the top-level urfave.Command structure defined in pkg/cli/runner.go.

The aqua CLI from the aquaproj/aqua repository is a declarative CLI version manager written in Go. Understanding how the aqua CLI runner file registers commands reveals the modular, constructor-based architecture that makes the tool extensible and maintains clean separation between command definitions and execution logic.

The Entry Point: pkg/cli/runner.go

The command registration flow begins in pkg/cli/runner.go with the Run function, which serves as the primary entry point for the executable. This function initializes the runtime environment by creating a util.Param object that encapsulates I/O streams, structured logging via slogutil.Logger, runtime detection, and version metadata.

func Run(ctx context.Context, logger *slogutil.Logger, env *urfave.Env) error { … }

After parameter initialization, the function constructs a cliargs.GlobalArgs structure to hold configuration for flags that apply across all sub-commands. These parameters are then passed to the commands helper function, which returns a slice of *cli.Command instances ready for execution.

The Constructor Pattern for Command Registration

The registration mechanism relies on a constructor function pattern (type newC) rather than static command definitions. In pkg/cli/runner.go, the commands function accepts a variadic list of these constructors and returns the assembled command slice:

// commands creates a slice of CLI commands by applying the given command constructors
func commands(param *util.Param, globalArgs *cliargs.GlobalArgs, newCs ...newC) []*cli.Command { … }

Each constructor follows the signature New(param *util.Param, globalArgs *cliargs.GlobalArgs) *cli.Command and resides in its own feature package. The commands helper iterates over the provided constructors, invoking each with the shared parameters to build the complete command tree.

Registered Command Constructors

The Run function explicitly declares all available sub-commands by passing their constructors to the commands helper:

Commands: commands(
    param,
    globalArgs,
    initcmd.New,
    install.New,
    generate.New,
    updateaqua.New,
    upc.New,
    update.New,
    which.New,
    info.New,
    remove.New,
    vacuum.New,
    token.New,
    cp.New,
    cpolicy.New,
    cpolicy.NewInitPolicy,
    exec.New,
    list.New,
    genr.New,
    root.New,
),

Each function—such as install.New from pkg/cli/install/command.go or exec.New from pkg/cli/exec/command.go—returns a fully configured *cli.Command instance with its specific flags, arguments, and action handlers.

Global Parameter Propagation and Flag Registration

The registration system ensures consistent behavior across all commands through shared parameter injection. The single util.Param instance created in Run propagates to every sub-command constructor, guaranteeing uniform I/O handling, logging configuration, and runtime environment detection.

Global flags—such as --log-level—are registered via cliargs.GlobalFlags(globalArgs) and apply to every sub-command automatically, eliminating redundant flag definitions in individual command packages.

Sub-Command Implementation Structure

Individual commands are modularized under pkg/cli/*/ directories, with each package containing a command.go file that exports its constructor. For example:

This structure enforces the Single Responsibility Principle by isolating command logic, flag definitions, and help text within dedicated packages. When the commands helper executes a constructor, it injects the shared parameters, allowing each command to access global resources without direct dependencies on the runner.

Execution Flow and Error Handling

After assembling the command tree, the Run function initializes the top-level urfave.Command with the returned slice and invokes the runtime:

urfave.Command(...).Run(ctx, env.Args)

The framework parses command-line arguments, dispatches to the appropriate sub-command based on the first positional argument, and executes the corresponding action. Error handling is centralized through exitErrHandlerFunc (defined at runner.go#L90-L98), which ensures consistent exit codes and error formatting across all command failures, including the critical exec command.

Practical Command Registration Examples

To observe the registration in action, invoke the aqua CLI help flag to view all wired sub-commands:


# Display all registered commands and global flags

$ aqua --help
Version Manager of CLI. https://aquaproj.github.io/
...
Commands:
  init      Initialize a new Aqua configuration
  install   Install tools defined in aqua.yaml
  generate  Generate a lock file from aqua.yaml
  update    Update installed tools
  which     Show the path of an installed tool
  exec      Execute a command with tools added to PATH
  list      List installed tools
  remove    Remove installed tools

You can also verify global flag propagation across commands:


# Apply a global flag to any sub-command

$ aqua --log-level=debug install
$ aqua --log-level=debug list

Summary

  • The aqua CLI runner file (pkg/cli/runner.go) serves as the central registry for all sub-commands via the Run function and commands helper.
  • Constructor functions (type newC) define a standard interface for command creation, accepting shared util.Param and cliargs.GlobalArgs parameters.
  • Seventeen distinct commands—including initcmd.New, install.New, exec.New, and root.New—are registered declaratively in the commands call within Run.
  • Modular packaging places each command's implementation in pkg/cli/<name>/command.go, maintaining clean separation of concerns.
  • Global parameters and flags propagate automatically to all sub-commands through dependency injection, ensuring consistent logging and I/O behavior.

Frequently Asked Questions

How does the aqua CLI runner file register sub-commands?

The runner file registers sub-commands by invoking a commands helper function with a variadic list of constructor functions (e.g., install.New, exec.New). Each constructor returns a *cli.Command, and the helper aggregates these into a slice that the top-level urfave.Command uses to build the CLI tree.

What is the role of the commands function in runner.go?

The commands function acts as a factory aggregator. It accepts shared parameters (util.Param, cliargs.GlobalArgs) and a variadic list of constructor functions (type newC), then iterates over each constructor to instantiate and return the complete slice of CLI commands. This pattern decouples command registration from command implementation.

Where are individual aqua CLI commands defined?

Each command is defined in its own package under pkg/cli/*/command.go. For example, the install command logic resides in pkg/cli/install/command.go and exports an install.New constructor that returns a configured *cli.Command with specific flags and action handlers.

How are global flags shared across all aqua commands?

Global flags are defined in cliargs.GlobalArgs and registered via cliargs.GlobalFlags(globalArgs). These are passed to every command constructor in the commands helper, ensuring all sub-commands recognize universal flags like --log-level without individual flag declarations in each package.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →