The lairs command-line interface

The lairs command exposes the library's maintenance, corpus, discovery, and authentication workflows. It is built on the standard-library argparse, so it adds no command-line dependency.

The subcommands fall into four families:

There is also tui, which launches the interactive explorer.

Run lairs with no subcommand to print help; the process then exits with code 2. Each subcommand has its own --help.

lairs --help
lairs publish --help

vendor

Refresh the vendored pub/layers lexicon tree from a local Layers checkout and rewrite MANIFEST.toml provenance.

lairs vendor --from /path/to/layers/lexicons/pub/layers \
  --layers-version 1.2.0 --layers-git-sha abc123 --vendored-at 2026-06-16

--from is required and points at a lexicons/pub/layers tree. The three provenance flags are optional and keep the existing values when omitted. When the copied tree is byte-identical to the one already vendored, the existing hash and vendoring date are kept, so a no-op re-vendor does not invalidate the committed generated models. A missing source tree exits non-zero. Fetching by a git ref is a thin wrapper over this step: check out the ref first, then point --from at its tree.

gen and gen --check

Regenerate the committed record models from the vendored lexicons.

lairs gen                                # write the modules
lairs gen --check                        # the CI drift gate

gen writes the generated modules and reports how many. gen --check compares the committed modules against a fresh generation and exits non-zero when they are stale, without writing. This is the drift gate. Records are generated, never hand-authored: edit the generator, not the output.

pull

Ingest an account's Layers records from a PDS into a local Repository for a git-like round trip.

lairs pull did:plc:author --endpoint https://pds.example --into ./repo \
  --message "pull layers records"

The DID is positional, and --endpoint and --into are required. Every Layers collection of the account is read and staged into the Repository under its AT-URI. The snapshot is then committed and the commit reported. When nothing is pulled, no commit is made.

materialize

Load a corpus from a PDS and write its normalized Arrow/Parquet views.

lairs materialize at://did:plc:author/pub.layers.corpus.corpus/abc \
  --endpoint https://pds.example --out ./views

The corpus AT-URI is positional, and --endpoint and --out are required. The expressions and annotations views are written into the output directory and listed.

publish

Diff a local Repository revision against a PDS and emit the minimal applyWrites plan. Publishing is a dry run by default. The plan is printed and nothing is written unless --yes is passed.

# dry run (default): show the plan, write nothing
lairs publish --repo ./repo --revision v1 --to did:plc:author \
  --endpoint https://pds.example

# live: apply the writes
lairs publish --repo ./repo --revision v1 --to did:plc:author \
  --endpoint https://pds.example --yes

Only --repo is required, and --revision defaults to HEAD. Both --to and --endpoint are optional and default to the logged-in session: --to falls back to the session's DID and --endpoint to the session's PDS, so after lairs login a publish needs only --repo. When --to is omitted and no session is saved, the command errors and exits non-zero.

--yes applies the writes instead of only showing the plan. A live publish needs a target PDS endpoint, from --endpoint or the logged-in session, and it always needs a logged-in session for credentials: --yes exits non-zero when there is neither an endpoint nor a session, and again when no session is logged in even if --endpoint was given. The printed summary counts creates, updates, and deletes, and a non-empty dry run reminds the caller to pass --yes --endpoint to apply. Writes target only the --to repository.

inspect

Load a corpus from a PDS and print a per-record-type count summary.

lairs inspect at://did:plc:author/pub.layers.corpus.corpus/abc \
  --endpoint https://pds.example

The corpus AT-URI is positional and --endpoint is required. The output is the total record count and a count per collection NSID.

datasets

Resolve a handle or DID and list its datasets, one row per corpus.

lairs datasets alice.example --language en --min-expressions 100

The actor (handle or DID) is positional. --source-type chooses the discovery mechanism (auto, pds, or appview, default auto), --appview sets the appview base URL, and --endpoint overrides the PDS base URL. The shared facet flags filter the rows: --language, --domain, --license, --min-expressions, --max-expressions, --text (a case-insensitive substring over name and description), and --has-adjudication / --no-has-adjudication. --limit caps the number of rows and --json prints JSON instead of a table.

toc

Resolve a handle or DID and print its repository collection inventory, starring the dataset-shaped collections, without dumping records.

lairs toc alice.example --counts

The actor is positional. --source-type chooses the discovery mechanism (auto, pds, or appview, default auto) and --endpoint overrides the PDS base URL. --counts counts records per collection, which drains each collection. --json prints JSON instead of a table.

Fan out over a seed of handles or DIDs and list the matching datasets, deduplicated by corpus.

lairs search alice.example bob.example --domain news

One or more actors are positional. --source-type, --appview, the shared facet flags (--language, --domain, --license, --min-expressions, --max-expressions, --text, --has-adjudication), --limit, and --json behave as for datasets.

index

Maintain a local, searchable dataset index in a panproto Repository, built from a crawl and kept fresh from the firehose. The group has four subcommands.

lairs index build --into ./index
lairs index build --into ./index --endpoint https://relay.example
lairs index update --index ./index --relay https://relay.example
lairs index search --index ./index "treebank" --domain news
lairs index diff --index ./index v1 v2

index build crawls repositories into the index. Only --into is required. The crawl target is --endpoint (a relay or PDS URL) or --source (a configured source name); with neither, it defaults to the first enabled source, which is the built-in Layers PDS repo.layers.pub unless you re-point or disable it in sources.toml. --seed-did (repeatable) restricts the crawl to specific DIDs (the default crawls every repo), --max-repos bounds the repositories visited, and --message sets the crawl commit message (default backfill crawl).

A re-run is incremental. When index build enumerates the service itself it reads each repository's current commit revision from the same listRepos pass and compares it against the revision recorded at the last crawl, describing only the repositories whose revision moved. The rest are counted as repos unchanged since last crawl in the summary. A crawl restricted with --seed-did carries no revisions, so those repositories are described every time.

index update tails the firehose into an existing index. --index and --relay are required; --limit stops after that many events.

index search searches the local index. --index is required and the free-text query is an optional positional. Filters are --domain, --language, --license, --min-expressions, --max-expressions, --metric (a required quality metric slug), and --min-rounds (minimum annotation rounds). --duckdb pre-filters through the DuckDB accelerator.

index diff diffs the index between two revisions. --index is required and the base and head revisions are positional.

tui

Launch the interactive Textual data explorer: an Explore tab over the discovery index, a Browse tab over every record type in a local repository, and a Query tab that runs DuckDB SQL, KWIC concordance, and CQL token-pattern searches over materialized data.

lairs tui --index ./index --repo ./repo --data ./views

All flags are optional. --index opens a discovery index on the Explore tab, --repo opens a local repository on the Browse tab (and feeds the Query tab unless --data is given), and --data opens a materialized Parquet directory on the Query tab. The TUI requires the optional textual dependency; when it is missing the command exits non-zero with a message.

login, logout, and whoami

Authenticate to a PDS and manage the saved session that the discovery and publish commands fall back to.

LAIRS_APP_PASSWORD=... lairs login alice.example
lairs whoami
lairs logout

login resolves a handle or DID to its PDS, exchanges an app password for a session, and saves it so later commands authenticate automatically. The identifier is positional. Prefer the LAIRS_APP_PASSWORD environment variable over --app-password so the secret does not appear in the process list; --pds supplies the PDS base URL directly and skips handle resolution. When neither --app-password nor LAIRS_APP_PASSWORD is set, login exits non-zero.

logout deletes the saved session, if any. whoami prints the saved session's identity, DID, and PDS, and exits non-zero when no session is saved.

See also