# Understanding the Architecture of the aqua CLI: A Modular Go Design

> Discover the modular Go architecture of the aqua CLI. Explore its distinct layers for entry points, commands, and logic, seamlessly connected by compile-time dependency injection.

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

---

**The architecture of the aqua CLI separates entry points, command definitions, and business logic controllers into distinct layers, wiring them together via compile-time dependency injection and shared runtime parameters.**

The aqua CLI, developed in the `aquaproj/aqua` repository, serves as a declarative package manager for CLI tools. Understanding the architecture of the aqua CLI reveals a well-structured Go application that maintains strict boundaries between command-line interface parsing, configuration management, and core business logic through distinct architectural layers.

## Architectural Overview of the aqua CLI

The architecture follows a clean separation of concerns across multiple layers:

| Layer | Responsibility | Key Source File |
|-------|--------------|-----------------|
| **Entry Point** | Program initialization and delegation | [`cmd/aqua/main.go`](https://github.com/aquaproj/aqua/blob/main/cmd/aqua/main.go) |
| **Runner** | Shared parameter creation and command registration | [`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go) |
| **Global Arguments** | Flag definitions applicable to all commands | [`pkg/cli/cliargs/cliargs.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/cliargs/cliargs.go) |
| **Command Packages** | Sub-command definitions (install, list, etc.) | `pkg/cli/*/command.go` |
| **Util.Param** | Runtime state container (IO, logger, version) | [`pkg/cli/util/util.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/util/util.go) |
| **Controllers** | Business logic implementation | `pkg/controller/*` |
| **Configuration** | aqua.yaml parsing and validation | `pkg/config/*` |
| **Dependency Injection** | Wire-generated constructor wiring | [`pkg/controller/wire.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/wire.go) |

## Entry Point and Command Orchestration

### The Main Entry Point ([`cmd/aqua/main.go`](https://github.com/aquaproj/aqua/blob/main/cmd/aqua/main.go))

The program begins in [`cmd/aqua/main.go`](https://github.com/aquaproj/aqua/blob/main/cmd/aqua/main.go), which serves as the minimal bootstrap layer. This file invokes the runner's entry function, immediately delegating control to the orchestration layer. This design keeps the main package clean and focused solely on program startup.

### The Runner Layer ([`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go))

The [`runner.go`](https://github.com/aquaproj/aqua/blob/main/runner.go) file acts as the central orchestrator. It performs three critical functions:

1. **Creates shared runtime parameters** via `util.Param`, encapsulating stdin/stdout handles, the logger, runtime information, and version data
2. **Parses global flags** defined in `cliargs.GlobalFlags` that apply to every sub-command
3. **Registers all sub-commands** by instantiating command packages and assembling them into the CLI hierarchy

This layer ensures that every command receives consistent access to IO streams, logging, and configuration without duplicating setup code.

## Command Structure and Registration

### Global CLI Arguments ([`pkg/cli/cliargs/cliargs.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/cliargs/cliargs.go))

Global flags that apply across all commands—such as `--log-level`, `--config` file path, and tracing options—are defined in [`pkg/cli/cliargs/cliargs.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/cliargs/cliargs.go). The `GlobalFlags` struct captures these values, which the runner then injects into every sub-command context. This centralized definition ensures consistent flag behavior and validation across the entire CLI surface.

### Sub-command Packages (`pkg/cli/*/command.go`)

Each sub-command (such as `install`, `root-dir`, or `list`) resides in its own package under `pkg/cli/`. For example, the install command lives in [`pkg/cli/install/command.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/install/command.go).

Every command package exports a `New` function with this signature pattern:

```go
func New(r *util.Param, globalArgs *cliargs.GlobalArgs) *cli.Command

```

This function:
- Embeds global arguments into a command-specific `Args` struct
- Returns a `*cli.Command` (from the `urfave/cli` library) with the `Action` field set to a method that invokes the controller

This pattern keeps CLI concerns (flag parsing, help text) separate from business logic.

## The Controller Layer: Business Logic Implementation

### Controller Architecture (`pkg/controller/*`)

The actual work of each command happens in `pkg/controller/*`. For example, [`pkg/controller/install/install.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/install/install.go) contains the `Install` method that downloads binaries, validates checksums, and creates symlinks.

Controllers are instantiated with configuration parameters parsed from flags and environment variables. They operate independently of CLI parsing, receiving only the structured data they need to perform operations. This separation makes the business logic testable without requiring a full CLI context.

### Dependency Injection with Wire ([`pkg/controller/wire.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/wire.go))

The project uses Google's Wire tool for compile-time dependency injection. The [`pkg/controller/wire.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/wire.go) file contains Wire provider sets that wire together concrete implementations of interfaces used by controllers.

For example, when a controller needs a downloader or checksum validator, Wire generates the constructor code that supplies the correct implementation. This eliminates manual service wiring while maintaining type safety and making dependencies explicit.

## Configuration and Runtime Parameters

