The command-line client of Appo, which turns an existing
website into native iOS and Android apps and operates their publication on the
App Store and Google Play. Create and manage 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/*).
- In a terminal or CI:
npx @appolabs/appo new --url <website URL>creates the app,runputs it on a phone andshippublishes it. - With an AI agent (Claude Code, Codex, Cursor):
npx @appolabs/appo mcpregisters the Appo MCP connector,https://apps.goappo.io/mcp, and the agent drives the same lifecycle. See mcp.
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 off # you or your developer maintain the site's code
appo new --url <u> --prepare on --owner-authorized --accept-tos # Appo's team adapts the site
appo new --url <u> --json # raw creation response envelope--prepare on|off settles who adapts the website to the app. off: you, or a
developer working for you, maintain the site's code; the app can be tried right
away. on: Appo's team adapts the site, usually within 24-48 hours; the app can
be tried once that is done. on requires --owner-authorized (the website is
yours or you may publish it) and --accept-tos (acceptance of the Terms of
Service, https://goappo.io/en/terms). A bare --prepare means on.
In a terminal, appo new asks these questions in plain words when the flags are
absent. Under --json, or without a terminal, nothing is created and no request
is issued: the command prints an input_required envelope on stdout (stderr
without --json) and exits 2. Its code is preparation_choice_required
(no --prepare) or attestation_required (--prepare on without both
attestation flags); details carries the question, the matching flags and the
Terms URL. An agent asks the user, then re-runs with the flags named in the
envelope. appo apps create follows the same rule, and
appo apps update <id> --prepare on accepts --owner-authorized and
--accept-tos.
On success it prints the new app id and the two next steps: appo run <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 run <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> --prepare on|off [--owner-authorized --accept-tos]
appo apps list # list your apps
appo apps show <id> # show one app
appo apps check <id> # check the site of an app again
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. The
try line is the readiness answer, as given by the server (the same as
get_app_overview); while the app is not ready it lists what can be done meanwhile
and when to check again, and once it is ready it prints the preview link. With
--json it prints the raw v1 response body verbatim.
appo run [id]Prints the one preview link for the app, its QR code and how to open it. The
link opens a page that detects the phone and installs the right preview; the
page also states when iPhone testing is still being set up. While Appo is still
preparing or building the app, appo run prints what is happening, what can be
done meanwhile and when to check again. It only reads: it starts no build
(iPhone builds start automatically once the app is ready). Every sentence comes
from the server, the same answer appo status and the MCP tool
get_app_overview give.
The id is optional. Without it, appo run 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.
--json prints the preview response verbatim, including its try block. Exit 1
on any 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 }. Builds start automatically once the app is ready; appo run <id> prints the preview link.
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 mcp # register the Appo MCP connector on your AI agentappo mcp registers the remote Appo MCP connector, https://apps.goappo.io/mcp
(or the active profile's API host plus /mcp, so --env/--api select another
host), on every supported agent CLI found on your PATH. It runs the same command
you would type by hand:
claude mcp add --transport http --scope user appo https://apps.goappo.io/mcpcodex mcp add appo --url https://apps.goappo.io/mcpFor other clients (Cursor, Windsurf, VS Code) it prints the .mcp.json snippet
{ "mcpServers": { "appo": { "url": "https://apps.goappo.io/mcp" } } }, and for
clients that only accept stdio servers the npx mcp-remote <url> bridge. Sign-in
is the browser consent that opens the first time the agent uses Appo; no token is
configured. Exit code is 0 when at least one agent was registered, 1
otherwise.
Run without a terminal (from an agent's shell tool, or in CI), the command does
not wait for the browser sign-in that Codex's mcp add opens: the connector is
registered and the sign-in is completed later with codex mcp login appo.
In a terminal, appo mcp asks, for each agent it registers, whether to add a short
section, delimited by
<!-- appo-start --> and <!-- appo-end -->, to that agent's user-level
instructions file; an empty answer means yes, n skips it. Without a
terminal, or with --json, nothing is written unless --agent-instructions is
given, and the command prints one line naming that flag; the connector
registration and the exit code are the same in both cases. The files are
~/.claude/CLAUDE.md for Claude Code (or
$CLAUDE_CONFIG_DIR/CLAUDE.md), ~/.codex/AGENTS.md for Codex (or
$CODEX_HOME/AGENTS.md). An agent may load MCP tools on demand, in which case a
request such as "what is the status of my app" is read as a question about the
working directory; the section tells the agent the app lives in Appo. Text
outside the markers is left untouched, a rerun replaces the section in place,
and it can be edited or removed freely.
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, so other tools can reuse 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.