EvenHub CLI
Command-line interface for EvenHub development and app management. Covers the whole loop a developer runs from a terminal: scaffold a project, pack it, create the app, upload new builds, invite beta testers, and put a build on the beta channel. Public review and publishing stay in the portal.
Both evenhub and eh run the same program.
Install
npm install -g @evenrealities/evenhub-cli
The published package carries its compiled JavaScript and the WebAssembly packer, so installing it needs neither Bun nor Rust. Supported Node.js versions are 20.17+ on Node 20, 22.13+ on Node 22, or 23.5 and later.
Log in first
login prompts for a password and therefore needs an interactive terminal. Run
it once, by hand:
evenhub login
Credentials land in ~/.config/evenhub/credentials.yaml (XDG_CONFIG_HOME
wins when set; Windows uses %APPDATA%), readable only by you. Every other
command reuses them, so scripts, CI jobs and agents never prompt. An expired
access token is refreshed automatically; when the refresh token has expired too,
commands exit 2 and ask you to log in again.
evenhub logout invalidates the refresh token server-side and deletes the local
file.
Two output modes
Every command accepts --json. Without it you get aligned, human-readable
lines. With it stdout carries exactly one JSON object and nothing else; prompts,
warnings and progress go to stderr.
Failures under --json are a JSON object too:
{"ok": false, "exit": 5, "code": 1020, "message": "...", "hint": "..."}
A failure that already changed something on the server adds partial, which
lists those effects by name:
{"ok": false, "exit": 10, "code": 1500, "message": "Description update failed", "hint": "...", "partial": {"package_id": "com.example.demo", "initial_build": "1.0.0", "app_created": true, "draft_id": "...", "description_applied": false}}
Exit codes
One failure type per code, mapped from the server's own error codes.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Usage or argument error |
| 2 | Not logged in, or both tokens expired |
| 3 | Target does not exist, or this account does not own it |
| 4 | Target already exists |
| 5 | Version is pinned to a channel and cannot be deleted |
| 6 | Icon rejected |
| 7 | App is published or under review; listing edits go through review |
| 8 | Limit reached or rate limited |
| 9 | A local step failed and server state is unchanged |
| 10 | Network or server error |
| 11 | A server-side change landed, then a local step after it failed |
| 130 | Interrupted |
Anything local validation can settle is settled before a request goes out, so
exit 9 means server state is unchanged and the same command can be run again as
it stands. Exit 11 is the one code that reports a completed write: the server
accepted the change, and a local step after it failed (saving credentials or
removing them again). It carries
partial in the JSON payload, printed as Completed effects: on stderr, which
names every effect that already landed; the message and hint name the local step
to repair. Repair that step rather than replaying the command.
Project commands
init — scaffold app.json
evenhub init # ./app.json
evenhub init --directory ./my-app
evenhub init --output ./config/app.json
The scaffold carries the min_app_version and min_sdk_version of the SDK
release this CLI shipped against, so a fresh project packs without tripping the
floor check.
pack — build an .ehpk
evenhub pack app.json ./dist -o out.ehpk
evenhub pack app.json ./dist --sdk-ver 0.0.14
evenhub pack app.json ./dist --check
| Option | Effect |
|---|---|
-o, --output <file> | Output path (default out.ehpk) |
--no-ignore | Include dotfiles |
-c, --check | Check whether the package ID is still free |
--sdk-ver <version> | Derive the floor from this SDK release instead of npm latest |
--enforce-manual-version | Stamp app.json's min_app_version as written, even below the floor |
During packing the CLI reads the SDK's minAppVersion from npm and stamps it
into the packaged manifest. A higher value in app.json is kept; a lower one is
raised to the floor. app.json on disk is never modified.
If npm cannot be reached the CLI warns and falls back to the mapping bundled at
release time. That value is necessarily older than the registry's, so pass
--sdk-ver <version> when packing offline against a recent SDK.
qr — development-server QR code
The only command needed while sideloading through dev mode in the Even app.
evenhub qr
evenhub qr --url http://192.168.1.100:3000
evenhub qr --ip 192.168.1.100 --port 3000
evenhub qr --external # write a PNG and open it
evenhub qr --clear # forget the cached scheme, IP, port and path
IP is auto-detected and the last settings are remembered between runs.
Account commands
| Command | Purpose |
|---|---|
evenhub login [-e <email>] | Interactive login (see above) |
evenhub self-check | Show the signed-in account, its role and namespace |
evenhub logout | Invalidate the refresh token and delete local credentials |
App commands
create — new app plus its first build
evenhub create out.ehpk --name "My App" --tagline "One line" --icon icon.png
| Option | Limit |
|---|---|
--name <text> | 1 to 20 characters |
--tagline <text> | 1 to 50 characters |
--description <text> / --description-file <path> | 1 to 2000 characters |
--icon <file.png> | 24x24 PNG, see below |
--package-id <id> | Defaults to app.json in the current directory |
--new-draft | Ignore the cached draft and upload again |
Lengths are counted in Unicode code points, so a CJK character counts as one.
Without --icon the app gets a placeholder and the output says so.
upload — a new version of an existing app
evenhub upload out.ehpk -m "What changed"
Bump version in app.json and pack again first; re-uploading an existing
version exits 4. -m, --changelog is required and capped at 500 characters.
update — edit an unpublished app's listing
evenhub update --name "New name" --tagline "New line"
evenhub update --description-file ./store.md
evenhub update --icon icon.png
The CLI submits listing edits directly. If an app has an approved or published version, or a pending review, the server rejects the update with code 1022; the CLI preserves that error and exits 7. Make those edits in the portal.
ls — apps, or one app's builds
evenhub ls # every app on the account
evenhub ls --channels # add the beta/public pointers
evenhub ls <package_id> # every build of one app
evenhub ls <package_id> --verbose
Builds are ordered by semantic version, so 1.0.10 sits above 1.0.9.
Bare ls makes a single request; the channel columns cost one request per app,
which is why they sit behind --channels. Only --verbose requests app and
review details and includes the top-level app and review JSON fields.
download — fetch a build
evenhub download 1.0.1 -o ./build.ehpk
The CLI asks the server for a fresh signed storage URL and fetches the package from it without sending any credential.
rm — delete a build or an app
evenhub rm 1.0.1 # one build
evenhub rm --app <package_id> --yes # the app and all of its builds
A build that a channel still points at cannot be deleted; delist the channel
first. Deleting an app is irreversible, so it prompts unless --yes is passed.
Distribution commands
promote and delist
evenhub promote 1.0.1 --to beta
evenhub delist --from beta
promote handles the beta channel only. Public publishing needs review and is
refused locally, with no request sent. delist clears one channel pointer and
keeps every build.
testers
evenhub testers # the roster and each status
evenhub testers --search name@
evenhub testers add [email protected],[email protected]
evenhub testers add --from ./testers.csv
evenhub testers add [email protected] --reinvite
evenhub testers rm [email protected]
add takes comma, semicolon or newline separated addresses, or a file through
--from (one address per line, or a single-column CSV whose email header row
is skipped). Cells may be quoted, the way the portal exports them, and a leading
byte-order mark is ignored. Addresses are sent exactly as written - the local
part is case-sensitive at some mail hosts - and the batch is validated before
anything is sent: one malformed address rejects the whole file with exit 9.
Repeats of the same spelling are dropped; two spellings of one address are both
sent and reported, since whether they are one tester is the server's call.
Roster matching uses the exact email spelling, including case. Matching
Invited or Active addresses are skipped and reported, so re-running the same
spellings preserves pending links. --reinvite forces a new invitation to the Invited ones,
which invalidates the link in the earlier email. Active members are held back
even then: the server keeps an accepted membership as it stands, and there is
no invitation link left to replace. Invitations expire after seven days.
Rejected and Expired addresses can be submitted again without --reinvite.
Success reports submitted requests, not per-address email delivery. JSON
invited lists submitted addresses; reinvited lists submitted addresses that
matched an existing non-Active row under --reinvite.
Installing a beta build needs an Active membership and a build on the beta
channel.
Icon rules
The server is stricter than the portal's upload box, so the CLI runs the same checks locally and reports the exact pixel.
- PNG, at most 1 MB.
- Exactly 24x24.
- Every pixel is either fully transparent or opaque
#F4F4F4. - Lit pixels form raster-aligned 2x2 blocks: scanning in raster order, each unconsumed lit pixel must have lit neighbours to its right, below, and below right, and cannot sit in the last row or column.
Colour is checked across the whole image before blocks are checked, so a stray coloured pixel inside an otherwise valid block is reported at its own coordinate, with its colour, rather than at the corner of the block.
Draw icons on the portal's canvas if you would rather not hand-build the bitmap.
Resuming an interrupted upload
create and upload send the package first and commit it second. Before
committing, the CLI tries to cache the draft id, package SHA-256, base URL and
account identity in ~/.config/evenhub/state.yaml. If committing fails, a later
run with the same package can reuse an available, unexpired cached draft.
Check server state before retrying a write whose outcome is uncertain.
The cache is optional. If account identity cannot be established safely, or
cache reading, saving or cleanup fails, the CLI warns on stderr and continues
with the core operation. A cache failure alone does not change the exit code;
the API result still determines success. Cache validation continues to isolate
accounts, apps and environments. Failure output reports draft_cached accurately;
false means the uploaded draft is unavailable for automatic reuse.
Server-side drafts live for 30 minutes. A cache entry older than that, or one
belonging to a different account, app or manifest, is discarded and the package
is sent again. --new-draft always sends again.
Environment variables
| Variable | Effect |
|---|---|
EVENHUB_BASE_URL | Override the API base (default https://hub.evenrealities.com) |
EVENHUB_API_URL | Override the API root directly |
EVENHUB_API_DEBUG | Log every API request and its status to stderr |
EVENHUB_API_HEADERS | Extra request headers, for debugging |
EVENHUB_NPM_REGISTRY | Registry used to look up the SDK's minAppVersion |
XDG_CONFIG_HOME | Where credentials, QR and draft state are stored |
Shell completion
Scripts cover both evenhub and its eh alias.
# bash — source from .bashrc
evenhub --completion-bash > ~/.evenhub-completion.bash
# zsh — any directory on your fpath
evenhub --completion-zsh > ~/.zfunc/_evenhub
# fish — needs a second file, since fish autoloads by command name
evenhub --completion-fish > ~/.config/fish/completions/evenhub.fish
echo 'complete -c eh -w evenhub' > ~/.config/fish/completions/eh.fish
CHANGELOG
0.2.0
- add the app management loop:
create,upload,update,ls,download,rm,promote,delist,testers,logout self-checkprints the account and role, and gains--json--jsonon every command: one object on stdout, diagnostics on stderr- one exit code per failure type, mapped from the server's error codes
- exit 9 is reserved for failures that left server state unchanged; a local step
that fails after a write reports exit 11 and names the completed effects in
partial - shell completion covers the whole surface for both
evenhubandeh:packoffers--jsonagain now that its first positional is<manifest>instead ofjson, andself-checkcompletes. In zsh and fish, file names are offered only where an argument takes a path; bash keeps readline's filename fallback for anything the completion script leaves empty - validate name, tagline, description, changelog, email and icon locally, so a rejected input never reaches the server
- icon checks run colour before blocks and name the offending pixel
- resume an interrupted
createoruploadfrom a cached draft, with the server's 30-minute lifetime honoured - order builds by semantic version
testers addskips addresses already invited or active;--reinviteforces a new invitation to the invited ones and holds back active members- drop the runtime dependencies that the bundled
main.jsnever loads, so installing fetches the package alone - load the PNG QR renderer only for
qr --external, which removes a Node deprecation warning from every other command
0.1.14 (2026-08-06)
- derive and stamp
min_app_versionfrom SDK npm metadata during packing - add
--sdk-verand the--enforce-manual-versioncompatibility override - add shell completion support for both
evenhubandeh - return a failing exit status when packing fails
0.1.13
- add TERM/COLORTERM detection for printing qrcode in terminal
0.1.12
- now uses
qrcode-terminalfor terminal qrcode andqr-imagefor external qrcode. - fix login command (not super useful at this time though)
0.1.11
- fix packing issue in windows
0.1.10 (2026-03-23)
- small restriction adjustments to app.json
0.1.9 (2026-03-21)
- adjust app.json size limits
0.1.8 (2026-03-19)
- adjust app.json format
0.1.7 (2026-03-13)
- fix '-d' option for init command
0.1.6 (2026-03-12)
- adjust app.json file format