Air — Structure#

Layout pattern#

Flat / Custom — Air does not follow the Standard Go Layout. There is no cmd/ directory; the single binary’s entry point (main.go) lives at the repository root alongside version.go. All business logic is consolidated into one package (runner/). This is a deliberate minimalist choice appropriate for a small, single-binary CLI tool.

Directory map#

air/
├── main.go                  # Binary entry point: flag parsing, signal handling, engine startup
├── version.go               # Version variables injected via ldflags at build time
├── air_example.toml         # Reference configuration file (ships with project)
├── Makefile                 # Local dev targets: build, install, test, release, docker
├── Dockerfile               # Multi-stage Docker image (golang builder → golang runtime)
├── .goreleaser.yml          # Release automation: linux/windows/darwin, tar.gz + binary
├── install.sh               # Shell installer for direct binary download
├── go.mod / go.sum          # Module definition
├── runner/                  # All business logic (single package)
│   ├── engine.go            # Core orchestration: build loop, file event dispatch
│   ├── config.go            # Config loading from .air.toml (go-toml) and defaults
│   ├── watcher.go           # File system watching via fsnotify
│   ├── proxy.go             # Optional HTTP reverse proxy with live-reload injection
│   ├── proxy_stream.go      # SSE/WebSocket stream support for the proxy
│   ├── flag.go              # Reflection-based CLI flag generation from config struct
│   ├── logger.go            # Colored console logger (fatih/color)
│   ├── exiter.go            # OS exit wrapper (enables testable exit calls)
│   ├── common.go            # Shared constants and small utilities
│   ├── util.go              # Cross-platform helpers
│   ├── util_linux.go        # Linux-specific utilities (build tag: linux)
│   ├── util_unix.go         # Unix-specific utilities (build tag: !windows)
│   ├── util_windows.go      # Windows-specific utilities (build tag: windows)
│   └── _testdata/           # Test fixtures (toml configs, watching trees)
│       ├── both/
│       ├── invalid_toml/
│       ├── toml/
│       └── watching/
├── docs/
│   ├── air.png              # Logo/mascot image used in README
│   └── check_rebuild        # (unclear artifact, likely docs CI script)
├── hack/
│   └── check.sh             # Pre-CI linting/formatting check script
├── hooks/
│   └── pre-commit           # Git pre-commit hook installed by `make init`
└── smoke_test/
    ├── smoke_test.py        # Python-based end-to-end smoke test
    └── check_rebuild/       # Helper scripts for smoke test rebuild verification

Entry points#

FileBinaryPurpose
main.goairThe only binary. Parses flags (-c, -d, -v, --color, plus dynamic config overrides), initializes config, creates the engine, installs signal handlers for graceful shutdown, and calls r.Run().

There is no cmd/ directory. The root main.go is the sole entry point.

Package organization#

  • Internal packages: None — there is no internal/ directory.
  • Public packages (pkg/): None — there is no pkg/ directory.
  • runner/ package: The single non-main package. It is exported (public), but treated as the project’s implementation package. It contains the entire application: engine orchestration, config loading, file watching, HTTP proxy, flag parsing, logging, and platform utilities. Users of Air as a Go library (unusual but possible) would import github.com/air-verse/air/runner.
  • Layering: Flat — just two layers: main (wiring + signal handling) and runner (everything else). No clean architecture, no hexagonal layering, no internal sub-packages. The entire domain lives in one package namespace.

Build system#

  • Build tool: make for local development; goreleaser for official releases
  • Key Makefile targets:
    • make build — runs hack/check.sh then go build with ldflags (version, timestamp)
    • make install — same as build but go install
    • make testgo test ./... -v -race -timeout=3m
    • make test-ciCI=true go test ./... -v -timeout=5m (disables color in output)
    • make release — cross-compiles for darwin/linux/windows amd64 into bin/
    • make check — runs hack/check.sh (linting/formatting)
    • make init — installs golangci-lint, goimports, and git pre-commit hook
    • make docker-image / make push-docker-image — builds and pushes Docker image
  • GoReleaser targets: linux, windows, darwin (excludes darwin/386); produces tar.gz archives and raw binaries; injects version and Go version via ldflags
  • Docker: Yes, multi-stage. Stage 1: golang:1.26 builder runs make ci && make install. Stage 2: golang:1.26 final image copies /go/bin/air binary. Uses BuildKit cache mounts for go/pkg/mod and Go build cache. Entrypoint is air directly.
  • CGO: Disabled (CGO_ENABLED=0), producing fully static binaries.

Notable structural decisions#

  1. Root-level main.go instead of cmd/air/main.go: For a single-binary tool with no plans for multiple commands, this is a pragmatic simplification. It avoids the boilerplate of a cmd/ tree and keeps the repo root navigable.

  2. Everything in one runner/ package: With only ~20 non-test Go files in runner/, keeping them in a single package avoids the overhead of inter-package imports and circular dependency management. The tradeoff is that runner has no internal boundaries — engine.go can freely call anything in config.go, proxy.go, etc. This works fine at this scale but would become problematic if the project grew.

  3. Platform-specific files via build tags inside runner/: Rather than separate packages per OS, Air uses three files (util_linux.go, util_unix.go, util_windows.go) with Go build constraints within the same package. This is idiomatic Go for small platform divergences.

  4. Smoke tests in Python, not Go: The smoke_test/ directory contains a smoke_test.py script rather than a Go integration test. This suggests the smoke test existed before or alongside the Go test suite and tests the binary as an external black-box process — reasonable for an end-to-end rebuild verification.

  5. version.go at the root with ldflags injection: Version information is injected at link time (-X "main.airVersion=...", -X "main.goVersion=..."), with a fallback to debug.ReadBuildInfo() at runtime. This is a well-established Go pattern that keeps version info out of source while supporting both go install and official release builds.

  6. Git pre-commit hook via Makefile: The make init target symlinks hooks/pre-commit into .git/hooks/, ensuring linting/formatting checks run before every commit. This is developer-ergonomics infrastructure that lives in the repo rather than relying on external tooling.