Air — API Surface#
API types#
CLI (primary) + HTTP (internal proxy, optional) + Library (thin public Go API in the runner package)
CLI#
- Framework: stdlib
flag— no Cobra, no urfave/cli - Entry binary:
air(single binary, no subcommands registered via a framework)
Command structure#
Air has a flat command model with one optional positional subcommand and a set of flags:
| Invocation | Behaviour |
|---|---|
air | Start the file watcher and hot-reload loop using defaults or .air.toml |
air init | Write a default .air.toml to the current directory and exit |
air -v | Print version banner and exit |
Subcommand dispatch is manual: engine.Run() checks os.Args[1] == "init" at runtime (runner/engine.go:107), not via a framework router.
Flags#
Registered in main.go:parseFlag() via stdlib flag:
| Flag | Type | Default | Description |
|---|---|---|---|
-c | string | "" | Path to config file |
-d | bool | false | Debug mode (verbose internal logging) |
-v | bool | false | Show version and exit |
--color | string | "auto" | Colored output: auto, always, never |
Config-derived flags (reflection-generated)#
Beyond the four hard-coded flags, runner.ParseConfigFlag (runner/flag.go:8) uses reflection to walk the entire Config struct and register a flag.Flag for every field that has a usage struct tag. This means every config key is also available as a CLI flag, e.g.:
air --build.cmd="make" --build.delay=500 --proxy.enabled=true --proxy.proxy_port=8090The mapping is driven by the TOML key names, flattened with dot notation. Users can override any config setting without editing the TOML file. This is the project’s most unusual CLI design decision.
Flag patterns#
- Global flags only: no subcommand-scoped flags; the single flag set applies to the whole tool.
- No env-var binding for CLI flags themselves (env vars only affect config file discovery via
air_wd). - Pass-through args: any positional arguments remaining after
flag.Parse()are appended tocfg.Build.ArgsBin, forwarding them to the compiled binary at runtime (runner/config.go:421).
HTTP API (Proxy — optional)#
The proxy is enabled with proxy.enabled = true in .air.toml (or --proxy.enabled=true). When active, Air starts an HTTP server on proxy_port (default: not set, must be configured) that sits in front of the user’s app running on app_port.
- Router: stdlib
net/httpdefault mux (http.HandleFunc) - Server struct:
runner.Proxy(runner/proxy.go) - Route registration: hard-coded in
Proxy.Run()(runner/proxy.go:67-73)
Endpoints#
| Method | Path | Handler | Description |
|---|---|---|---|
ANY | / (catch-all) | proxyHandler | Reverse proxy — forwards all requests to localhost:<app_port>. Injects live-reload <script> into HTML responses. Handles gzip and brotli decompression transparently. Supports chunked/SSE streaming pass-through. |
GET | /__air_internal/sse | reloadHandler | Server-Sent Events endpoint. Connected browser clients receive reload or build-failed events. Used by the injected proxy.js to trigger page reloads. |
GET | /__air_internal/worker.js | workerScriptHandler | Serves the embedded worker.js asset (a Web Worker used by the live-reload client). |
Middleware / headers#
No middleware chain. Each handler sets headers directly:
reloadHandlersetsContent-Type: text/event-stream,Cache-Control: no-cache,Connection: keep-alive,Access-Control-Allow-Origin: *proxyHandlersetsX-Forwarded-For,Via,Access-Control-Allow-Origin: *, and stripsContent-Lengthwhen rewriting HTML
Authentication#
None. The proxy is a local development tool; no auth is applied.
SSE event types#
| Event payload | Meaning |
|---|---|
data: reload\n\n | A successful build completed; browser should reload |
data: build-failed\n\n (with JSON body) | Build failed; browser may show an overlay |
The SSE stream is managed by ProxyStream / Subscriber in runner/proxy_stream.go. Subscribers are goroutine-safe via sync.Mutex; each subscriber gets a private msgCh chan Message.
Library API (runner package)#
The runner package is the only importable package. It is not designed as a reusable library — there is no stability guarantee and no versioned API surface — but its exported symbols form a usable embedding API.
Exported constructors#
| Symbol | Signature | Description |
|---|---|---|
InitConfig | func InitConfig(path string, cmdArgs map[string]TomlInfo) (*Config, error) | Load, merge, and validate configuration |
NewEngineWithConfig | func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) | Create engine from pre-loaded config |
NewEngine | func NewEngine(cfgPath string, args map[string]TomlInfo, debugMode bool) (*Engine, error) | Convenience: load config + create engine in one call |
ParseConfigFlag | func ParseConfigFlag(f *flag.FlagSet) map[string]TomlInfo | Register all config fields as flags on an external FlagSet |
NewProxy | func NewProxy(cfg *cfgProxy) *Proxy | Create proxy (normally done inside NewEngineWithConfig) |
NewProxyStream | func NewProxyStream() *ProxyStream | Create SSE broadcast stream |
Key exported methods on Engine#
| Method | Description |
|---|---|
(*Engine).Run() | Start the watch/build/run loop; blocks until exit |
(*Engine).Stop() | Signal graceful shutdown (closes exitCh) |
Key exported types#
| Type | Description |
|---|---|
Config | Root config struct (all TOML sections embedded) |
TomlInfo | Carries a flag pointer, TOML key, default value, and usage string — used by ParseConfigFlag |
Streamer | Interface: AddSubscriber, RemoveSubscriber, Reload, BuildFailed, Stop — testability seam for the SSE stream |
API style#
Straightforward constructors returning concrete types or a single interface (Streamer). No functional options, no builder pattern, no generics. The only abstraction layer exposed is Streamer, which allows tests to substitute the SSE broadcaster.
Backward compatibility#
No explicit versioning strategy. The project’s go.mod module path is github.com/air-verse/air; the runner package has no v2 or semver-locked sub-path. Breaking changes to the runner API happen without notice — this is consistent with Air’s self-description as a CLI tool, not a library.
Plugin / Extension system#
None. Air has no plugin architecture, no hook points for third-party code, and no RPC or shared-library extension mechanism. The only designed extension points are:
Streamerinterface — swap the SSE broadcaster in testsexiterinterface — swapos.Exitin tests (unexported)pre_cmd/post_cmdconfig arrays — run arbitrary shell commands before build and after shutdown respectively; effectively a scriptable hook system at the shell level, not a Go API
Key observations#
Reflection-driven CLI flags are the headline API design choice. Rather than hand-maintaining a parallel list of CLI flags and config keys,
ParseConfigFlagauto-generates flags from theConfigstruct’susagetags. This keeps the CLI and config in sync with zero maintenance cost, at the price of an unusual calling convention.The proxy API surface is minimal and internal. The three HTTP endpoints are never intended for external consumption —
/__air_internal/sseis consumed only by the injectedproxy.js, and the proxy catch-all is transparent to the user’s app. There is no versioning or stability contract.No gRPC, no REST API, no plugin bus. Air is a narrow-purpose CLI tool; its API surface is intentionally small. The design maxim visible throughout is “add an abstraction only when you need to mock a boundary.”