# CLI overview

The shape of the CLI: authentication, exit codes, the JSON action contract, agents and CI.

The CLI ships inside the `deepspace` package, which is added to every scaffolded app. You run it via `npx`:

```bash
npx deepspace <command> [options]
```

No global install is needed. To create a new app, use `npm create deepspace@latest` - it fetches the latest scaffolder on demand.

## The shape of the CLI

The surface is **noun-verb**. Related commands live under a noun, and the noun on its own just prints its subcommands:

```bash
npx deepspace dev          # prints the subcommands - does NOT start a server
npx deepspace dev start    # starts the server
```

| Group          | Purpose                                                                                                                                         |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth`         | `login`, `logout`, `whoami` - one session for every app on the machine                                                                          |
| `app`          | The app as a platform object: `create`, `init`, `list`, `files`, `source`, `update`, `undeploy`, `transfer`, `collaborators`, `domain`, `usage` |
| `dev`          | `start`, `kill` - the local dev server                                                                                                          |
| `test`         | `run`, `screenshot`, `accounts`                                                                                                                 |
| `secrets`      | `list`, `set`, `get`, `delete`, `upload`, `download`, `pull`, `configs`                                                                         |
| `workspace`    | `new`, `attach`, `sync`, `list`, `status`, `land`, `drop` - parallel-agent workspaces                                                           |
| `integrations` | `list`, `info`, `invoke`                                                                                                                        |
| `agent`        | `tools`, `invoke` - discover and call tools exposed by an app                                                                                   |

And at the top level: `status`, `activity`, `logs`, `push`, `pull`, `clone`, `releases`, `rollback`, `deploy`, `add`, `feedback`.

[Full command reference →](/cli-reference/commands)

Typed an older command name? The CLI points you at where it moved: `deepspace login` prints ``did you mean `deepspace auth login`?``

## Login state

`npx deepspace auth login` opens a browser and signs you in with **GitHub or Google**. DeepSpace accounts are OAuth-only: there is no password to choose, and public email signup is closed.

You log in **once per machine**. The session is shared across every app and covers `dev`, `test`, `deploy`, billed integration calls, and [local agent tools](/guides/local-agent-tools).

```bash
npx deepspace auth whoami        # human-readable
npx deepspace auth whoami --json # machine-readable
```

Login writes two files under `~/.deepspace/`, both mode `0600`:

* `~/.deepspace/session` - the long-lived session token. This is the credential.
* `~/.deepspace/token` - a cached short-lived JWT, re-minted from the session automatically by every other command.

Treat both as secret - never commit them.

Credentials are stored **per auth plane**. Production keeps the plain `session` / `token` names; a non-production `DEEPSPACE_ENV` or `DEEPSPACE_AUTH_URL` gets its own suffixed pair (for example `~/.deepspace/session.auth-deepspacesites-com`), so a staging login cannot clobber your production session. The flip side: `DEEPSPACE_ENV` and `DEEPSPACE_AUTH_URL` **select which stored credential a command reads**. A shell with `DEEPSPACE_ENV=staging` set is logged out on staging even when `~/.deepspace/session` holds a perfectly good production login.

When no usable session exists for the selected plane, every command refuses `not_authenticated` (exit 1) with a sentence that names the plane it selected and what selected it - `Not logged in on staging (selected by DEEPSPACE_ENV=staging)`, or `… on the auth service at <url> (selected by DEEPSPACE_AUTH_URL)` - and, when another plane does hold a session, says so and how to select it (`You are signed in on production - select that plane (unset DEEPSPACE_ENV), or log in here`). The same sentence spells out the headless login form up front (`auth login --email you@example.com --password-stdin`, or `DEEPSPACE_EMAIL` / `DEEPSPACE_PASSWORD`). An executable `action` is included only when it can run in the current environment: bare `auth login` with a TTY, or the fully supplied environment-credential form headlessly. Without either, the refusal deliberately has no action, because replaying bare login would only produce `interactive_required`. Read the plane before following any action: a wrong plane is fixed by unsetting a variable, not by logging in again.

## Agents and CI

Agents do not get DeepSpace accounts. An agent runs **as you**, using your session.

On your own machine that is already true: any agent you launch inherits `~/.deepspace/session` and every `deepspace` command it runs is attributed to you. Nothing extra to configure.

For an agent in a container, or a CI job, do the same thing deliberately - log in once where a browser exists, then supply that environment with the session file:

```bash
# once, on your machine
npx deepspace auth login

