Skip to main content
Documentation

CLI overview

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

On this page

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

npx deepspace <command> [options]
bash

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:

npx deepspace dev          # prints the subcommands - does NOT start a server
npx deepspace dev start    # starts the server
bash
GroupPurpose
authlogin, logout, whoami - one session for every app on the machine
appThe app as a platform object: create, init, list, files, source, update, undeploy, transfer, collaborators, domain, usage
devstart, kill - the local dev server
testrun, screenshot, accounts
secretslist, set, get, delete, upload, download, pull, configs
workspacenew, attach, sync, list, status, land, drop - parallel-agent workspaces
integrationslist, info, invoke

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

Full command reference →

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, and billed integration calls.

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

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.

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), because the bare auth login action it ships refuses interactive_required without a TTY. Read the plane before running the 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:

# once, on your machine
npx deepspace auth login

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

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.

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 to drive multi-user auth flows in Playwright tests.
  • 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 skips confirmations on app undeploy, feedback, integrations invoke, and test accounts clear.
  • 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 lists them by command.

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.

ExitMeaning
0Operation completed.
1Failure or refusal. Retrying unchanged will not help - fix the stated cause first.
2Safe 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:

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

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 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):

CodeMeaningAction
not_authenticatedNo usable session for the selected plane. The sentence names the plane, what selected it, and any plane that does hold a session (above).auth login - but check the plane first
interactive_requiredauth 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_repoNo 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_idDEEPSPACE_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_foundAn explicit --app <id or name> matched nothing you can access (a never-deployed app has no name yet - target it by id).none
network_errorThe platform service could not be reached (offline, DNS, refused). Names the URL and the override variable.none - retry once connectivity is back

Per command:

CommandCodeMeaningAction
auth logininvalid_credentialsPassword login answered 401 - wrong email or password.none
auth loginlogin_failedAny other auth-service refusal, or a session issued without a cookie or token.none
app initapp_not_registeredThe id in wrangler.toml was minted locally by an older SDK and no server registered it.app init --new-id
app initnot_app_ownerThe id is registered to another account (a cloned repo). Forking is a data decision.none
app undeployundeploy_declinedThe interactive confirmation was declined; nothing changed.none
app undeployregistry_takedown_failed (exit 2)The script was removed but the registry did not release the route.app undeploy <id> again
app updateinvalid_package_manifest / invalid_migration_manifestThe explicit app manifest is malformed. The read-only guide will not repair or replace it.none
deployapp_not_registeredThe id no server has registered (an older scaffold).none - app init --new-id
deployforbiddenThe app belongs to another account; names the app and the signed-in account.none - be added as a collaborator, or app init --new-id
deploydeploy_in_progressAnother deploy of this directory holds .deepspace/deploy.lock (holder.pid, startedAt).none - wait; remove the lock only if no deploy is running
deployrelease_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
deploysecrets_config_missingThe selected secrets config does not exist.secrets configs create <name>
deployapp_id_env_mismatch / app_id_define_unsubstitutedThe 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
deploydirty_worktree, behind_trunk, stale_base, workspace_unsynced, rename_required, owner_jwt_missingLineage and identity guards - see the refusal taxonomy.varies
push / pull / deploymerge_in_progressThe worktree is mid-merge, -rebase, -cherry-pick, or -revert; HEAD is the pre-operation commit.none - --continue or --abort
push / pull / clonesource_managed_by_githubThe app's source of record is GitHub; use Git.none
pushno_commits (exit 2 on an uninitialized scaffold), unknown_branch, push_too_largeSee push.app init on a scaffold; else none
logsapp_not_deployedThe app has never been deployed, so there are no logs to read.deploy from the app's checkout
rollbackno_bundle, do_class_verify_failed, do_class_deletionSee releases and rollback.none
secrets uploadfile_not_foundThe path does not exist (- reads stdin).none
test rununknown_suiteNot a suite name, a .spec.ts path, or --grep.none
integrations invokecost_confirmation_requiredA paid call outside an interactive terminal (or under --json) without --yes. No call was made.none - re-run with --yes
integrations invoke / infounknown_integration, unknown_endpointThe target does not exist in the catalog.integrations list
app collaboratorsnot_a_collaborator, owner_already_authorizedSee collaborators.none
app transfer offeruser_not_foundThe recipient has never signed in to DeepSpace.none

Local dev workflow#

Scaffold

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

Sign in

npx deepspace auth login
bash

One session covers every app on the machine.

Develop

npx deepspace dev start
bash

Vite + worker run on localhost:5173 with HMR.

Test

npx deepspace test run
bash

Smoke + API specs run against the dev workers.

Commit, then deploy

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

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:

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

Next steps#