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]
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
| 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 |
And at the top level: status, activity, logs, push, pull, clone, releases, rollback, deploy, add, feedback.
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
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
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.testtest accounts you create withdeepspace test accounts createto 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#
--jsonnearly everywhere. Commands emit a single-line{ ok, ... }envelope.logs --jsonandactivity --follow --jsonemit 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. --yesskips confirmations onapp undeploy,feedback,integrations invoke, andtest 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.
| 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:
{"action":{"cwd":"/absolute/app","argv":["deepspace","pull"]}}
Human output renders the same thing as a Next: line. Two rules:
- Execute
argvdirectly incwd. 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
actionmeans 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):
| 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). | auth login - but check the plane first |
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. | 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 | 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. | 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. | 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 |
app collaborators | not_a_collaborator, owner_already_authorized | See collaborators. | none |
app transfer offer | user_not_found | The recipient has never signed in to DeepSpace. | none |
Local dev workflow#
Scaffold
npm create deepspace@latest my-app
cd my-app
Sign in
npx deepspace auth login
One session covers every app on the machine.
Develop
npx deepspace dev start
Vite + worker run on localhost:5173 with HMR.
Test
npx deepspace test run
Smoke + API specs run against the dev workers.
Commit, then deploy
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:
npx deepspace dev kill # default port 5173
npx deepspace dev kill --port 5180
npx deepspace dev kill --all # sweep every workerd/wrangler
Next steps#
- Command reference - every command, every flag.
- Quickstart - walk through the full dev loop.
- Collaborators - let teammates deploy your app.