---
name: pollyglot/gplay-cli-usage
source: https://app.decimal.ai/s/pollyglot-gplay-cli-usage@1/SKILL.md
source_sha256: e9de134de119
---

# gplay CLI conventions (foundation)

This is the **foundation skill**: the conventions that hold for *every* gplay
command, factored out once so the workflow skills (see the **Map of skills**
table at the end) can reference it
instead of repeating them. The normative source of truth is
[`docs/DESIGN.md`](https://github.com/PollyGlot/google-play-cli/blob/main/docs/DESIGN.md)
in the CLI repo; this skill summarizes it for agents and does **not** freeze
per-command flag lists.

## `--help` is the source of truth

gplay's commands are self-describing. Confirm the current verbs, flags, and
defaults from the binary, never from memory:

```bash
gplay --help                       # the whole command tree
gplay <group> --help               # a namespace (releases, tracks, team, …)
gplay <group> <command> --help     # one command, with its real flags
gplay exit-codes                   # the semantic exit-code table
```

If this skill and `--help` ever disagree, trust `--help`.

## `gplay schema` — the offline API map

Where `--help` documents gplay's own surface, `gplay schema` introspects the
underlying **Android Publisher API** offline (no credential, no HTTP call) from
an index compiled into the binary — does a method exist, what does it send and
return, what fields and enums does a type carry:

```bash
gplay schema --list                 # compact catalog: id · http · path
gplay schema tracks                 # match across method id, REST path, type name
gplay schema Track                  # expand a schema's fields, types, enums
gplay schema edits.tracks.update    # a method's request/response, one hop deep
gplay schema --method PATCH         # filter the method surface by HTTP verb
```

`[experimental]` (gplay v0.5.0) — confirm the surface with `gplay schema --help`.

## Which credential / account (resolution order)

Every authenticated command resolves the service-account credential in this
order, highest priority first:

1. `--service-account <path-or-inline-json>` — a JSON file path **or** inline
   JSON content. Overrides everything below.
2. `--account <name>` — a specific stored Account. Overrides env + the active
   Account.
3. `GPLAY_SERVICE_ACCOUNT` env var — a path or inline JSON.
4. The **active** stored Account (set up via `gplay auth login`).

Setting this up from scratch is the `gplay-setup` skill. Auth problems exit
`10` (bad/again credential) or `11` (the service account is not invited on the
app/account).

## Which app (`--package` + project pinning)

Most commands need a target package. Resolution (ADR-0004):

1. `--package com.example.app` on the command, else
2. the package pinned in `.gplay/config.json` for the current repo.

Pin once with `gplay init` (or `gplay apps init`) so day-to-day commands need
no `--package`. Managing the registry of packages is the `gplay-apps` skill.

## Output: table on a TTY, JSON in a pipe

`--output` takes `table`, `json`, or `markdown`. The default is **auto**
(ADR-0005): a human table on a terminal, JSON when piped or in CI. For
machine consumption, ask for `--output json` explicitly — read commands pass
the API payload through (ADR-0003), and write commands return the request/diff
body, so a CI gate is usually one `jq` line. The pass-through promise is about
*not reshaping the API*, not that every `--output json` is an API echo: the
**offline reference** commands that wrap no API call — `team permissions` and
`schema` — synthesize their own (gplay-owned, documented) JSON instead.

**stdout is data, stderr is logs.** Parse stdout; warnings, progress, and
`-v/--verbose` flow steps go to stderr and never pollute the JSON. (Example:
`reviews list` prints its "last 7 days only" warning to stderr.)

## Exit codes — branch on the number, not the text

`gplay exit-codes` prints the full table. The semantic codes:

| Code | Meaning | Retry-safe |
|---|---|---|
| 0 | Success | — |
| 1 | Generic error (fallback) | no |
| 2 | CLI misuse (unknown flag/command, bad value, missing arg) | no |
| 3 | A named safety flag is missing (`--confirm` / `--grant-admin`) — re-run with it | yes, with the flag |
| 4 | Denied by environment policy (`GPLAY_READONLY`) — a mutating command was refused | no — change the environment |
| 10 | Authentication failure | no |
| 11 | Authorization (403 — SA not invited) | no |
| 20 | Client-side validation (bad AAB, unknown locale, …) | no |
| 30 | API 4xx (not found, conflict, gone, …) | no |
| 40 | API 5xx (upstream unhealthy) | **yes** |
| 50 | Network (timeout, DNS, refused) | **yes** |
| 60 | State conflict (open edit, rate-limited, ambiguous target) | sometimes |

Agents should treat `3` as "append the named flag and re-run", `4` as "the
environment forbids this write — do not retry, change the deployment", `40`/`50`
as "back off and retry", and `2`/`10`/`11`/`20`/`30` as "fix the input, do not
retry blindly".

## Safety: `--dry-run` everywhere, `--confirm` for live writes

- **`--dry-run`** is available on write commands: it validates inputs and
  prints the payload/diff it *would* send, with no HTTP call (and usually no
  auth needed). Reach for it before any production-affecting write.
- **`--confirm`** gates the writes that reach real users or the live store —
  production releases, `metadata apply`, `compliance datasafety set` — and
  destructive local writes like `auth logout`. Omitting it fails with exit `3`
  and names the flag (in the JSON error envelope: `requires: ["confirm"]`).
  `CI=true` never auto-confirms.
- **`--grant-admin`** is the stronger gate for conferring admin in
  `gplay-team`.
- **`GPLAY_READONLY=1`** (truthy = enforced) is the environment-level guard for
  agent deployments that must only read. Because the safety flags above are
  *advisory* — an agent holding the credential can pass them itself — set this in
  the environment and the kernel refuses **every mutating command** before
  credential resolution and before any network call, regardless of flags, while
  read commands and `--dry-run` previews keep working. Its refusal exits **`4`**
  (denied by environment policy) — which, unlike `3`, is **not** resolvable by
  adding a flag; the only fix is to change the environment.

When a write refuses for a missing flag, the message names it — that refusal is
*agent-resolvable* (ADR-0017): re-run with the flag it asked for. The
`GPLAY_READONLY` refusal (exit `4`) is the deliberate exception: it is **not**
agent-resolvable.

## The Edit lifecycle — implicit by default, explicit when you batch

Google Play mutations run inside a transactional **Edit**
(`edits.insert → change → edits.commit`). gplay offers two ways to drive it.

**Implicit (the default).** Each write command performs the whole three-step
dance on its own: it opens its own Edit, makes the change, and commits,
discarding the Edit automatically on failure. You do not manage Edit IDs by
hand. `--keep-edit-on-failure` skips that auto-discard for debugging.

**Explicit (`gplay edits …`, when several changes must land together).** To
batch multiple writes into **one atomic commit**, open an Edit yourself:

```bash
gplay edits begin            # opens an Edit, pins its id to .gplay/edit-<package>.json
gplay metadata apply …       # these writes detect the pin and reuse the open Edit
gplay releases upload … --release-notes-dir ./notes
gplay edits status           # local read (no auth/network): shows the open edit, or none
gplay edits commit           # publish everything at once, and clear the pin
# gplay edits discard        # …or abandon the whole batch, clearing the pin
```

While the pin exists, subsequent write commands reuse the open Edit instead of
opening their own, so the changes commit together or not at all. In explicit
mode there is **no** auto-commit and **no** auto-discard — the lifecycle is
yours until you `commit` or `discard`. Notes: a project (`gplay init`) is
required since the pin lives under `.gplay/`; opening a second Edit while one is
pinned is refused (**exit 60**); if `commit` fails (e.g. a validation error) the
Edit stays open and the pin remains — fix and re-run, or `discard`.

(A few surfaces sit *outside* the Edit model on purpose — `compliance
datasafety`, `device-tiers`, `recovery`, `orders`, `vitals`, `games`,
`subscriptions`, `iap` are direct writes/reads with no `editId`, so
`edits begin` does not batch them; their skills call that out.)

## Map of skills

| Surface | Skill |
|---|---|
| Auth onboarding | `gplay-setup` |
| App registry + details | `gplay-apps` |
| Releases (upload/promote/rollout) | `gplay-release-flow` |
| Tracks + testers | `gplay-tracks` |
| Reviews | `gplay-reviews` |
| Store listings + images | `gplay-metadata-sync` |
| Data Safety | `gplay-compliance` |
| Team users + grants | `gplay-team` |
| Managed Play private apps | `gplay-customapps` |
| Post-launch vitals (crashes/ANRs) | `gplay-vitals` |
| Orders (view/refund) | `gplay-orders` |
| Play Games config (achievements/leaderboards) | `gplay-games` |
| App recovery (bad-release remediation) | `gplay-recovery` |
| Device tier configs | `gplay-device-tiers` |
| Subscriptions + one-time products (catalog) | `gplay-monetization` |