| id | reference/cli | ||
|---|---|---|---|
| title | CLI reference | ||
| audience | dev | ||
| summary | Every hoplight command and flag, verified against src/cli.ts and src/cli-io.ts, with usage, behavior, JSON output shapes, and exit codes. | ||
| tags |
|
||
| related |
|
hoplight is the command line for the same engine the desktop Studio runs (see architecture.md
and ui.md): one canonical model, adapters discovered from src/formats/. src/cli.ts is the whole
dispatcher, one main(argv) function with an if-chain per command. src/cli-io.ts holds the pure IO policy
the convert command leans on: flag parsing, path-identity comparison, container agreement, and atomic file
publish. This page documents every command and flag exactly as the code implements them today.
- In the repo:
bun run src/cli.ts <command> [args], or the package scriptbun run hoplight <command> [args]. package.json'sbinfield maps thehoplightcommand straight tosrc/cli.ts, so an installed or linked package exposes it directly.bun run build:clicompiles a standalone binary todist/hoplight(bun build --compile src/cli.ts).bun run devishoplight ui 8321.
| Command | Does |
|---|---|
hoplight convert <in> <out> [--to <format>] [--yes | -y] [--strict] |
Convert a file and print its loss report. |
hoplight inspect <file> |
Show what is inside a file: format, kind, key fields. |
hoplight validate <file> |
Detect and parse a file against its format and the canonical schema. |
hoplight label <file> |
Guess a card's format spec and its likely origin app. |
hoplight formats |
List every adapter the registry discovered. |
hoplight ui [port] [studioDir] |
Launch the desktop Studio's primary loopback server. |
hoplight version |
Print the version. |
hoplight help |
Print the built-in help text. |
| (no arguments) | Same as hoplight help. |
| (an unrecognized first argument) | Prints an error naming it and exits 1. |
| Flag | Where | Does |
|---|---|---|
-v, --version |
Standalone, before any command | Same as hoplight version (cli.ts:135). |
-h, --help |
Standalone, before any command | Same as hoplight help (cli.ts:141). |
--json |
version, formats, validate, convert |
Emit one JSON object to stdout instead of the human-readable form. inspect, label, ui, and help do not read this flag; they always print the human-readable form (see Quirks). |
--to <format> |
convert only |
Force the output adapter id, bypassing extension resolution. |
--yes, -y |
convert only |
Allow replacing an existing output file. Never overwrites the input. |
--strict |
convert only |
Refuse before writing when fields are dropped or target coverage is undeclared. |
hoplight convert <in> <out> [--to <format>] [--yes | -y] [--strict] [--json]
hoplight convert vera.png vera.charx --to risu
hoplight convert card.json out.charx --to lumiverse --yes
Flags may appear anywhere after convert, in any order: parseConvertFlags (cli-io.ts:17-45) scans the
whole tail and pulls --yes/-y, --strict, and --to <value> out wherever it finds them, leaving exactly two
positional tokens as <in> and <out>. A repeated boolean flag or --to is an error (duplicate --yes,
duplicate --strict, or duplicate --to), a --to with no value or a value starting with - is an error (--to requires a format id), and any other token starting with - is an error (unknown flag: <token>). Exactly two positional
tokens are required; more or fewer print the usage line and exit 1.
The conversion follows these checks before a byte is written (cli.ts:299-414):
- Guard the output path (
guardConvertOutput,cli-io.ts:112-132). Refuses to run when<out>resolves to the same file as<in>(compares realpaths when both exist, else resolved parent directory plus basename, case-insensitively on Windows). When<out>already exists, requires--yes/-y; otherwise fails withoutput already exists: <out> (pass --yes to replace). - Detect the source adapter (
registry.detect). Fails when no adapter recognizes<in>, listing every known adapter id. - Resolve the target adapter (
resolveTarget,cli.ts:55-66).--to <format>wins outright; a bad id fails withUnknown format "<format>". Otherwise the adapter is resolved from<out>'s extension viaregistry.targetsForExtension: zero matches fails as unresolvable, more than one match fails as ambiguous and names the candidate ids (an extension two adapters both write, such as.jsonfor bothsillytavernandvaud-json, is never guessed). - Run the conversion (
convertFile,src/convert.ts): import to the canonical entity, export to the target, extracting and re-embedding any bundled lorebook (see architecture.md, "Bundles"). An adapter that throws surfaces its message and exits 1. - Check container agreement (
assertContainerAgreement,cli-io.ts:159-207). The adapter's own suggested extension must match the requested one..charx/.byaf/.zipmust be real ZIP-magic bytes;.json/.lorebook/.txtmust be text that parses as JSON (for.json) or decodable UTF-8 bytes; every other extension just needs a non-empty payload. - Enforce the loss contract. Every target must return a serialize report. With
--strict, the command refuses undeclared coverage or any reported dropped path. - Publish atomically (
publishAtomic,cli-io.ts:213-239). Writes a temp sibling file in the same directory, fsyncs it, then renames it over the destination. A failure at any point removes the temp file and leaves the prior destination untouched.
After container agreement and before publication, --strict refuses the conversion if its structured
serialize report names one or more dropped paths or cannot determine loss because target coverage is
undeclared.
Only the success report honors --json; every failure above (guard, detect, resolve, convert,
container-agreement, write) prints the same plain-text line whether or not --json was passed.
--json success shape:
{ "ok": true, "from": "sillytavern", "to": "risu", "in": "vera.png", "out": "vera.charx", "extension": "charx", "lorebooks": 1, "bytes": 48213, "textChars": 0, "report": { "coverage": "declared", "counts": { "escrowed": 0, "dropped": 2, "escrowShadowed": 0, "warnings": 0 }, "escrowed": [], "dropped": ["body.behavior.triggers", "original.sillytavern.raw"], "escrowShadowed": [], "warnings": [] } }lorebooks is a count of bundled lorebooks carried across, not the lorebooks themselves; bytes/
textChars reflect whichever payload shape the target adapter produced (one of the two is always 0).
The plain-text report prints the from/to adapter ids, paths, resolved extension, bundled lorebook count,
the report counts, every confirmed dropped path, and every warning. Undeclared coverage prints an
unknown dropped count rather than zero; JSON uses coverage: "unknown" and counts.dropped: null.
@fig convert
hoplight inspect <file>
Detects the source adapter, converts to the canonical entity, validates the complete runtime schema
and confirms that the entity kind matches the adapter contract, then prints the format id, the adapter's
label, the entity kind, and kind-specific fields (cli-validation.ts, cli.ts):
| Kind | Extra fields printed |
|---|---|
character |
name (identity.name), greeting (first 60 chars of greetings.firstMessage), alts (greetings.alternateGreetings count), tags (discovery.tags) |
lorebook |
name, type (lorebookType), entries (count), budget (tokenBudget and budgetMode) |
persona |
name, brief (first 60 chars), content (character count), sections (the keys of sections) |
regex |
name, rules (count) |
preset |
name, prompts (count), groups (count), choices (count) |
Does not honor --json; always prints the human-readable form. <file> is read as args[1]
(cli.ts:234), the literal second token, so a flag placed before the path is read as the path instead.
The canonical inspect formatter has a media-pack branch, but no live format adapter currently imports a
pack, so that branch is not reachable from this command.
hoplight validate <file> [--json]
Detects the adapter, converts through toCanonical, then parses the result through the exhaustive
canonical runtime schema and confirms that the returned kind matches the adapter contract before
reporting pass/fail (cli-validation.ts). No adapter recognizing <file>, an adapter parse error, an
invalid canonical result, or a kind mismatch is a failure (INVALID); otherwise it is OK and prints
the format id, kind, and name. Exit code is 0 on OK, 1 on any INVALID case.
<file> is resolved as the first argument that is neither validate nor --json (cli.ts:178), so
--json may appear before or after the path.
--json shapes, one of three:
{ "ok": true, "path": "vera.png", "format": "sillytavern", "kind": "character", "name": "Vera" }
{ "ok": false, "path": "vera.png", "error": "unrecognized" }
{ "ok": false, "path": "vera.png", "format": "sillytavern", "error": "<the thrown message>" }hoplight label <file>
Guesses a card's spec and likely origin app without fully parsing it into the canonical model
(labelCard/sniffContainer, src/entities/character/provenance.ts). The source object it labels is the
best one available: the detected adapter's escrowed raw card when one exists, else a direct JSON.parse of
the decoded text, else undefined (sourceCardOf, cli.ts:73-90), so label can still report
unknown/(none) on a file no adapter recognizes.
Prints:
| Field | Value |
|---|---|
format |
The card spec: chara_card_v3 | chara_card_v2 | chara_card_v1 | agnai | backyard | unknown. Not an adapter id (contrast inspect's format, which is src.id). |
container |
The physical wrapper: png | charx | json | unknown. |
origin |
The likely authoring app plus confidence to two decimals, or (unknown). |
signals |
Every fingerprint that fired, comma-joined, or (none). |
Does not honor --json. <file> is args[1] (cli.ts:279), same positional caveat as inspect.
hoplight formats [--json]
Loads every adapter the loader discovers under src/formats/*/index.ts (loadFormats) and lists each
one's id, kind, label, and output extensions (cli.ts:146-175). The plain-text form also prints the doc
path for the generated coverage matrix (docs/FORMAT-SUPPORT.md, regenerated with bun run matrix) and a
reminder that a new format is a folder drop-in (copy src/formats/_template).
--json shape:
{ "schema": "1", "adapters": [ { "id": "sillytavern", "kind": "character", "label": "SillyTavern character card (v2/v3, png/json)", "outputExtensions": ["json"] } ] }hoplight ui
hoplight ui 8321
hoplight ui 8321 "C:\Users\me\Documents\Hoplight Studio"
Starts the Studio's primary loopback server (startUi, see ui.md) and blocks forever:
Bun.serve
keeps the process alive and main awaits a promise that never resolves, so the process ends only on
interrupt (cli.ts:219-231).
port is Number(args[1]) || 8321 (cli.ts:222): a missing, non-numeric, or literal 0 value all fall
back to 8321, since 0 is falsy. studioDir is args[2], defaulting to <home>/Documents/Hoplight Studio;
it can only be set by also passing a port first, since it reads the third positional token regardless of
the second. Does not honor --json.
hoplight version
hoplight -v
hoplight --version
All three are equivalent; the -v/--version forms are checked before any other command match, ahead of
the no-arguments and help fallback (cli.ts:135-139). Plain form prints just the version string.
--json shape: { "version": "0.1.0", "schema": "1" }.
hoplight help
hoplight -h
hoplight --help
hoplight
All four print the same built-in HELP banner: usage line, the command table, the flag list, one example
block, and a status line naming the schema version and the format-matrix doc path (cli.ts:97-126). No
arguments falls through to the same branch as help (cli.ts:141). Does not honor --json.
--jsonis honored byversion,formats,validate, andconvert;inspect,label,ui, andhelpremain human-readable only.- Convert's
--jsononly covers the success report; every failure branch (guard, detect, resolve, convertFile, container-agreement, write) prints plain text regardless of the flag. -yis a working alias of--yesthat theHELPtext does not mention (cli-io.ts:23).- Every exit path is 0 or 1. There is no exit code 2, and no command writes to stderr; all output, success
or failure, goes through
console.logto stdout. - Positional resolution differs by command.
validateaccepts the path in any position relative to--json(cli.ts:178);inspectandlabeltakeargs[1]literally, so a flag placed before the path is read as the path. label'sformatcolumn is the card's spec (fromlabelCard);inspect'sformatcolumn is the adapter id (src.id). Same column name, different value, on two different commands.
| Concern | File |
|---|---|
Command dispatch, every command body, the HELP banner |
src/cli.ts |
| Canonical runtime validation and adapter-kind agreement | src/cli-validation.ts |
| Convert flag parse, path-identity guard, container-agreement check, atomic publish | src/cli-io.ts |
| Registry (detection, extension-to-adapter resolution) | src/core/registry.ts |
| Canonical wrapper, schema version | src/core/canonical.ts |
Folder discovery (loadFormats) |
src/core/loader.ts |
| Bundle extract/re-embed on convert | src/convert.ts |
Card provenance labeler (labelCard, sniffContainer) |
src/entities/character/provenance.ts |
Studio server started by hoplight ui |
src/ui/server.ts (see ui.md) |
Invocation surfaces (bin, scripts) |
package.json |