# hopple: the command line

The command-line tool for game developers:

- `hopple create` starts a new game with the recommended setup (optional).
- `hopple dev` runs a built game in the **dev shell**, a host made for
  testing, which runs it with the same code as the real clients.
- `hopple smoke` plays a built game headless, at every screen shape and
  device tier, and says what a player would trip over.
- `hopple check` says whether a game can be published. `hopple upload`
  uploads it to a Hopple server, for a link to try it, and `hopple publish`
  uploads it and asks for it to be published.
- `hopple versions`, `withdraw`, `untag`, `unpublish`, `rollback` and
  `listing` say and change where a game's versions stand.
- `hopple list`, `login`, `logout` and `whoami` manage what you've uploaded
  and who you upload as.
- `hopple collaborators` chooses who else may publish a game.

It is a dev dependency of the game (`npm install --save-dev hopple`, which
`hopple create` sets up), run with `npx hopple`. It is one compiled file with
no dependencies, and needs Node 22.18 or later.

`hopple` is not a bundler. It takes a game that is already built, by
whatever tool the game uses.

## The project's config

The commands about a game (`dev`, `check`, `upload`, `publish`, `versions`,
`withdraw`, `untag`, `unpublish`, `rollback`, `listing`, `collaborators`) work on a **project**: a directory with a hopple
config in it. They find it in the current directory, or the nearest one above
it (like `tsconfig.json`). The config says everything they need to know:

```ts
// hopple.config.ts
import type { HoppleConfig } from 'hopple';

export default {
  build: 'npm run build',
  watch: 'npm run watch',
  page: 'dist/index.html',
  manifest: {
    id: 'my-game',
    version: '0.1.0',
    title: 'My Game',
    orientation: 'portrait',
    audio: null,
  },
} satisfies HoppleConfig;
```

- **`build`** (optional) is the command that builds the game. It runs first, in
  the config's directory, and a failed build stops the command. So
  `npx hopple publish` can't upload a stale build. `--no-build` skips it.
- **`watch`** (optional) is the command that rebuilds the game on every change
  and keeps running. `hopple dev` runs it, after the build, until it stops.
- **`page`** is the built HTML file, relative to the config: the `index.html`
  of the build's output directory, which is played in the dev shell and
  published whole. A page beside the config has no output directory, and is
  published alone.
