@appforge-ci/cli
v0.3.13
Published
`appforge` — the customer-facing CLI: sign up/log in, manage apps, trigger/watch builds, buy build-token top-ups, start an in-browser device-test session. No fleet commands — that's a separate tool, `appforge-admin` (`packages/admin-cli`), for the platfor
Readme
@appforge-ci/cli
appforge — the customer-facing CLI: sign up/log in, manage apps,
trigger/watch builds, buy build-token top-ups, start an in-browser
device-test session. No fleet commands — that's a separate tool,
appforge-admin (packages/admin-cli), for the platform operator only.
Install
On a machine that already has this repo cloned:
pnpm install # once, from the repo root
pnpm --filter @appforge-ci/cli build
cd packages/cli && npm link # symlinks `appforge` onto PATHFrom npm, once published: .github/workflows/publish.yml publishes
this package (tag-triggered), but needs an NPM_TOKEN repo secret
that isn't configured yet — until a version is actually published,
npm install -g @appforge-ci/cli will 404. The npm link path above is
what works today.
Usage
appforge signup --email [email protected] --org "My Org" # or: appforge login --email [email protected]
appforge login --email [email protected] --totp-code 123456 # if 2FA is enabled (--recovery-code also works)
appforge apps create --name demo --platform flutter --repo-url https://github.com/you/app.git
appforge apps list
appforge apps update <appId> [--name] [--repo-url] [--default-branch] # fix a stale field without recreating the app
appforge apps webhook-secret <appId> # the secret GitHub's webhook "Secret" field needs
appforge apps delete <appId> # permanently delete an app and its builds/OTA history
appforge apps secrets set <appId> --name KEYSTORE_PASSWORD # --value omitted -> prompted, echo off (never in shell history); injected as $KEYSTORE_PASSWORD on every build for this app
appforge apps secrets list <appId> # names only, never values
appforge apps secrets unset <appId> --name KEYSTORE_PASSWORD
appforge fleet-secrets set JWT_SECRET # value omitted -> prompted, echo off (never in shell history)
appforge fleet-secrets list # names only, never values
appforge fleet-secrets unset JWT_SECRET_PREVIOUS
appforge orgs rename --name "New Org Name" # fix a typo'd/stale org name (e.g. a placeholder from signup)
appforge orgs set-build-webhook --url https://example.com/hook # POST build id/status/appId/costTokens/artifacts on every build's completion
appforge orgs set-build-webhook --clear # turn build-status notifications back off
appforge build trigger --app-id <appId>
appforge build list --app-id <appId>
appforge build logs <buildId> --follow
appforge build artifact <buildId>
appforge build cancel <buildId>
appforge build explain <buildId> # AI explanation for a failed build (needs ANTHROPIC_API_KEY on the deployment)
appforge build analyze-smoke-test <buildId> # re-run AI analysis of the build's smoke-test screenshots
appforge build test <buildId> # in-browser device test session, on a succeeded build
appforge init # draft an appforge.yaml from what's actually in the current repo
appforge billing balance
appforge billing packs
appforge billing topup --pack starter # via LHV Paytech, see docs/payments-lhv.md
appforge billing ledger # every balance change (top-ups, charges, refunds), newest first
appforge billing usage # billing mode + this period's accrued usage, on postpaid
appforge billing setup-card # save a card — required before switching to postpaid
appforge billing mode <prepaid|postpaid-monthly>
appforge billing invoices # monthly invoices (postpaid billing only)
appforge ota channels --app <appId> # list channels and each one's active release
appforge ota releases --app <appId> [--channel production]
appforge ota promote <buildId> --app <appId> [--channel production] [--rollout 25] [--mandatory]
appforge ota set-rollout <releaseId> --app <appId> --rollout 50 # progress an existing release's rollout in place
appforge ota app-key --app <appId> # generate/rotate the manifest key a shipped app embeds
appforge ota activate <releaseId> --app <appId> --channel production # jump directly to a specific release
appforge ota activate --app <appId> --channel production --clear # deactivate the channel instead
appforge ota rollback --app <appId> --channel production # step back exactly one release
appforge tokens list # every API token for this org — id, label, created/last-used (never the token value)
appforge tokens revoke <tokenId> # permanently revoke one — 'appforge login' again mints a fresh one
appforge vpn status [--json] # the org's site-to-site VPN tunnel (any member)
appforge vpn create --name hq --endpoint vpn.example.com:51820 --gateway-key <base64> --lan 10.20.0.0/16 [--dns 10.20.0.2 --domain corp.example.com] [--tunnel-cidr 169.254.200.0/30]
appforge vpn peer-config # re-show the [Peer] block for your gateway (prints a secret; admin, audited)
appforge vpn rotate-keys [--yes] # new AppForge keys; tunnel returns to pending (a disabled tunnel stays disabled) until you update your gateway
appforge vpn disable [--yes] | enable # disable cancels + refunds queued VPN builds
appforge vpn delete [--yes]
appforge vpn events [--json] # newest first
appforge vpn test [--json] # start a build-free handshake test of the gateway (owner/admin; nothing charged; once per 5 minutes)
appforge vpn test --status [--json] # the latest test's verdict (any member); exits 0 only on handshake_ok
appforge apps update <appId> --vpn # opt an app in (--no-vpn to opt out; org owner/admin)Site-to-site VPN
You describe your own WireGuard gateway; AppForge's Mac dials out to it for
builds of apps that opted in. vpn create prints a [Peer] block to paste
into your gateway; update the gateway again after rotating keys or changing
LAN/tunnel ranges. It contains a secret (the preshared key) — treat it
like a password, and only create, peer-config and rotate-keys print it.
All changes need an org owner/admin; any member can read status/events.
vpn test checks that your gateway accepts the handshake without building anything: it
releases the tunnel's keys to one build machine for the test (see docs/vpn.md, "Test tunnel"),
and vpn test --status prints the verdict and when it ran.
Limits: tools that ignore proxy settings (raw sockets, UDP, ICMP,
xcodebuild, simulators) don't use the tunnel; SSH git remotes
(ssh://user@host/path or user@host:path) go through it when you pin
your server's host keys (vpn ssh-hosts set) and install the app's deploy
key (apps deploy-key show), while a bare ssh in a build script is not
routed; your gateway must accept inbound UDP on its endpoint port; status
becomes ready after the first successful VPN build (vpn test checks the
handshake alone; a build is the end-to-end check); builds appear from the tunnel's AppForge-side /30
address, so allow-list it or masquerade.
--api-url (default http://localhost:8787, or set APPFORGE_API_URL)
only needs passing on login/signup — after that it's saved to
~/.appforge/config.json and reused. See the root README.md's Quick
start for the full local-dev flow and docs/self-hosting.md for pointing
this at a real deployment.
login prompts interactively for a 2FA code if the account has it
enabled and neither --totp-code nor --recovery-code was passed — see
docs/security.md's "2FA (TOTP)" note.