# then mount ~/.deepspace into the container
docker run -v ~/.deepspace:/root/.deepspace my-agent-image
```

In CI, store the **contents** of `~/.deepspace/session` as an encrypted secret and write it to `~/.deepspace/session` before the first `deepspace` command. Copying `~/.deepspace/token` is unnecessary - it is only a cache, and the CLI re-mints it from the session.

Don't mount `~/.deepspace` read-only. The JWT in `~/.deepspace/token` is short-lived, and commands rewrite it in place when it expires - a read-only mount fails on the first refresh.

`~/.deepspace/session` is a long-lived credential for your account: whatever holds it can deploy as you. Scope it like a production secret. `npx deepspace auth logout` revokes the session server-side, which invalidates every copy of it.

### The password flags

`deepspace auth login --help` lists `--email`, `--password`, and `--password-stdin`, and the CLI reads `$DEEPSPACE_EMAIL` / `$DEEPSPACE_PASSWORD` when stdin is non-interactive or `--json` was passed. These exist for the two kinds of account that *do* have a password:

* **`@deepspace.test` test accounts** you create with [`deepspace test accounts create`](/cli-reference/commands#test-accounts) to drive multi-user auth flows in [Playwright tests](/guides/testing#provisioning-test-accounts).
* **Internally provisioned accounts**, minted by SDK maintainers through an admin endpoint (the CI bot, for instance). Not available to the public.

Your own DeepSpace account is neither, so these flags cannot log you in - it has no password. Use the stored session above.

### The `interactive_required` contract

If you ask for `--json`, or stdout is not a TTY, and you supply no credentials, the CLI refuses immediately rather than opening a browser you cannot reach:

```
Browser login needs an interactive terminal. Use --email with
--password-stdin (or $DEEPSPACE_EMAIL/$DEEPSPACE_PASSWORD).
```

The refusal carries the machine-readable code `interactive_required`. Retrying will not help.

The remedy named in the message applies only to test and internally provisioned accounts. **For a real DeepSpace account the fix is upstream of the failing call:** log in once in a browser on a machine that has one, then give this environment the resulting `~/.deepspace/session`, as above.

## Agent-friendly defaults

* **`--json` nearly everywhere.** Commands emit a single-line `{ ok, ... }` envelope. `logs --json` and `activity --follow --json` emit NDJSON - one frame per line - so they pipe cleanly while following.
* **Non-interactive by default.** Missing required arguments produce a refusal with a machine-readable code and a `Next:` suggestion, rather than a stdin prompt.
* **`--yes` supplies explicit consent where a command requires it.** Destructive gates such as `rollback`, `app undeploy`, secrets-config deletion, domain purchase/detach, and test-account cleanup refuse `confirmation_required` under `--json` or non-interactive stdin without it. A paid `integrations invoke` similarly refuses `cost_confirmation_required`; free endpoints do not. On `feedback`, `--yes` only skips the interactive review prompt - a flag-complete headless submission does not need it.
* **Refusals carry codes.** `interactive_required`, `dirty_worktree`, `push_too_large`, `not_a_collaborator`, and friends are stable identifiers - branch on the code, never on the prose. The [table below](#refusal-codes-by-command) lists them by command.

`npx deepspace status` is the fastest way for an agent to orient in an unfamiliar checkout: it prints session, app identity, workspace, and the live release in one screenful, and takes `--json`.

## Exit codes

Every command follows one exit contract. Human text is complete on its own; `--json` is the machine mirror of the same result, and refusals carry a stable `code`.

| Exit | Meaning                                                                                                                                                                                                                                                                                                                                         |
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0`  | Operation completed.                                                                                                                                                                                                                                                                                                                            |
| `1`  | Failure or refusal. Retrying unchanged will not help - fix the stated cause first.                                                                                                                                                                                                                                                              |
| `2`  | Safe partial progress or stop, with `actionRequired: true` - the operation did what it could and a local step, or a judgment, remains. The envelope is still `ok: false` with a `code` naming the state: `release_in_progress` after a deploy that built and uploaded but did not release, or `app_not_initialized` with the `app init` action. |

