Skip to content
Login

CLI Reference ​

@botiverse/hands-cli is the command-line client for Hands. Use it from local scripts or CI to inspect apps, upload Android builds, and publish releases.

Install ​

Install it globally:

bash
npm install -g @botiverse/hands-cli

Or run it without a permanent install:

bash
npm exec --package @botiverse/hands-cli@0.5.15 -- hands --help

In CI, pin a version so release scripts stay reproducible.

Authentication ​

The CLI reads a Hands API server and bearer token from environment variables:

bash
export HANDS_API=https://hands.build
export HANDS_BEARER_TOKEN=<deploy-token>

If HANDS_API is unset, the CLI defaults to https://hands.build. HANDS_AUTH_TOKEN is also accepted as an alias for HANDS_BEARER_TOKEN.

Legacy QUIVER_* names still work: every variable is read as HANDS_<name> first, then QUIVER_<name>, so existing CI keeps working unchanged.

Use app-scoped deploy tokens for CI. Mint them with hands deploy-tokens create (below) or in the app's Settings page, choose the minimum role required, and store the raw token in your CI secret store.

Raft Agent Login sessions are not exported to this CLI. raft integration login authenticates raft integration invoke; it does not populate HANDS_AUTH_TOKEN. An admin agent that needs to bootstrap CI should call the manifest create-deploy-token action, write the returned one-time token directly to the CI secret store, and then let the CLI consume that deploy token.

Deploy Tokens ​

App-scoped tokens for CI to publish (publisher) or read (viewer). Managing them requires app admin; if you lack it, the CLI prints an actionable error (which role you need, who can grant it, and that an admin can do it for you).

bash
# Mint a publisher token for CI (printed ONCE — capture it immediately)
hands deploy-tokens create raft-android --name github-ci --role publisher

# Optional expiry, and machine-readable output for scripting
hands deploy-tokens create raft-android --name github-ci --expires-in-days 90 --json

# List an app's tokens (metadata only, never the secret)
hands deploy-tokens list raft-android

# Revoke by id
hands deploy-tokens revoke raft-android <tokenId>

The created token is what you set as HANDS_BEARER_TOKEN in CI. The server stores only a hash, so a lost token can only be revoked and re-minted, never recovered.

Basic Commands ​

Show the installed version:

bash
hands version

Create an app in the current Hands organization (requires org member or higher):

bash
hands apps create \
  --slug hands-example-web \
  --name "Hands Example Web" \
  --platform web \
  --description "Hands web app example"

List apps visible to the current token:

bash
hands apps list

Read the app's public SDK client key explicitly (requires app admin; this does not rotate the key or return any deploy token):

bash
hands apps client-key hands-example-web

List builds for an app:

bash
hands builds list raft-android

Register External Node / CLI Bytes ​

For an app created with platform node, builds publish-version records one externally hosted target without copying it into Hands storage:

bash
hands builds publish-version raft-computer \
  --version-name 0.72.13 \
  --target linux-x64 \
  --source-url https://cdn.raft.build/computer/0.72.13/linux-x64 \
  --raw-sha256 "$RAW_SHA256" --raw-size "$RAW_SIZE" \
  --gzip-sha256 "$GZIP_SHA256" --gzip-size "$GZIP_SIZE" \
  --node-version 22.23.1 \
  --source-commit "$GIT_COMMIT"

Run the command once per target. Hands stores the URL, raw/gzip hashes and sizes, Node version, and provenance under one app/version build. An identical declaration replays successfully; changing version-level or target-level immutable fields returns a conflict. This command does not upload bytes or activate a release/channel pointer.

Publish Hands-hosted CLI Binaries ​

builds publish-cli-binary uploads one or more platform binaries directly to Hands storage and creates one release only after every declared artifact has been verified:

bash
hands builds publish-cli-binary raft-computer \
  --version-name 1.0.33 \
  --channel main \
  --target darwin-arm64 --binary ./raft-computer-darwin-arm64 \
  --target linux-x64 --binary ./raft-computer-linux-x64

--target and --binary pair by position. Optional --runner and --sha256sums values also repeat once per target when used.

The command creates a pending build with its complete artifact slot set frozen, streams each file to a short-lived single-object upload URL, and asks Hands to verify the exact SHA-256 digest and byte size. Hands marks the build succeeded and creates the release only after every frozen slot is ready. Interrupted runs can replay the same declaration: pending assets retain their declared digest and size, while ready assets return no upload URL. The client never receives an R2 bucket credential.

