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 nameslewis 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.
POST /auth/deviceto the control plane to start the flow.- Prints the one-time user code and verification URL, and opens the URL in your browser (best effort —
openon macOS,starton Windows,xdg-openelsewhere; the URL is printed either way). - Polls
POST /auth/device/pollon the server-provided interval until the deadline (expires_in). API error codes are handled:authorization_pendingkeeps polling,slow_downadds 5 seconds to the interval,expired_tokenand timing out both fail with "The code expired. Run 'slew login' again.", andaccess_deniedfails with "Authorization was denied on GitHub." - On success, writes the token to the credentials file (mode
0600), then callsGET /meto 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.
- Requires a token (stored or
SLEW_TOKEN); fails with "Not logged in" otherwise. - Validates the token via
GET /mefirst, so a dead token doesn't just bounce you to the login form. - 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.jsonand no name is given, it printsAlready 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 codename_taken, the CLI triesGET /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.jsonin 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/--followkeeps 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 15mlimits output to lines newer than that age (90s,15m,2h,1d).--timestampsprefixes each line with its timestamp.- After scale-to-zero,
slew logsprints 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 importdetects the platform from the directory (vercel.json/.vercel/vsnetlify.toml/.netlify/); with traces of both, it asks you to name one. - Reads the platform's config files locally. With a token (
--token, orVERCEL_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,--orgas withinit), 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. --branchoverrides the branch to deploy from;--dry-runreports 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.jsonifSLEW_CONFIG_DIRis 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
0700and the file is written and re-chmodded to0600. slew logoutremoves 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.
The slew.json project link
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.
- Resolve the project —
--projectflag, elseslew.jsonin the cwd. - Resolve and check the directory — the optional
[dir]argument (default.) must exist and be a directory. - List files: recursive directory walk producing sorted,
/-separated relative paths.- Excluded: any path containing a
.gitornode_modulesdirectory component (at any depth), and the top-levelslew.jsonlink file. - Symlinks are never followed — only regular files are included, so a site cannot package files outside its build directory (e.g.
~/.sshor a CI secret mount). - An empty result fails with
Nothing to deploy: no files found in <dir>.
- Excluded: any path containing a
- Enforce size limits before packing:
- Any single file over 25 MB →
File exceeds the 25 MB limit: <file>. - Total uncompressed size over 250 MB →
Site exceeds the 250 MB uncompressed limit.
- Any single file over 25 MB →
- Pack the files into an in-memory gzip tarball (
tar.createwithgzip: true,portable: true,follow: false). - Compressed-size check: the tarball must be at most 100 MB or the deploy fails.
- Capture the git SHA (best effort):
git rev-parse HEADin the deploy directory; skipped silently if it isn't a git repo. - Upload:
POST /projects/<name>/deploymentswith the raw tarball body,content-type: application/gzip, anx-slew-cli-versionheader, andx-slew-git-shawhen available. - 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 theactivateendpoint.
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>.