Branch on the exit code and the `code` field, never on the prose. Do not
pre-check authentication before an operation - run the operation and let it
refuse: an unauthenticated call fails with `code: "not_authenticated"` and
exit 1, which is cheaper and more truthful than a separate probe.

## The `action` contract

When exactly one deterministic follow-up exists, the JSON result names it:

```json
{"action":{"cwd":"/absolute/app","argv":["deepspace","pull"]}}
```

Human output renders the same thing as a `Next:` line. Two rules:

* **Execute `argv` directly in `cwd`.** Spawn it as an argv array; never join it into a shell string.
* **Absence is not "done".** Terminal results, status reports, consent decisions, destructive overrides, and input-dependent choices deliberately omit the field. An exit-2 result without an `action` means its facts need inspecting - never infer a command from the field's absence, and never invent one.

One-shot `--json` writes exactly one document. The exceptions are streams: `logs --json` is NDJSON in both snapshot and follow modes, `activity --follow --json` is NDJSON, and `dev start` / `test screenshot` inherit their child's output on stdout, so their final envelope follows the stream as the **last** line. Parse one frame per line; the [command reference](/cli-reference/commands) documents each stream's frame shapes.

`test run --json` is not one of those exceptions. It routes the spawned suite's output to **stderr**, so its stdout is exactly one JSON envelope - `npx deepspace test run --json | jq` works with no last-line handling, and the live suite transcript is still visible on stderr. (`dev start --json` writes *two* envelopes on stdout: a readiness line - `{"ok":true,"ready":true,"url":...}` - the moment the port answers, and the exit envelope last.)

## Refusal codes by command

The codes an agent branches on, grouped by where they come from. Codes are stable identifiers; the sentences beside them change. "Action" says whether the refusal ships an executable `action` - when it does not, the remedy is a decision, and the prose names the choices. Exit is `1` unless marked `2`.

**Every command** (the shared preconditions, raised before any work):

