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:

InvocationBehaviour
airStart the file watcher and hot-reload loop using defaults or .air.toml
air initWrite a default .air.toml to the current directory and exit
air -vPrint 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:

FlagTypeDefaultDescription
-cstring""Path to config file
-dboolfalseDebug mode (verbose internal logging)
-vboolfalseShow version and exit
--colorstring"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=8090

The 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 to cfg.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/http default mux (http.HandleFunc)
  • Server struct: runner.Proxy (runner/proxy.go)
  • Route registration: hard-coded in Proxy.Run() (runner/proxy.go:67-73)

Endpoints#

MethodPathHandlerDescription
ANY/ (catch-all)proxyHandlerReverse 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/ssereloadHandlerServer-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.jsworkerScriptHandlerServes the embedded worker.js asset (a Web Worker used by the live-reload client).

Middleware / headers#

No middleware chain. Each handler sets headers directly:

  • reloadHandler sets Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, Access-Control-Allow-Origin: *
  • proxyHandler sets X-Forwarded-For, Via, Access-Control-Allow-Origin: *, and strips Content-Length when rewriting HTML

Authentication#

None. The proxy is a local development tool; no auth is applied.

SSE event types#

Event payloadMeaning
data: reload\n\nA 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#

SymbolSignatureDescription
InitConfigfunc InitConfig(path string, cmdArgs map[string]TomlInfo) (*Config, error)Load, merge, and validate configuration
NewEngineWithConfigfunc NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error)Create engine from pre-loaded config
NewEnginefunc NewEngine(cfgPath string, args map[string]TomlInfo, debugMode bool) (*Engine, error)Convenience: load config + create engine in one call
ParseConfigFlagfunc ParseConfigFlag(f *flag.FlagSet) map[string]TomlInfoRegister all config fields as flags on an external FlagSet
NewProxyfunc NewProxy(cfg *cfgProxy) *ProxyCreate proxy (normally done inside NewEngineWithConfig)
NewProxyStreamfunc NewProxyStream() *ProxyStreamCreate SSE broadcast stream

Key exported methods on Engine#

MethodDescription
(*Engine).Run()Start the watch/build/run loop; blocks until exit
(*Engine).Stop()Signal graceful shutdown (closes exitCh)

Key exported types#

TypeDescription
ConfigRoot config struct (all TOML sections embedded)
TomlInfoCarries a flag pointer, TOML key, default value, and usage string — used by ParseConfigFlag
StreamerInterface: 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:

  • Streamer interface — swap the SSE broadcaster in tests
  • exiter interface — swap os.Exit in tests (unexported)
  • pre_cmd / post_cmd config 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#

  1. Reflection-driven CLI flags are the headline API design choice. Rather than hand-maintaining a parallel list of CLI flags and config keys, ParseConfigFlag auto-generates flags from the Config struct’s usage tags. This keeps the CLI and config in sync with zero maintenance cost, at the price of an unusual calling convention.

  2. The proxy API surface is minimal and internal. The three HTTP endpoints are never intended for external consumption — /__air_internal/sse is consumed only by the injected proxy.js, and the proxy catch-all is transparent to the user’s app. There is no versioning or stability contract.

  3. 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.”