hgr CLI Reference
Command structure, noun-verb pattern, output modes, authentication, config file, fake backend.
hgr is Wingsmith’s command-line interface. One Go binary that talks to the control plane’s HTTP API. It never holds a kubeconfig, never dials a cluster directly, and never talks to the hub.
Command structure
Every command follows a noun-verb pattern:
hgr <noun> <verb> [args]
Examples:
hgr cluster ls
hgr cluster get iad-1
hgr workload create
hgr operation create node.drain iad-1-n01 --reason firmware --wait
Nouns
Commands are organized by the object model:
| Group | Nouns | Type |
|---|---|---|
| Supply | cluster, node, device, device-class, topology-domain, pool | Observed, except Cluster and Pool (declared halves too) |
| Demand | artifact, workload, deployment, endpoint, discovered | Declared, except Discovered (observed) |
| Binding | tenant, project, quota, serving-quota, priority-class, policy | Declared |
| Machinery | operation, allocation, component, event, benchmark, alert-threshold | Imperative, observed, and mixed |
Local commands: login, logout, context, config, whoami, explain, version.
Verbs
All nouns have ls and get. Verbs that change something:
| Noun | Verbs |
|---|---|
workload | create, scale, pause, resume, drain, rm |
operation | create, cancel |
serving-quota | create, set, rm |
alert-threshold | create, set, rm |
benchmark | run, request create |
discovered | track, ignore, promote, rm, restore |
Operations are created with hgr operation create <type> <target>. Available types: node.drain, node.cordon, node.uncordon, and workload operations like workload.scale.
Declared state, observed state, and operations stay separate. Draining a node is an operation (hgr operation create node.drain ...), not a flag on node, so there’s a record of who asked, why, and how it ended.
Output modes
Set with --output / -o, HANGAR_OUTPUT, or output: in the config file.
| Mode | Description |
|---|---|
auto (default) | Human table on a TTY, json when piped |
human / wide | Table; wide reveals columns marked wide (id, operator version) |
json | Stable machine contract, indented |
jsonl | One object per line, for streaming consumers |
yaml | Same data, round-tripped through JSON tags |
tsv | Same columns the table shows, tab-separated |
name | Bare identifiers, for xargs |
template=... | Go text/template over the JSON |
jsonpath=... | Dotted path with [n] and [*] projection |
The JSON contract for a list is the API’s own envelope, an object not a bare array:
{
"items": [...],
"count": 50,
"next_cursor": "MTAwMQ"
}
count is the size of this page. A listing with next_cursor is not the whole collection. Use --cursor to get the next page or --all to follow cursors to the end.
Agent mode
--agent or HANGAR_AGENT=1 wraps everything in one envelope: schema_version, ok, command, data, error, warnings, next_actions, meta. Prompts and animation are off, and meta.simulated is true when the fake backend answered.
Authentication
Human access to Wingsmith is a single admin login. hgr login reads the admin password and stores access and refresh tokens in the active context.
hgr login # prompts for password
hgr login --password-stdin # reads from stdin
hgr logout # revokes tokens
hgr whoami # shows current context
Password sources (in order):
--password-stdinHANGAR_ADMIN_PASSWORD- No-echo prompt
The access token is short-lived. hgr renews it with the refresh token when it expires. AUTH_EXPIRED means the refresh token was refused.
For CI, log in as admin and let the session expire with the job:
printf '%s' "$HANGAR_ADMIN_PASSWORD" | hgr login --server "$HANGAR_SERVER" --password-stdin
Do not give CI the operator or gateway service credentials. Those are narrow service principals with no admin rights. Use HANGAR_TOKEN or --token to supply an access token without storing it.
Tokens are never printed in any output mode and are redacted from re-run commands suggested in errors.
Configuration
Config file: ~/.config/hangar/config.yaml (or HANGAR_CONFIG, or $XDG_CONFIG_HOME/hangar/config.yaml), written mode 0600.
current_context: prod
contexts:
prod:
server: https://plane.example.com
token: ... # written by hgr login
refresh_token: ... # written by hgr login
project: default
output: auto
fake: false
Precedence: flag → environment → config file → default.
| Flag | Environment | Config key |
|---|---|---|
--context | HANGAR_CONTEXT | current_context |
--server | HANGAR_SERVER | context.server |
--token | HANGAR_TOKEN | context.token |
--project | HANGAR_PROJECT | context.project |
--output | HANGAR_OUTPUT | output |
--fake | HANGAR_FAKE | fake |
--agent | HANGAR_AGENT | none |
--timeout | HANGAR_TIMEOUT | none |
--wait-timeout | HANGAR_WAIT_TIMEOUT | none |
There is no default server. Until one is set, every command that needs the plane fails with USAGE and says how to set it. config, context, explain, and version work without a server or token.
Fake backend
hgr --fake runs against an offline in-memory plane for demos, tests, and CI without a server.
- Opt-in: selected by
--fake,HANGAR_FAKE=1, orfake: truein the config file. A command never answers from the fake when you expect a real plane. - Shaped like the server: field names, envelope, cursor behavior, and error vocabulary match the plane.
- Seeded, not random: four clusters in every phase, including an unreachable one and one that never connected, plus a cordoned node, an unhealthy device, an allocation holding fewer devices than it asked for, and a quota over its limit.
- Persistent state:
HANGAR_FAKE_STATE=/path/to/state.jsonpins the state to a file and persists mutations, so create-then-poll workflows work offline. - Failure injection:
HANGAR_FAKE_FAIL=NOT_FOUNDmakes every call fail with that code, for testing error paths.
Examples:
hgr --fake cluster ls
hgr --fake node ls --cluster iad-1 --cordoned
hgr --fake device ls --cluster iad-1 --allocatable --group-by domain
hgr --fake operation create node.drain iad-1-n02 --reason firmware --wait
login and logout never touch the network under --fake.
Error codes
Every failure is an error with a stable code. hgr explain lists them; hgr explain NOT_FOUND describes one.
| Exit | Codes |
|---|---|
| 0 | success |
| 1 | INTERNAL |
| 2 | USAGE, INPUT_REQUIRED |
| 3 | AUTH_REQUIRED, AUTH_EXPIRED, FORBIDDEN |
| 4 | NOT_FOUND |
| 5 | CONFLICT, INVALID_STATE, AMBIGUOUS, CONFIRMATION_REQUIRED |
| 8 | TIMEOUT |
| 9 | RATE_LIMITED |
| 10 | SERVER_ERROR, NETWORK |
| 11 | OPERATION_FAILED (a waited operation or benchmark ended failed or cancelled) |
| 130 | INTERRUPTED |
Human failures print the message, cause, runnable fixes, and code:
✗ no control plane URL configured for context "default"
cause looked in --server, HANGAR_SERVER, and ~/.config/hangar/config.yaml
try hgr config set context.server http://localhost:8000 Set one
hgr cluster ls --fake Or work offline
code USAGE · hgr explain USAGE
Special commands
hgr endpoint sla <endpoint> shows an endpoint’s service level for the current month, a --month YYYY-MM, or a --from/--to range, with the verdict against the success-rate objective its serving quota declares.
GET /v1/models is counted under the reserved endpoint _catalog. A request naming a model Wingsmith does not serve is counted under _unknown.