# CLI commands

> Every disko command and flag, exit codes, error codes and environment variables.

The `disko` command ships in `@disko-game/cli` (Node.js 22 or later). Every
command accepts `--json` and never prompts: a missing value fails with exit
code 2 and names the flag. Commands, flags, exit codes and error codes below
are generated from the CLI's own definitions.

## Commands

### disko dev

Run the game in a test room, reloading it on every edit.

```sh
disko dev [--project <dir>] [--json] [--no-open]
```

| Flag | Description |
| --- | --- |
| `--project <dir>` | the game project directory (default: the current directory) |
| `--json` | print one JSON event per line on stdout; progress stays on stderr |
| `--no-open` | print the sign-in URL and the room link without opening them |
| `--profile <production\|local>` | endpoint defaults (DISKO_PROFILE; default production) |
| `--api <url>` | disko-master API base URL (DISKO_API_URL) |
| `--registry <url>` | registry base URL (DISKO_REGISTRY_URL) |
| `--help` | show this help |

### disko publish

Build, check and upload a game release.

```sh
disko publish [--dry-run] [--out <file>] [--visibility <visibility>] [--project <dir>] [--json]
```

| Flag | Description |
| --- | --- |
| `--project <dir>` | the game project directory (default: the current directory) |
| `--dry-run` | build and check only: no credentials, no network, nothing uploaded |
| `--out <file>` | also write the archive to this file (overwritten if it exists) |
| `--visibility <public\|unlisted\|private>` | release visibility: required for public games, private by default for private games |
| `--json` | print one JSON object on stdout; progress stays on stderr |
| `--no-open` | print the browser sign-in URL without opening it |
| `--profile <production\|local>` | endpoint defaults (DISKO_PROFILE; default production) |
| `--api <url>` | disko-master API base URL (DISKO_API_URL) |
| `--registry <url>` | registry base URL (DISKO_REGISTRY_URL) |
| `--help` | show this help |

### disko login

Sign in with the browser, or store a CLI token read from stdin.

```sh
disko login [--with-token] [--json]
```

| Flag | Description |
| --- | --- |
| `--with-token` | read a CLI token (dpt_…) from stdin and store it |
| `--json` | print one JSON object on stdout; progress stays on stderr |
| `--no-open` | print the browser sign-in URL without opening it |
| `--profile <production\|local>` | endpoint defaults (DISKO_PROFILE; default production) |
| `--api <url>` | disko-master API base URL (DISKO_API_URL) |
| `--registry <url>` | registry base URL (DISKO_REGISTRY_URL) |
| `--help` | show this help |

### disko logout

Delete the stored credential.

```sh
disko logout [--json]
```

| Flag | Description |
| --- | --- |
| `--json` | print one JSON object on stdout; progress stays on stderr |
| `--profile <production\|local>` | endpoint defaults (DISKO_PROFILE; default production) |
| `--api <url>` | disko-master API base URL (DISKO_API_URL) |
| `--registry <url>` | registry base URL (DISKO_REGISTRY_URL) |
| `--help` | show this help |

## `disko dev` events

With `--json`, `disko dev` writes one JSON event per line (NDJSON) on stdout;
progress stays on stderr. `stopped` is always the last event.

- `sign_in_required`
- `room_ready`
- `reload`
- `runtime_error`
- `host_restarted`
- `stopped`

### Event fields

Combine `--json` with `--no-open` and `DISKO_TOKEN` to run without
a browser:

```sh
DISKO_TOKEN="$TOKEN" npx disko dev --json --no-open
```

| Event              | Fields                                                                                      |
| ------------------ | ------------------------------------------------------------------------------------------- |
| `sign_in_required` | `url`                                                                                       |
| `room_ready`       | `room_id`, `link`, `expires_at` (Unix seconds)                                              |
| `reload`           | `status` (`"ok"` or `"failed"`), and `digest` or `problems`                                 |
| `runtime_error`    | `phase` (`"load"`, `"start"`, `"event"`, or `"host"` for the host file), `message`, `stack` |
| `host_restarted`   | `reason`, for example `"server.js changed"`                                                 |
| `stopped`          | `reason`; after a failure also `error`, `docs` and any `problems`                           |

```json
{"event": "room_ready", "room_id": "…", "link": "https://play.disko.ooo/?room=…", "expires_at": 1790000000}
{"event": "reload", "status": "failed", "problems": [{"code": "import_unresolved", "path": "src/main.ts", "line": 3, "column": 18, "message": "…", "docs": "…"}]}
{"event": "reload", "status": "ok", "digest": "blake3:…"}
```

