CLI
atlibs is one Rust binary: run and serve modules, fetch published ones, stage them for a site or a
server, sign in, and publish. atlibs help lists everything; a bad invocation prints that command's
usage. Results go to stdout, progress and errors to stderr; a reader that stops early
(atlibs help | head -1) ends the command quietly, exit 0.
Install
cargo install --locked --git https://knotless.dev/johnandersen777.bsky.social/atlibs --rev 88b2ba8 atlibs
Into ~/.cargo/bin (--root <dir>: <dir>/bin). From a clone: cargo install --locked --path rust/cli,
or cargo host (into rust/host/target/<host triple>/release/).
Refs
Every command takes modules by ref, resolved the same way everywhere:
| ref | example |
|---|---|
| a published module | bobvmbuilder.bsky.social/echo, did:plc:.../echo, at://did:plc:.../com.fedfork.exec.wasm.module/echo |
| pinned | bobvmbuilder.bsky.social/echo@<32+ hex of its pin> or @<record CID> |
a name in atlibs.lock.json | echo (after atlibs add) |
| a module directory | examples/rust/hello (its module.json, the cargo wasm build above it, records/<session>.json) |
| a bare example name | hello, for examples/rust/hello |
| a composition | examples/rust/compose/composition.json, or a published one |
| a wasm and its spec | module.wasm module.json |
| a staged module | staged/stage.json compose |
Output and atlibs.lock.json's ref show <handle>/<lib>; what identifies a module is its uri
(by DID), cid and pin, so a handle that moves breaks nothing. A published composition names its
modules by at:// URI.
A <ref> is <handle>/<lib>[@pin|@cid] (or <did>/<lib>), an at:// URI, a name in atlibs.lock.json, a module directory or a name under examples/rust, a composition or clientMetadata .json, <module.wasm> <module.json>, or <stage.json|atlibs.lock.json> <name>.
run
run <ref> [<instance>] <entry> ['<json>' | - | @file] [--handler <name>=<spec> ...] [--allow-exec] [--params '<json>' | @file]
Prints { entry, result, error?, events, calls }; exits 1 when the run failed. - reads the input from
stdin and @file from a file, which keeps secrets out of ps and out of error messages.
atlibs run hello 2>&1 | tail -1
echo '{"n":3}' | atlibs run examples/rust/compose lookup - | jq -c .result
hello entries: count
{"ack":null,"caller":null,"doubled":6}
A composition runs one instance's entry. Its declared handlers come from --handler: another module's
entry (wasm:<ref>#<entry>), an https:// URL (http:// to localhost only), or a command
(exec:<command>, with --allow-exec; JSON in on stdin, out on stdout, an empty environment).
atlibs run examples/rust/compose/composition.json ui caller '{"nsid":"com.example.compose.confirm","input":{"n":1}}' \
--handler 'confirm=exec:jq -c "{approved: true, n: .input.n}"' --allow-exec | jq -c .result.q
{"ok":true,"output":{"approved":true,"n":1}}
serve
serve <ref> [--port 8787] [--host 127.0.0.1] [--origin URL | ATLIBS_ORIGIN] [--oauth-path oauth/] [--handler <name>=<spec> ...] [--allow-exec]
GET / lists the entries (a form page in a browser), POST /run/<entry> runs one
(/run/<instance>/<entry> for a composition), GET /oauth/<session>/client-metadata.json serves the
modules' OAuth clients. Sign-in and the request guard: Server sessions. With
--origin and a session record that declares app, it also serves the app's own identity
(App identity).
atlibs serve examples/rust/compose/composition.json --handler confirm=wasm:examples/rust/compose#lookup --port 8793 &
curl -s -X POST localhost:8793/run/ui/caller -H 'content-type: application/json' -d '{"nsid":"com.example.compose.lookup","input":{"n":6}}' | jq .result.q.output.doubled
12
add
add <handle>/<lib>[@pin|@cid] | <at://...> ... [--dir .]
Resolves the handle to a DID (checked both ways), fetches the module record (or a composition), checks
the blob against its CID, and writes atlibs.lock.json and modules/<lib>.wasm + .json. A module's
hook defaults and a composition's modules come too.
atlibs add bobvmbuilder.bsky.social/echo
jq -c '.modules.echo | {ref, session, wasm}' atlibs.lock.json
{"ref":"bobvmbuilder.bsky.social/echo","session":"echo","wasm":"modules/echo.wasm"}
Every read of a vendored module checks its wasm and spec against the lockfile's pin: the sha256 of
the wasm's sha256 and the record's grant fields (calls, anyRecords, hooks, implements,
anyHooks, didkey). A republished record whose calls changed no longer matches. add again to update.
stage
stage [--target spa|server] [--out dist] [--origin https://site/] [--oauth-path oauth/] [--name <client> [--record <file>]] [--dir .] [<ref> ...]
--target spa writes a static site's files (Sign in from a page);
--target server writes modules and a stage.json with each session's record, for a server to mount
(Node, Rust). No ref: everything in atlibs.lock.json. A spa with
--origin and a session record that declares app gets the app identity's static files too
(App identity).
atlibs stage --target server --out staged examples/rust/compose examples/rust/statusphere | jq -c '{modules: (.modules | keys), sessions: (.sessions | keys)}'
{"modules":["compose","statusphere"],"sessions":["statusphere"]}
oauth
oauth --origin https://site/ [--oauth-path oauth/] [--name <client>] [--dir .] [<ref> ...]
Prints the client metadata documents an origin serves, by session.
atlibs oauth --origin https://you.example/ examples/rust/statusphere | jq -r .statusphere.client_id
https://you.example/oauth/statusphere/client-metadata.json
login, logout
login <handle|did> [--scope "atproto transition:generic"] [--no-open]
logout <handle|did>
Signs in with OAuth through a loopback redirect in your browser (ATLIBS_BROWSER opens it; the URL is
printed either way). The session is saved per DID in the state dir and refreshed as it expires.
atlibs login you.bsky.social
atlibs run examples/rust/statusphere clear_status --as you.bsky.social
atlibs logout you.bsky.social
| flag (every command) | acts as |
|---|---|
--as <handle|did> | the OAuth session atlibs login saved |
--app-password <handle> with --password-file <path|-> or --password-env <NAME> [--pds <url>] | an app password (tests, operator accounts); saved, so later commands may give --app-password <handle> alone |
| neither | signed out: public reads only |
new
new <name>
atlibs new my-module > /dev/null && find my-module -type f | LC_ALL=C sort
my-module/.cargo/config.toml
my-module/.gitignore
my-module/Cargo.toml
my-module/modules/my-module/Cargo.toml
my-module/modules/my-module/module.json
my-module/modules/my-module/src/lib.rs
publish
publish [<name> ...] [--dir modules] [--dry-run] [--check-live] [--no-build] [--allow-dirty] [--out records/wasm]
publish module <dir|name> ... [--no-build] [--out records/wasm] [--allow-dirty]
publish records [<file> ...]
publish tile <dir> <name> [--out records/tiles]
publish composition <file> [--rkey <name>]
publish delete <at://did/collection/rkey | collection/rkey> ...
atlibs publish builds every module, compares each with what it last published (a cache under
$XDG_CACHE_HOME/atlibs/<did>/, or the live records with --check-live), publishes what changed and
prints a table: published, unchanged, would-publish (--dry-run), skipped:dirty (uncommitted
sources), drift (changed elsewhere), failed. The subcommands publish one kind of thing each.
Details: Publish a module.
atlibs publish --dry-run --as you.bsky.social
verify
verify <handle>/<lib> | <at://.../com.fedfork.exec.wasm.module/...>
Rebuilds a published module from its source records in a container and compares the sha256 with
the manifest and the blob: atlibs publish built it in that same rust:<version> container. The
engine is CONTAINER_ENGINE, else the first of podman, docker and Apple's container (macOS) that
answers info (container system status): a docker CLI whose daemon is stopped is skipped. A build
that exits 0 without writing the manifest's artifact is reported as the engine's fault (a docker
look-alike whose run returns before the command ends).
atlibs verify bobvmbuilder.bsky.social/echo
compose
compose bindings [--dir .] [<module dir> ...]
compose check <composition.json>
compose bindings writes atlibs.modules.js and .d.ts, the modules a compose.ts imports
(Compositions): those atlibs.lock.json vendors (pinned; atlibs add writes
them too), modules/<name> directories and the ones named.
compose check runs the hosts' load checks without the wasm: modules from atlibs.lock.json or stage.json beside it
(pins too), module directories (module.json) or published refs. Prints the composition as it would
run, hook defaults added; exits 1 with the reason it would not load.
atlibs compose check examples/rust/compose/composition.json | jq -c '[.instances[].name]'
jq '.instances[1].input.n = "four"' examples/rust/compose/composition.json > bad.json
atlibs compose check bad.json 2>&1 || echo "exit $?"
["ui","lookup","even"]
composition: instance lookup: input n must be of type integer
exit 1
Environment
Every variable: reference/env.md.
Previous: MCP servers | Next: Write a module