Hosted release URLs under /dl/:slug/releases/:releaseId/:file redirect to a short-lived, object-scoped R2 download URL when R2 signing is configured. The channel URL /dl/:slug/:channel/:file first redirects to an immutable release URL. Download clients must follow redirects (for example, curl -L) and verify the declared digest as before. Keep the Hands release URL for future downloads; the final R2 URL expires. A HEAD request receives a URL signed for HEAD, while GET receives one signed for GET. Redirects are Cache-Control: no-store; this does not promise CDN caching of object bytes. Self-hosted instances without R2 signing continue to stream through Hands.

Publish Android ​

Use builds publish-android to upload an APK and create a release. Per the release policy, CI should pass --draft so a human or agent reviews the changelog before the release goes live.

bash
hands builds publish-android raft-android \
  --apk ./androidApp-release.apk \
  --channel preview \
  --version-name 1.0.0 \
  --version-code 1000000 \
  --changelog-file ./changelog.txt \
  --draft

Package id, SDK levels, and the launcher icon are extracted from the APK automatically on the server — no extra flags needed.

Add support artifacts when available:

bash
hands builds publish-android raft-android \
  --apk ./androidApp-release.apk \
  --mapping ./mapping.txt \
  --symbols ./native-symbols.zip \
  --metadata ./metadata.json \
  --channel preview \
  --version-name 1.0.0 \
  --version-code 1000000

Public update checks only use the installable artifact. Mapping files, native symbols, and metadata stay available through authenticated admin APIs.

Publish iOS / TestFlight ​

CI should upload the signed IPA exported by macOS/Xcode, not an unsigned intermediate IPA. Hands stores the IPA and dSYM; Apple signing certificates, profiles, and passwords stay in the CI secret boundary. The separate App Store Connect API key used for server-side upload/distribution stays encrypted in Hands and is never copied to CI or an agent.

Use builds publish-ios after xcodebuild archive and xcodebuild -exportArchive:

bash
hands builds publish-ios raft-ios \
  --ipa ./build/Raft.ipa \
  --dsym ./build/Raft.dSYM.zip \
  --channel main \
  --version-name 1.0.0 \
  --version-code 1000000 \
  --changelog-file zh=./changelog.zh.md \
  --changelog-file en=./changelog.en.md \
  --source-commit "$GITHUB_SHA" \
  --ci-provider github-actions \
  --ci-run-id "$GITHUB_RUN_ID" \
  --ci-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
  --export-method app-store \
  --appstore-build-number 42 \
  --draft

--version-code must match the value the app reports (CFBundleVersion), or iOS crashes will not symbolicate against the right dSYM. The --dsym file is a .dSYM.zip of the archive's *.dSYM bundles; without it, iOS crashes for that version show only raw frames. --export-method, --appstore-build-number, and --testflight-status are recorded as build metadata. publish-ios creates the Hands build/release; it does not itself upload to Apple or activate TestFlight testing.

An app admin then starts the server-side upload through the Hands console, API, or Raft integration action upload-testflight-build. Poll the returned Apple Build Upload id with get-testflight-upload-status until COMPLETE or FAILED (response.state.state; Apple errors/warnings/infos remain alongside it). This stage only uploads and processes the binary: it never assigns a beta group, notifies testers, activates the Hands release, or submits an App Store production release.

After Apple exposes the exact build as VALID, list stable beta group ids:

bash
hands builds testflight-groups raft-ios <hands-build-id>

Distribute to internal testers:

bash
hands builds testflight-publish raft-ios <hands-build-id> \
  --distribution internal \
  --group-id 11111111-2222-3333-4444-555555555555 \
  --what-to-test en-US="Verify login and Activity." \
  --what-to-test zh-Hans="验证登录和活动页。" \
  --wait

External distribution uses the same processed build, but submits TestFlight Beta App Review. --notify-testers enables Apple's automatic notification after approval; for an already-approved build without auto-notify pending, Hands sends the official build beta notification immediately.

bash
hands builds testflight-publish raft-ios <hands-build-id> \
  --distribution external \
  --group-id aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee \
  --what-to-test-file en-US=./testflight.en.txt \
  --what-to-test-file zh-Hans=./testflight.zh.txt \
  --notify-testers \
  --json > testflight-publish-receipt.json

Preserve that mutation response: notification=sent or already_sent is the notification operation receipt. Do not add --wait to this receipt-producing call. With --wait --json, the CLI prints the later polled GET status instead of the original POST response, so notification and operation_id are not preserved.

