agentsclimarketplace

Better update

Skill aislopware/better-update/skills/better-update

Drive the better-update CLI (@better-update/cli, the `better-update` command) to ship OTA JS/asset updates, run local native iOS/Android builds, manage an end-to-end-encrypted signing-credential vault, set server-side env vars, and route releases through channels/branches with staged rollouts and instant rollback — for Expo, React Native, Kotlin Multiplatform, and native apps (any project type via custom build commands). Use this whenever you are working in a project that uses better-update (an `@better-update/cli` dependency, a `better-update …` command, `expo.extra.betterUpdate.projectId` in app.json, or a better-update manifest URL), OR when the user asks to publish / ship an OTA update, stage a rollout or roll back a release, cut a release candidate, promote an update between channels, build an IPA/APK/AAB locally, manage keystores / distribution certs / provisioning profiles / APNs push keys, sign or notarize a macOS app with a Developer ID certificate, or configure per-environment secrets — even if they never say the words "better-update". Covers every command, every flag, and the publish → branch → channel → device routing model.From its SKILL.md

Install
npx -y skills add aislopware/better-update --skill better-update

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

17.9 KB, as published. Nobody here has run it

better-update CLI

better-update is a self-hosted mobile release platform for Expo, React Native, Kotlin Multiplatform, and native apps — any project type, including ones driven by a custom build command — running on Cloudflare. It does OTA JS updates (Expo Updates protocol-compatible), local native builds (EAS Build-compatible), an end-to-end-encrypted credential vault, server-side env vars, store submission, and macOS Developer ID signing + notarization (macos sign / macos notarize) — all driven from one CLI, better-update. This skill is how you operate that CLI on a user's behalf.

The one mental model that explains everything

  publish ──► branch ◄── channel ──► device
  • You publish an update group (a JS bundle + its assets) to a branch. Branches are server-side streams of updates; they need not match git branches.
  • A channel points at exactly one branch at a time. The channel a device reads is baked in at build time and cannot change without a new native build.
  • A device on channel X fetches the latest compatible update from whatever branch X currently points at — if its installed binary's runtime version matches the update's.

This indirection is the whole point: you ship a release candidate to a branch, vet it on a staging channel, then repoint production at that branch with one command — no rebuild. Rollback is the same move in reverse.

Two facts that prevent most mistakes:

  1. Channel ↔ branch is a pointer you move (channels update <id> --branch <name>), not a recompile. There is no channels point command.
  2. A published update only reaches a device whose installed build advertises a matching runtime version. Publish to a runtime no installed build matches → zero devices get it. Verify with better-update builds compatibility-matrix.

Setup (do this before anything else)

bun add -g @better-update/cli      # or: bunx @better-update/cli <cmd>   (no install)
better-update login                # browser OAuth; --api-key for headless paste
better-update init                 # from the Expo project root: links app.json → a project