### Shared Runtime State ([`pkg/cli/util/util.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/util/util.go))

The `util.Param` struct defined in [`pkg/cli/util/util.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/util/util.go) serves as the shared runtime context passed to every command. It contains:

- **IO streams**: `Stdin`, `Stdout`, `Stderr`
- **Logger**: Structured logging interface
- **Runtime**: OS and architecture information
- **Version**: Build version and commit information

This centralization ensures consistent access to system resources and application metadata across all commands without global variables.

### Configuration Parsing (`pkg/config`)

Configuration handling resides in `pkg/config/`. Files like [`pkg/config/reader.go`](https://github.com/aquaproj/aqua/blob/main/pkg/config/reader.go) parse [`aqua.yaml`](https://github.com/aquaproj/aqua/blob/main/aqua.yaml), resolve the root installation directory, handle environment variable overrides, and construct a `config.Param` struct.

Controllers consume this `config.Param` to determine which tools to install, where to place them, and which registries to consult. The separation between CLI argument parsing (`cliargs`) and configuration file parsing (`config`) allows the tool to handle complex configuration hierarchies independently of command-line flag processing.

## Command Lifecycle: How `aqua install` Works

Understanding the architecture becomes concrete when tracing the `install` command through the system:

1. **Entry**: [`cmd/aqua/main.go`](https://github.com/aquaproj/aqua/blob/main/cmd/aqua/main.go) invokes the runner
2. **Global Setup**: [`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go) creates `util.Param` and parses `cliargs.GlobalFlags`
3. **Command Registration**: The runner calls `install.New(r, globalArgs)` from [`pkg/cli/install/command.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/install/command.go)
4. **Action Execution**: The command's `Action` calls `command.action(args)`, which:
   - Starts profiling via `profile.Start`
   - Converts arguments to `config.Param` using `util.SetParam`
   - Creates the controller via `controller.InitializeInstallCommandController`
   - Invokes `controller.Install` to perform downloads and linking
5. **Business Logic**: The controller in [`pkg/controller/install/install.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/install/install.go) orchestrates lower-level packages (`download`, `checksum`, `policy`) to complete the installation

This flow illustrates how the architecture maintains strict boundaries: CLI concerns stop at the command package, while business logic remains isolated in controllers.

## Summary

The architecture of the aqua CLI demonstrates a clean, modular design that separates concerns across distinct layers:

- **Entry and Orchestration**: [`cmd/aqua/main.go`](https://github.com/aquaproj/aqua/blob/main/cmd/aqua/main.go) and [`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go) handle startup and command registration
- **Command Definitions**: Individual packages in `pkg/cli/*` define sub-commands using the `urfave/cli` library
- **Business Logic**: Controllers in `pkg/controller/*` contain implementation details, instantiated via Wire-generated dependency injection
- **Configuration**: `pkg/config` and `pkg/cli/util` manage runtime parameters and aqua.yaml parsing
- **Global State**: `util.Param` provides consistent IO, logging, and runtime information across all commands

This structure makes the codebase testable, extensible, and maintainable, allowing new commands to be added by implementing the standard pattern in `pkg/cli/` and corresponding controllers in `pkg/controller/`.

## Frequently Asked Questions

### What is the role of the Runner in the aqua CLI architecture?

The Runner in [`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go) serves as the central orchestrator that initializes shared runtime parameters, parses global flags defined in [`pkg/cli/cliargs/cliargs.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/cliargs/cliargs.go), and registers all sub-commands. It creates the `util.Param` struct containing IO streams, logger, and version information, ensuring every command receives consistent runtime context without duplicating setup code.

### How does the aqua CLI handle dependency injection?

The project uses Google's Wire tool for compile-time dependency injection, configured in [`pkg/controller/wire.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/wire.go). Wire generates constructor code that wires together concrete implementations of interfaces used by controllers, such as downloaders and checksum validators. This approach eliminates manual service wiring while maintaining type safety and making dependencies explicit before compilation.

### What is the difference between Command packages and Controllers in aqua?

Command packages located in `pkg/cli/*` (such as [`pkg/cli/install/command.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/install/command.go)) handle CLI-specific concerns including flag parsing, help text, and argument validation using the `urfave/cli` library. Controllers located in `pkg/controller/*` (such as [`pkg/controller/install/install.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/install/install.go)) contain the actual business logic for executing commands, operating independently of CLI parsing and receiving only structured configuration data. This separation makes business logic testable without requiring a full CLI context.

### How does the aqua CLI manage global flags across all sub-commands?

Global flags such as `--log-level`, `--config`, and tracing options are defined centrally in [`pkg/cli/cliargs/cliargs.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/cliargs/cliargs.go) within the `GlobalFlags` struct. The Runner in [`pkg/cli/runner.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/runner.go) parses these flags once during startup and injects them into every sub-command context through the `New` function pattern. This centralized definition ensures consistent flag behavior, validation, and help text across the entire CLI surface without duplicating flag definitions in individual command packages.