- **`manifest`** is what the game is: its id, title, description, and what it
  declares. See [the manifest's reference](manifest.md). Its `api`
  may be left out: every command fills it from the installed `@hopple/sdk`
  (`1.6.2` is `"1.6"`, a pre-release is `"next"`), and refuses a manifest
  whose `api` differs from it. The manifest is checked with the validator the
  installed `@hopple/sdk` carries, which is of that API, so a CLI newer or
  older than the SDK judges it the same; with no SDK installed, with the
  CLI's own. Likewise **`previews.network`**: a project depending on
  `@hopple/sdk-preview-network` (its `package.json` lists it) declares the network
  preview at the installed revision (`0.1.4` is `1`) without saying so, and
  a manifest naming another revision than the one installed, or naming one
  with none installed, is refused. A `@hopple/sdk-preview-network` merely found above the
  project, as another game in a workspace installs it, is checked against
  but declares nothing.

Only the page and the manifest are uploaded; `build` and `page` never leave
your machine.

The config can be `hopple.config.ts`, `hopple.config.js` (both default-export
it) or `hopple.config.json` (which is it). Its type, `HoppleConfig`, is this
package's: `import type { HoppleConfig } from 'hopple'`, whose `manifest` is
the type the installed `@hopple/sdk` gives it. A TypeScript config needs no build
of its own: Node runs it. Any other key is an error, so a misspelt `build`
can't be silently skipped, and two configs in one directory are an error too.

A JSON config can start with
`"$schema": "./node_modules/hopple/config.schema.json"` for completion
and errors in an editor, the manifest's included.

## Commands

```
hopple create  <dir>
hopple dev     [project] [--port <n>] [--server <url>] [--no-audio] [--no-build]
hopple check   [project] [--server <url>] [--no-build]
hopple upload  [project] [--anonymous] [--server <url>] [--no-build]
hopple smoke   [project] [--shapes <list>] [--script <file>] [--cover] [--sound | --no-audio] [--out <dir>] [--seed <n>] [--server <url>] [--no-build]
hopple publish [project] [@<build>] [--beta] [--anonymous] [--no-live-check] [--server <url>] [--no-build]
hopple versions [project] [--server <url>]
hopple withdraw [project] [--beta] [--server <url>]
hopple untag   beta [project] [--server <url>]
hopple unpublish [project] [--server <url>]
hopple rollback [project] [--server <url>]
hopple listing [project] [listed | unlisted] [--server <url>]
hopple list    [--server <url>]
hopple share   <link | id> [--data] [--json] [--server <url>]
hopple collaborators [add <handle> | remove <handle>] [project] [--server <url>]
hopple login   [--server <url>]
hopple logout  [--server <url>]
hopple whoami  [--server <url>]
hopple --version
```

`[project]` is a project's directory (or a directory inside it), or its config
file. Left out, it is the current directory.

The server is taken from `--server`, then `$HOPPLE_SERVER`, then
`https://hopple.app`. It is named by its site; the CLI finds the API under
`/api` on it.

### Out of date

`create`, `dev`, `check`, `upload` and `publish` end by saying when what the
agent works from is out of date:

- **The Hopple skill.** Each copy of the `hopple-game` skill in an agent's
  skills (`~/.agents/skills`, `~/.claude/skills`, `$CLAUDE_CONFIG_DIR/skills`,
  and `.agents/skills` and `.claude/skills` in the project and the
  directories above it) whose `Skill version:` is older than the one
  `https://docs.hopple.app/skill.md` serves is named, with the command that
  updates it: `npx skills update hopple-game -g -y` for one the skills
  installer put there for the user (`npx skills add https://docs.hopple.app
  -g`), `-p -y` in the project for one it put in a project, and for a copy
  made by hand, to install or read the current one.
- **This CLI.** `create`, `dev` and `check` ask the server whether it would
  refuse this CLI, which it does below the version it supports, and say what
  to update; `upload` and `publish` are told by the server itself.

It never fails or slows a command: it runs beside it, each request gives up
after 1.5 seconds, and what it learns is kept in `updates.json` beside the
sessions file (the site's skill version for 6 hours, a server's answer for an
hour). With no skill installed it asks the site nothing, and offline it says
nothing. `HOPPLE_NO_UPDATE_CHECK=1` turns it off; `$HOPPLE_DOCS` points it at
another documentation site.

### `hopple create`

```bash
npx hopple create my-game     # or --next, for @hopple/sdk@next
cd my-game && npm install
npx hopple dev        # build, then play it in the dev shell; it rebuilds on every change
npx hopple upload     # build, check, then upload: a link to try it
npx hopple publish    # build, check, then publish
```

Writes a new project into `<dir>`, which must be new or empty. The directory's
name becomes the game's id (lowercase letters, digits and hyphens). It is the
recommended setup, not a requirement:

- **`@hopple/sdk`** is the one npm's `latest` names now (`^1.6.0`), or with
  `--next` the one `next` names, exactly (`2.0.0-next.3`): `next` is the
  API's next release, which breaks whenever it needs to, so the game stays on
  it until it is moved on by hand. Only accounts an admin allows may publish
  on `next`. `create` asks the registry `npm_config_registry` names, npm's own
  otherwise.

- **Vite** builds it into `dist/`, with `base: './'` so every address in it
  is relative, and `dist/index.html` as its page.
- **`index.html` loads the runtime as its first script**,
  `<script src="_rt/hopple.js"></script>`. The runtime is the SDK, served
  beside the game by whatever runs it; the `@hopple/sdk` package the game
  imports is its types and a stub that forwards to it. So a fix to the SDK
  reaches the game without a rebuild. Keep the tag first.
- **`hopple.config.ts`** builds with `npm run build` (typecheck with `tsc`,
  then bundle), rebuilds with `npm run watch` (`vite build --watch`), names
  `dist/index.html` as the page, and holds the manifest.
- **`vite.config.ts`** turns `HOPPLE_DEV` into `import.meta.env.HOPPLE_DEV`,
  and marks the page of a build for `hopple dev` (see
  [below](#developer-only-builds)).
  Being TypeScript, a mistake in the manifest is a type error.
- **There is no `publish` script**: `npm publish` would run it, then upload the
  project to the npm registry. The project is also `"private": true`.
- `src/game.ts` is a small working game (a ball you keep in the air) that uses
  the SDK the way every game should: `init`, the loop, layout, input and pause.

A game built any other way (another bundler, or a hand-written HTML file) is
played and published with the same commands, once it has a config.

### `hopple dev`

```bash
npx hopple dev                          # this project's game, built: http://localhost:5173
npx hopple dev path/to/project --port 8080
```

The dev server serves:

- `/`: the dev shell.
- `/game/`: the config's `page`, and `/game/…` the other files in its
  directory, with caching disabled. Nothing outside that directory is
  served.
- `/game/_rt/hopple.js`: the runtime, beside the game as it is beside every
  published version. It is the release of the game's label (the track, the
  major its manifest's `api` names or `next`, then each preview it declares
  at its revision, `next+network.1`) the server's players get, fetched from
  the server (`--server`, as for the other commands) when `hopple dev`
  starts, or this CLI's own build of that label when the server can't be
  reached or answers with another; the terminal says which. A server's
  release can be newer than this CLI's shell understands, and the shell then
  says to update `hopple`.

A valid manifest is shown in the shell. An invalid or missing one is reported
in the terminal and on the page, naming the bad fields, and the game still
runs. The config is read again every time the shell page loads, so **an edited
manifest shows up on reload**, with no restart. So does a rebuilt game.

It plays a build that loads files from beside its page (a separate script, a
stylesheet, images), but says in the terminal that it can't be published like
that.

**On a phone.** The dev server listens on this machine alone. *Open on a
phone*, in the shell's Device panel, opens it to this machine's network too:
the shell on one port at this machine's address and the game on another, so
the game keeps an origin of its own, and a QR code of the shell's address
there, carrying the panel's settings (all but the shape, the host chrome and
the notch, which the phone has of its own). Scan it on a phone on the same
Wi-Fi. Everything the dev server serves, the relay included, is open to the
network from then until *stop network access*, or until the server stops;
only the shell on this machine can turn it on or off (`/network`, from its
own origin, with a header no form can send). The terminal says when it is on.

**Without sound.** `--no-audio`, or `HOPPLE_NO_AUDIO=1` in the environment,
starts the shell muted, the way a player's mute does: the game still runs its
sound and obeys `audio.allowed`, and is told it is muted, so its sound is
exercised and makes none. The pill over the game shows it muted, and a person
can unmute it. The developer's own mute setting, kept for the next page, is
left as it was. Set `HOPPLE_NO_AUDIO=1` once for an agent or a CI machine:
`hopple smoke` and the check `hopple publish` makes are silent anyway, and
`--sound` does not turn `hopple smoke`'s sound on while it is set.

#### Developer-only builds

`hopple dev` runs the config's `build`, then its `watch`, with `HOPPLE_DEV=1`
in their environment. Every other command runs `build` with `HOPPLE_DEV` taken
out, even when the terminal has it. So a game can build in what only its
developers should have, such as cheats or a level select, and leave it out of
everything else: in a project `hopple create` made,

```ts
const dev = import.meta.env.HOPPLE_DEV ? await import('./dev.ts') : null;
```

is `null` in a published build, which doesn't contain `dev.ts`. A runtime test
(the address, the host) would publish what it guards.

A page built this way carries `<meta name="hopple-dev">`, which `check`,
`upload` and `publish` refuse: a page built by `hopple dev` and uploaded with
`--no-build`, or rebuilt by its `watch` while they run, is never published.

`watch` runs in a process group of its own, stopped with `hopple dev`. When it
stops by itself, `hopple dev` says so and keeps serving the last build.

**With no project to be found**, the shell starts empty and waits for a URL
like `/?server=<server url>&game=<owner>/<name>@<build>` (or `@beta`). The dev shell link
`hopple-admin review` prints for each waiting version is exactly that, and so
can a link you write for any uploaded game (yours or someone else's). The shell downloads the game and its
manifest from that server's feed and runs it exactly as it would a local build.

#### Playing with others

`hopple dev` runs Hopple's own relay, at `/net` on its port, so a game
declaring the network preview gets parties and matches on the same rules as
on the site, with no server and no account. The sheet has the site's
**Party** tab.

A networked game opens on **two players tiled in one window**, each a whole
dev shell in a frame of its own, already in one party:

- **Tiling** works as in an IDE. Drag a player's title bar onto the edge of
  another to split that pane there, or onto its middle to swap the two; drag
  a divider to resize; double-click a title bar (or ⤢) to play that player
  full size. The bar above has side by side, stacked and grid, and **add a
  player**, up to 8. The layout is kept for the game.
- **The focused player** (the one clicked last, outlined) is the only one
  heard and the one the keyboard reaches. A real pad goes to its player:
  the first pad to Player 1, the second to Player 2.
- **Each player keeps their own** id, save and connection, as a separate
  device would. A pane's × takes that player out of the party and the match.
- **`/?player=N`** opens one player alone, at full size. A party's or a
  match's link opens one shell as a guest, which is how a phone on the same
  network joins.

The Dev tab's **Network** part, for each player:

- **Network**: `none`, `lan`, `wired`, `wifi`, `mobile`, `weak` or `far`,
  the delays, jitter, loss, stalls and bandwidth of `@hopple/netlab`'s
  profiles, put between the player and the relay.
- **Faults**: drop the connection (it comes back on its own), stall it for
  3 s, take it offline for 35 s (past the 30 s the relay holds for a player
  coming back), switch away for 5 s, or reload.
- **Match**: its code, seats, lock, kept messages, and its people with who
  hosts it and who is away or gone.
- **Traffic**: what the game sent and heard, against the relay's limits (a
  message at most 4096 bytes, 60 a second in bursts of 120), with a warning
  when it comes near them and every refusal counted, and the latest messages
  each way.

The shell a server hosts as plain files, with no `hopple dev` behind it, has
no relay and offers no matches.

### `hopple check`

```bash
npx hopple check                  # this project's game, built
npx hopple check --no-build       # as it was last built
```

Everything `publish` checks before it uploads, without uploading:

1. The manifest validates.
2. The **page** exists and is its output directory's `index.html`, and loads
   nothing the directory doesn't hold: every `<script src>`, stylesheet
   `<link>`, `<img>` and CSS `url()` is a `data:` URL or a file in the
   directory, referred to relatively. That directory is what is published, at
   the root of the version's own address, and a game reaches nothing else.
   (Files the game's code fetches can't be seen this way; they fail the same
   way.) A page beside its config is published alone, so it must contain the
   whole game. The runtime, `_rt/`, is not the game's: it is served beside
   every game, and the directory may hold nothing there.
3. The page loads the runtime as its first script,
   `<script src="_rt/hopple.js"></script>`, as it is, and no page or
   script carries a copy of the SDK.
4. Nothing is addressed from the root (`/assets/x.png` names the root of
   whatever serves the game, not the game), no page has a `<base>`, and every
   page is UTF-8.
5. No script uses `localStorage`, `sessionStorage`, IndexedDB, Cache
   Storage or `document.cookie`: a game's origin keeps nothing past a reload,
   and the runtime makes them throw. Use `save` instead.
6. The runtime the server serves the game's label is not older than the
   installed `@hopple/sdk`: a game may use anything its SDK describes, so on
   an older runtime it can fail as it starts. On `next` every pre-release
   counts; on a major, the minor. Asked of the server (`--server`, else
   `$HOPPLE_SERVER`, else hopple.app), and skipped when it can't be reached.
   `hopple dev` warns of the same.

The server checks the same, in the same words. The CLI also refuses a page
built for `hopple dev` (see [Developer-only builds](#developer-only-builds)).

It exits with 1 and says what is wrong, or prints the game's id, version and
size. Over 2 MB compressed it warns that a player on a phone waits for the
game, and over 10 MB `publish` refuses it.

### `hopple smoke`

```bash
npx hopple smoke                              # every shape the game is played at
npx hopple smoke --shapes desktop,low-tier    # some of them
npx hopple smoke --script smoke.json          # with scripted input
npx hopple smoke --cover                      # and a cover
```

Plays the built game headless, in the real runtime and host, in the Chrome
or Chromium this machine has (`$HOPPLE_CHROME` names one). The runtime is the
release players get, as `hopple dev` serves it, and drives the game's clock: time
moves only when the run steps it, a frame at a time, `Math.random` is seeded
(`--seed`, 1 unless given), and every press lands on a frame fixed in advance,
so two runs of one build do the same.

**Shapes.** A phone held each way the manifest's `orientation` allows
(`phone-portrait`, 390 × 844, and `phone-landscape`), a `desktop` (1280 ×
800), and `low-tier`, a phone the way the game is held at the low device tier
with its CPU slowed four times; `tablet` (1024 × 768) when named. Phones are
touch, with the SDK's on-screen controls; the desktop has a keyboard and
mouse. Each is laid out as the site lays it out: the rail and a phone's notch
are reserved out of the frame, or told to a game that draws under them.

**What a run does.** It waits up to ten seconds of the game's time for it to
say it is ready, looks at its title, taps or clicks the middle and presses
Enter and Space, then for eight seconds presses each declared action's first
key in turn and, on a touch screen, taps around it. `--script <file>` replaces
all of that after the boot with a JSON list of steps: `{"wait": frames}`,
`{"press": "<action or key code>", "frames": n}`, `{"tap": [x, y], "frames":
n}`, `{"drag": [[x, y], [x, y]], "frames": n}` and `{"shot": "<name>"}`, x and
y being fractions of the frame from 0 to 1.

**What fails it**, each said with the shape it happened at:

- the game throws, or never says it is ready;
- in what the page draws as HTML, at each shot: text under 10 CSS pixels, cut
  off by its box, or past the screen's edge where nothing scrolls it into
  view; text the colour of what is behind it; a control off the screen,
  covered by something a tap lands on instead (the SDK's own on-screen
  controls included), or under the host's rail or the notch;
- on a desktop, the game drawing in a phone-shaped box less than 60% of the
  screen wide;
- at the low tier, half the frames taking over 33 ms (under 30 frames a
  second): its own work, and the drawing it left the canvas to finish.

It warns of text under 12 pixels or of low contrast, a picture that never
changes through the eight seconds of input, a game paused at the end, and
analytics events the manifest declares that no run sent. Text a canvas draws
can't be read, so a person looks at the pictures.

**What it writes**, into `.hopple/smoke/` beside the config (`--out` for
elsewhere): `sheet.png`, every shape's key moments side by side, for a person
to look over in one go; each moment's picture; and `report.json`, with the
problems and warnings and, for each run, what the game sent as analytics,
saved, scored and unlocked, and how long its frames took, for a test to check.
`--cover` also plays it at 1200 × 630 and writes the last picture, or the one
a script's `{"shot": "cover"}` takes, where the manifest's `cover` names, or as
`cover.png` beside the config.

It makes no sound: Chrome is muted and the host tells the game it is muted.
`--sound` turns it on, unless `$HOPPLE_NO_AUDIO=1`. It exits with 1 when
anything failed.

### `hopple upload` and `hopple publish`

```bash
npx hopple upload                 # this project's game, built: uploaded, not published
npx hopple publish @3             # publish build 3, the one the upload printed
npx hopple publish                # build, upload, and publish in one go
npx hopple publish --beta         # the same, as the beta
npx hopple publish path/to/project
```

`upload`:

1. Makes every check `hopple check` makes, so mistakes are caught before
   anything is uploaded.
2. Says which account it uploads as, or that it uploads anonymously, before
   it sends anything. An anonymous session is refused a project an account
   published to that server from here before, or whose manifest names an
   `owner`: its upload would be a separate anonymous copy of the game, and
   the account's would keep its old build. It says to sign in as that
   account; `--anonymous` uploads the copy anyway. Who published a project is
   kept beside its config, in `.hopple/published.json`, by server, after
   every upload from an account.
3. Uploads the manifest and the page as `<owner>/<manifest id>`, naming the
   installed `@hopple/sdk`'s version: the server refuses a version built
   against an SDK newer than the runtime its label has out, until that
   runtime is released. The owner is
   the manifest's `owner`, or you when it names none; only the owner and the
   game's collaborators may upload it. The game's title and description are
   the manifest's `title` and `description`: to change them, change the
   manifest.
4. Prints the version, as the manifest's `version` and the build the server
   numbered it (`owner/name 1.5.2 (build 3)`), a link that opens that exact
   version, and how to publish it (`hopple publish @3`).

The version is not published: nobody browsing the site finds it, and only
people given the link can play it. Until an admin approves it, it plays
without scores, saves or achievements. Files and a manifest the game already
has are that version again, not a new one: uploading files already uploaded
prints the build they are. So are files the game already has with only the
manifest's `version` raised: it prints that build, under the `version` it was
uploaded with, and says no new version was made.

`publish` uploads the same way, then asks for **`latest`** to point at the
version: `latest` is what the game's address opens, and what the feed shows
while the game is listed. With `--beta` it asks for **`beta`**: a newer
version that players who opted in get, while it is newer than `latest`, with
its own boards and achievements. A beta no newer than `latest` is refused, and
publishing `latest` at the beta or past it ends the beta. With
`@<build>` it builds and uploads nothing, and asks for that build, already
uploaded. A `publish` after an `upload` of the same files asks for the
uploaded version, since the upload is that version again; as does a `publish`
of the same files with only `version` raised.

A published version names one build: a version whose manifest `version` is
already a published build's is refused, and its `upload` warns about it. Raise
`version` and upload again.

- **Once a tag moves to the version**, `publish` loads it once, headless,
  from its own address with the runtime that address serves, as `hopple
  smoke` does, and says whether it starts and throws nothing. It exits with 1
  when it does not, so whoever published it hears about it, and roll it back
  with `hopple rollback`. With no Chrome here it says nothing; `--no-live-check`
  skips it.
- **A trusted user's or an admin's request** moves the tag at once, whoever
  owns the game, and so does a request for a version already approved: that
  is a rollback (`hopple publish @2`).
- **Anybody else's waits** for an admin to approve the version, while the tag
  stays where it was. Approving it moves the tag. A game has one request
  waiting per tag: a new one replaces it, and the version it named stays,
  uploaded. You can have at most 5 waiting, across games.

It prints whether the tag moved or the request waits for review, and a link
to the version. `hopple withdraw` takes back a request still waiting.

Publishing needs an account. Without `hopple login`, `publish` uploads as
`upload` does, asking for nothing, and says to sign in, then `hopple publish
@<build>`; `publish @<build>` uploads nothing, says to sign in and fails.
`upload` uploads, says it was anonymous, and says to sign in before
publishing. Sign in before or after: the games uploaded anonymously from this
machine move into your account.

### `hopple versions`, `withdraw`, `untag`, `unpublish`, `rollback`, `listing`

```bash
npx hopple versions               # every version: its review, its tags, what waits
npx hopple withdraw               # take back the request waiting for latest
npx hopple withdraw --beta        # or for beta
npx hopple untag beta             # end the beta
npx hopple unpublish              # take the game down: no latest, no beta, nothing waiting
npx hopple rollback               # latest back to the approved version before it
npx hopple listing unlisted       # out of the feed and search; listed puts it back
```

They work on the game `publish` would upload to, for its owner, its
collaborators and admins.

- `versions` lists every version, newest first: whether it is approved,
  turned down (with the admin's note) or not reviewed, the tags pointing at
  it, the tag it waits for, who uploaded it and when, and its link. It says
  whether the game is listed.
- `withdraw` closes the request waiting for `latest`, or with `--beta` for
  `beta`. The version stays, and its link still opens it.
- `untag beta` ends the beta: its players get `latest`.
- `unpublish` takes the game down: it removes `latest` and `beta` and
  withdraws what waits for review, so nothing brings it back until somebody
  publishes it again. Its versions stay, and their links still open them.
- `rollback` points `latest` at the newest approved version before the one it
  points at; run again, it goes further back. After an `unpublish` it points
  `latest` back at the version it had. With nothing to go back to, it fails.
- `listing` says whether the game is listed: in the feed and search while it
  has a `latest`. `listed` or `unlisted` sets it, which only the owner may do.
  An unlisted game keeps its tags, its scores and its links.

### `hopple login`, `logout`, `whoami`, `list`

- The first command that talks to a server starts an **anonymous session**,
  named `anon-` and three words, so you can upload without an account.
- `hopple login` signs that same session in. It prints a link and a short
  code, and waits: open the link in a browser where you are signed in to the
  site (or sign in there, any way the site offers), check that the page shows
  the same code, and approve. Nothing is typed in the terminal, and the link
  can be opened on another machine. It is good for ten minutes.
  Anything you uploaded anonymously from this machine moves into your account.
  A sign-in lasts 90 days from when it was last used. Once one ends (expired,
  or gone from the server), commands stop with an error until you run
  `hopple login` again; they do not quietly carry on anonymously.
- `hopple whoami` shows who you are on that server and how you signed in.
- `hopple logout` ends the session and forgets it.
- `hopple list` shows each game you own or collaborate on, whether it is
  listed, the versions its tags point at, those waiting for review, its newest
  version, and a link to each.

Sessions are stored per server in `~/.config/hopple/sessions.json` (or under
`$XDG_CONFIG_HOME`, or `$HOPPLE_CONFIG_DIR` if set), readable only by you.
The file contains your session tokens, so treat it like a password.

Every request names the CLI and its version (`User-Agent: hopple-cli/<version>`).
A server refuses a CLI older than it accepts, saying to update it. It also
uses the version to count games published and terminals signed in from
the CLI, under your Hopple player in our usage analytics. Nothing else is
collected, and nothing is collected on your machine. Set `DO_NOT_TRACK=1` to
leave your publishes and sign-ins out of those counts.

### `hopple collaborators`

```bash
npx hopple collaborators              # who else may publish this project's game
npx hopple collaborators add ada      # let ada publish it too
npx hopple collaborators remove ada
```

A collaborator may upload new versions of the game, ask for its tags, withdraw
its requests and end its beta. Nothing else: listing it, deleting it and
choosing who publishes it stay with its owner. The game is the one
`publish` would upload to, `<owner>/<manifest id>`. Only accounts can be added,
and each is told in their inbox.

To publish a game you collaborate on, set the manifest's `owner` to the
owner's handle. Without it, `publish` makes a game of your own instead. Your
requests move its tags or wait for review by your own trust, not the owner's.

### `hopple share`

```bash
npx hopple share 'https://hopple.app/ada/pocket?share=…'   # the record behind a link
npx hopple share <link> --data > state.txt                 # the game's data alone, byte for byte
npx hopple share <link> --json
```

The record behind a link a player shared: the game's `data` and `meta`, when
it was made and on which build of the game, where it was made and how it
left the page, and how many people opened it. It takes the link as the player
sent it, or its id. To get debug data from a player, have the game put it in a
link (`hopple.share.offer({ data, meta })`) and ask them to send you the link.
Data a game handed over as bytes (a `Uint8Array`) is written by `--data` as
those bytes, and shown in base64 otherwise.

For the game's owner, collaborators and admins. Reading a record does not
count as opening the link. Links made in the dev shell are kept in its browser,
not on a server, and are read in its log.

## The dev shell

The dev shell runs the game the way real hosts do (same sandbox, same bridge),
but surrounds it with controls for testing. Its panel (opened from the rail's
handle or the corner button) has:

- **Device**: the frame's shape (portrait phone, landscape phone, or the whole
  browser window), where the host's buttons go, and a fake notch.
- **Lifecycle**: switch the game between `active`, `passive` and `hidden`; the
  log shows the orders each one sends (sound, loop, resources). Ping it.
- **Info**: device tier, locale, whether the player is returning, and "be a new
  player". Each of these **restarts the game**, because info is only sent once,
  at the start.
- **Manifest**: what the game declares.
- **The log**: every analytics event, every refusal, every save read, write and
  clear (with sizes), every share link, and every error the game threw, with
  its stack. A new error is also said over the game in a notice that goes
  away on its own, and is printed in the terminal `hopple dev` runs in: once
  with its stack, then again only when it has been thrown 10, 100, 1000…
  times, and whole again after a restart.

Switches that make the host misbehave on purpose, so you can see your game
cope:

- Reserve the host's buttons even though the game said `draw-under`.
- A host with no save store, a full one, or one that takes 2 seconds to reply.
- A host with no scores, no achievements, no panel, no input settings, no
  audio settings, or that never asks the game to pause. These restart the game,
  since what a host offers is settled at the start.
- Analytics rate limits like a real host's: 10 events a second, 15 in 15
  seconds, 30 a minute.

What each feature does in the shell:

| Feature | In the dev shell |
|---|---|
| Save | Stored in the browser's `localStorage`. The panel shows what is stored, its size and age, and can delete it. |
| Score | A made-up leaderboard, so you can see how a rank looks before any real players exist. You can set the range. Your submitted scores join it. Reloading generates a new one. |
| Achievements | Kept in memory for one player. *Reset* clears them **without** restarting the game (the running game keeps what it was told, just as in a real client). |
| Share | Every link (the game's, or the rail's Share button) gets a record kept in the browser's `localStorage`: where it came from, and the game's `data` and `meta`. The record is written to the log, the link is copied to the clipboard, and the game is told `copied`. Opening the link in the same browser restarts the game with that data. *Open without the link* drops it. |
| Panel | The same panel as a real host's, with the game's pages in it. The game can ask for one; opening the panel yourself, on the game's pages or on others, shows how the game reacts to a panel it didn't ask for. |
| Input | The key-binding page, a switch for which input device the player is using, and a virtual pad to play the game with. |
| Sound | Mute, master volume and a slider per declared channel, in the panel, on the game's page. The log says whether the game answered with the mix it was asked for. |
| Pause | A button in the corner, shown only when the game says it can pause. |

## License

MIT; see `LICENSE`. The files `hopple create` writes into a new
project are under 0BSD instead, so a game made from them owes nothing to
anyone. The license covers the code, not the Hopple name or logo.
