slew docs/reference/cli-reference

CLI reference

slew is the command-line tool for slew — an EU-native deploy platform: git in, live URL out.

Installation and requirements

  • Runtime: Node.js >= 20.19.
  • Distribution: the npm package @slew-sh/cli (MIT license). It installs a single binary, slew. The bare npm name slew is an unrelated package — install @slew-sh/cli.
  • Dependencies: deliberately one runtime dependency (tar) — the CLI is small enough to read before you trust it with your deploys.
npm install -g @slew-sh/cli

# after install, verify:
slew --version   # prints the CLI version, e.g. 0.1.0
slew --help      # prints the logo and usage

Running slew with no command prints the help and exits with code 1; slew --help prints the same help and exits 0.

Global options

Flag Effect
--project <name> Override the slew.json project link (used by deploy, deployments, rollback, stats)
--hours <n> Time window for stats (default 24, max 72)
--version Print the CLI version and exit
--help Show the ASCII logo and help text

Flag parsing is strict: an unknown flag is an error.

Commands

Git integration

Command Effect
slew git Connected repository, build recipe, and recent builds
slew git connect <owner/repo> Deploy this project on every push (flags: --branch, --build-cmd, --install-cmd, --output, --no-previews); opens the GitHub App install page when needed and waits
slew git build Queue a build of the branch head now
slew git log [build-id] Print a build's log (default: the latest build); header on stderr, log on stdout
slew git disconnect Stop deploying from git

Organizations

Shared team workspaces — see Organizations for the model and roles.

Command Effect
slew org list Your orgs and your role in each
slew org create <name> Create an org (--display-name <s>); you become its owner
slew org members <org> List members with roles
slew org invite <org> <email> Email an invite (--role member|admin); always prints the accept link, and says so when no email was sent
slew org invites <org> Pending invites
slew org revoke-invite <org> <id> Revoke a pending invite
slew org remove <org> <login> Remove a member (admin+)
slew org leave <org> Leave the org yourself
slew org token <org> <ai|deploy> [label] Mint an org-owned token (admin+); the secret prints once. deploy = CI credential limited to deploying the org's projects, ai = gateway key drawing the org's credit
slew org tokens <org> / slew org revoke-token <org> <id> List / revoke org tokens (admin+)
slew org installations <org> GitHub installations shared with the org
slew org share-github <org> [installation-id] Share one of your GitHub installations with the org (no id = pick from a list)
slew org unshare-github <org> <installation-id> Unshare (the sharer or an admin)

Org tokens exist so nothing a team depends on hinges on one person's account: put a deploy token in CI as SLEW_TOKEN and deploys keep working no matter who leaves. slew whoami makes org-token identity explicit.

Scheduled jobs

Cron for the linked (or --project) project — see Scheduled jobs for schedules, execution, and the secret to verify.

Command Effect
slew cron list The project's jobs: schedule, path, method, state, last run
slew cron add "<schedule>" <path> [--method POST] Create a job (5-field cron expression, UTC; method defaults to GET)
slew cron rm <id> Delete a job
slew cron enable <id> / slew cron disable <id> Turn a job on or off without deleting it

slew projects [--org <name>]

List your personal projects — or, with --org, an organization's. Org-owned projects in any listing show (org: <name>).

slew login

Authenticate via the GitHub device flow.

  1. POST /auth/device to the control plane to start the flow.
  2. Prints the one-time user code and verification URL, and opens the URL in your browser (best effort — open on macOS, start on Windows, xdg-open elsewhere; the URL is printed either way).
  3. Polls POST /auth/device/poll on the server-provided interval until the deadline (expires_in). API error codes are handled: authorization_pending keeps polling, slow_down adds 5 seconds to the interval, expired_token and timing out both fail with "The code expired. Run 'slew login' again.", and access_denied fails with "Authorization was denied on GitHub."
  4. On success, writes the token to the credentials file (mode 0600), then calls GET /me to confirm.
$ slew login
First, copy your one-time code: ABCD-1234
Then authorize at: https://github.com/login/device
Waiting for authorization…
Logged in as hessel.

slew logout

Deletes the credentials file. Prints Logged out. if a file was removed, Already logged out. if there was nothing to delete. Note this only removes the stored file — it does not affect a SLEW_TOKEN environment variable.

slew whoami

Calls GET /me with the current token and prints the account as <github_login> (<id>, <email>) — the email part is omitted when the account has no email.

$ slew whoami
hessel (acc_1, hessel@example.com)