External review can take longer than a normal CLI session. Inspect the live state separately:

bash
hands builds testflight-status raft-ios <hands-build-id> \
  --distribution external \
  --json

When --wait is useful and the POST operation receipt is not part of the acceptance contract, tune its bounded poll contract with --poll-interval-seconds and --timeout-seconds (defaults: 15 and 3600). Terminal failure/rejection/expiry states fail the command. External mode requires an existing or supplied What to Test localization; selected group ids must all match the requested internal/external mode.

Notification values are not_requested, scheduled, sent, and already_sent. Only sent/already_sent, paired with a fresh exact-build external_build_state=IN_BETA_TESTING, prove notification completion. If the first response is scheduled, wait for that exact testing state, replay the same publish without --wait to preserve already_sent, then fresh-read the status again. VALID, group assignment, BETA_APPROVED, and auto_notify_enabled=false are not notification receipts.

Hands follows Apple's role boundary: uploading the IPA requires Hands app admin; TestFlight group distribution requires Hands app publisher. The stored App Store Connect key must have an Apple role permitted for the requested operation (external testing: Account Holder, Admin, or App Manager; internal testing also permits Developer or Marketing). Apple limits beta builds to 90 days and permits at most one build of a version in Beta App Review at a time.

Official Apple references:

TestFlight crash reports and screenshot feedback ​

Pull what testers submitted from TestFlight, using the app's stored App Store Connect key (app viewer is enough). Pulling never changes anything on Apple's side.

bash
# crashes: newest 20 for one build, each crash log saved as .ips
hands testflight crashes raft-ios --build 11200001 --download ./crashes
# screenshot feedback: tester text, device, build, screenshots
hands testflight feedback raft-ios --build 11200001 --download ./shots
# pin the marketing version too, JSON for scripts
hands testflight crashes raft-ios --build 11200001 --app-version 1.12.0 --json

Only crashes a tester chose to share from the TestFlight prompt are available from Apple's API. Unshared crashes appear only in Xcode Organizer, so an empty list does not prove the build never crashed. Some shared crashes have no log on Apple's side (CRASH_LOG_NOT_AVAILABLE). Screenshot URLs are signed by Apple and expire (expires_at). Tester emails are not returned.

Closing (separate, explicit step). After triage, close the item on Apple's side. This calls Apple's official TestFlight feedback API, and Apple's only resolution for a submission is delete: the crash or screenshot feedback is removed from TestFlight feedback in App Store Connect. Needs app publisher or the feedback:triage permission.

bash
hands testflight close raft-ios crash <crashId>
hands testflight close raft-ios feedback <feedbackId>

API:

ActionRequest
List crashesGET /api/apps/<appId>/testflight-crashes?build=&version=&limit=
Crash logGET /api/apps/<appId>/testflight-crashes/<id>/log (text/plain)
List screenshot feedbackGET /api/apps/<appId>/testflight-feedback?build=&version=&limit=
Close crashDELETE /api/apps/<appId>/testflight-crashes/<id>
Close screenshot feedbackDELETE /api/apps/<appId>/testflight-feedback/<id>

For raw CI drafts, a single --changelog-file ./changelog.txt is still valid. For reviewed notes, prefer repeatable lang=file entries such as --changelog-file zh=zh.md --changelog-file en=en.md.

Publish HarmonyOS / OHOS ​

Use builds publish-ohos after CI has assembled and signed the App Pack. The command stores both distribution paths on one build: .app for AppGallery and the standalone signed .hap for user sideloading.

bash
hands builds publish-ohos raft-ohos \
  --app ./raft-ohos-1.0.0-1000000.app \
  --hap ./raft-ohos-entry-1.0.0-1000000.hap \
  --symbols ./ohos-symbols-1.0.0-1000000.tar.gz \
  --metadata ./ohos-release-metadata.json \
  --channel main \
  --version-name 1.0.0 \
  --version-code 1000000 \
  --source-commit "$GITHUB_SHA" \
  --ci-provider github-actions \
  --ci-run-id "$GITHUB_RUN_ID" \
  --ci-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
  --draft

Both .app and .hap are installable assets with distinct file types and distribution metadata. Public update consumers can request filetype=app or filetype=hap; authenticated build views expose both files individually. Signing certificates, profiles, P12 files, and passwords remain in CI and are never uploaded to Hands.

AppGallery invitation testing (OHOS) ​

