Where Is the Command Registration Logic in Aqua? A Deep Dive into the CLI Architecture
Aqua registers all CLI commands in pkg/cli/runner.go, where the Run function assembles the command tree using a commands helper that aggregates sub-command constructors like install.New and root.New before attaching them to the root aqua command.
Aqua is a declarative CLI version manager written in Go that leverages the urfave/cli/v3 framework. Understanding its command registration logic is essential for contributors extending the tool or developers studying CLI patterns in Go. The entire command tree is constructed in a single location using a functional constructor pattern that keeps sub-command definitions modular and testable.
Entry Point: From main.go to the Registration Layer
The journey begins at cmd/aqua/main.go, which serves as the application entry point. According to the Aqua source code, this file invokes urfave.Main("aqua", version, cli.Run), where urfave.Main is a thin wrapper that immediately forwards execution to the internal cli.Run function.
This handoff passes control to pkg/cli/runner.go, the central location where the command registration logic actually executes. The entry point itself contains no command definitions; it merely bootstraps the runtime and delegates to the registration layer.
Core Registration Logic in runner.go
The Run function (lines 31‑88 in pkg/cli/runner.go) orchestrates the entire command assembly. It initializes two critical context objects:
util.Param– The shared runtime parameter containing configuration and environment details.cliargs.GlobalArgs– The global argument definitions available to all sub-commands.
The function then creates the root command by calling urfave.Command with a top-level cli.Command struct named aqua.
The commands Helper
Inside Run, a variadic helper function named commands (lines 39‑48) performs the actual aggregation. This helper accepts the runtime context along with a list of constructor functions—such as initcmd.New, install.New, update.New, and root.New—and iterates over them to collect their *cli.Command return values. Each constructor returns a fully configured command ready for attachment to the parent tree.
Sub-Command Construction and Organization
Aqua follows a strict package-per-command convention. Each sub-command resides in its own directory under pkg/cli/ (for example, pkg/cli/root/command.go for the root command) and exports a constructor function, typically named New, with the signature:
func New(r *util.Param, globalArgs *cliargs.GlobalArgs) *cli.Command
These constructors return *cli.Command instances defined using the urfave/cli/v3 API. The commands helper in runner.go invokes these constructors and appends the results to the root command's sub-command slice. This pattern keeps command logic encapsulated while allowing the central runner.go file to maintain a clear inventory of available commands.
Practical Example: Adding a New Command to Aqua
To extend Aqua with a custom command, create a new package file (e.g., pkg/cli/mycmd/command.go) implementing the constructor pattern:
// pkg/cli/mycmd/command.go
package mycmd
import (
"context"
"github.com/aquaproj/aqua/pkg/cli/util"
"github.com/aquaproj/aqua/pkg/cliargs"
"github.com/urfave/cli/v3"
)
func New(r *util.Param, globalArgs *cliargs.GlobalArgs) *cli.Command {
return &cli.Command{
Name: "mycmd",
Usage: "My custom command",
Action: func(_ context.Context, _ *cli.Command) error {
// Implementation here
return nil
},
}
}
Then register the command by adding the constructor to the variadic list in pkg/cli/runner.go:
// Inside Run function, within the commands() call
commands(
param,
globalArgs,
initcmd.New,
install.New,
update.New,
// ... existing constructors ...
root.New,
mycmd.New, // ← new command registration
),
Summary
cmd/aqua/main.goserves as the application entry point, delegating to the internal CLI runner.pkg/cli/runner.gocontains the core command registration logic in theRunfunction (lines 31‑88) and thecommandshelper (lines 39‑48).- Constructor pattern: Each sub-command (e.g.,
install,root,update) lives inpkg/cli/<name>/command.goand exports aNewfunction returning*cli.Command. - Registration flow: The
commandshelper aggregates all constructors and attaches them to the rootaquacommand defined inrunner.go.
Frequently Asked Questions
Where is the main entry point for the Aqua CLI?
The main entry point is cmd/aqua/main.go, which invokes urfave.Main("aqua", version, cli.Run). This function immediately forwards execution to the Run function in pkg/cli/runner.go, where the actual command tree is built.
How does Aqua organize its sub-command packages?
Each sub-command resides in a dedicated package under pkg/cli/, such as pkg/cli/root/ or pkg/cli/install/. Each package contains a command.go file that exports a New constructor function returning a configured *cli.Command struct.
What CLI framework does Aqua use for command registration?
Aqua uses urfave/cli/v3 for its command-line interface. The registration logic constructs cli.Command structs from the urfave library and assembles them into a hierarchical tree in pkg/cli/runner.go.
How do I add a custom command to Aqua?
Create a new package under pkg/cli/<yourcommand>/ with a command.go file implementing the New(r *util.Param, globalArgs *cliargs.GlobalArgs) *cli.Command constructor. Then add your constructor (e.g., yourcommand.New) to the variadic commands function call inside the Run function in pkg/cli/runner.go.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →