Files
yatm/docs/architecture/cli.md
T
Samuel CuiandClaude Opus 5.5 55608b175c Preserve complete Library exports and existing output files
Keep CLI and HTTP export consumers streaming with explicit error handling. Validate selections and replace named outputs only after a complete export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:01:27 +08:00

12 KiB

Command-line interface

Status: Current v1 development architecture.

The yatm-cli executable is the scripted and agent-facing surface. It calls the same gRPC and HTTP API the browser does, so a CLI request carries no separate server semantics; Interface contracts owns the rules shared with those surfaces.

Shape

Command registration attaches one command to each public service method, so every operation has a CLI spelling. CLI coverage tests hold the two together by resolving every public RPC to one real parser leaf and rejecting a binding whose RPC no longer exists, so a new public method without its command fails that check instead of leaving the surface unreachable. The development guide owns the commands that run it.

The bundled Skill routes agent work to command domains and defines operating rules. It ships inside the release package beside the operations guides that own the workflows. Agents read the matching installed --help down to the selected command for its inventory, arguments and flags, and interpret its JSON output as the operational source of truth. The Skill's domain table does not duplicate the installed command inventory; this document owns the surface rules.

Connection settings are global flags or their YATM_* environment variables: --server, --timeout (0 removes the deadline), --basic-user and --basic-password-file. Registered Location paths, Volume mount points and Tape devices are paths on the server; --input, --output and --basic-password-file are paths on the machine running the agent.

Unary replies print protobuf JSON, and streaming operations print JSON Lines with one item result per line and a final summary. A Files listing is the one stream with no summary: it writes one message per batch as the server produces it, so a reader sees the first rows before the read finishes. Exit status distinguishes intent from execution: 0 succeeded, 1 failed at runtime, and 2 rejected the command as a usage or safety error. An executed stream that settles with failed, unprocessed or publication-pending items exits nonzero even when the request itself completed. A completed --dryrun plan succeeds even though its proposed entries remain UNPROCESSED; planning failures and pending Library publication still exit nonzero.

A read that returns a complete directory takes no page size and no cursor; those flags belong to the query form of the same command, which returns one bounded page and the cursor that continues it. Passing either for a complete read is a usage error rather than a silently ignored argument.

Long work belongs to a Job, and a waiting command always has a timeout. Physical decisions such as choosing a device, loading Media, formatting, or continuing writes stay explicit rather than implicit in another command. There is no file or Preview download command; full archived content is obtained through Restore. The CLI has no command for an operation the API does not expose.

Instant fields in JSON use decimal strings and names ending in _ns, following the shared time contract. Preserve them as signed 64-bit integers when processing output. Restore --before accepts a timestamp with its stated timezone and retains its nanosecond precision; dates outside the supported range are usage errors.

job wait succeeds for a durable COMPLETED Job whose phase is COMPLETED or UNSPECIFIED, including after a service restart. It keeps waiting while an attached runner reports QUEUED for an admitted attempt waiting for shared Media resources, or a finalizing phase. FAILED is terminal: no Job kind supports Job-level retry. An idle READY Job reports UNSPECIFIED and requires an explicit Media choice. Only an Archive or Restore Media operation failure or operator cancellation, including while QUEUED, returns the Job to pre-Media READY, retaining its prepared manifest and error or cancel reason for another explicit Media operation. Initial preparation and Scan failure or cancellation remain terminal. Waiting returns nonzero with action_required for FAILED or idle READY, including the persisted error when present.

archive creation ID, restore creation ID and scan creation ID call their kind's read-only GetCreation method once and print its typed response. request contains the retained creation inputs; a nonempty unavailable_reason explains missing original inputs that require reselection. Reading does not create a Job or change the old Job. Review the selections and options, resolve missing inputs, and explicitly use the normal Create flow with its current validation and pre-submit checks to submit new work.

identical groups and identical members include a disposable result_id in their first reply. Later pages require --result ID with the returned cursor; an unavailable ID fails and requires a new Find instead of silently repeating the complete search. identical find returns the same kind of ID and both row counts for identical rows --result ID --offset N; identical positions locates File IDs within both hidden-file projections, and identical close releases the result. An idle result expires after 30 minutes and does not survive a service restart; run Find again if it is unavailable. Row pages default to 100 and accept at most 200 rows; group and member CLI pages default to 20. identical rows and identical positions accept the same --sort-key file-id|name|size and --order asc|desc. The default is File ID ascending; name starts ascending and size starts descending when selected. Row pages and position lookups use the same requested order under the Library sorting contract. These reads never scan or hash physical files.

job log reads a raw bounded byte page and returns JSON with text in logs and the next byte offset as a decimal string in offset. Follow that returned offset, not the text's character count; an empty logs ends the read. This CLI representation does not Base64-encode the log, although the underlying RPC uses a protobuf bytes field. JSON text can replace invalid UTF-8, including a character split by a byte-page boundary; preserve the original Job bundle's job.log when exact bytes are required. job log-lines reads a line page from the tail or a returned byte cursor, toward older or newer content, with optional exact --level and case-insensitive --query filters. The server owns page limits; the CLI supplies no limit. An empty filtered page can still advance its cursor, so use the returned has_older and has_newer flags to continue. Continuation fragments can appear as context when their line start lies outside the bounded read, even if that fragment does not establish a filter match.

Directory paths

ls accepts one optional positional directory operand. A bare ls selects the Library root. /Photos/2026, Photos/2026, ./Photos/2026 and library:///Photos/2026 all select the same logical directory relative to that root. The CLI has no persistent working directory, never uses the client's PWD to resolve the operand, and never falls back from a failed lookup to filesystem paths. Positional path syntax applies only to ls; du, mv, mkdir, rm and other commands retain their existing arguments and flags.

location://NAS/DCIM selects DCIM relative to the registered root of the Location whose full, case-sensitive display name is exactly NAS. Both location://NAS and location://NAS/ select that Location's root. --server still selects the YATM server; the URI's Location name is not a network host. Missing or duplicate display names fail resolution. Use --location-id ID --path PATH to select an ambiguous name by ID. Lookup enumerates existing registrations in bounded pages and compares names exactly, including Unicode names, without a fuzzy text filter, aliases or a display-name uniqueness constraint.

The existing ID selectors remain available: --file-id selects a Library directory, and --location-id with optional --path selects a registered Location directory. A positional operand is mutually exclusive with either ID selector and with a nonempty --path. More than one directory operand is a usage error.

Plain paths are literal and are not percent-decoded; a colon alone does not select a URI, so library:2026 names an ordinary Library directory. URI components are percent-decoded once; encode literal %, ? and # as %25, %3F and %23. Quote operands containing spaces for the shell. A percent-encoded slash in a Location name is rejected. After URI decoding, or directly for a plain path, . and .. segments are normalized within the selected root; traversal above that root is rejected.

Library path resolution uses existing API reads and compares each candidate directory name by exact bytes; a fuzzy search match alone never establishes identity. Resolution and the final List or Search share one command context and the global --timeout deadline. No API, entity or storage changes are required for path lookup.

Ordinary ls still reads a complete directory, without --limit or --cursor. --query selects a paged Search within the resolved directory. --recursive also selects a paged Search over a Library subtree, even without a query expression. Both search forms accept --limit and --cursor. An unreadable child remains in List output with error and no actionable reference. The command drains the remaining rows, then returns the existing failure exit code with code: incomplete; directory or transport failures retain their own errors. Supported filenames are legal UTF-8, including literal backslashes, whitespace and control characters. Non-UTF-8 names are escaped for display only and are not usable path operands. Quote shell operands so the shell preserves their bytes. The Library guide shows usage.

Reports and mutations

The shared rule owns which commands report under --dryrun, what a report contains and which operations are excluded. This surface adds only where the CLI differs: archive write tape format keeps --confirm-format, because confirming the Tape barcode authorizes a write to a device rather than a dry-run decision, and a report needs no follow-up command — the same command without --dryrun performs the resolved work.

Tape FORMAT first inspects the device and Library registration. A successful inspection with an empty electronic barcode accepts the user-supplied --barcode, normalized to six uppercase alphanumeric characters; a nonempty inspected barcode must match it. --confirm-format must exactly match that resolved barcode. Probe errors and registered Media stop before the write. APPEND retains its requirement for a matching inspected identity and compatible registered Media.

Skill

The CLI surface and its bundled Skill change together: a command, flag, output or safety rule it describes must update the Skill in the same commit. The Skill supplies operational guidance alongside the matching installed CLI help and JSON output. Installed copies refresh through the optional Skill step.

The standalone Library exporter defaults to file,media,position, rejects unknown types, and replaces a named output only after a complete successful export. Stdout remains streaming. HTTP Library exports abort failed partial responses; clients must validate the JSONL end record when transport framing cannot identify truncation.