Thanks for looking at uploads. This repo is the monorepo behind
uploads.sh — the API worker, the auth worker, the MCP
server, the Astro web app, the shared packages, and the @buildinternet/uploads
CLI that ships to npm.
The conventions below keep changes easy to review and keep the published CLI behaving consistently for the people and agents that install it. Day-to-day working conventions for agents live in AGENTS.md; this document covers the contribution loop around them.
Be respectful and assume good intent. Maintainers may edit, lock, or remove contributions that are abusive or off-topic.
- Open an issue first for anything significant — a new command, a wire format change, a new storage provider, or anything that changes how the CLI behaves for existing users. A short discussion saves a rewrite.
- Small fixes (typos, docs, an obvious bug with a test) can go straight to a PR.
- External contributors work through a fork and a pull request.
The core loop never requires access to the production infrastructure:
- No external accounts.
pnpm install,pnpm check,pnpm typecheck, andpnpm testall run secret-free — that is how CI runs them.pnpm devboots the API worker withwrangler dev, which simulates R2, KV, and D1 on disk. - Cloudflare account (optional). You need one only to deploy, or to register a workspace against a real bucket in HTTP mode.
- Hosted-only. The production bindings in each
wrangler.jsonc, the GitHub App, Stripe billing, and the deployed contract tests are not reproducible from a fork by design.
Prerequisites: Node ≥24 and pnpm ≥11 (corepack enable; versions pinned in
package.json and .nvmrc).
pnpm bootstrap # tooling, deps, env files from their *.example, wrangler types,
# local D1 migrations, and the local `default` workspace
pnpm doctor # read-only diagnosis: what is present, what is missing, the fixbootstrap is idempotent. It never overwrites an existing env file and never
re-mints an existing local workspace. doctor only reads.
Manual setup, the full dev-stack detail, and a curl smoke test live in docs/local-dev.md.
pnpm dev # API worker on :8787 (local R2 + KV + D1)
pnpm dev:web # Astro site
pnpm dev:stack # authenticated Auth + API + Web stack (portless)
pnpm dev:stack:check --json # machine-readable readiness + session smoke proofpnpm dev:stack runs through portless, so WEB
gets https://local.uploadrouter.dev instead of a bare port. Local
sign-in behaves like production — /account/* and /admin/* work in a
browser. docs/local-dev.md has
the origin table, OAuth redirect URIs, and a curl smoke test.
Local wrangler … --local calls boot miniflare, and an orphaned one can balloon
to multiple gigabytes of RAM. Prefer the repo scripts (pnpm doctor,
pnpm workspace:add, pnpm migrate:d1:local) — they wrap wrangler in a
time-bounded runner. See
AGENTS.md and
docs/ops.md.
pnpm check # oxlint + format:check — the same gate CI runs
pnpm lint:fix # autofix lint
pnpm format # autofix formatting (oxfmt + Prettier for *.astro)
pnpm typecheck # wrangler types + tsc across every workspaceMost of the repo formats with oxfmt. *.astro is the exception: oxfmt
has no Astro parser, so those files go through Prettier with
prettier-plugin-astro (options aligned with .oxfmtrc.json). Keep scripts
with define:vars or non-trivial bodies out of JSX conditionals — the plugin
cannot parse those (see the note in prettier.config.mjs). A Husky
pre-commit hook runs pnpm types then lint-staged on staged files;
pnpm install installs it.
Run pnpm types after any wrangler.jsonc change. Env is generated into a
gitignored worker-configuration.d.ts and is never hand-written, and
type-aware lint rules need those files to exist.
pnpm test # the whole suite in one Vitest process — CI's Test job
pnpm test:api # one package (also test:mcp / test:auth / test:web / test:cli)pnpm test loads vitest.projects.ts, which registers every workspace package
as a Vitest project. The filename is deliberate: a root vitest.config.ts would
hijack the per-package pnpm --filter … test runs, which use Vitest defaults.
Tests are plain Vitest with in-process fakes. There is no @cloudflare/vitest-pool-workers
setup, so a test that needs a binding fakes it rather than booting a worker.
CI runs three gates on every pull request: Lint & Format (including a changeset lint), Test, and package/bundle verification for the npm package and the remote MCP worker.
If your change is visually observable — web UI, an email template, rendered output — capture screenshots at each milestone while you work. Do not wait for the PR to exist.
uploads put ./after.png --meta path=/settings --state after
uploads screenshot http://localhost:4321/settings --out after.png --state afterOn a non-default branch both commands stage the file automatically. When the PR
opens, everything staged is promoted into one managed comment.
The github-screenshots skill has the full
workflow.
Product-facing examples use the installed binary (uploads …). Reserve
pnpm uploads … for in-repo work, where the root script builds the package
first.
Write PR descriptions for humans first. Prefer plain language over a dense bullet dump of identifiers.
Shape:
- In plain terms — one short paragraph: what changes and why it matters.
- What it does / what it is not — a few concrete bullets. Call out opt-in versus breaking, and anything deliberately deferred.
- How to try it — only when useful, using the installed CLI.
- Technical notes — optional, for implementers. Keep it secondary.
- Test plan — checkboxes for what you ran and what remains.
Titles use a conventional-commit type prefix and a plain-language subject:
feat: organize upload paths (typed destinations + optional folder rules)
fix(web): don't offer Stripe portal for comped workspaces
docs: document before/after pairing
- Common types:
feat,fix,docs,chore,refactor,test,perf. - Start the subject with a lowercase letter and lead with the outcome.
- Avoid sensational language ("comprehensive", "world-class").
Keep a PR focused, keep CI green, and add tests for behavior you change.
Want an automated review? CodeRabbit runs on demand rather than on every
PR. If a second pass would help — a tricky migration, anything touching auth,
a change you would like another read on — comment @coderabbitai review or add
the coderabbit:review label, and it will pick the PR up.
Any user-visible change to @buildinternet/uploads — CLI, client, or the
bundled MCP server — needs a changeset:
pnpm changesetTwo rules matter. The changeset header may name "@buildinternet/uploads"
and/or "@uploads/plugin". A changeset naming any other private @uploads/*
package produces an empty version PR that blocks the next npm publish, which
pnpm changeset:lint rejects as a CI gate. Never hand-edit those package
version fields. @uploads/plugin is the Claude/Codex plugin version; it
does not publish to npm.
Merging to main opens a "version packages" PR; merging that PR publishes to
npm, then to the MCP Registry as sh.uploads/mcp. Do not merge one unless
shipping is intentional. The full process is in
docs/releasing.md.
Production worker deploys normally happen through Cloudflare Workers Builds on
push to main. .github/workflows/d1-migrations.yml applies remote D1
migrations when apps/api/migrations/** changes. Manual deploys
(pnpm run deploy, or deploy:api / deploy:web / deploy:mcp /
deploy:auth / deploy:read-gate) need CLOUDFLARE_ACCOUNT_ID and
CLOUDFLARE_API_TOKEN in .env. Use pnpm run deploy, not bare pnpm deploy
— that is pnpm's built-in. pnpm run deploy does not deploy the read-gate
Worker. That project is pnpm run deploy:read-gate.
Pushing a branch does not preview the web, auth, api, or read-gate workers. Upload a preview only when the change needs a live worker; the command is in docs/previews.md.
Full setup is in docs/deploy.md; the operator runbook is docs/ops.md.
- All storage access goes through
createStorage()inpackages/storage. Never import a files-sdk adapter or touch an R2 binding from route code. Adding a provider is one new case plus its peer deps. - No workspace is special in code, including
default. A workspace is a tenant record in theREGISTRYKV namespace. Mutate those records through the versionedmutateWorkspaceRecordhelper, never a bareREGISTRY.put. - HTTP errors throw
AppErrorsubclasses from@uploads/errors. LetrespondErrorserialize the envelope; never hand-rollc.json({ error }). - Secrets never go in
wrangler.jsoncor source. Workspace secrets live in KV records. Global secrets go throughwrangler secret putor a gitignored.dev.vars. Never edit somebody's.env— change the*.exampletemplate. - Upload guardrails live in
apps/api/src/guards.ts— the byte cap and the content-type allowlist verified by magic-byte sniffing. The stored content type comes from the bytes, never the client header. - Auth is per-workspace bearer tokens, hashed and compared in constant time, with uniform 401s so workspace names cannot be enumerated.
- Keep the three skills in sync with the CLI.
skills/github-screenshotsis the workflow skill,skills/annotate-screenshotscovers callouts and redaction, andskills/uploads-cliis the full reference; all three ship to users, so a new flag belongs in the reference (and annotate-related changes also belong in annotate-screenshots). - Docs follow a house style — active voice, one idea per sentence, one term per concept. See AGENTS.md.
Open an issue for bugs, and include the CLI version (uploads --version), the
command you ran, and the output.
For a security problem, do not open a public issue. Report it privately through GitHub security advisories.
This project is licensed under Apache 2.0. Contributions you submit are licensed under the same terms. There is no separate CLA. If you contribute work owned by an employer, make sure you have their sign-off first.