@labelbox/horizon-cli
v0.0.1
Published
Command-line client for Horizon
Maintainers
Keywords
Readme
@labelbox/horizon-cli
horizon — the Horizon CLI. It mirrors the TypeScript SDK
(@labelbox/horizon-sdk) exactly: where the SDK is horizon.synthesizers.create(...), the
CLI is horizon synthesizers create …. Dots become spaces; you get --help at every
level. horizon is the only executable name.
horizon --help # list nouns (synthesizers, synthesizer-runs)
horizon synthesizers --help # list verbs (create, get, list, …)
horizon synthesizers create --help # list flagsHow it works (fully live, zero per-operation code)
The CLI ships no baked API reference and has no @labelbox/horizon-sdk dependency.
On each run it revalidates the manifest from GET /v1/cli/manifest when the server
selected by --base-url is reachable (production by default, or staging, or
localhost). If the server is unavailable, it falls back only to a locally cached
manifest that was previously validated. From that manifest it builds its entire
command tree, --help, request/response shapes, and docs browse surfaces
(src/manifest.ts). Dispatch is generic (src/dispatch.ts):
each request is built straight from the manifest operation's HTTP method + path
template + params + body — there is no hand-written command per operation and no
baked client.
The result: adding or changing a backend endpoint needs zero CLI release — the
live CLI reflects it as soon as the backend deploys. The CLI is re-released only when
its own engine code changes. When the server is reachable, the manifest is
revalidated on every run via a conditional fetch (ETag / If-None-Match) and cached
per base-url under ~/.cache/horizon/. When the server is unreachable, the CLI may
use the last locally validated copy so commands remain available offline. Validated
fresh HTTP 200 responses use atomic same-directory rename and therefore retain
last-network-writer behavior. Replacement preserves an existing file's POSIX mode
bits, but deliberately publishes a new inode and does not preserve its ACLs or
extended attributes. Publication requires parent-directory write/search permission;
if unavailable, the CLI keeps using the validated in-memory result and never falls
back to a partial direct write.
Docs browse surfaces
Beyond the executable horizon <noun> <verb> operations, the manifest carries the docs,
exposed as one consistent positional shape — horizon <group> [<id>] (bare lists, an id
shows that one):
horizon resources [<id>] # Reference — resource hubs: object shape, operations, recipes
horizon recipes [<id>] # How-to — multi-step user-goal guides (--format cli|ts|curl)
horizon explain [<concept>] # Explanation — concept pages
horizon tutorials [<id>] # Tutorials — getting-started docsInstall / run
The package is not published yet, so no npm install command is valid today.
The manual Release / CLI workflow builds and externally consumes an
immutable 0.0.1 bootstrap archive, but executable release authority refuses
npm and GitHub mutation until a Labelbox npm owner reserves the package and
registers its Trusted Publisher.
When activated, the CLI remains standalone and pulls in no
@labelbox/horizon-sdk. It is versioned independently of the SDK, remains below
1.0.0, and may include breaking changes.
From a monorepo checkout, run the bin directly without installing:
yarn workspace @labelbox/horizon-cli build
node ./packages/horizon-cli/dist/bin.js --helpAuth
Set LABELBOX_API_KEY in the environment without printing it or putting it on a
command line. Override the host with --base-url <url> (defaults to the public
Horizon API gateway, see DEFAULT_BASE_URL in src/manifest.ts).
Flags
- Path / query params are individual flags:
--environment-id,--limit, … Required path and query params must be passed as flags. A query param whose type is an array or object (e.g.--models,--enrichment-filters) takes a JSON value (--models '["a","b"]'); its help text is tagged[pass as JSON]. - Operation headers declared by the API are individual flags too. Required
concurrency and replay controls therefore appear as
--if-matchand--idempotency-keyand are forwarded under their declared HTTP names. - Request body: scalar top-level fields are individual flags (
--name,--system-prompt, …); pass the full body — including complex fields likecontextInputs/targetFields— with--from-json <file>or--data <json>(mutually exclusive — pass only one). Scalar flags override values from--from-json/--data. A required scalar body field can be supplied by either its flag or the JSON body. - Multipart request body: binary fields are local-path flags such as
--file ./skill.zip; scalar form fields remain ordinary flags. The CLI reads the file only in its terminal process and lets the HTTP runtime set the multipart boundary. Filesystem-less embedded callers expose JSON operations only. - Output: prints every non-binary result as the JSON HTTP envelope
{ data, status, headers }by default. A bodyless204or304is encoded asdata: nullbecause JSON has noundefined.--quietdeliberately discards that HTTP metadata and prints only the resulting resource'sid; an operation may instead declare one scalar response field for quiet output (for example, the handoff-access command's ephemeral URL). Results without either value print a blank line in quiet mode. Binary downloads are written to stdout byte-for-byte with no encoding or trailing newline, so redirect them to a file.
The embedded dispatcher exposes JSON/text operations and returns each success as
{ data, status, headers }. headers contains the response headers declared for
that status in OpenAPI (including ETag, Location, and
Idempotency-Replayed). Bodyless data is undefined. Operations with a binary
success representation are excluded from embedded and MCP callers; use the
terminal CLI, which streams their bytes directly to stdout.
horizon environments get --environment-id 784e2386-e297-4f9d-a886-838422383b65
horizon synthesizers get --synthesizer-job-id b45c081d-3069-4e32-98d4-aa5ec3d442c6The command surface is the live server
There is nothing to regenerate or commit for the CLI — the command surface is
revalidated against the target server's GET /v1/cli/manifest on every run when the
server is reachable, with a validated local-cache fallback when it is offline. A
backend change therefore flows through automatically after deploy with no CLI step.
The manifest itself is assembled by yarn generate cli:manifest (included in
yarn generate prerequisites) from the spec-derived reference files and embedded
into the backend; to add or change a command, change the backend @SdkRoute — not
this package.
The engine (manifest fetch + cache + validation, generic dispatch, flag mapping,
help formatting, the request-body / returns shape trees, and the docs browse
renderers) is verified by src/*.test.ts, which build the program from a fixture
manifest in-memory and assert the realized flags + --help + browse output — no
committed golden snapshot to maintain.
