CLI
Every command, flag, environment variable, exit code, and JSON output shape in @steadylink/cli 0.2.0.
On this page
The SteadyLink CLI uploads files, replaces them without changing their links, and prints plain, resized, or signed links from a terminal or a CI job. Use this page to look up the exact behavior of a command or flag, or to script against its JSON output and exit codes.
The CLI is @steadylink/cli 0.2.0, built on the TypeScript SDK. It needs Node.js 18 or later.
Install#
Run it without installing through the unscoped steadylink package, which is an alias that depends on @steadylink/cli 0.2.0:
npx steadylink login
npx steadylink upload ./hero.webp --bucket marketing --publicOr install it globally to get the steadylink command:
npm install -g @steadylink/cli
steadylink --versionIn CI, pin the version so a new release cannot change behavior under you: npx @steadylink/[email protected] upload ....
Quick reference#
| Command | What it does |
|---|---|
login | Verifies an API key and saves it. |
logout | Removes the saved key. |
upload | Uploads files and folders, and prints one stable link per file. Alias: migrate. |
replace | Publishes a new revision of an existing file. The link does not change. |
ls | Lists buckets, or the contents of a bucket folder. Alias: list. |
link | Prints a delivery link, optionally resized or signed. |
rm | Deletes a file and every revision. Alias: delete. |
versions | Lists a file's revisions. |
rollback | Makes an older revision current again. |
inspect | Prints file details as JSON. |
focal | Sets the crop focal point for an image. |
help | Prints usage. Same as --help or -h, or running with no command. |
version | Prints 0.2.0. Same as --version or -v. With --json: {"version":"0.2.0"}. |
Addressing files and buckets#
Commands that take a file accept any of these forms:
- An asset ID:
3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d. - A delivery link:
https://cdn.steadylink.io/a/3f2a9c1e-...or/a/3f2a9c1e-.... Any host works, so custom-domain links do too. bucket:key, wherebucketis a bucket ID, slug, or name andkeyis the path inside it:marketing:campaign/hero.webp. A leading slash on the key is ignored.- A bare key together with
--bucket:steadylink rm campaign/hero.webp --bucket marketing.
Buckets are matched by ID, slug, or name, case-insensitively. A value shaped like a UUID is used as an ID without a lookup.
versions, rollback, inspect, and focal take an asset ID only.
Global options#
These work with every command.
Global options
--jsonflag- Print machine-readable JSON to stdout and suppress progress. Errors are printed to stdout as JSON too; see JSON output.
--quiet, -qflag- Hide progress and summary lines on stderr. Results still go to stdout.
--api-keystring- Use this key for this command only. Takes precedence over
STEADYLINK_API_KEYand the saved login. --api-urlstringDefaulthttps://api.steadylink.io- API origin.
--apiis an accepted alias. --cdn-urlstringDefaulthttps://cdn.steadylink.io- Delivery origin for printed links, for example a custom delivery domain.
--help, -hflag- Print usage and exit 0.
--version, -vflag- Print the version and exit 0.
How flags are parsed#
- A flag that takes a value uses the next token unless you write
--name=value. If the next token starts with--, the flag is treated as having no value. - These flags never take a value:
--json,--yes,--public,--private,--no-wait,--quiet,--signed,--help,--version,--stdin. - Single-letter shortcuts:
-bis--bucket,-fis--folder,-yis--yes,-qis--quiet,-his--help, and-vis--version. Note that-his help while--his height, and-qis quiet while--qis quality. --ends option parsing; everything after it is a positional argument, which is how you upload a file whose name starts with--.
Credentials#
The CLI looks for an API key in this order and uses the first it finds:
--api-key- The
STEADYLINK_API_KEYenvironment variable - The key saved by
steadylink login
The API origin follows the same pattern (--api-url, then STEADYLINK_API_URL, then the saved value), and so does the delivery origin (--cdn-url, then STEADYLINK_CDN_URL, then the saved value). Without a key, any command that calls the API exits with code 3.
The key needs assets:read for ls, link, versions, and inspect, and assets:write for upload, replace, rm, rollback, and signed links. focal calls a workspace-level route, so with an API key it needs workspace:admin.
Config file#
steadylink login saves JSON with owner-only permissions (0600, in a 0700 directory) at the first of these that applies:
| Condition | Path |
|---|---|
STEADYLINK_CONFIG is set | That path |
| Windows | %APPDATA%\steadylink\config.json |
| Otherwise | $XDG_CONFIG_HOME/steadylink/config.json, or ~/.config/steadylink/config.json |
{
"apiKey": "slk_live_...",
"apiUrl": "https://api.steadylink.io",
"cdnUrl": "https://media.example.com",
"bucket": "marketing"
}apiUrl, cdnUrl, and bucket are only written when you pass them to login. Set STEADYLINK_CONFIG to a temporary path in CI so a job never reads or writes a developer's saved login.
Environment variables#
| Variable | Used for |
|---|---|
STEADYLINK_API_KEY | API key, when --api-key is not given. |
STEADYLINK_API_URL | API origin, when --api-url is not given. |
STEADYLINK_CDN_URL | Delivery origin for printed links, when --cdn-url is not given. |
STEADYLINK_BUCKET | Default bucket for upload, when --bucket is not given. |
STEADYLINK_CONFIG | Config file path. |
Empty values are treated as unset.
Commands#
login#
Verifies an API key by listing your buckets, then saves it to the config file.
steadylink login [--api-key <key>] [--bucket <default-bucket>] [--stdin]Options
--api-keystring- The key to save. Without it,
loginprompts for the key with hidden input when run in a terminal. --stdinflag- Read the key from standard input. When stdin is not a terminal and no key is given,
loginreads it from stdin automatically. --bucket, -bstring- Save a default bucket (ID, slug, or name) for
upload. --api-urlstring- Save an API origin.
--cdn-urlstring- Save a delivery origin for printed links.
Reading from stdin keeps the key out of your shell history:
printf '%s' "$STEADYLINK_API_KEY" | steadylink login --stdin
# Logged in with slk_…9f3a. 4 buckets visible.
# Saved to /home/ana/.config/steadylink/config.jsonThe printed key is masked to its first and last four characters. An invalid key exits with code 3 and nothing is saved.
logout#
Removes the saved key. Other saved settings are kept; if none remain, the config file is deleted.
steadylink logoutupload#
Uploads files and folders and prints one stable link per file.
steadylink upload <files or folders...> [--bucket <bucket>] [--folder <path>] [--public | --private]Options
--bucket, -bstring- Destination bucket ID, slug, or name. Falls back to
STEADYLINK_BUCKET, then the bucket saved bylogin. Required one way or another. --folder, -fstringDefaultbucket root- Folder inside the bucket.
--pathis an accepted alias. --publicflag- Make each uploaded file public. Cannot be combined with
--private. --privateflag- Make each uploaded file private. Private files need a signed link.
--no-waitflag- Return as soon as the bytes are sent, before SteadyLink finishes processing. Links may be missing from the output.
--viaapi | presignedDefaultpresignedapistreams bytes through the SteadyLink API instead of directly to object storage, for networks that block the storage host.--concurrencynumberDefault4- Parallel transfers.
How files are collected:
- A file argument is uploaded into
--folderunder its own name. - A folder argument is walked recursively, and the folder structure below it is kept.
steadylink upload ./dist --folder siteturnsdist/css/app.cssintosite/css/app.css. The folder's own name is not included. - Symbolic links are skipped. Files within a folder are uploaded in sorted path order.
- Files are streamed from disk, so large files are never loaded into memory. Content types are guessed from extensions.
- Files go up in batches of 100, and each one is finalized with an idempotency key so a retried completion cannot create a duplicate revision.
- Uploading to a key that already exists adds a new revision to that file; its link stays the same.
steadylink upload ./public/media --bucket marketing --folder campaign --public
# Uploading 3 files (4.2 MB)
# campaign/hero.webp https://cdn.steadylink.io/a/3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d
# campaign/logo.svg https://cdn.steadylink.io/a/9d4e2b7a-1c3f-4a8e-b6d0-7f2c5e1a9b38
# campaign/press/kit.pdf https://cdn.steadylink.io/a/c81f0a6d-5e2b-4d7c-9a3e-1b6f8d2c4e07
# 3 uploaded.Progress is drawn as a bar on stderr in a terminal, or printed at 25 percent steps when stderr is not a terminal. If some files fail, the others are still uploaded, each failure is printed as FAILED with its reason, and the command exits with code 4.
replace#
Publishes a new revision of an existing file. The asset ID and link stay the same, so every page, email, and QR code that uses the link serves the new file.
steadylink replace <asset-id | link | bucket:key> <file>Options
--bucket, -bstring- Treat the first argument as a key inside this bucket.
--keystring- Older form:
steadylink replace --key marketing:docs/price-list.pdf ./price-list.pdf. The target comes from--keyand the file is the only positional argument.
The second argument must be exactly one file, not a folder.
steadylink replace marketing:docs/price-list.pdf ./price-list-2026.pdf
# Replaced docs/price-list.pdf with price-list-2026.pdf. Now on revision 4.
# https://cdn.steadylink.io/a/3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3dIf the new file is caught by the malware scan before commit, the command exits with code 1 and the previous revision keeps serving. If the background scan finds malware after the replacement went current, the file is left with no current revision and its link returns 404 until you rollback to a clean revision. See The scan window after a replacement. To undo a replacement, use rollback.
ls#
Lists your buckets, or the folders and files directly inside one bucket folder.
steadylink ls [bucket[:folder/]]Without an argument (and without --bucket), ls lists buckets: ID, slug, public or private, and name. With a bucket, it lists one level: folders first, then files with size, asset ID, key, and [private] for private files. Files still processing show (processing) in place of an asset ID. --folder sets the folder when you pass a bucket without a colon.
steadylink ls marketing:campaign/
# campaign/press/
# 812 KB 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d campaign/hero.webp
# 14 KB 9d4e2b7a-1c3f-4a8e-b6d0-7f2c5e1a9b38 campaign/logo.svg [private]ls does not read STEADYLINK_BUCKET or the saved default bucket; with no argument it always lists buckets.
link#
Prints a delivery link. With transform flags the link is resized or converted on delivery; with --signed it is an expiring, revocable link for a private file.
steadylink link <file> [--w 800] [--h 600] [--fm webp] [--q 80] [--fit cover] [--revision 3]
steadylink link <file> --signed [--ttl 3600] [--name label]Options
--wnumber- Width in pixels.
--widthalso works. --hnumber- Height in pixels.
--heightalso works. --fmwebp | jpg | png- Output format.
--formatalso works. Other values exit with code 2. --qnumber- Quality, clamped to 30 through 95.
--qualityalso works. --fitcover | contain | inside | outside- Resize mode. Other values exit with code 2.
--revisionnumber- Pin the link to one revision. Without it, the link follows the current revision.
--signedflag- Create a signed link. With
--revision, the grant itself is pinned to that revision. --ttlnumberDefault300- Signed link lifetime in seconds, clamped by the API to 60 through 2,592,000 (30 days).
--namestring- Label for the signed link, shown when listing grants.
<file> can be any of the addressing forms. For bucket:key, the CLI looks up the asset ID first and fails if the file is still processing.
steadylink link 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d --w 1200 --fm webp
# https://cdn.steadylink.io/a/3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d?w=1200&fm=webp
steadylink link 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d --signed --ttl 86400 --name "Acme review"
# https://cdn.steadylink.io/a/3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d?token=...
# Expires 2026-10-09T14:03:11.482913Z. Revoke with grant 6b0e3d5a-....The token in a signed link is shown only once. Revoke it early through the dashboard or revokeSignedLink.
rm#
Deletes a file and every one of its revisions and cached transforms. Its link stops working for everyone. This cannot be undone.
steadylink rm <asset-id | link | bucket:key> [--yes]Options
--yes, -yflag- Skip the confirmation prompt.
--bucket, -bstring- Treat the argument as a key inside this bucket.
Without --yes, the CLI asks Delete {key} and every revision? Its link will stop working. [y/N]. Anything other than y or yes cancels and exits 0. When there is no terminal to ask (a script or CI job), rm without --yes refuses and exits with code 2.
versions#
Lists a file's retained revisions, newest first. * marks the current one.
steadylink versions 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d
# * v4 2026-10-08 09:12:44 812 KB image/webp
# v3 2026-09-30 16:40:02 798 KB image/webp "Approved"
# v2 2026-09-12 11:05:19 1.1 MB image/pngrollback#
Makes an older revision current again. The link stays the same and serves that revision.
steadylink rollback <asset-id> <version>version must be a whole number of 1 or more.
steadylink rollback 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d 3
# Revision 3 is current again. https://cdn.steadylink.io/a/3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3dinspect#
Prints file details as JSON: bucket, key, size, content type, ETag, last modified, scan status, and the link. If the ID is a bucket rather than a file, it prints the bucket instead. Output is always JSON, with or without --json.
steadylink inspect 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3dfocal#
Sets the focal point that fit=cover crops keep in frame.
steadylink focal <asset-id> --x 0.42 --y 0.31--x and --y are both required and run from 0 (left or top) to 1 (right or bottom).
JSON output#
With --json, stdout carries exactly one JSON document and progress is suppressed, so you can pipe it into jq:
steadylink upload ./dist/*.zip --bucket releases --json | jq -r '.[].url'| Command | Output |
|---|---|
login | { "loggedIn": true, "configPath": "...", "key": "slk_…9f3a", "buckets": 4 } |
logout | { "loggedOut": true } |
upload | An array of { "file", "status", "assetId", "url", "visibility"?, "error"? }, one per file. |
replace | { "replaced", "version", "bucketId", "key", "assetId", "url" } |
ls (no bucket) | An array of buckets: { "id", "name", "slug", "isPrivate", ... } |
ls (bucket) | { "prefix", "folders", "items", "revision" }, where each item adds a url (or null while processing). |
link | { "assetId", "url" }, or with --signed: { "assetId", "url", "expiresAt", "grantId" } |
rm | { "deleted": true, "bucketId", "key" }, or { "deleted": false, "key" } when cancelled. |
versions | An array of revisions: { "id", "versionNumber", "isCurrent", "byteSize", "mime", "label", "note", "createdAt", ... } |
rollback | { "promoted", "versionNumber", "revisionId", "url" } |
focal | { "x", "y" } |
[
{
"file": "releases/app-1.4.0.zip",
"status": "ready",
"assetId": "c81f0a6d-5e2b-4d7c-9a3e-1b6f8d2c4e07",
"url": "https://cdn.steadylink.io/a/c81f0a6d-5e2b-4d7c-9a3e-1b6f8d2c4e07",
"visibility": "public"
},
{
"file": "releases/app-1.4.0.dmg",
"status": "failed",
"assetId": null,
"url": null,
"error": { "code": "upload_failed", "message": "Storage rejected the upload (HTTP 403)" }
}
]When an upload partly fails with --json, the full array is still printed and the exit code is 4. Any other error is printed to stdout as:
{
"error": {
"message": "API key expired or revoked (HTTP 401). Check the API key and its scopes.",
"status": 401,
"requestId": "7d3c1f2e-9a4b-4c6d-8e1f-0a2b3c4d5e6f"
}
}status and requestId appear for API errors only, and code only when the CLI can read a machine-readable code from the response. For an error such as a missing scope, where the API returns a structured detail, the message falls back to SteadyLink request failed with HTTP 403; run the same call with curl to see the full body. Without --json, errors go to stderr as steadylink: {message}, with [request {id}] appended when the API returned a request ID.
Exit codes#
| Code | Meaning |
|---|---|
0 | Success, including a cancelled rm prompt. |
1 | API, network, or file error, such as a missing local file, a 404, or a rejected scan. |
2 | Invalid usage: unknown command, missing argument, bad flag value, --public with --private, or rm without --yes where there is no terminal. |
3 | No API key found, or the API rejected the key with 401 or 403 (including a missing scope). |
4 | Some files in an upload failed; the others succeeded. |
Examples#
# Deploy a build folder and keep the links
steadylink upload ./dist/assets --bucket web --folder "releases/$GITHUB_SHA" --json > links.json
# Swap a PDF that is already linked from emails and QR codes
steadylink replace marketing:docs/price-list.pdf ./price-list-2026.pdf
# Share a private file for one day
steadylink link marketing:contracts/acme.pdf --signed --ttl 86400 --name "Acme review"
# Undo a bad replacement
steadylink versions 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d
steadylink rollback 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d 3
# Delete from a script
steadylink rm marketing:campaign/old.webp --yes