login writes ~/.better-update/auth.json (mode 0600). init reads app.json, finds/creates a project by expo.slug, and writes the project id into expo.extra.betterUpdate.projectId — every other command reads it from there. Default server is the one baked into the CLI build (its operator's instance); override with BETTER_UPDATE_URL / BETTER_UPDATE_WEB_URL or ~/.better-update/config.json. Auth can also come from BETTER_UPDATE_ROBOT (a robot account, for CI). Full detail: references/getting-started.md.

To know whether a project already uses better-update, look for @better-update/cli in package.json, expo.extra.betterUpdate.projectId in app.json, or a better-update manifest URL in expo.updates.url.

Command map

better-update
├── login / logout / whoami   Auth + identity (token at ~/.better-update/auth.json, mode 0600)
├── org                       list · switch — the session's active organization (all commands scope to it)
├── init / status / doctor    Link project · status · diagnostics
├── open / autocomplete       Open the dashboard · print a shell completion script
├── projects                  list · create · get · rename · archive · unarchive · delete
├── branches                  list · view · create · rename · delete
├── update                    publish · list · view · edit · promote · republish · rollback ·
│                             revert · rollout(set/complete/revert) · revert-rollout ·
│                             roll-back-to-embedded · embedded:upload · configure · insights · delete
├── channels                  list · view · create · update(repoint) · pause · resume · delete · insights · rollout
├── build [+ configure]       Build the app locally and (by default) upload the artifact
├── builds                    list · get · download · run · install-link · compatibility-matrix · upload · resign · delete
├── credentials               Signing vault + E2E encryption (identity/access/device/unlock/lock);
│                             account/env-vault subcommands (or web self-enroll + grant) for browser env editing;
│                             certificate/bundle-id/profile/capability list+enable (read-only ASC inventory, CI-safe)
├── env                       Project env vars: list/get/set/update/delete/history/rollback/import/export/pull/push/exec
├── environments              Org environment definitions: list/create/rename/delete
├── fingerprint               generate · compare (runtime-compatibility hashes)
├── analytics                 adoption · updates · downloads · channels · platforms
├── audit-logs                list (every mutation, with actor + timestamp)
├── apple                     login · logout · whoami · accounts (multi-account switch); builds · users (CI-safe); asc-key · sandbox (Apple ID login; `sandbox list` prefers an ASC key)
├── submit                    Submit a build to App Store Connect / Google Play
├── testflight                group · tester · review · build · feedback — full TestFlight beta lifecycle (CI-safe)
├── app-store                 version · submit/cancel/status/release/reject · rollout · review-detail · info · categories · age-rating · privacy · apps (list + create) · pricing · availability (show + set) · territories · config (pull/push) (CI-safe; apps create needs Apple ID login)
├── reviews                   list · reply — App Store customer reviews (CI-safe)
├── metadata                  media (list/sync) · screenshots (upload/clear) · previews (upload) — store media (CI-safe)
├── app-review                list · view · rejections · reply — Apple App Review / Resolution Center (Apple ID login, NOT CI-safe)
├── devices                   Register Apple device UDIDs for ad-hoc/development provisioning
└── webhooks                  update.published / build.completed subscriptions

Core workflows (the 90% cases)

Ship a JS/asset update — you do NOT run expo export first; publish does it for you:

better-update update publish --branch main --message "Fix login crash on iOS 17"

Stage it to a slice, then ramp (default-on safety net for risky changes):

better-update update publish --branch production --rollout-percentage 10 --message "Cache migration"
better-update update rollout set <updateId> 50
better-update update rollout complete <updateId>     # → 100%
better-update update rollout revert <updateId>       # → 0%, stops the spread

Cut a release candidate, vet, promote — no re-export, no second upload:

better-update update publish --branch staging --message "v1.4 candidate"
better-update update list --branch staging --limit 1          # grab its id
better-update update promote <updateId> --channel production   # or repoint the channel:
better-update channels update <prodChannelId> --branch staging

Roll back production now (devices fall back to the embedded bundle):

better-update update rollback --branch production --message "Revert e7f3 — login crash"

Build a new native binary (needed whenever native code / runtime version changes):

better-update build --platform ios --profile production
better-update builds compatibility-matrix     # confirm channels are covered before you publish OTA

Per-environment server-side env vars, injected into expo export at publish time:

better-update env set EXPO_PUBLIC_API_URL=https://api.example.com
better-update env set STRIPE_KEY=sk_live_xxx --visibility sensitive
better-update update publish --branch production --environment production

Submit a build to the stores (App Store Connect / Google Play), from the CLI:

better-update submit --platform ios --latest --what-to-test "Fixed the login crash and refreshed onboarding"
better-update build --platform android --auto-submit          # or build + submit in one step

Gotchas worth memorizing

  • No channels point. Repoint with channels update <id> --branch <name>.
  • Runtime version gates delivery. Match builds and updates; use fingerprint + builds compatibility-matrix to verify before publishing.
  • The channel is build-time. Set it in the build profile / --release-channel; it cannot be changed OTA. You move branches under channels, never recompile to change which update serves.
  • --commit-time for rollback must be canonical ISO 8601 with milliseconds and a trailing Z (2026-05-06T14:00:00.000Z). The on-device expo-updates client only parses YYYY-MM-DDTHH:mm:ss.SSSZ; numeric offsets / missing millis are rejected.
  • Env-var visibility is plaintext or sensitive — there is NO secret tier. Passing --visibility secret is rejected by the CLI. sensitive values are masked (******) on env get unless --include-sensitive, and readable in the console only by owners/admins; they are still returned by env export/env pull. Visibility gates who on the team can read — it does not harden a value against device extraction (anything in a JS bundle is extractable; use a backend).
  • env get <key> and env delete <key> both take the KEY name (not an ID). env delete with no --environment deletes the key across every environment.
  • env pull writes .env.local by default, not stdout — use --stdout to source into a shell (eval "$(better-update env pull --stdout)").
  • update rollout is per-update %; channels rollout is a branch-level traffic split. They compose — you can ramp a channel onto a new branch while updates inside it have their own %.
  • better-update DOES submit to stores from the CLI. better-update submit --platform ios|android uploads to App Store Connect (via Apple's Build Upload API when an ASC key is the upload auth — live progress bar — with xcrun altool as the app-specific-password path and automatic fallback) or Google Play; build --auto-submit chains build → submit. (It does not poll store review — only the upload/submission.)
  • --what-to-test has a length floor Apple won't document. submit rejects empty or >4000-byte text before upload, but Apple also rejects short strings ("too short" — e.g. Fix, and even Bug fixes in some reports) with no published minimum. Write a full sentence. If it trips after upload, fix it without re-uploading: testflight build whats-new --latest --whats-new "<longer text>".
  • submit is idempotent — just re-run it after a metadata failure. It checks App Store Connect for the IPA's build number before uploading (and Apple 409s the Build Upload API's reserve step for a duplicate — before any bytes upload); if the binary is already there it skips the upload and only re-applies the TestFlight config (so the "already been used" duplicate-build error can't strand you). When the upload succeeds but config fails, the submission is still recorded as metadata-incomplete (dashboard shows amber "Metadata pending"; a finished one reads a plain "Complete"); the re-run that completes config updates that same row.
  • App Store Connect operations run from the CLI, headless. testflight … (group / tester / review / build / feedback), app-store … (version / submit / cancel / status / release / reject / rollout / review-detail / info / categories / age-rating / privacy / apps / pricing / availability), apple builds/apple users, reviews …, metadata … (store media), and the credentials ASC inventory (certificate / bundle-id / profile / capability) all drive App Store Connect with a stored ASC API key — no browser, CI-safe. testflight group create is the fix for a submit ios that fails with TESTFLIGHT_GROUP_NOT_FOUND. apple builds compliance --no-uses-encryption clears a build stranded in MISSING_EXPORT_COMPLIANCE. app-store age-rating set / privacy set are authored from a JSON document (--from), not a flag matrix. metadata media sync declaratively pushes a screenshots/<locale>/<device>/*.png tree (with --dry-run / --prune). apple users needs an Admin-role key. See references/cli.md.
  • A few App Store Connect ops are cookie-only (Apple ID login + 2FA, NOT CI-safe). app-review … (Resolution Center: read App Review threads / rejection guideline codes / reply — text only), apple asc-key list + credentials revoke asc-key (the upstream ASC-key lifecycle), app-store apps create, credentials bundle-id create --app-clip, and apple sandbox create/delete (IAP testers) all hit Apple's Iris API, which a JWT key can't reach — they log in via apple login and fail with a clear error under --non-interactive / CI. (apple sandbox list is token-first: it prefers the public ASC API when an ASC key is configured via --asc-api-key-id or the submit profile, falling back to Apple ID login.) A rejected app-store submit is closed out with app-review list → app-review rejections --thread <id> → app-review reply.

Reference index — read the file that matches the task

The depth lives in references/. Pull the one you need; don't load them all.

Read thisWhen you're…
references/getting-started.mdinstalling, logging in, pointing at a self-hosted server, linking a project, or using whoami/doctor/open
references/publishing.mdpublishing updates, every update publish flag, runtime-version policies, promote/republish/edit, the audit log
references/channels-and-branches.mdnew-branch-vs-channel, repointing channels, view/insights, the cut→vet→promote pattern, channel rollouts
references/rollouts-and-rollbacks.mdstaging a % rollout, the three "revert" verbs, whole-branch rollback, the symptom→action decision table
references/native-builds.mdrunning build (+configure), managing builds (download/run/resign), fingerprint, store submit
references/credentials.mdthe signing + E2E credential vault: certs/profiles/keystores/APNs, identity/access/device, unlock/lock, project-scoped robot accounts, credential→project bindings; account/env-vault for browser env-vault access + its troubleshooting
references/environments.mdenv vars (set/get/push/pull/export, visibility), history/rollback, the org environments command
references/access-control.mdroles (org owner/admin/member + project maintainer/developer/reporter), project-scoped robots, credential→project bindings, protected environments & credentials, Apple device registration, webhooks
references/cli.mdyou need the exhaustive command/flag table for ANY command (every group), or exit codes for CI branching

When a command's exact flags or exit-code semantics matter (especially in CI scripts), confirm against references/cli.md — it mirrors apps/cli/src/commands and is the source of truth.

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.