Command-line client
Queries, mutations and streams.
rom-cli builds the rom executable. It uses the generic rom-http protocol.
To add a Resource, register it on the server. No CLI command or client schema is needed.
The CLI does not open application databases directly. Studio and login/session
ceremonies are outside this release.
Build and check from the repository with Rust 1.99:
cargo build -p rom-cli
./crates/rom-cli/verify
./target/debug/rom --help If you set CARGO_TARGET_DIR, the executable is under that target instead.
The source distribution includes the optional CLI alongside the libraries. To run
the remaining examples from the repository without installing the binary, define:
rom() { cargo run --quiet --locked -p rom-cli -- "$@"; } This respects CARGO_TARGET_DIR. Alternatively, run
cargo install --path crates/rom-cli --locked. Make sure that Cargo's bin directory is
on your PATH.
Connect and inspect
Start the workshop in another terminal:
./demo/run serve sqlite ./demo.db 8080
# redb uses the identical declarations and CLI protocol:
# ./demo/run serve redb ./demo.redb.db 8080 The workshop's Demo local header is a public synthetic marker, not a real
login or a private credential. Create a local fixture file, then run:
printf '%s\n' 'Demo local' > /tmp/rom-demo-auth
rom --endpoint http://127.0.0.1:8080 --auth-file /tmp/rom-demo-auth discover
rom --endpoint http://127.0.0.1:8080 --auth-file /tmp/rom-demo-auth discover tasks
rom --endpoint http://127.0.0.1:8080 --auth-file /tmp/rom-demo-auth query tasks --endpoint is always explicit. A base path is preserved, so
https://example.org/rom-api sends reads to /rom-api/read. HTTPS verifies the
server certificate. Plain HTTP is accepted only for numeric loopback addresses
(including IPv6), not hostnames. URL credentials, query strings and fragments are
rejected. Redirects, proxies and automatic network retries are disabled.
--auth-file is optional. The host decides whether a request without credentials
has authority. When used, the file contains the complete Authorization value
(e.g. its host-defined scheme and credential), not a JSON document. The CLI removes a single final
LF or CRLF. It rejects embedded line breaks, empty values and files larger than
8 KiB. Credentials never enter command-line flags or diagnostic
messages. The CLI does not save, refresh or acquire credentials. Protect the
input file according to the host's credential policy.
Discovery returns only deliberately disclosed metadata. It does not grant read,
query, mutation or action access. discover KIND selects from that authorized
snapshot; unavailable kinds are not distinguished from hidden kinds. Known-kind
commands work without discovery. Custom action input remains opaque. Consult the
application's action contract. Let its registered codec validate the value.
Commands and JSON input
Global connection/output options can appear before or after a command.
--output human is the default and prints readable, indented JSON with IDs and
revisions intact. --output json prints compact JSON: one value for a finite
result, one complete value per line for live/journal frames. Human and machine
output both escape terminal control characters. Diagnostics go to stderr.
| Command | Input and meaning |
|---|---|
discover [KIND] | Authorized metadata snapshot or one disclosed kind. |
read KIND ID | Current projected Resource, including its revision. |
query KIND [--query-file FILE] | All by default, or a bounded equality/conjunction query. |
create KIND ID --idempotency KEY --input-file FILE | Complete Resource value; expected revision is null. |
replace KIND ID --expected REV --idempotency KEY --input-file FILE | Complete replacement, not a partial patch. |
patch KIND ID --expected REV --idempotency KEY --input-file FILE | Explicit set/remove field operations. |
delete KIND ID --expected REV --idempotency KEY | Delete at an explicit revision. |
action KIND ID NAME --expected REV --idempotency KEY --input-file FILE | Registered custom action, separate from standard operations. |
invoke --request-file FILE | Exact generic Invocation envelope, including its original identity and expected revision. |
live KIND [--query-file FILE] | Whole authorized current snapshots; no replay cursor. |
journal KIND [--after-file FILE] | One bounded journal batch and its cursor. |
journal-head KIND | Current cursor; explicitly establishes a new checkpoint. |
subscribe KIND [--after-file FILE] | Ordered journal batches with their cursors. |
Every JSON source flag accepts - for stdin. Requests and input files are limited
to 64 KiB, with duplicate object keys rejected at every nesting level before
conversion. Unknown request/query/invocation properties are rejected. Values retain
exact signed/unsigned 64-bit integers. If downstream JSON tools will replay a request,
they must also preserve those integers.
For example, create one Resource:
rom --endpoint http://127.0.0.1:8080 --auth-file /tmp/rom-demo-auth \
create tasks first --idempotency first-create --input-file - <<'JSON'
{"title":"Draft workshop","done":false}
JSON Then patch it using the revision from the response:
rom --endpoint http://127.0.0.1:8080 --auth-file /tmp/rom-demo-auth \
patch tasks first --expected 1 --idempotency first-rename --input-file - <<'JSON'
{"title":{"op":"set","value":"Review workshop"}}
JSON Omitted patch entries remain unchanged. {"op":"set","value":null} writes
an explicit null where supported; {"op":"remove"} removes an optional field.
False, zero and empty strings are values. Projected reads can omit protected
fields, so do not convert a projected read blindly into a complete replacement.
A query file can contain:
{"filters":[{"field":"done","value":false}],"after_id":null,"limit":20} Predicates are conjunctive equality or scalar comparisons. Optional-field absence uses
{"field":"memo","value":null,"absent":true}. Query files can also supply comparisons,
order and a client after anchor; see shared query semantics.
after_id remains available for ID-only ordering. Moving pages retain no snapshot
and provide no total count. Collection snapshot bounds still apply with a small page limit.
Mutations and uncertain outcomes
Supply an explicit idempotency key for each intended mutation. Non-create mutations also require the expected revision, including in a raw invocation. Keep the original request when reconciling an unavailable result. A repeat request with the same principal-scoped identity and semantic input recovers its authorized durable receipt. Changed input with the same identity is an identity mismatch. After a conflict, a deliberately revised mutation is a new intent and needs a new key.
Every failed remote mutation response is treated as unresolved. HTTP error categories do not identify the execution phase. Even conflict, validation, size-limit, denial or unavailable-resource errors can arise from a host authority check after commit. The CLI shows the safe category without claiming rollback. A malformed, oversized or lost success response is unresolved too. A local input error before submission is distinct.
Replay only as the same principal, with the original key, expected revision and
semantic input. The CLI cannot certify that a replacement credential still
represents the original principal. It never retries automatically or stores
pending requests/credentials. invoke --request-file accepts the existing wire
Invocation shown in HTTP documentation; it does not invent another
mutation identity.
Streams and process behavior
live emits full snapshots, initially and when state changes; updates can coalesce.
subscribe emits whole journal batches, including empty batches whose cursors
advance. Before you consume another frame, flush each complete output frame. A successful
pipe write does not prove the receiving application processed or persisted it.
Persist a journal cursor only after your application has processed its batch.
Finite response bodies and individual SSE frames are limited to 2 MiB. Parsing handles arbitrary chunk boundaries, split UTF-8, CRLF, comments and multiline data. Response identities must match the requested kind/ID. Journal batches must preserve the supplied and preceding cursor generation, kind and forward position. Events inside each cursor range must be ordered. Malformed/oversized/incomplete streams stop visibly. There is no automatic reconnect, unbounded frame queue or implicit history reset. Keepalive bytes count as connection activity, not application progress.
| Timeout option | Default | Accepted seconds |
|---|---|---|
--connect-timeout | 10 | 1–300 |
--request-timeout | 30 | 1–3600 |
--idle-timeout | 30 | 1–3600 |
The request timeout bounds response-header waiting and, separately, finite body consumption. Stream bodies instead use the inactivity timeout. A slow stdout consumer applies backpressure; no additional output frames are queued. Ctrl-C also exits while blocked on stdin/stdout. Closing stdout terminates cleanly. The interrupt handler does not write diagnostics, so a full stderr pipe cannot prevent exit; use its exit status. Stopping a mutation only stops waiting and does not cancel accepted server work.
A history gap needs an explicit decision about recovery. If you accept lost
history, use this sequence to rebuild state. Obtain journal-head. Obtain an
authorized snapshot. Subscribe from that head. Reconcile overlap by Resource revision. This cannot
recover missing business-event processing, and retention can race recovery again.
| Exit | Meaning |
|---|---|
| 0 | Success, intentional non-mutation interruption, or stdout pipe closure. |
| 2 | Invalid arguments, credentials, endpoint or local request input. |
| 3 | Explicit remote rejection of a read/discovery/observation operation. |
| 4 | Read/stream transport, response protocol or output failure. |
| 5 | Mutation submitted; outcome unresolved, including any non-success reply. |
| 6 | Read/stream history gap. |
| 130 | Interrupted while waiting for a submitted mutation; outcome unresolved. |
The executable tests run two unrelated Resource kinds against real HTTP on SQLite and redb, with actual stream updates and revocation. Fault coverage includes lost commit replies, post-commit denial and arbitrary host-gate errors, identical retry, identity mismatch, exact integers and partial presence, malformed inputs/frames, redirect rejection, stream inactivity, broken output pipes and signal interruption.
Operator work recovery
The work capabilities, work list, work show, work retry and work reconcile commands use the shared operator contract.
See operator recovery for exact request files, authority, receipt replay and support limits.
These commands do not open the database, reset budgets or infer a compensation.