Skip to content

The CLI

The omniglass binary is both the server and the client. Its data commands are generated from the OpenAPI (make gen, via cmd/cligen), so the CLI cannot drift from the API: a new route is a new command on the next regeneration. A small set of commands (the run modes and the trusted bootstrap) are hand-written and compose with the generated tree on the same root.

This page is the mental model and the setup. For the exhaustive, generated list of every command, its flags, and an example, see the CLI reference.

make dev brings up the whole stack for a browser session: a dev Postgres (docker compose, matching the default DSN), the migrations and boot seed, a bootstrapped dev owner whose token is printed once, a dev example fleet (a few campuses with rooms, a few sign-in-able users, and their grants, so the console is not empty), and the server with the operator console at http://localhost:8080/web. Ctrl-C stops the server; make down stops Postgres (the named volume persists data between runs; docker compose down -v wipes it and re-mints a token next run). make up / make down manage just the database. Tests never touch this stack: they spin their own ephemeral Postgres via testcontainers.

The example fleet deliberately types almost none of its own names: its devices and its systems all ask the platform to name them, so what you sign in to is the name generator’s own output (display-1 in three different rooms, boardroom and boardroom-2 for the two halves of one) rather than a list somebody wrote out. The rows that do carry a typed name are the ones no rule could produce: every LOCATION, since no shipped location type carries a name rule (a campus, a building, a floor and a room each have a name you hold and the platform does not).

omniglass server serves the API on OMNIGLASS_ADDR (:8080 by default) and hosts the embedded bus nodes connect to on OMNIGLASS_NATS_ADDR (127.0.0.1:4222). Either address may name port 0, which binds whatever free port the kernel hands out, so a second server on the same host (a test run, another worktree’s stack) never collides with the first. To learn the ports it got, set OMNIGLASS_READY_FILE to a path: once both listeners are bound, the server writes the addresses there as one JSON object, written whole or not at all. A file already at that path (one a killed server left) is cleared first thing at boot, before anything that can fail, and the server removes its own at a graceful shutdown (SIGINT or SIGTERM), so a file you find there names a live server’s ports.

{"addr":"127.0.0.1:41234","nats_addr":"127.0.0.1:41236","nats_url":"nats://127.0.0.1:41236"}

nats_url is the bus URL the node-claim exchange hands each node: OMNIGLASS_NATS_URL when that is set, otherwise the address the bus bound (read as loopback when it bound a wildcard host such as 0.0.0.0), so a bus on port 0 tells nodes the port it got. Read the ports from the file rather than finding a free port and passing it in: a port that is free when you look can be taken by another process before the server binds it. The end-to-end tests start every server this way (test-driven).

Every generated command is a client of a running server and takes two shared flags, each with an environment default:

FlagEnvDefault
--serverOMNIGLASS_SERVERhttp://localhost:8080
--tokenOMNIGLASS_TOKEN(none)

The token is a bearer credential (mint the first one with omniglass bootstrap; see Authentication below). The server enforces the same capability and scope for the CLI as for any caller: the CLI is just another client, with no privileged path.

Terminal window
export OMNIGLASS_SERVER=https://omniglass.example.com
export OMNIGLASS_TOKEN=ogp_...
omniglass location list
omniglass location create --name hq --location-type campus
omniglass location get hq
omniglass location rename hq --name hq-west # needs location:rename, not location:update

Output is JSON. A non-2xx response prints the server’s error body and exits non-zero, so the CLI is safe in scripts.

