Skip to content

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:

Terminal
npx steadylink login
npx steadylink upload ./hero.webp --bucket marketing --public

Or install it globally to get the steadylink command:

Terminal
npm install -g @steadylink/cli
steadylink --version

In CI, pin the version so a new release cannot change behavior under you: npx @steadylink/[email protected] upload ....

Quick reference#

CommandWhat it does
loginVerifies an API key and saves it.
logoutRemoves the saved key.
uploadUploads files and folders, and prints one stable link per file. Alias: migrate.
replacePublishes a new revision of an existing file. The link does not change.
lsLists buckets, or the contents of a bucket folder. Alias: list.
linkPrints a delivery link, optionally resized or signed.
rmDeletes a file and every revision. Alias: delete.
versionsLists a file's revisions.
rollbackMakes an older revision current again.
inspectPrints file details as JSON.
focalSets the crop focal point for an image.
helpPrints usage. Same as --help or -h, or running with no command.
versionPrints 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, where bucket is a bucket ID, slug, or name and key is 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_KEY and the saved login.
--api-urlstringDefault https://api.steadylink.io
API origin. --api is an accepted alias.
--cdn-urlstringDefault https://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: -b is --bucket, -f is --folder, -y is --yes, -q is --quiet, -h is --help, and -v is --version. Note that -h is help while --h is height, and -q is quiet while --q is 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:

  1. --api-key
  2. The STEADYLINK_API_KEY environment variable
  3. 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:

ConditionPath
STEADYLINK_CONFIG is setThat path
Windows%APPDATA%\steadylink\config.json
Otherwise$XDG_CONFIG_HOME/steadylink/config.json, or ~/.config/steadylink/config.json
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#

VariableUsed for
STEADYLINK_API_KEYAPI key, when --api-key is not given.
STEADYLINK_API_URLAPI origin, when --api-url is not given.
STEADYLINK_CDN_URLDelivery origin for printed links, when --cdn-url is not given.
STEADYLINK_BUCKETDefault bucket for upload, when --bucket is not given.
STEADYLINK_CONFIGConfig 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.

Terminal
steadylink login [--api-key <key>] [--bucket <default-bucket>] [--stdin]

Options

--api-keystring
The key to save. Without it, login prompts 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, login reads 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:

Terminal
printf '%s' "$STEADYLINK_API_KEY" | steadylink login --stdin
# Logged in with slk_…9f3a. 4 buckets visible.
# Saved to /home/ana/.config/steadylink/config.json

The 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.

Terminal
steadylink logout

upload#

Uploads files and folders and prints one stable link per file.

Terminal
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 by login. Required one way or another.
--folder, -fstringDefault bucket root
Folder inside the bucket. --path is 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 | presignedDefault presigned
api streams bytes through the SteadyLink API instead of directly to object storage, for networks that block the storage host.
--concurrencynumberDefault 4
Parallel transfers.

How files are collected:

  • A file argument is uploaded into --folder under its own name.
  • A folder argument is walked recursively, and the folder structure below it is kept. steadylink upload ./dist --folder site turns dist/css/app.css into site/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.
Terminal
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.

Terminal
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 --key and the file is the only positional argument.

The second argument must be exactly one file, not a folder.

Terminal
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-5d8e6a1b0c3d

If 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.

Terminal
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.

Terminal
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.

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.

Terminal
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. --width also works.
--hnumber
Height in pixels. --height also works.
--fmwebp | jpg | png
Output format. --format also works. Other values exit with code 2.
--qnumber
Quality, clamped to 30 through 95. --quality also 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.
--ttlnumberDefault 300
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.

Terminal
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.

Terminal
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.

Terminal
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/png

rollback#

Makes an older revision current again. The link stays the same and serves that revision.

Terminal
steadylink rollback <asset-id> <version>

version must be a whole number of 1 or more.

Terminal
steadylink rollback 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d 3
# Revision 3 is current again. https://cdn.steadylink.io/a/3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d

inspect#

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.

Terminal
steadylink inspect 3f2a9c1e-7b4d-4e1a-9c2f-5d8e6a1b0c3d

focal#

Sets the focal point that fit=cover crops keep in frame.

Terminal
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:

Terminal
steadylink upload ./dist/*.zip --bucket releases --json | jq -r '.[].url'
CommandOutput
login{ "loggedIn": true, "configPath": "...", "key": "slk_…9f3a", "buckets": 4 }
logout{ "loggedOut": true }
uploadAn 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.
versionsAn array of revisions: { "id", "versionNumber", "isCurrent", "byteSize", "mime", "label", "note", "createdAt", ... }
rollback{ "promoted", "versionNumber", "revisionId", "url" }
focal{ "x", "y" }
upload --json
[
  {
    "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
{
  "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#

CodeMeaning
0Success, including a cancelled rm prompt.
1API, network, or file error, such as a missing local file, a 404, or a rejected scan.
2Invalid usage: unknown command, missing argument, bad flag value, --public with --private, or rm without --yes where there is no terminal.
3No API key found, or the API rejected the key with 401 or 403 (including a missing scope).
4Some files in an upload failed; the others succeeded.

Examples#

Terminal
# 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

Next steps#