`stopped` is always the last event. After Ctrl-C its `reason` is
`"interrupted"` (`"terminated"` for `SIGTERM`); after a failure the exit code
follows the [exit-code table](/reference/cli#exit-codes).

## `disko dev` host file

A game decides how a match plays. The **room** decides everything around it:
who may join, what happens to chat, which game is loaded. `disko dev` provides a simple room for you, and you can replace it with your
own.

Without a host file, `disko dev` uses a built-in host: it shows every chat
message, and runs the `start-game`, `stop-game`, `pause-game` and
`resume-game` game events as `room.start()`, `room.stop()`, `room.pause()`
and `room.resume()`. A game that starts itself with `game.start()`, as the
scaffold does, needs nothing more.

To try your own room behavior, add a `server.js` to the game project that
default-exports a **host function**, like a room project's `room.js`:

```js title="server.js"
import { Room } from "@disko-game/room";

// The host function. `main.js` calls it in production and `disko dev` calls
// it with a test room: pass `options` through to `Room.launch` and return
// the room.
/** @param {import("@disko-game/room").HostOptions} options */
export default async function host(options) {
  const room = await Room.launch({ ...options, name: "My disko room" });

  // Nothing appears in chat unless the room sends it.
  room.on("chat-request", ({ player, text }) => {
    void room.send({ author: player, content: text });
  });

  room.on("player-join", ({ player }) => {
    void room.send(`${player.name} joined`);
  });

  return room;
}
```

- `disko dev` calls it with the test room's `id` and `token`, the project as
  `game` (`{ path }`), a loopback `listen` address and the `endpoints`. It
  must pass them through to `Room.launch` and resolve to the room it
  launched. See [`HostOptions`](/reference/room#hostoptions).
- It imports `@disko-game/room`; install it in the game project
  (`npm install @disko-game/room`) so the import resolves.
- It runs in its own Node process. An edit to `server.js` closes the room and
  runs the new `server.js` in a fresh process with the same test room;
  players reconnect. `server.js` is not part of the game and is never
  published.
- A TypeScript project may use `server.ts` instead, which Node 22.18 or later
  runs directly (type stripping, so only erasable syntax). `server.js` wins
  when both exist.
- If the host function throws or its process fails, the error is reported
  with the phase `host`, and the next edit of any file tries again.

## `disko dev` limits

A test room expires ten minutes after its last lease renewal, which
`disko dev` renews while it runs. Each session creates a new test room, and
an account can run at most two at a time; a third fails with
[quota_exceeded](/errors/quota_exceeded). A test room counts only while its
host holds a lease: a session you stop frees its slot within seconds, and one
that was killed frees it when its lease runs out (about 30 seconds).

## Exit codes

| Code | Name | Meaning |
| --- | --- | --- |
| 0 | `ok` | Success. |
| 1 | `problems` | Build or validation problems (`problems` present). |
| 2 | `usage` | Usage error: a bad or missing flag. |
| 3 | `auth` | Authentication or authorization failure. |
| 4 | `unavailable` | disko-master or the registry is unreachable or unavailable. |
| 5 | `conflict` | Conflict (for example `version_exists`) or quota exceeded. |

## Error codes

With `--json`, a failure prints `{"ok": false, "command": …, "error": <code>, …}`
on stdout (for `disko dev`, the final `stopped` event carries `error`). An
exit code 1 always comes with `problems`; an unexpected failure is reported as
the problem [internal_error](/errors/internal_error). Each code has a page.

| Error | Exit code |
| --- | --- |
| [`api_unavailable`](/errors/api_unavailable) | 4 |
| [`archive_too_large`](/errors/archive_too_large) | 1 |
| [`auth_unavailable`](/errors/auth_unavailable) | 4 |
| [`build_failed`](/errors/build_failed) | 1 |
| [`conflict`](/errors/conflict) | 5 |
| [`credential_store_unavailable`](/errors/credential_store_unavailable) | 3 |
| [`game_project_required`](/errors/game_project_required) | 2 |
| [`game_required`](/errors/game_required) | 1 |
| [`internal_error`](/errors/internal_error) | 1 |
| [`invalid_package`](/errors/invalid_package) | 1 |
| [`invalid_token`](/errors/invalid_token) | 3 |
| [`missing_permission`](/errors/missing_permission) | 3 |
| [`not_found`](/errors/not_found) | 3 |
| [`quota_exceeded`](/errors/quota_exceeded) | 5 |
| [`registry_error`](/errors/registry_error) | 1 |
| [`registry_unavailable`](/errors/registry_unavailable) | 4 |
| [`sign_in_failed`](/errors/sign_in_failed) | 3 |
| [`token_required`](/errors/token_required) | 2 |
| [`unauthorized`](/errors/unauthorized) | 3 |
| [`usage`](/errors/usage) | 2 |
| [`version_exists`](/errors/version_exists) | 5 |
| [`visibility_not_allowed`](/errors/visibility_not_allowed) | 2 |
| [`visibility_required`](/errors/visibility_required) | 2 |

## Environment variables

Flags win over variables, which win over the profile.

| Variable             | Meaning                                                                             |
| -------------------- | ----------------------------------------------------------------------------------- |
| `DISKO_TOKEN`        | A CLI token (`dpt_…`). Takes precedence over stored credentials; for CI and agents. |
| `DISKO_PROFILE`      | Endpoint defaults: `production` (the default) or `local`. Same as `--profile`.      |
| `DISKO_API_URL`      | The API base URL. Same as `--api`.                                                  |
| `DISKO_REGISTRY_URL` | The registry base URL. Same as `--registry`.                                        |
| `DISKO_AUTH_ISSUER`  | The OpenID issuer used for browser sign-in.                                         |
| `DISKO_PLAY_URL`     | The web client used for room links.                                                 |

URLs must use `https`, except on localhost. Stored credentials are kept per
API host, so a local credential is never sent to production.

## Credentials

Signed-in commands use, in order: `DISKO_TOKEN`; a token stored by
`disko login --with-token`; a stored browser sign-in; otherwise a browser
sign-in, which prints the URL (and opens it unless `--no-open` is given) and
waits up to five minutes. Credentials are stored only in the OS credential
store. See [CLI tokens & CI](/registry/cli-tokens).