The commands are generated from the API, so a request field’s type is a fact the spec already states and the flag carries it (#711). An integer field is an integer flag and --settle-window-seconds soon is refused by the shell, before a request is issued, rather than by the server’s 422 after a round trip. A boolean field is a bool flag, which is the one shape with a spelling to learn: write --propagates=false, since a bare --propagates means true and --propagates false reads false as a positional argument.

A field with no scalar type of its own (an object, an array, or a free-shape value) takes one string flag parsed as JSON, which is a deliberate choice rather than a leftover. A nested value has no shell-native flag type, and the alternatives (repeated key=value pairs, or a flag per leaf) can express neither an array member nor null, which is the value that clears a field named in an update_mask (ADR-0106):

Terminal window
omniglass location-type create --name wing --label Wing \
--name-rule '{"stem":"wing","bare_first":true}' # an object, as JSON
omniglass location-type update wing --update-mask name_rule --name-rule null # clears it
omniglass tag create --name rack-position --applies-to '["location"]' # an array, as JSON

A bare word that is not valid JSON falls back to itself, so the common case needs no quoting (--value HDMI1); a string that would parse as JSON is quoted to force a string (--value '"30"').

A name is renameable, and a rename is its own command rather than a flag on update: component, system, location, and principal-group each carry one, gated by <resource>:rename, which is granted separately because moving a name breaks the references stored outside Omniglass (bookmarks, runbooks, integration config). Inside it nothing breaks, since every reference holds the entity’s id, which every write returns (ADR-0076). A taken name is a 409, an illegal or uuid-shaped one a 422.

Every command takes a name, never only a uuid

Section titled “Every command takes a name, never only a uuid”

Every resource you type the name of answers to it, so a script or a runbook never has to look a uuid up first (ADR-1133). The uuid each command prints keeps working as well, and it is what to store when you need a handle that survives a rename:

Terminal window
omniglass component get 'boi.17c.415a.$comp.display-1' # a dotted address (quote the $comp)
omniglass endpoint retry 'boi.17c.415a.$comp.display-1/https' # <component>/<endpoint name>
omniglass secret update demo-panel-login --fields '{"password":"rotated"}' # its name
omniglass secret reveal '$platform/demo-panel-login' # <owner>/<name> where the name is shared
omniglass file download codec-firmware-2.1.4.txt # a filename
omniglass principal-group member create night-shift --principal-id alice
omniglass principal grant create alice --role viewer --scope-kind location --scope-id boi.17c

A name only you can see resolves for you even where someone else’s out-of-scope row shares it, and a name more than one row you can see answers to is a 404 listing each one in the spelling that resolves, so the error tells you what to type next:

secret "demo-panel-login" not found: 2 you can see answer to it; address one as $platform/demo-panel-login or boi.17c/demo-panel-login

A name is a lookup, so it lands on whatever the one row you can see by that name is now: a write answers with the row it reached (a secret’s owner_kind and owner_name), and a script that must never drift, a credential rotation above all, addresses <owner>/<name> or stores the uuid.

There are two ways to authenticate, both accepted on every request. A bearer token in the Authorization header (the --token flag above) is the path for services and the CLI. A username and password is the path for a human at the web console: the server verifies it and sets an httpOnly session cookie. The CLI itself always uses a bearer token.

The first owner is created directly against the database with bootstrap (the trusted lane, no running server needed):

Terminal window
# Mints a bearer credential (the token is printed once) and, with --password, a password
# credential (argon2id) so the owner can sign in to the console. This is a trusted direct-DB
# lane, so it is exempt from the password policy (unlike the console/API paths). The bootstrap
# token expires after --ttl (default 90 days, hard maximum 365 days).
omniglass bootstrap ops --password 'set-a-strong-one' --email [email protected] --label "Ops Lead"
# Mint a fresh bearer token for an existing user (direct-DB, owner lane). A --description
# (required) names what the token is for. Every credential is time-bounded: the token expires
# after --ttl (default 90 days, hard maximum 365 days; a --ttl above the cap is an error). A
# web-login session cookie has its own, shorter fixed lifetime.
omniglass token ops --description 'ci pipeline'
omniglass token ops --description 'nightly backup' --ttl 720h # a 30-day token
# A signed-in user can mint its own token over the API (the console Create token action), which
# returns the secret once: omniglass auth create-token --description 'my laptop cli'.
# Set or rotate a user's password (direct-DB, owner lane; also policy-exempt as the recovery path).
# A break-glass reset also revokes the user's live SESSIONS, so a stolen login stops at once; API
# tokens are kept unless --revoke-tokens is given (a full lockout of a compromised account).
omniglass set-password ops 'set-a-strong-one'
omniglass set-password ops 'set-a-strong-one' --revoke-tokens # full lockout: sessions and tokens

Once a server is running, a signed-in principal manages its own account through the generated self-scoped commands (each edits only the caller’s own profile):

Terminal window
omniglass auth me # your principal, permissions, and grants
omniglass auth update-profile --label "Ops Lead"
omniglass auth change-password --current-password 'orange-boat-42x' --new-password 'purple-canyon-7'
omniglass auth set-avatar --image-base64 "$(base64 -w0 me.jpg)" # set your profile picture
omniglass auth remove-avatar # clear it, falling back to initials
omniglass auth avatar # read your picture back as { image_base64 }

--image-base64 takes a plain base64 string, not a file path (base64-encode the image yourself, as the $(base64 …) above does); the server accepts JPEG, PNG, or WebP and normalizes it to a 256x256 JPEG. An administrator manages any principal’s picture with omniglass principal setAvatar alice --image-base64 … and omniglass principal removeAvatar alice (gated by principal:set-avatar), reading one back with omniglass principal avatar list alice (gated by principal:read:admin). A principal with no picture is a 404.

The collection surface regenerates into three top-level command groups, node, endpoint, and task; the composed reachability read hangs off the component that owns it (component reachability). They follow the same derivation as every other resource (POST /endpoints is endpoint create, GET /endpoints/{endpoint} is endpoint get <endpoint>), so nothing here is special-cased. All examples require the matching permission on the running server.

Register and enroll an edge node (the day-one handshake):

Terminal window
omniglass node list
omniglass node create --name edge-hq --label "HQ Edge Node" --location hq-west --description "HQ network closet" # needs node:create (all-scope)
omniglass node get edge-hq
omniglass node update edge-hq --label "HQ Edge" --location hq-west # needs node:update; the name is immutable
omniglass node delete edge-hq # needs node:delete; decommissions the node (cascades its endpoints, tasks, and enrollment)

Every command above is new in practice, not only in the docs: a hand-written node run mode occupied the same name as the generated node group, so cobra resolved omniglass node list to the daemon and it failed asking for --token. The run mode is now omniglass node run, a verb beside the others:

Terminal window
omniglass node run --name edge-hq --token ogp_... # the edge daemon: claim, pull the worklist, heartbeat

The simulator runs beside a node with that node’s identity, serving the devices placed on it, and the node’s simulation view shows what it serves and what reached it:

Terminal window
omniglass sim run --server https://omniglass.example --name edge-hq --token ogp_... --port-offset 10000 # serve edge-hq's simulated devices
omniglass node simulation edge-hq # connected?, capabilities, and hits per simulated device
omniglass component impairment set panel-1 --unplugged # break it: dark on every protocol
omniglass component impairment set panel-1 --latency-ms 300 # or slow it
omniglass component impairment set panel-1 # repair it
omniglass simulation get # every simulator, its devices and their hits
omniglass simulation profiles # the profile catalog and the products each simulates
omniglass component simulation get panel-1 # its API surface: protocols, routes, commands, OIDs
omniglass component simulation fire panel-1 --surface nvp --command "SET INPUT hdmi2" # the device answers
omniglass simulation add-component edge-hq --name panel-9 --product newtron-panel-7 # served on the next pull
omniglass simulation plan-system meeting-room # each role and the product it would get
omniglass simulation add-system edge-hq --standard meeting-room --system-type meeting --location boardroom-a # the whole system

A set replaces the whole impairment, so name every fault you want at once: --latency-ms 300 on an unplugged device plugs it back in.

Impairments apply only on a simulator started with --impairments and NET_ADMIN, in a namespace of its own, since it rewrites that namespace’s firewall.

Locally, make dev-sim (beside make dev) enrolls the dev fleet’s node once, keeps its token in bin/edge-hq.token, and runs the simulator and the node side by side, so the seeded fleet is collected for real; its devices’ ports below 1024 are lifted by SIM_PORT_OFFSET (default 10000, where the dev seed aims its endpoints).

The node-facing claim exchange is public (the enrollment token is the authentication), so a node presents its name and token to receive its NATS credential:

Terminal window
omniglass node claim --name edge-hq --token ogp_...

omniglass node enroll <name> mints (or re-mints) a node’s enrollment token and prints it once (gated by node:enroll), the same action as the console’s Enroll / Re-enroll.

Author a reachability check by creating an endpoint (its poll task is derived automatically):

Terminal window
# An endpoint owned by a component, placed on a node, with its probe target in params.
# It is named by its transport: --transport picks from the code registry, there is no --name flag.
omniglass endpoint create \
--transport tcp --component disp-1 --node edge-hq \
--params '{"target":"10.0.0.1:22"}' # needs endpoint:create
# Or ATTACH a driver instead of picking a transport: the driver's spec derives the
# transport, the target (from the host and port inputs), and the endpoint's tasks
# (a poll task per poll function, a standing listen task per listener). Secret
# inputs are references: the NAME of a secret of the declared shape, never a value.
omniglass endpoint create \
--driver snmp-generic --component amp-1 --node edge-hq \
--inputs '{"host":"10.0.0.40","community":"lab-community"}' # needs endpoint:create
omniglass endpoint list
# An endpoint is <component>/<endpoint name>: its name is its transport, unique on its component,
# and the component is any spelling the component commands take (a name, a dotted address, a uuid).
omniglass endpoint get disp-1/tcp
omniglass endpoint update disp-1/tcp --node edge-hq --params '{"target":"10.0.0.2:22"}'
# A driver-attached endpoint's address and secret edit in place, through its inputs
# (validated against the driver; its tasks re-derive in the same transaction):
omniglass endpoint update amp-1/snmp --inputs '{"port":"2323","community":"lab-community-2"}'
omniglass endpoint delete disp-1/tcp # refused (409) while its task references it
# An endpoint a refusal gated (endpoint-rejection names the rung, endpoint-rejection-reason says why,
# held session or not) is not attempted again until what it connects with changes, or an operator
# retries it:
omniglass endpoint retry disp-1/ssh # needs endpoint:retry; audited
omniglass endpoint retry disp-1/ssh --host-key SHA256:... # ssh: trust the offered key the refusal names (409 for any other)
omniglass endpoint retry disp-1/https --certificate SHA256:... # tls or https: the same for a certificate's public key (--host-key works too)
# A tls or https endpoint's verification mode is one of its driver's inputs, edited in place:
omniglass endpoint update disp-1/https --inputs '{"tls-mode":"verify"}' # verify, pin or skip
# The poll task is derived from the endpoint, so the task surface is read-only.
omniglass task list
omniglass task get <id>

The transports are a code registry the binary ships (omniglass transport list reads it): icmp and tcp probe layers 3 and 4, telnet reads its first bytes, tls runs the handshake and reports the certificate’s expiry, ssh, http and https (the handshake, then a response) climb to layer 7 (the probe draws a real response, so reached-but-not-responsive and responded-but-not-authenticated are visible states; an http probe sends no credential, so its driver’s polls prove the login), udp proves its rung with its driver’s request (a bare udp endpoint has nothing to send), and snmp proves its rung with a standard get (sysUpTime.0) carrying its driver’s community. An endpoint is named by its transport (the --transport, or the attached driver’s spec), unique within its component. An endpoint update changes only its node placement and params. A task is derived when its endpoint is created, so there is no task create, update, or delete; its placement follows the endpoint’s. A node purge cascades its endpoints and their derived tasks.

Read a component’s composed reachability (the verdict, the probe-layer signals, and the recent transitions the availability strip draws):

Terminal window
omniglass component reachability list disp-1 # needs component:read

Read a component’s property reconciliation, the want/told/is pivot (the current declared value with the contract default coalesced in, the intended value a command set, and the latest observed value, all series reads) with drift computed on read:

Terminal window
omniglass component reconciliation list disp-1 # needs component:read

The secret commands are generated like every other resource. secret covers the encrypted values, secret-type lists the shape registry, and component effective-secret reads the masked cascade onto one component. Output is masked JSON, the same as the console; plaintext lives behind reveal, which the server audits and which only admin and owner may call.

Terminal window
omniglass secret-type list # the shape registry (snmp-community, basic-auth)
omniglass secret list # the all-scope admin directory (masked fields)
omniglass secret create --name core-snmp --secret-type snmp-community \
--owner-kind location --owner hq --fields '{"community":"public"}'
omniglass secret update core-snmp --fields '{"community":"s3cret"}' # an omitted field keeps its value
omniglass secret reveal hq/core-snmp # audited plaintext decrypt (secret:reveal), by <owner>/<name>
omniglass secret delete core-snmp
omniglass component effective-secret list codec-1 # the masked cascade resolved onto a component

The effective read answers which secret applies to a device and where it comes from, never what it contains: fields are masked exactly as the directory masks them, and plaintext is only ever secret reveal. It rides secret:read, which the viewer floor does not carry.

--owner-kind is one of platform | location | system | component; --owner names the owning entity and is omitted for a platform secret (the install-wide tier, which needs an all-scope grant plus platform:create). Field maps pass as a JSON object to --fields, validated against the type’s shape.

A secret’s name is unique within its owner, and the cascade makes one name at several owners the ordinary case (a room’s core-snmp overriding the platform’s), so the bare name resolves only where exactly one secret you can see carries it. <owner>/<name> always resolves: the owner by name, dotted address or uuid, or $platform for the install-wide tier, which has no name of its own (omniglass secret reveal '$platform/core-snmp').

The variable commands are generated the same way. variable covers the plaintext values and component effective-variable reads the cascade onto one component. There is no reveal: the value is shown in the clear.

Terminal window
omniglass variable list # the all-scope admin directory
omniglass variable create --name poll-interval --value-type int \
--owner-kind system --owner east-auditorium-av --value 30
omniglass variable create --name retry --value-type json --owner-kind platform \
--value '{"retries":3,"backoff":"1s"}'
omniglass variable update east-auditorium-av/poll-interval --value 60 # validated against the fixed value_type
omniglass variable delete retry
omniglass component effective-variable list codec-1 # the cascade resolved onto a component

Both effective reads return the winner and the candidates it beat, with the tier each came from, so the answer explains itself rather than just asserting a value. The variable cascade resolves the system band from the component’s primary membership; resolving against a named system is offered only by component effective-tag list today.

--value-type is one of string | int | float | bool | json. --value is parsed as JSON, so a bare 30, true, or {"k":"v"} sends the number, the boolean, or the object; a bare word like HDMI1 falls back to a string, so the common case needs no quoting. A string value that would otherwise parse as JSON (30, true) is quoted to force a string: --value '"30"'. (secret create --fields parses the same way.)

A signed-in principal manages its own account through the generated self-scoped commands (omniglass auth me, auth update-profile, auth change-password, auth set-avatar / auth remove-avatar); an administrator manages other principals, roles, groups, secrets, and variables through the resource commands, all covered in the admin guide and listed in full in the CLI reference.

The tag commands split along the governance line. The tag resource covers the key vocabulary (minting, editing, and deleting keys, plus the install-wide platform binding), while binding a value onto an entity is a custom method on the entity (component setTag and friends), so it needs only the write the operator already holds on that entity. effective-tag reads the resolved cascade onto one component.

Terminal window
omniglass tag list # the governed key vocabulary
omniglass tag create --name environment # mint a key (tag:create, admin)
omniglass tag create --name rack-position --applies-to '["location"]' --propagates=false
omniglass tag update environment --applies-to '["component","system"]'
omniglass tag setPlatform environment --value prod # an install-wide default (tag:update + platform:update)
omniglass tag clearPlatform environment
omniglass tag delete environment # cascades its bindings
omniglass component setTag codec-1 --key environment --value dev # component:update
omniglass component listTags codec-1 # the bindings set directly on the component
omniglass component removeTag codec-1 --key environment
omniglass system setTag east-auditorium-av --key environment --value prod
omniglass location setTag hq --key environment --value staging
omniglass component effective-tag list codec-1 # the cascade resolved onto a component

Binding is a custom method on the entity (component setTag), like the principal lifecycle verbs, so it stays clear of the top-level tag commands. A tag’s --name is an ordinary entity name (lowercase letters, digits, and hyphens, no dots and no underscores, never a uuid), and minting one that breaks the rule is a 422; that name is then the key a binding carries (--key). --applies-to is an entity-kind allow-list passed as a JSON array ('["component","system"]'; empty means universal), checked when a value is bound. --propagates defaults true (the value cascades to descendants); --propagates=false binds a flat per-entity value that resolves only on its own entity. Resolving onto a component unions keys and overrides values most-specific-wins down the cascade.

The component-classification catalog commands cover the vendor and driver registries: flat, official-vs-custom catalogs on the same pattern as the type registries. Each resource’s :read sits on the viewer floor; the three writes (:create, :update, :delete) are admin-gated.

A vendor names an organization, carrying a --kind of manufacturer, integrator, or developer (default manufacturer):

Terminal window
omniglass vendor list # the vendor registry
omniglass vendor create --name barco --label Barco --kind manufacturer \
--icon monitor --support-phone "+1-555-0100" --website https://www.barco.com
omniglass vendor get barco
omniglass vendor update barco --support-phone "+1-555-0199"
omniglass vendor delete barco # refused (422) if official

A driver names the implementation that gets, emits, or sets a product’s signals, with an optional --version:

Terminal window
omniglass driver list # the driver registry
omniglass driver get snmp-generic # includes the spec body, when authored
omniglass driver create --name barco-snmp --label "Barco SNMP" --version 1.0.0 \
--spec '{"version":1,"transport":"snmp", ...}' # validated at write: a broken spec is a 422 naming the fault
omniglass driver update barco-snmp --version 1.1.0
omniglass driver delete barco-snmp # refused (422) if official, or while an endpoint is attached through it

An official row, for example the crestron vendor, is read-only: update and delete both 422. A vendor’s website is validated to an http/https scheme on write; any other scheme (for example javascript:) is a 422.

The product commands cover the product registry: the concrete SKU that ties the vendor and driver catalogs together, classified under a component_type, and the thing a component points at. product:read sits on the viewer floor; the three writes (product:create, product:update, product:delete) are admin-gated.

A product names its kind (device, app, service, or vm, default device), the component_type it is classified under, and optionally its vendor, driver, and a parent product it is a variant of:

Terminal window
omniglass product list # the product registry
omniglass product create --name barco-ub12 --label "Barco UB12" --kind device \
--component-type projector --vendor-id barco --driver-id barco-snmp
omniglass product get barco-ub12
omniglass product update barco-ub12 --label "Barco UB12 Pro"
omniglass product delete barco-ub12 # 422 if official, 409 if a component points at it

An official product, for example cisco-room-bar, is read-only: update and delete both 422. A product still referenced by a component (component.product_id) cannot be deleted (409); an unknown vendor, driver, parent, or component_type id is a 422.

The standard commands cover the system-side counterpart of a product: the blueprint a system conforms to. standard:read sits on the viewer floor; standard:create, standard:update, and standard:delete are admin-gated.

Terminal window
omniglass standard list # the standard catalog
omniglass standard create --name lecture-hall --label "Lecture Hall" \
--parent-standard-id classroom # a variant of an existing standard
omniglass standard get lecture-hall
omniglass standard delete lecture-hall # 409 if a system still conforms to it

Unlike a seeded product, the shipped standards are official: false: they are forked from an in-code template once, with no inheritance, so they are yours to rename, re-parent, or delete, and the boot seed installs one only if absent rather than reasserting over your edit. The same holds for the shipped location types. See the seed model.

The event type commands cover the occurrence keyspace, the twin of the property catalog: the discrete happenings an event is typed by (call-started, cable-unplugged). event_type:read sits on the viewer floor; event_type:create, event_type:update, and event_type:delete are admin-gated. Official event types are read-only.

Terminal window
omniglass event-type list # the event type catalog
omniglass event-type create --name cable-unplugged --label "Cable unplugged"
omniglass event-type get call-started
omniglass event-type delete cable-unplugged # 409 on an official type

Every metric, property and event type reads back its writer_class (node-probe, platform, command-lifecycle, calc, collected): who may write the key (who writes a key). omniglass event-type get collection-failed reads node-probe, the node’s own account, and command-issued reads command-lifecycle, a command’s alone; a create under a reserved name is a 422 on every catalog.

The command type commands cover the “do” catalog: what a component can be told, with a target property and a settle window. command_type:read sits on the viewer floor; create/update/delete are admin-gated, official types read-only. Issuing a command (command:issue) records the invocation, writes a caused event, and returns the computed settlement verdict; the invocation then rides the per-node command queue to the placed node, which actuates it through the attached driver’s command binding, and the settle loop closes against the observed value the driver’s polls report back (commands).

Terminal window
omniglass command-type list # the command type catalog
omniglass command-type create --name set-input --target-property-type video-input --settle-window-seconds 15
omniglass command-type get reboot
omniglass component command issue disp-1 --command-type set-input --value '"hdmi2"' # needs command:issue

A target must be a key a command may write: a native probe key, health or a calc rule’s output is refused (422) naming the key.

A classifier declares which properties its instances carry; an instance sets a value. Both sides are the same three verbs, and the contract commands hang off the classifier that owns them:

Terminal window
omniglass product property list cisco-room-bar # a product's contract
omniglass standard property list huddle-room # a standard's contract
omniglass location-type property list room # a location type's contract
omniglass standard property update huddle-room room-capacity --default-value 6 --required=true
omniglass standard property delete huddle-room room-capacity # systems keep any value they set
omniglass component property list dsp-boardroom-3 # the effective read
omniglass system property list boardroom # same shape, system side
omniglass location property list east-campus # same shape, location side
omniglass system property update boardroom room-capacity --value 12 # idempotent
omniglass system property delete boardroom room-capacity # falls back to the contract default

The read resolves the classifier’s contract against the instance’s own values, so a one-off system (one conforming to no standard) and a productless component still resolve, to their off-contract values alone. The value commands are scope-injected: an instance outside your read scope is a non-disclosing 404 on the read and on the write. The registry and its contract share one noun: omniglass location-type list is the registry, omniglass location-type property list <id> its contract.

  • Generated (internal/cli/api_gen.go, do not edit): one command per API operation. The resource and verb come from the AIP-style path (POST /locations is location create, GET /locations/{name} is location get <name>, a :verb custom method is <resource> <verb> <id>, so the principal lifecycle is principal disable <id>, principal archive <id>, principal restore <id>, and principal purge <id>); path parameters are positional args, the request body is --flags, and OpenAPI query parameters become optional --flags (a set flag is appended to the request query string, an unset one keeps the server default), so principal list --include-archived --kind service filters the listing. A principal <id> argument accepts either the uuid or a human’s username (omniglass principal archive alice), resolved by the server, so you rarely need to look a uuid up first. --help plus the example come from the operation’s summary and description.
  • Hand-written (internal/cli/api_hooks.go and the run-mode files): the client runtime the generated tree calls, plus commands that are not API operations, the server, node run, and migrate run modes, the trusted direct-DB owner lane (bootstrap, token, set-password), and the idempotent dev-fleet seeder (seed-dev, the same trusted lane, never for production).

To add a hand-written command, write a newXxxCmd() returning a *cobra.Command and add it in newRoot, exactly as bootstrap does. Regenerating the API commands never touches it.