Hands can push a build's signed .app to AppGallery Connect as an invitation test (Huawei App Testing, releaseType=6). Store listing submission and phased release are not automated yet; do those in AppGallery Connect. The app needs AGC Service Account credentials (Settings → AppGallery) and the channel bundle_id set to the HarmonyOS package name.

CLI (0.5.30+, app admin role):

bash
hands agc groups raft-ohos
hands agc upload raft-ohos <buildId> --package-name build.raft.mobile \
  --test-desc "1.12.0 (11200003)" --wait
hands agc submit raft-ohos <submissionId> --group <groupId>
hands agc status raft-ohos <submissionId> --events   # or: <buildId> --build

upload is idempotent per build. After failed, rejected, or stopped it starts a new attempt (a new AGC test version); the earlier submission keeps its history.

The same flow over the admin API with hands api:

bash
APP=/api/apps/<appId>
# 1. Upload the build's .app and create the test version (idempotent per build).
hands api POST $APP/builds/<buildId>/agc-invitation-test \
  --data '{"package_name":"build.raft.mobile","test_desc":"1.12.0 (11200003)"}' --json
# 2. Poll until state is "ready" (Huawei compiles the package, ~1-3 min).
hands api GET $APP/agc-submissions/<submissionId> --json
# 3. List groups, then submit to one or more of them.
hands api GET $APP/agc-test-groups --json
hands api POST $APP/agc-submissions/<submissionId>/submit \
  --data '{"group_id":"<groupId>"}' --json
# 4. Re-read for Huawei's review outcome.
hands api GET $APP/agc-submissions/<submissionId> --json

submit requires an explicit group_id/group_ids when the app has more than one test group; with exactly one group it uses that group. Every GET of a submission re-reads Huawei while the outcome is still open, so the stored state follows AppGallery:

Hands stateHuawei test releaseState
processingpackage still compiling (successStatus 1)
readycompiled and bound; 7 准备提交 / 11 撤销审核
testing_review4 正在审核 / 12 预审中
testing_scheduled3 待生效
testing_active0 正在测试
rejected1 审核不通过 / 13 预审不通过 (provider_state.audit_opinion has Huawei's reason)
stopped2 / 10 已失效
failedupload error, or Huawei could not compile the package (successStatus 2); upload again to retry

The raw release_state, audit_opinion, and test window are kept in provider_state. If Huawei cannot be reached the response still returns the stored submission plus sync_error.

Publish Electron (generic provider) ​

Hands can host Electron apps that use electron-updater with the generic provider:

ts
autoUpdater.setFeedURL({
  provider: "generic",
  url: "https://hands.build/electron/raft-desktop/main"
});

Hands hosts electron-builder output as-is. Use builds publish-electron to upload the files from dist/ as build assets on an electron-installer build/release, then create a draft release for review:

bash
hands builds publish-electron raft-desktop \
  --channel main \
  --version-name 1.2.3 \
  --version-code 10203 \
  --platform win32 \
  --arch x64 \
  --metadata dist/latest.yml \
  --installer "dist/Raft Setup 1.2.3.exe" \
  --blockmap "dist/Raft Setup 1.2.3.exe.blockmap" \
  --changelog-file ./changelog.txt \
  --draft

--metadata, --installer, and --blockmap are repeatable. For multi-platform Electron apps, run the command once per platform/channel so each release has a clear platform/arch pair. The command preserves original filenames through variant and metadata_json.filename, which lets relative URLs inside latest*.yml resolve unchanged.

Required files depend on target OS:

TargetRequired files
Windows NSISlatest.yml, installer .exe, optional .exe.blockmap
macOSlatest-mac.yml, signed .zip for auto-update, optional .dmg for downloads, .blockmap files if generated
Linuxlatest-linux.yml, AppImage or other configured target, .blockmap files if generated

CI systems that need custom orchestration can also call the build, asset, and release APIs directly. Register the original filenames using the same fields:

Fileplatformarchfiletypeartifact_kindFilename field
latest.ymlwin32x64 or nullymlelectron-metadatavariant or metadata_json.filename
latest-mac.ymldarwinarm64 or x64ymlelectron-metadatavariant or metadata_json.filename
latest-linux.ymllinuxx64 or arm64ymlelectron-metadatavariant or metadata_json.filename
Raft Setup 1.2.3.exewin32x64exeinstallablemetadata_json.filename
Raft Setup 1.2.3.exe.blockmapwin32x64blockmapelectron-blockmapmetadata_json.filename

Keep the same draft-first policy as Android: CI creates a draft Electron release, then a human or agent reviews release notes and explicitly publishes. macOS update artifacts must be signed before upload; Hands hosts the signed files but does not sign Electron applications.

Review and Publish (draft flow) ​

CI creates drafts; publishing is an explicit step after changelog review:

bash
# inspect the draft (status, rollout, changelog)
hands releases show raft-android <release-id>

# write the reviewed changelog; repeatable [lang=]file entries
hands releases update raft-android <release-id> \
  --changelog-file zh=changelog.zh.md \
  --changelog-file en=changelog.en.md

# make it live
hands releases publish raft-android <release-id>

Bilingual changelogs are stored per language; clients receive the language matching their locale (zh normalizes to zh-CN; plain single-value changelogs are served as-is).

Exact device-group rollout ​

Create a named group, add stable installation ids reported by the Hands update SDK, then bind a draft release to that group:

bash
hands device-groups create raft-android --name "Artin test devices"
hands device-groups add-member raft-android <group-id> \
  --device-id <installation-device-id> --label "Huawei test tablet"
hands device-groups update raft-android <group-id> \
  --name "Artin test tablets" --description "Physical acceptance devices"
hands releases update raft-android <release-id> --device-group <group-id>
hands releases publish raft-android <release-id> --device-group <group-id>

The final publish remains an explicit authorization step. The CLI first reads release detail, canonicalizes every stored scope, and sends the complete set as expected_scopes together with that detail's expected_revision. The update command also fresh-reads and sends the revision before PATCH. Empty, duplicate, malformed, or unsupported scope state is rejected locally without a publish request. A repeatable --device-group asserts the exact stored device-group subset. Activation returns 409 if any scope or revision drifts between the detail read and the guarded server transition. Only exact group members receive the release; other devices fall back to the prior active release. List groups with hands device-groups list <app> and remove members with device-groups remove-member. Rename or change the operator note with device-groups update. Do not use IMEI or hardware serial numbers.

Percentage rollout with mandatory groups ​

One release can combine a percentage-gated full:all scope with device groups whose members are always included:

bash
hands releases update raft-android <release-id> \
  --full \
  --rollout-percent 25 \
  --always-include-group <acceptance-group-id> \
  --always-include-group <qa-group-id>

hands releases publish raft-android <release-id> \
  --device-group <acceptance-group-id> \
  --device-group <qa-group-id>

--always-include-group is repeatable and implies full:all; --full by itself resets the scope to full only. --rollout-percent accepts integers from 0 through 100, with 100 stored as an unrestricted full rollout. The legacy update form --device-group <id> still replaces the scope with one exact group-only rollout and cannot be combined with the full-rollout options.

There is one non-cancelled release owner per app/channel/product/release-type/version code. The publish commands preflight that coordinate before creating a build or uploading assets. Cancelling disables the old release and frees the version for a corrected upload while preserving its build, assets, and audit history. Restore reactivates an old release only while no replacement owns the version. A rare conflict that wins after CLI preflight marks the new build failed, writes the conflict identity to provenance, and includes the build ID in the terminal error. For Android releases that already reached devices, publish the correction with a higher version code so clients can update.

bash
hands releases share raft-android <release-id> --password <pw>   # password optional
hands releases shares raft-android <release-id>                   # list
hands releases update-share raft-android <release-id> <share-id> --ttl-seconds 1209600
hands releases revoke-share raft-android <release-id> <share-id>

--password can also come from HANDS_SHARE_PASSWORD (legacy QUIVER_SHARE_PASSWORD still works) to keep it out of shell history. Share URLs are printed once at creation; tokens are stored hashed.

Feedback Tickets ​

Agents can triage feedback and crash tickets entirely from the CLI (viewer role for read, publisher for changes):

bash
hands feedback list raft-android --status open --kind crash
hands feedback show raft-android <ticket-id>
hands feedback update raft-android <ticket-id> --status in_progress --assignee me
hands feedback comment raft-android <ticket-id> "已复现,修复中"
hands feedback update raft-android <ticket-id> --status resolved

--assignee none unassigns. All subcommands accept --json for scripting.

App Webhooks ​

App admins can subscribe an endpoint to one app's events without org-admin rights. The server pins these webhooks to the app; org-wide webhooks (all apps) remain org-admin-only and are not listed here.

bash
# Secret generated and printed once:
hands webhooks create my-app --url https://example.com/hands --events release:new,build:failed
# Or bring your own secret (never on argv):
printf %s "$HOOK_SECRET" | hands webhooks create my-app --url https://example.com/hands --secret-stdin
hands webhooks list my-app
hands webhooks deliveries my-app <webhook-id>
hands webhooks update my-app <webhook-id> --disable
printf %s "$NEW_SECRET" | hands webhooks update my-app <webhook-id> --secret-stdin
hands webhooks delete my-app <webhook-id>

release:new payloads carry release_id, app_id, build_id, channel_id plus version_name, version_code, app_slug and channel (slug), so a consumer can link the pinned public page https://hands.build/apps/<app_slug>/v/<version_name> without an API call.

Omitting --events subscribes to all events. Deliveries carry X-Hands-Signature: sha256=<hex> (HMAC-SHA256 of the raw body with the secret). REST: /api/apps/:appId/webhooks (list/create), /api/apps/:appId/webhooks/:id (PATCH/DELETE), /api/apps/:appId/webhooks/:id/deliveries.

Direct API Access (hands api) ​

A power-user / scripting affordance for calling a Hands API endpoint directly, over the same server-side RBAC, audit, and per-endpoint guards a dedicated subcommand uses. It is not a permission bypass and not a generic-SQL escape hatch — every write still hits the same server route.

bash
# GET with repeatable query params
hands api GET /api/apps --param platform=android

# PATCH with an inline JSON body. NOTE: path params are raw — <appId> is the app
# UUID, not a slug: `hands api` passes the path through and does not resolve slugs.
hands api PATCH /api/apps/<appId>/releases/<releaseId>/shares/<shareId> \
  --data '{"expires_at":null}'

# ...or read the same body from a file with @path
hands api PATCH /api/apps/<appId>/releases/<releaseId>/shares/<shareId> \
  --data @body.json

# Binary / same-origin streaming response: write the raw bytes to a file
hands api GET /api/apps/<appId>/feedback/<ticketId>/attachments/<attachmentId> \
  --output attachment.bin

# Machine-readable: print only the response body (no status line)
hands --json api GET /api/apps
  • Raw path params. <appId> and the other ids are the resource UUIDs, not slugs — hands api passes the path through verbatim and does not resolve slugs the way the dedicated subcommands do.
  • Same-origin /api/* only. <path> is resolved against the active Hands API base (honoring the root --api flag). A relative path or a same-origin absolute URL is accepted; cross-origin absolute URLs, //protocol-relative hosts, and ../ traversal that escapes the origin are rejected — so the Authorization bearer never leaves the Hands origin.
  • Redirects are never followed. A 3xx is reported, not chased; following a redirect off-origin would leak the bearer.
  • Auth + RBAC unchanged. Reuses your existing CLI login/token; the server enforces the same role checks as any other call.
  • Flags: --param key=value (repeatable), --data <json|@file>, --output <file> (required for binary). Both the global --api and --json flags are honored.

CI Environment Variables ​

Every variable is read as HANDS_<name> first, then the legacy QUIVER_<name>, so existing CI keeps working unchanged.

VariableRequiredPurpose
HANDS_APINoHands business API base URL. Defaults to https://hands.build. HANDS_CLI_API takes precedence if set.
HANDS_BEARER_TOKENYesApp-scoped deploy token for CI.
HANDS_AUTH_TOKENNoAlias for HANDS_BEARER_TOKEN (tried first).

Legacy equivalents QUIVER_API, QUIVER_BEARER_TOKEN, QUIVER_AUTH_TOKEN are still accepted.

CI Examples ​

Copy-ready pipelines built on these commands live in botiverse/hands-examples:

  • A reusable GitHub Action: uses: botiverse/hands-examples/publish-android@v1.
  • Complete GitHub Actions and GitLab CI workflows for Android, iOS (publish + server-side TestFlight upload), and Electron (per-platform channels for the generic provider).
  • Plain-bash scripts for Jenkins, Buildkite, or any runner with bash and npm.

Versioning Guidance ​

For Android releases, keep APK versionCode and Hands version_code identical. Clients only update when the server release has a higher version code than the installed app.

One common scheme is:

text
versionCode = major * 1_000_000 + minor * 10_000 + patch * 100 + build
versionName = major.minor.patch[-suffix]

Example: 1.0.3-rc2 becomes versionName=1.0.3-rc2 and versionCode=1000302.

Security ​

Do not paste deploy tokens, package tokens, signing passwords, or keystore data into public chat, issue comments, logs, or release notes. Store them in the CI secret store and pass them to Hands through environment variables.