| Code                                               | Meaning                                                                                                                                                                                                                                                                        | Action                                                                                            |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `not_authenticated`                                | No usable session for the selected plane. The sentence names the plane, what selected it, and any plane that does hold a session (above).                                                                                                                                      | Conditional: executable login only with a TTY or complete environment credentials; otherwise none |
| `interactive_required`                             | `auth login` needs a browser and there is no TTY (or `--json`); no credentials were supplied.                                                                                                                                                                                  | none - supply a session file or the password form                                                 |
| `not_in_app_repo`                                  | No `wrangler.toml` at or above the working directory (and no `--app`). `dev`/`test` also list sibling app directories when they see any.                                                                                                                                       | none - `cd`, scaffold, or pass `--app`                                                            |
| `app_not_initialized` (exit 2)                     | Inside an app whose `wrangler.toml` has no `DEEPSPACE_APP_ID` (or the scaffold's `__APP_ID__` placeholder). Raised by `deploy`, `dev start`, `test run`, `secrets`, and every command that resolves the local app (`logs`, `releases`, `push`, …) - one builder, one sentence. | `app init`                                                                                        |
| `no_app_id_for_env`                                | `--env <name>` names a `[env.<name>]` block with no id of its own - each environment is its own app.                                                                                                                                                                           | none - `app init --env <name>`, or omit `--env`                                                   |
| `invalid_app_id`                                   | `DEEPSPACE_APP_ID` is present but is not an app id (`app_` + 26 characters). Ids are server-minted, so it was hand-edited or corrupted; there is deliberately **no** `app init` action, which would mint a fresh id over the top and orphan the app.                           | none - restore the id (`app list` shows yours), or `app init --new-id` to fork                    |
| `invalid_app` / `invalid_env` / `ambiguous_target` | `--app` or `--env` is blank or malformed, or both were passed.                                                                                                                                                                                                                 | none                                                                                              |
| `app_not_found`                                    | An explicit `--app <id or name>` matched nothing you can access (a never-deployed app has no name yet - target it by id).                                                                                                                                                      | none                                                                                              |
| `network_error`                                    | The platform service could not be reached (offline, DNS, refused). Names the URL and the override variable.                                                                                                                                                                    | none - retry once connectivity is back                                                            |

**Per command:**

| Command                                         | Code                                                                                                                           | Meaning                                                                                                                                                                                                                                          | Action                                                                                  |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| `auth login`                                    | `invalid_credentials`                                                                                                          | Password login answered 401 - wrong email or password.                                                                                                                                                                                           | none                                                                                    |
| `auth login`                                    | `login_failed`                                                                                                                 | Any other auth-service refusal, or a session issued without a cookie or token.                                                                                                                                                                   | none                                                                                    |
| `app init`                                      | `app_not_registered`                                                                                                           | The id in `wrangler.toml` was minted locally by an older SDK and no server registered it.                                                                                                                                                        | `app init --new-id`                                                                     |
| `app init`                                      | `not_app_owner`                                                                                                                | The id is registered to another account (a cloned repo). Forking is a data decision.                                                                                                                                                             | none                                                                                    |
| `app undeploy`                                  | `undeploy_declined`                                                                                                            | The interactive confirmation was declined; nothing changed.                                                                                                                                                                                      | none                                                                                    |
| `app undeploy`                                  | `registry_takedown_failed` (exit 2)                                                                                            | The script was removed but the registry did not release the route.                                                                                                                                                                               | `app undeploy <id>` again                                                               |
| `app update`                                    | `invalid_package_manifest` / `invalid_migration_manifest`                                                                      | The explicit app manifest is malformed. The read-only guide will not repair or replace it.                                                                                                                                                       | none                                                                                    |
| `deploy`                                        | `app_not_registered`                                                                                                           | The id no server has registered (an older scaffold).                                                                                                                                                                                             | none - `app init --new-id`                                                              |
| `deploy`                                        | `forbidden`                                                                                                                    | The app belongs to another account; names the app and the signed-in account.                                                                                                                                                                     | none - be added as a collaborator, or `app init --new-id`                               |
| `deploy`                                        | `deploy_in_progress`                                                                                                           | Another deploy of **this directory** holds `.deepspace/deploy.lock` (`holder.pid`, `startedAt`).                                                                                                                                                 | none - wait; remove the lock only if no deploy is running                               |
| `deploy`                                        | `release_in_progress` (exit 2)                                                                                                 | Another deploy of this app, from another checkout, is between prepared and live. This run built and uploaded but did not release.                                                                                                                | the same deploy, again                                                                  |
| `deploy`                                        | `secrets_config_missing`                                                                                                       | The selected secrets config does not exist.                                                                                                                                                                                                      | `secrets configs create <name>`                                                         |
| `deploy`                                        | `app_id_env_mismatch` / `app_id_define_unsubstituted`                                                                          | The built client bundle carries another app's id, or the unreplaced `__DEEPSPACE_APP_ID__` define. The message is the three-file retrofit; `app update` reports the matching guidance without editing it.                                        | none                                                                                    |
| `deploy`                                        | `dirty_worktree`, `behind_trunk`, `stale_base`, `workspace_unsynced`, `rename_required`, `owner_jwt_missing`                   | Lineage and identity guards - see [the refusal taxonomy](/guides/releases-and-rollback#the-refusal-taxonomy).                                                                                                                                    | varies                                                                                  |
| `push` / `pull` / `deploy`                      | `merge_in_progress`                                                                                                            | The worktree is mid-merge, -rebase, -cherry-pick, or -revert; `HEAD` is the pre-operation commit.                                                                                                                                                | none - `--continue` or `--abort`                                                        |
| `push` / `pull` / `clone` / the workspace verbs | `source_managed_by_github`                                                                                                     | The app's source of record is GitHub; use Git.                                                                                                                                                                                                   | none                                                                                    |
| `push`                                          | `no_commits` (exit 2 on an uninitialized scaffold), `unknown_branch`, `push_too_large`                                         | See [push](/cli-reference/commands#push).                                                                                                                                                                                                        | `app init` on a scaffold; else none                                                     |
| `logs`                                          | `app_not_deployed`                                                                                                             | The app has never been deployed, so there are no logs to read.                                                                                                                                                                                   | `deploy` from the app's checkout                                                        |
| `rollback`                                      | `no_bundle`, `do_class_verify_failed`, `do_class_deletion`                                                                     | See [releases and rollback](/guides/releases-and-rollback).                                                                                                                                                                                      | none                                                                                    |
| `secrets upload`                                | `file_not_found`                                                                                                               | The path does not exist (`-` reads stdin).                                                                                                                                                                                                       | none                                                                                    |
| `test run`                                      | `unknown_suite`                                                                                                                | Not a suite name, a `.spec.ts` path, or `--grep`.                                                                                                                                                                                                | none                                                                                    |
| `integrations invoke`                           | `cost_confirmation_required`                                                                                                   | A paid call outside an interactive terminal (or under `--json`) without `--yes`. No call was made.                                                                                                                                               | none - re-run with `--yes`                                                              |
| `integrations invoke` / `info`                  | `unknown_integration`, `unknown_endpoint`                                                                                      | The target does not exist in the catalog.                                                                                                                                                                                                        | `integrations list`                                                                     |
| `agent tools` / `agent invoke`                  | `invalid_agent_app`, `agent_endpoint_not_found`, `forbidden`, `agent_network_error`, `agent_timeout`, `agent_redirect_refused` | The target is invalid, the app has not exposed local tools, access was denied, or the app could not be reached safely. `invoke` also has input- and tool-specific refusals documented in the [command reference](/cli-reference/commands#agent). | none - correct the target or input, sign in to the app, or ask its developer for access |
| `app collaborators`                             | `not_a_collaborator`, `owner_already_authorized`                                                                               | See [collaborators](/guides/collaborators).                                                                                                                                                                                                      | none                                                                                    |
| `app transfer offer`                            | `user_not_found`                                                                                                               | The recipient has never signed in to DeepSpace.                                                                                                                                                                                                  | none                                                                                    |

## Local dev workflow

Scaffold

```bash
npm create deepspace@latest my-app
cd my-app
```

Sign in

```bash
npx deepspace auth login
```

One session covers every app on the machine.

Develop

```bash
npx deepspace dev start
```

Vite + worker run on `localhost:5173` with HMR.

Test

```bash
npx deepspace test run
```

Smoke + API specs run against the dev workers.

Commit, then deploy

```bash
git commit -am "my changes"
npx deepspace deploy
```

A deploy records the commit it ships, so the worktree must be clean.

## Cleaning up leaked processes

If a previous `dev` session crashed, leaked `workerd` / `wrangler` processes can hold ports:

```bash
npx deepspace dev kill                   # default port 5173
npx deepspace dev kill --port 5180
npx deepspace dev kill --all             # sweep every workerd/wrangler
```

**Don't kill a parallel session's processes.** If a sibling session is running on port 5173, use `--port 5174` (and update `tests/playwright.config.ts` to match) instead of `--all`.

## Next steps

* [Command reference](/cli-reference/commands) - every command, every flag.
* [Quickstart](/get-started/quickstart) - walk through the full dev loop.
* [Collaborators](/guides/collaborators) - let teammates deploy your app.
