Create and manage native Appo apps from the terminal or an agent — at parity with
the dashboard. A thin, dependency-free Node client over the Appo public API
(/api/v1) and the RFC 8628 device-authorization grant (/api/oauth/device/*).
Looking for the JavaScript bridge inside the app (push, biometrics, camera from your web code)? That is
@appolabs/sdk. Note: versions of this package below 3.0.0 were that legacy bridge SDK — they are deprecated on npm and point here. Docs: https://goappo.io/docs.
npm install -g @appolabs/appoRequires Node 18 or newer. After install the appo command is on your PATH.
Check the version with:
appo --version # prints: appo/<version> node/<version>The two canonical commands: appo new — the app on your phone; appo ship —
the app on the stores.
appo new creates your app from a URL:
npm install -g @appolabs/appo
appo init # bootstrap config + first login
appo new --url tuosito.com # create the app (name: Tuosito)--url is required; a bare domain gets https:// prepended. --name is
optional — without it the name derives from the URL hostname: leading www.
stripped, first label, capitalized (www.pizza-mario.it -> Pizza-mario).
appo new --url <u> --name <n> # explicit name
appo new --url <u> --prepare # request Appo preparation
appo new --url <u> --json # raw creation response envelope--prepare — requests that Appo prepare the app content (icons, splash screen,
store copy). Default: you prepare it yourself. Omit the flag when the app's
source code is available locally and you can configure the content directly.
On success it prints the new app id and the two next steps: appo preview <id>
(the app on your phone) and appo ship <id> (the app on the stores). A missing
--url is a usage error (exit 2, no request issued); API errors exit 1.
appo ship <id> signals publish-intent on an existing app — Appo issues the
build server-side and submits it to the stores for you. It stops at a
confirm-gate before the publish unless you pass --yes. The same verb covers
republishing and resubmitting after an App Store rejection — there is no
separate reship/build/resubmit verb.
appo ship <id> # publish preview (gate), no write
appo ship <id> --yes # confirm and shipAppo builds and submits server-side; track progress with appo status <id> or
appo preview <id>. Flags: --stores <list> (override the target stores;
defaults to the app's stores), --yes (confirm the publish step; --confirm
is an alias), --json (emit one {steps, final_state} object — final_state
in {shipped, gated, blocked} — instead of the live stream). The build platform
is decided by Appo (the operator) — you ship an outcome, not a build
configuration. ship maps its final lifecycle state to the
exit codes: 0 shipped, 1 blocked, 2 usage error,
3 gated (publish preview shown, no write — re-run with --yes).
appo init # device-flow login, then a readiness report
appo init --token <pat> # non-interactive first login for CI/agentsBootstraps the config in ~/.appo/config.json (owner-only) and performs the first
login. It is idempotent: if the active environment already has a stored token,
init reports the active env and writes nothing (no clobber). On success it prints
the active env, the API base, and the next step (appo new --url <u>).
appo login # authenticate via the browser (device flow)
appo login --token <pat> # authenticate non-interactively with a dashboard PAT
appo logout # revoke the token server-side and clear it locally
appo whoami # show the active environment + API base + livenessappo login prints a link and a short code and opens your browser. Register or
sign in, approve the connection, and the CLI receives its token automatically. The
token is stored in ~/.appo/config.json (owner-only) and is never printed.
appo login --token <pat> validates a pasted personal access token against the API
before storing it; a rejected token is not written. Create a PAT in the dashboard.
appo logout revokes the token server-side, then always clears it locally (even if
the revocation call fails). appo whoami reports the environment, API base, and an
app count as a liveness probe — it never prints the token.
appo env list # list configured environments
appo env use <name> # switch the active environmentEach environment is a named profile in ~/.appo/config.json with its own stored
token and API base. Authenticating against a new environment (appo login --env staging) adds a profile without clobbering the others. env list marks the active
profile with * and never prints tokens. Select an environment per-command with
--env <name> or the APPO_ENV variable.
appo apps create --name <n> --url <u>
appo apps list # list your apps
appo apps show <id> # show one app
appo apps update <id> [--name <n>] [--url <u>] [--icon <https-url>] [--permission <name>=<on|off>] # edit name, base URL, icon, permissionsapps create registers a new app from a name and a base URL. apps list prints id,
name, publication state, and base URL per app. apps show <id> prints the full app
overview. apps update <id> edits only the fields you supply — the app name, its base
URL, the app icon, and its native permissions; at least one field is required. --icon
takes an https image URL, which Appo fetches and sets via the icon endpoint.
--permission takes <name>=<on|off> and is repeatable; name is one of tracking,
camera, microphone, nfc. Only the toggles you pass change — the rest are left as
they are. When --name/--url, --icon, and --permission are given together the
field update runs first, then the icon, then the permissions. --json prints null
for a name/URL-only update (it returns no body) and, for the parts that ran,
{ icon_url } and/or { permissions }. Not confirm-gated (reversible).
To republish an existing app (or resubmit after a rejection), use
appo ship <id>— Appo rebuilds and resubmits server-side. There is no separatereship/build/resubmitverb. You ship an outcome, not a build configuration.
appo status <id> # app overview
appo status <id> --build <buildId> # one build's statusPrints the app overview, or a single build's status when --build is given. With
--json it prints the raw v1 response body verbatim.
appo preview [id]Shows the preview target for an app — per-platform readiness, the iOS TestFlight
URL, the Android deeplink, the canonical preview_url, and a scannable terminal QR
code (when at least one platform is preview-ready).
The id is optional. Without it, appo preview targets your only app directly, or —
when the account has several — shows a numbered picker on an interactive terminal.
In scripts and with --json, several apps produce an error listing the ids instead
of a prompt.
The output prints readiness first (ios: preview-ready / not preview-ready yet,
same for android), then the platform-specific URLs (TestFlight URL only when iOS
is ready, Android deeplink only when Android is ready), then the preview_url
(always present), then the QR encoding preview_url.
When neither platform is ready the QR is skipped and a (no preview target yet)
line is printed instead. With --json the raw v1 response body is emitted verbatim
— no QR, no curation. Exit 1 on API error (including app not found, or no apps to
resolve); exit 2 when several apps exist and no id was given outside a TTY.
appo rejection <id>Shows the active App Store rejection (status + required action). When there is no
active rejection it reports so and exits non-zero. --json emits the raw envelope.
appo fix-recipe <id>Shows the fix recipe for a rejection — slug, fix type, agent steps, and
limitations. --json emits the raw envelope.
appo publish <id> --confirmPublishes to the app's stores. Destructive: without --confirm it prints a preview
and exits with code 3 (confirm required) — no write is performed. By default it
targets the app's stores; --stores <list> is an optional override accepting the
canonical store tokens or the apple/google aliases.
appo push <id> --title <t> --body <b> [--target-url <u>] [--image-path <p>] [--scheduled-at <when>] --confirmSends a push notification. Destructive: without --confirm it prints a preview and
exits with code 3 — no write. The preview omits the recipient count (exposed only
after send). On success it reports the number of devices reached.
appo download <id> # newest ready artifact
appo download <id> --build <n> # a specific build
appo download <id> --output <path> # choose the destination fileDownloads the installable artifact (APK/AAB for Android, ad-hoc IPA for iOS) once
the build is ready. Without --build it picks the newest ready build; if the
latest build is still running it reports that build's status instead. The filename
derives from the artifact URL unless --output is given. --json reports
{ build_id, file, bytes }. Prepare a build with appo preview <id>.
appo devices list # registered iOS test devices
appo devices register # registration link + QR (open on the iPhone)iOS test builds are signed ad-hoc against your registered devices — no Apple
developer account and no Apple login needed. devices register prints a signed
24h link and a scannable QR; open it on the iPhone and follow the enrollment
prompt to register the device's UDID, one time per device. devices list shows
your pool (UDIDs are truncated server-side).
appo upgrade # update to the latest @appolabs/appo via npm
appo --version # print the CLI + Node version (alias: -v)appo upgrade runs npm install -g @appolabs/appo@latest and reports the result.
The CLI also performs a daily, best-effort update check and prints a one-line
notice to stderr when a newer version is available; the check is skipped under
--json and silently swallows network errors.
| Variable | Purpose |
|---|---|
APPO_TOKEN |
Ephemeral token, highest precedence, never written to disk |
APPO_ENV |
Active environment/profile (overridden by --env) |
APPO_API_BASE |
API base URL (overridden by --api) |
Create a PAT in the dashboard, then appo login --token <pat> or set APPO_TOKEN
in your environment (for example in CI/agents) to authenticate without the browser
flow. The default API base is https://apps.goappo.io (production); override it with
--api or APPO_API_BASE (for example http://localhost:8002 for local development).
| Code | Meaning |
|---|---|
0 |
success |
1 |
runtime / API error (including auth failure — run appo login) |
2 |
usage error (missing or invalid arguments) |
3 |
confirm required (destructive verb invoked without --confirm; no write) |
ship maps these to its final lifecycle state: 0 shipped, 1 blocked,
2 usage, 3 gated (publish preview shown, no write — re-run with --yes).
For non-interactive contexts (CI pipelines, agents) authenticate without the browser device flow:
export APPO_TOKEN=<pat> # ephemeral, never written to disk
# or, to persist into a profile:
appo login --token <pat>APPO_TOKEN has the highest precedence and is never persisted. appo login --token <pat> validates the token and stores it in the active profile. Combine
with APPO_ENV and APPO_API_BASE to fully configure a headless environment.
Releasing @appolabs/appo to the npm registry is a deliberate human action. The
CLI tooling never publishes automatically.
The release workflow (.github/workflows/release.yml) publishes on push to
master/main using npm OIDC trusted publishing (--provenance --access public).
There is no NPM_TOKEN secret to manage.
One-time setup (user-performed):
- On npmjs.com, open the package's Settings → Trusted Publisher.
- Provider: GitHub Actions; Organization/user:
appolabs; Repository:appo; Workflow filename:release.yml; Environment: blank. - Confirm
package.jsonrepository.urlmatches the GitHub repository exactly.
First publish: npm trusted publishing requires the package to already exist on the registry. Two paths are available, and you choose:
- Run one manual
npm publish --access publicforv0.1.0, then rely onrelease.ymlfor subsequent patch releases; or - If your local npm is OIDC-capable, publish via that path for the first release.
Before any publish, confirm package ownership and the intended version: a package
named @appolabs/appo already appears on the registry. Verify you own the name and
that the version you are publishing is correct before running npm publish — do not
assume the name is unclaimed.
The API core ships as importable modules alongside the CLI — the first consumer
is @appolabs/appo-mcp, which reuses the same lifecycle calls, auth resolution,
and profiles instead of maintaining a parallel client:
import { getPreview, listApps } from "@appolabs/appo/ops";
import { resolveApiBase, activeProfileName, storedToken } from "@appolabs/appo/config";
const env = activeProfileName();
const apiBase = resolveApiBase(undefined, env);
const preview = await getPreview(apiBase, 128, env);Token resolution matches the CLI: APPO_TOKEN first, then the active profile's
stored token from appo login. The exported modules (./ops, ./api,
./config) never print or exit — errors throw with the v1 envelope attached
(err.status, err.envelope). Type stubs ship as .d.mts next to each module.