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 verificationEntry points#
| File | Binary | Purpose |
|---|---|---|
main.go | air | The 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 importgithub.com/air-verse/air/runner.- Layering: Flat — just two layers:
main(wiring + signal handling) andrunner(everything else). No clean architecture, no hexagonal layering, no internal sub-packages. The entire domain lives in one package namespace.
Build system#
- Build tool:
makefor local development;goreleaserfor official releases - Key Makefile targets:
make build— runshack/check.shthengo buildwith ldflags (version, timestamp)make install— same as build butgo installmake test—go test ./... -v -race -timeout=3mmake test-ci—CI=true go test ./... -v -timeout=5m(disables color in output)make release— cross-compiles for darwin/linux/windows amd64 intobin/make check— runshack/check.sh(linting/formatting)make init— installs golangci-lint, goimports, and git pre-commit hookmake 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.26builder runsmake ci && make install. Stage 2:golang:1.26final image copies/go/bin/airbinary. Uses BuildKit cache mounts forgo/pkg/modand Go build cache. Entrypoint isairdirectly. - CGO: Disabled (
CGO_ENABLED=0), producing fully static binaries.
Notable structural decisions#
Root-level
main.goinstead ofcmd/air/main.go: For a single-binary tool with no plans for multiple commands, this is a pragmatic simplification. It avoids the boilerplate of acmd/tree and keeps the repo root navigable.Everything in one
runner/package: With only ~20 non-test Go files inrunner/, keeping them in a single package avoids the overhead of inter-package imports and circular dependency management. The tradeoff is thatrunnerhas no internal boundaries —engine.gocan freely call anything inconfig.go,proxy.go, etc. This works fine at this scale but would become problematic if the project grew.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.Smoke tests in Python, not Go: The
smoke_test/directory contains asmoke_test.pyscript 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.version.goat the root with ldflags injection: Version information is injected at link time (-X "main.airVersion=...",-X "main.goVersion=..."), with a fallback todebug.ReadBuildInfo()at runtime. This is a well-established Go pattern that keeps version info out of source while supporting bothgo installand official release builds.Git pre-commit hook via Makefile: The
make inittarget symlinkshooks/pre-commitinto.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.