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: an npm package named
slew(MIT license). It installs a single binary,slew. - Dependencies: deliberately one runtime dependency (
tar) — the CLI is small enough to read before you trust it with your deploys.
# 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.
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 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 … | 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 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 logs [--tail <n>]
Runtime logs of the live server instance (static sites have none). "Scaled to zero" means no instance is running — hit the URL once to wake it.
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>.