# Where Is the Command Registration Logic in Aqua? A Deep Dive into the CLI Architecture

> Discover where the command registration logic resides in Aqua's CLI architecture. We explore the runner go file and how commands are assembled for a full understanding of the CLI.

- Repository: [aquaproj/aqua](https://github.com/aquaproj/aqua)
- Tags: deep-dive
- Published: 2026-02-25

---

**Aqua registers all CLI commands in [`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/root/command.go) for the `root` command) and exports a constructor function, typically named `New`, with the signature:

```go
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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/mycmd/command.go)) implementing the constructor pattern:

```go
// 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`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go)**:

```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.go`](https://github.com/aquaproj/aqua/blob/main/cmd/aqua/main.go)** serves as the application entry point, delegating to the internal CLI runner.
- **[`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go)** contains the core command registration logic in the `Run` function (lines 31‑88) and the `commands` helper (lines 39‑48).
- **Constructor pattern**: Each sub-command (e.g., `install`, `root`, `update`) lives in `pkg/cli/<name>/command.go` and exports a `New` function returning `*cli.Command`.
- **Registration flow**: The `commands` helper aggregates all constructors and attaches them to the root `aqua` command defined in [`runner.go`](https://github.com/aquaproj/aqua/blob/main/runner.go).

## Frequently Asked Questions

### Where is the main entry point for the Aqua CLI?

The main entry point is **[`cmd/aqua/main.go`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/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`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go).