slew dashboard

Opens the web console already signed in.

  1. Requires a token (stored or SLEW_TOKEN); fails with "Not logged in" otherwise.
  2. Validates the token via GET /me first, so a dead token doesn't just bounce you to the login form.
  3. Opens <dashboard-url>/#token=<token>. The token rides in the URL fragment, which browsers never send to the server; the dashboard stores it and strips it from the URL on load.
$ slew dashboard
Opening https://app.slew.cloud as hessel…
If no browser opened, run 'slew token cli' and paste it there.

slew init [name]

Create a project (or link an existing one) for the current directory. With --org <name> the project is created in that organization instead of your personal workspace.

  • If the directory is already linked via slew.json and no name is given, it prints Already linked to '<project>'. Pass a name to relink: slew init <name> and stops.
  • Without a name argument, the project name is derived from the directory basename: lowercased, runs of characters outside [a-z0-9-] replaced with -, leading/trailing dashes stripped.
  • The name must match ^[a-z0-9-]{3,40}$ or the command fails.
  • It then POST /projects. If the API returns error code name_taken, the CLI tries GET /projects/<name> — if that succeeds (it's your own project), it links it instead; otherwise it fails with "'<name>' is already taken. Pick another".
  • On success it writes slew.json in the current directory.
$ slew init my-site
Created project 'my-site' → https://my-site.slew.cloud
Linked in slew.json — run 'slew deploy' to ship.

Or, when the name exists and belongs to you:

Linked existing project 'my-site' → https://my-site.slew.cloud
Linked in slew.json — run 'slew deploy' to ship.

slew deploy [dir]

Deploy a built site directory (default: current directory). See How deploy works for the full pipeline.

$ slew deploy dist
Deploying 42 files (1.3 MB) to 'my-site'…
Deployed dep_9f3
Live at https://my-site.slew.cloud

Accepts --project <name> to override the slew.json link. Fails with Not a directory: <path> if the argument doesn't resolve to a directory.

slew deployments

List the linked (or --project) project's deployments via GET /projects/<name>. Prints No deployments yet — run 'slew deploy'. when empty. Each line shows: an marker if it is the active (live) deployment, the deployment id, status (one of uploading, live, superseded, failed), timestamp (YYYY-MM-DD HH:MM), file count, and size — the latter two omitted when null:

● dep_9f3  live        2026-07-10 09:41  42 files  1.3 MB
  dep_8a1  superseded  2026-07-09 17:02  41 files  1.2 MB

slew deployments rm <deployment-id>

Delete an inactive deployment — its stored files and its record — via DELETE /projects/<name>/deployments/<id>. The active (live) deployment is refused with 409 deployment_live; roll back to another deployment first. Deletion is permanent: a deleted deployment can no longer be rolled back to. remove and delete work as aliases for rm.

$ slew deployments rm dep_8a1
Deleted dep_8a1 from 'my-site'.

slew domains [list|add|remove|branch]

Manage the project's custom domains. slew domains lists them with certificate state and, for branch domains, the branch each one tracks. slew domains add <domain> [--branch <branch>] attaches a domain — with --branch, pushes to that git branch deploy to this domain instead of production, and the first build queues immediately. Retarget later with:

$ slew domains branch staging.example.com develop
staging.example.com now tracks 'develop'.
$ slew domains branch staging.example.com production
staging.example.com serves production again.

slew domains remove <domain> detaches it.

slew domains cert <domain>

Check the TLS certificate a custom domain is actually serving, via GET /projects/<name>/domains/<domain>/certificate:

$ slew domains cert www.example.com
www.example.com
  status   valid
  issuer   Let's Encrypt R11
  subject  www.example.com
  expires  2026-08-30 00:00 UTC (50 days)

If the domain is unreachable on :443 (DNS not pointed at the CDN yet), the command says so and suggests re-running slew domains add <domain> to retry issuance.

slew password [status|set <password>|rm]

Gate the whole site behind a shared password — see Password-protected sites. set takes 4–128 characters and calls PUT /projects/<name>/password; rm (DELETE) makes the site public again; bare slew password reports the current state. Changing or removing the password immediately invalidates every visitor's unlock cookie.

$ slew password set "correct horse"
'my-site' is password protected — visitors get an unlock screen on every domain it serves on.

slew rollback <deployment-id>

Make an earlier deployment live again. Requires the deployment id (fails with Which deployment? Usage: slew rollback <deployment-id> (see 'slew deployments') otherwise). Calls POST /projects/<name>/deployments/<id>/activate, then re-fetches the project to print the URL:

$ slew rollback dep_8a1
Rolled 'my-site' to dep_8a1
Live at https://my-site.slew.cloud

slew stats [--hours <n>]

Live traffic for the linked (or --project) project, straight from the CDN's edge logs via GET /projects/<name>/stats — part of slew's cookieless analytics. --hours is 1–72 (default 24):

$ slew stats
my-site — last 24 h (UTC)
  requests   12,405 (96.2% from cache)
  bandwidth  1.9 GB
  errors     23 (4xx 21 · 5xx 2)
  by hour    ▁▁▂▃▅█▆▅▃▂▂▃▄▅▆▅▄▃▂▂▁▁▁▁

  my-site.slew.cloud  12,001  1.8 GB
  www.example.com        404  84 MB

  top paths
      8,911  /
      1,204  /blog/launch

  countries  NL 4,102 · DE 3,377 · FR 1,559

Every hostname the project serves — the slew.cloud subdomain and custom domains — is counted and listed separately. An empty window prints No requests in this window.; a window busier than the aggregation ceiling is marked [sampled] next to the request count. Longer ranges (up to 365 days) and the audience view — pageviews, visitors, referrers — live in the console's Traffic tab and the HTTP API.

slew env [ls | set KEY=value … | import [file] | rm KEY]

Per-project environment variables, encrypted at rest. Each variable has a scope — where it is injected:

Scope Injected into
runtime (default) The running server container, at start
build Git builds (npm run build and friends)
both Both
slew env set DATABASE_URL=postgres://…        # runtime (default)
slew env set FLAGS_TOKEN=abc --scope build    # git builds only
slew env set API_URL=https://… --scope both
slew env                                      # list; non-runtime scopes shown as [build] / [both]
slew env import .env.production --scope both  # every var from a dotenv file
slew env rm FLAGS_TOKEN

Runtime changes restart the live server on its next request; build changes apply to the next git build. Updating a value without --scope keeps the variable's stored scope. PORT and HOSTNAME are set by the platform.

slew env import [file] (default .env) parses a standard dotenv file — comments, export prefixes, quoted and multiline values — and sets every variable in one go, all with the same --scope. Keys another platform injects (VERCEL_*, PORT, NODE_ENV) are skipped automatically, so the file vercel env pull writes imports cleanly. Values containing # must be quoted, like in any dotenv file.

slew logs [--tail <n>] [-f|--follow] [--since <t>] [--timestamps]

Runtime logs of the live server instance (static sites have none).

  • -f / --follow keeps watching and prints new lines as they appear. It survives scale-to-zero and cold starts: the stream tells you the instance stopped and picks up again when the next request starts it.
  • --since 15m limits output to lines newer than that age (90s, 15m, 2h, 1d).
  • --timestamps prefixes each line with its timestamp.
  • After scale-to-zero, slew logs prints the tail captured when the instance stopped (the notice goes to stderr, so piped stdout stays pure log). That tail is usually what you want when an app crashed and the instance is already gone.

slew import [vercel|netlify]

Migrate the project in the current directory — the full walkthroughs live on the Migrate from Vercel and Migrate from Netlify pages.

slew import --dry-run           # detects the platform; report + plan only
slew import vercel              # or name it explicitly
slew import netlify
  • Bare slew import detects the platform from the directory (vercel.json/.vercel/ vs netlify.toml/.netlify/); with traces of both, it asks you to name one.
  • Reads the platform's config files locally. With a token (--token, or VERCEL_TOKEN / NETLIFY_AUTH_TOKEN) and a linked directory, it also pulls build settings, env vars, the connected repo and domains from that platform's API — read-only; nothing on the old platform is ever changed.
  • Prints a compatibility report first. Hard incompatibilities (a Python or Go function runtime, say) stop the import before anything is created; warnings (Netlify Functions, rewrites/headers to move into framework config, a missing package-lock.json) are stated and the import continues.
  • Then: creates the project (--project, --org as with init), copies env vars, recreates crons (Vercel), translates redirects into slew's format, connects the GitHub repo, and prints a step-by-step DNS cutover checklist.
  • --branch overrides the branch to deploy from; --dry-run reports and plans without creating anything.

Secret env vars (which neither platform's API returns decrypted) are listed by name in the checklist to set by hand.

slew token <ai|cli> [label]

Create a token via POST /tokens. The first argument must be ai (an AI API key) or cli (an automation/deploy token); anything else fails with Usage: slew token <ai|cli> [label]. Remaining arguments are joined into the label; when omitted, the label defaults to CLI-created AI key or CLI-created deploy token.

The token value is printed to stdout and the warning to stderr, so slew token cli "CI deploy" > token.txt captures only the secret:

$ slew token cli "CI deploy"
slew_xxxxxxxxxxxxxxxx
Store this key now; it will not be shown again.

Help and version

slew --help      # logo + usage, environment variables, options
slew --version   # version read from package.json (falls back to 0.0.0)

Configuration

Credentials file

  • Location: $SLEW_CONFIG_DIR/credentials.json if SLEW_CONFIG_DIR is set, otherwise $XDG_CONFIG_HOME/slew/credentials.json, defaulting to ~/.config/slew/credentials.json.
  • Format: {"token": "..."} (JSON, trailing newline).
  • Permissions: the config directory is created with mode 0700 and the file is written and re-chmodded to 0600.
  • slew logout removes this file.

Environment variables

Variable Purpose Default
SLEW_API_URL Control-plane base URL; trailing slashes are stripped https://api.slew.cloud
SLEW_DASHBOARD_URL Web console URL used by slew dashboard; trailing slashes stripped https://app.slew.cloud
SLEW_TOKEN Token override for CI — when set, it takes precedence over the stored credentials file
SLEW_CONFIG_DIR Override the config directory $XDG_CONFIG_HOME/slew or ~/.config/slew
NO_COLOR Disable ANSI colors in the logo/branding output (colors are also disabled when stdout is not a TTY)

Gotcha: because SLEW_TOKEN always wins, a stale SLEW_TOKEN in your shell silently overrides whatever slew login stored.

Written by slew init into the project directory; read by deploy, deployments, and rollback (from the current working directory) unless --project is passed:

{
  "project": "my-site"
}

Only the project string field is read; a file without it is treated as no link. The slew.json file itself is excluded from deploys. When no link exists and no --project flag is given, commands fail with: No linked project here. Run 'slew init' first, or pass --project <name>.

How deploy works under the hood

Builds happen on your machine or in CI — the CLI ships already-built output.

  1. Resolve the project--project flag, else slew.json in the cwd.
  2. Resolve and check the directory — the optional [dir] argument (default .) must exist and be a directory.
  3. List files: recursive directory walk producing sorted, /-separated relative paths.
    • Excluded: any path containing a .git or node_modules directory component (at any depth), and the top-level slew.json link file.
    • Symlinks are never followed — only regular files are included, so a site cannot package files outside its build directory (e.g. ~/.ssh or a CI secret mount).
    • An empty result fails with Nothing to deploy: no files found in <dir>.
  4. Enforce size limits before packing:
    • Any single file over 25 MBFile exceeds the 25 MB limit: <file>.
    • Total uncompressed size over 250 MBSite exceeds the 250 MB uncompressed limit.
  5. Pack the files into an in-memory gzip tarball (tar.create with gzip: true, portable: true, follow: false).
  6. Compressed-size check: the tarball must be at most 100 MB or the deploy fails.
  7. Capture the git SHA (best effort): git rev-parse HEAD in the deploy directory; skipped silently if it isn't a git repo.
  8. Upload: POST /projects/<name>/deployments with the raw tarball body, content-type: application/gzip, an x-slew-cli-version header, and x-slew-git-sha when available.
  9. Activation is server-side: the response ({ deployment_id, url }) means the deployment is live; the CLI prints both. Rollback later re-activates an older deployment via the activate endpoint.

The CLI sends authorization: Bearer <token> on all authenticated calls and surfaces errors from the control plane's { error: { code, message } } wire shape.

Exit codes and error behavior

Situation Exit code Output
Success 0 Command output on stdout
Running slew with no command 1 Help text (with logo)
slew --help / slew --version 0 Help / version
User-facing failure (CliError) 1 slew: <message> on stderr, no stack trace
Control-plane error (ApiError) 1 slew: <message> (<code>) on stderr — e.g. slew: Missing token (unauthorized)
Unexpected error 1 The raw error (including stack trace) on stderr
Unknown command 1 slew: Unknown command '<cmd>'. See 'slew --help'.

Network failures (server unreachable) are surfaced as an ApiError with status 0 and code network_error: Could not reach <base-url> (…). When an error response has no parseable JSON body, the code falls back to unknown and the message to API returned HTTP <status>.