caskit
v1.2.0
Published
Content-addressed storage CLI for S3-compatible endpoints (R2), with automatic deduplication
Downloads
17
Maintainers
Readme
caskit
Content-addressed storage over an S3-compatible endpoint, built for org-wide "marketing as code" asset pipelines. Upload a file and get back a stable, content-derived URL — with automatic deduplication.
Uploading Logo FINAL.png produces an object like:
logo-final-cc451006.png...at the root of the bucket, where cc451006 is derived from the file's
bytes. Identical content always yields the same key, so re-uploading is a no-op
and the same URL comes back.
Install
npm install -g caskitOr run it without installing:
npx caskit logo.pngRequires Node 18+. The published package ships compiled JavaScript with type declarations — no build step for consumers.
Quick start
# 1. Write a .env with the variables caskit needs.
caskit init
# 2. Fill in your R2 credentials (see the table below).
$EDITOR .env
# 3. Upload.
caskit logo.png
# https://assets.example.com/logo-cc451006.pngNew to R2? CLOUDFLARE_R2_SETUP.md walks through creating a bucket, API tokens, and a public hostname from scratch.
Configuration
Set these as environment variables or in a .env file in the directory you run
caskit from. All variables are prefixed with CASKIT_ to avoid clashing with
other tooling. Real environment variables always override .env.
| Variable | Required | Description |
| --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| CASKIT_S3_ENDPOINT | yes | R2 S3 API endpoint (https://<account>.r2.cloudflarestorage.com) |
| CASKIT_S3_ACCESS_KEY_ID | yes | Access key |
| CASKIT_S3_SECRET_ACCESS_KEY | yes | Secret key |
| CASKIT_S3_BUCKET | yes | Target bucket |
| CASKIT_S3_REGION | no | Defaults to auto (correct for R2) |
| CASKIT_S3_PUBLIC_URL | no | Public base URL for the printed result (r2.dev or custom domain). Falls back to the S3 endpoint, which is not publicly readable. |
caskit init
Creates .env if it does not exist, or appends only the CASKIT_ variables
that are missing from it.
It is append-only: it never rewrites, reorders, or clears anything already
in the file, so it is safe to run against a .env that holds real secrets or
variables belonging to other tools. Re-running it is a no-op once every variable
is present.
Usage
# Upload one or more files; each public URL is printed to stdout, one per line.
caskit upload logo.png banner.jpg
# `upload` is the default command, so this also works:
caskit logo.png
# Capture the URL straight into a variable:
URL=$(caskit logo.png)
# Re-upload even if the content already exists:
caskit --force logo.pngJSON output
--json emits a single JSON array, one entry per file — so stdout is one
valid JSON document you can store as an asset manifest:
caskit --json assets/* > assets.json[
{
"file": "logo.png",
"key": "logo-cc451006.png",
"url": "https://assets.example.com/logo-cc451006.png",
"deduplicated": false
},
{
"file": "banner.jpg",
"key": "banner-e5f6a7b8.jpg",
"url": "https://assets.example.com/banner-e5f6a7b8.jpg",
"deduplicated": true
}
]It is always an array, even for a single file, so consumers never have to type-check the shape.
Output contract
- stdout carries only the URLs — or the JSON array under
--json. Clean for piping. - stderr carries dedup notices and errors.
- Exit code is non-zero if any file fails.
- A file that fails to upload is reported on stderr and omitted from the
--jsonarray. The array only ever describes objects that really landed in the bucket, so check the exit code rather than assuming the manifest is complete.
How it works
- Reads the file and computes a SHA-256 of its bytes (first 8 hex chars).
- Builds the key
slug(name)-<hash>.extat the bucket root. HEADs the key; if it already exists, skips the upload (dedup).- Otherwise
PUTs the bytes with a detectedContent-Type. - Prints the public URL.
Requests are SigV4-signed via aws4fetch.
Dependencies
Runtime dependencies are deliberately minimal — three zero-dependency,
maintained packages: aws4fetch (S3 signing), commander (CLI), dotenv
(.env loading). TypeScript is a dev-only dependency.
Contributing
See CONTRIBUTING.md for the development workflow.
Releases are automated with
semantic-release: the
version, changelog, git tag, GitHub release, and npm publish are all derived
from Conventional Commits on main.
Commit messages therefore matter — fix: cuts a patch, feat: a minor, and a
BREAKING CHANGE: footer a major. Anything else (chore:, docs:) releases
nothing.
License
MIT
