vercel-bulk-delete-projects
v0.1.0
Published
Bulk operations on Vercel projects — list, filter, and safely delete many projects at once from your terminal.
Downloads
136
Maintainers
Readme
vercel-bulk-delete-projects
Bulk operations on Vercel projects — list, filter, and safely delete many projects at once from your terminal.
Built because vercel project rm only deletes one project at a time, interactively, with no filtering. If you accumulate dozens of stale projects (old hackathons, preview branches, abandoned experiments), vercel-bulk-delete-projects lets you clean house in a single pass with proper safety rails.
Table of contents
- Why this exists
- Features
- Install
- Quick start
- Interactive mode
- Commands
- Filters
- Sorting
- Safety rails
- Config & token storage
- Security notes
- Architecture
- Development
- Troubleshooting
- Roadmap
Why this exists
Vercel still doesn't provide a way to delete multiple projects at once. vercel project rm handles one project at a time, interactively, with no filtering — so cleaning up a dozen old hackathons, preview branches, or abandoned experiments turns into a repetitive click-through chore. The pain is well documented on community.vercel.com:
- Feature Request: Delete multiple projects at once (open since 2025-03)
- Finally, a tool to bulk delete unwanted Vercel projects (2025-11)
- Delete multiple projects and deployments on Vercel (2025-11)
vercel-bulk-delete-projects uses the official Vercel REST API and keeps your token completely local — nothing is stored or sent anywhere except Vercel itself. No telemetry, no cloud component, no account to create.
Inspiration & prior art
The web-based Vercel Bulk Manager by @Skandan-V (aka Hyperdyn) pioneered this exact workflow as a browser UI. vercel-bulk-delete-projects is a terminal-native take on the same idea — filters, sort, dry-run, audit logs, and an interactive menu — for people who live in the CLI and want something scriptable (and a token that never leaves their machine).
Features
- Looping interactive menu when you run
vercel-bulk-delete-projectswith no subcommand — pick an action, see output, return to the menu, exit when you're done. list— print projects as a table, filter by age/name/framework, sort however you like, or dump JSON.delete— interactive checklist → review screen with refine/cancel → numeric confirmation → parallel deletes with live progress and audit log.history— browse past delete batches (date, totals, per-project outcomes). Audit logs are kept forever, one file per batch.- Filters:
--older-than <duration>,--name <glob>,--framework <name>,--no-repo. - Sort:
--sort name|last-deploy|updatedplus--reverse. - Safety: masked token prompt, file mode 0600, audit log written before any delete,
--dry-run, auth-error halt on bulk. - Storage: token in
~/.vercel-bulk/by default, or auto-discover a local.vercel-bulk/in your project (Git-style walk-up). - Zero-install usage:
npx vercel-bulk-delete-projectsonce published to npm — no global install required. - Lean: 4 direct production dependencies, ~1,200 lines of TypeScript, zero known CVEs, no install hooks.
Install
Zero-install via npx (recommended)
npx vercel-bulk-delete-projects # opens the interactive menu
npx vercel-bulk-delete-projects login # save a token
npx vercel-bulk-delete-projects list --older-than 6m
npx vercel-bulk-delete-projects delete --older-than 6mWhen invoked via npx, the CLI's working directory is wherever you ran it from. Token + audit logs go to ~/.vercel-bulk/ (the default) unless you happen to be inside a project that has its own .vercel-bulk/ folder.
Install globally
npm i -g vercel-bulk-delete-projects
vercel-bulk-delete-projects --helpBuild from source
git clone <your-repo> vercel-bulk-delete-projects
cd vercel-bulk-delete-projects
vp install
vp run build
npm link # exposes `vercel-bulk-delete-projects` on your PATHThis project uses Vite+ — install
vpglobally (npm i -g vite-plus) before runningvp install. Under the hood Vite+ wraps pnpm, vitest, and tsdown.
Dependencies
Just 4 production packages:
| Package | What for |
| ------------------- | ------------------------------ |
| @inquirer/prompts | Interactive checkbox / select |
| commander | CLI argument parsing |
| picocolors | Terminal colors (no deps) |
| zod | Runtime validation of API data |
Quick start
# 1. Save your Vercel access token
# (create one at https://vercel.com/account/tokens)
vercel-bulk-delete-projects login
# 2. Sanity check
vercel-bulk-delete-projects whoami
# 3. See what's stale
vercel-bulk-delete-projects list --older-than 6m
# 4. Interactively delete stale projects
vercel-bulk-delete-projects delete --older-than 6mInteractive mode
Run vercel-bulk-delete-projects with no subcommand to open a looping menu:
vercel-bulk-delete-projects — bulk operations on Vercel projects
? What would you like to do?
❯ List projects
Delete projects (interactive)
History (browse past delete batches)
Who am I?
Login (save Vercel token)
Logout (remove local token)
ExitPick an action, see its output, then the menu reappears automatically below a separator so you can run the next thing. The session ends only when you pick Exit or press Ctrl-C.
(output from your last action)
───────────────────────────────────────────
? What would you like to do?
❯ List projects
...Direct invocations are still one-shot — vercel-bulk-delete-projects list prints and exits, so scripts and pipes keep working.
Commands
login
Prompt for a Vercel access token (input is masked), validate it against GET /v2/user, then save to config.
vercel-bulk-delete-projects loginIf VERCEL_TOKEN is already set in your shell, login warns you that the env var will override the file.
whoami
Show the authenticated user and where the active token came from.
vercel-bulk-delete-projects whoami
# User: jane-doe ([email protected])
# Source: /Users/jane/.vercel-bulk/config.jsonlogout
Remove the locally stored token.
vercel-bulk-delete-projects logout # confirms, removes config.json
vercel-bulk-delete-projects logout --yes # skip confirmation (for scripts)
vercel-bulk-delete-projects logout --all # also delete audit logshistory
Browse past delete batches. Reads every audit log in <config-dir>/logs/ and shows them in an interactive list.
vercel-bulk-delete-projects historyFound 3 delete batches.
? Select a batch to inspect
❯ 2026-04-21 10:15:23 · 7 removed, 1 failed (jane-doe)
2026-04-20 16:02:48 · 3 removed (jane-doe)
2026-04-20 14:33:12 · 12 removed, 2 failed (jane-doe)
Exit historyPick a batch to see every project that was in it, with status icons (✓ deleted, ○ already gone, ✗ failed, … pending). The command loops — after viewing one batch, you go back to the list until you pick Exit.
Use --json to dump all batches (including per-project results) to stdout:
vercel-bulk-delete-projects history --json > all-deletes.jsonlist
Print all projects as a table (or JSON).
vercel-bulk-delete-projects list
vercel-bulk-delete-projects list --older-than 6m --sort name
vercel-bulk-delete-projects list --framework nextjs --json > projects.jsonOutput:
Name Framework Last Deploy Repo
───────────────────── ────────── ───────────── ─────────────────────
my-hackathon-2024 nextjs 14 months ago github.com/you/hack24
weekend-experiment vite 9 months ago —
portfolio-v2 nextjs 7 months ago github.com/you/port2
3 projects match (of 47 total).delete
Interactive bulk delete. The flow:
- Fetches all projects (paginated).
- Opens a checkbox picker — pre-selected to your filters (or empty if you passed no filters).
- Shows a review screen listing every selected project with the action menu:
Proceed/Remove items/Cancel. If you choose Remove, an unchecking sub-picker lets you drop items and loop back to review. - Numeric confirmation: type the batch size (e.g.,
7) to proceed. - Writes the audit log (pending state) to
~/.vercel-bulk/logs/. - Deletes in parallel (default 5 concurrent), respecting Vercel rate limits (429 → retry after header; 5xx → exponential backoff).
- Rewrites the audit log with final statuses.
- Prints
N deleted, M failed. Log: ....
vercel-bulk-delete-projects delete --older-than 6m # pre-select stale
vercel-bulk-delete-projects delete --name "*-preview" --dry-run # see what would happen
vercel-bulk-delete-projects delete --framework nextjs --yes # scripts (skips confirm)
vercel-bulk-delete-projects delete --concurrency 3 # gentler on rate limitsFilters
Shared by list and delete. Multiple filters AND together.
| Flag | Matches | Example |
| ------------------------- | ----------------------------------------------------------------- | ------------------------------------- |
| --older-than <duration> | No deployment since. Projects with zero deployments always match. | --older-than 6m, 30d, 1y |
| --name <pattern> | Shell-style glob (*, ?) against project name. | --name "*-preview", test-* |
| --framework <name> | Exact match against Vercel's framework field. | --framework nextjs, vite, nitro |
| --no-repo | Only projects without a connected git repo. | --no-repo |
Duration grammar: <N>d (days), <N>m (months — 30d), <N>y (years — 365d).
Sorting
vercel-bulk-delete-projects list --sort name # alphabetical (default asc)
vercel-bulk-delete-projects list --sort last-deploy # oldest first (default)
vercel-bulk-delete-projects list --sort updated --reverse # most-recently updated first| --sort | Order |
| ------------- | ------------------------------------------------------------------------- |
| name | Alphabetical by project name. |
| last-deploy | By most recent deployment timestamp. Default. "Never deployed" first. |
| updated | By Vercel's updatedAt project field. |
--reverse flips whichever order you picked.
Safety rails
Deletion is irreversible. vercel-bulk-delete-projects delete layers multiple protections:
- Empty default selection. Running
deletewith no filter flags opens the picker with nothing pre-checked. You must explicitly choose. - Filter pre-select. Passing filter flags pre-checks matching projects so the common path ("delete everything older than 6m") is fast, but you can still uncheck any of them.
- Review screen. After the picker, you see the full list of selected projects again, with three choices:
- Proceed → numeric confirmation.
- Remove items → a sub-picker with everything checked; uncheck to drop, then back to review.
- Cancel → exit without any change.
- Numeric confirmation. You type the exact batch count (e.g.,
7for 7 projects). Forces attention on the size. - Audit log. Before the first
DELETEcall,vercel-bulk-delete-projectswrites~/.vercel-bulk/logs/deleted-<ISO-timestamp>.jsonlisting every selected project withstatus: "pending". After the batch, the file is rewritten with finaldeleted/already_gone/failedstatuses. If the process is killed mid-batch, the pending log survives so you can see what was in-flight. - Dry run. Pass
--dry-runto see exactly which projects would be deleted without any API calls. - Auth halt. If a
DELETEreturns401/403, the whole batch stops immediately (per-project404"already deleted" is treated as success and continues).
Config & token storage
Storage locations (resolved in order, first wins)
--token <token>flag (per-invocation override).VERCEL_TOKENenv var../.vercel-bulk/config.jsonor nearest ancestor (Git-style walk-up, max 10 levels).~/.vercel-bulk/config.json(default).
Using a local config
mkdir .vercel-bulk in your project root, then vercel-bulk-delete-projects login — the token goes to ./.vercel-bulk/config.json instead of your home directory. Handy for keeping project-specific tokens separate. The .gitignore already excludes .vercel-bulk/ so the token never commits.
Config file shape
{
"token": "vercel_pat_...",
"user": { "id": "user_abc123", "username": "jane-doe" }
}Mode 0600 on Unix (owner read/write only). Windows uses default NTFS permissions.
Override the config directory entirely
Set VERCEL_BULK_CONFIG_DIR=/some/path to override both defaults. Mostly used by tests; useful if you want to keep configs out of $HOME.
Security notes
- Never commit
.vercel-bulk/. It's in.gitignore, but don'tgit add -fit. --token <value>is visible inpsoutput. On multi-user machines, preferVERCEL_TOKENenv var or the saved config.- Shell history. Avoid
VERCEL_TOKEN=xxx vercel-bulk-delete-projects ...— the token will land in~/.bash_history/~/.zsh_history. Export it for the session (export VERCEL_TOKEN=xxx) or use thelogincommand (masked input). - Audit logs contain project metadata only. No token, no session data, no API response bodies — just which projects you selected and their delete outcomes.
- Vercel API calls are HTTPS-only (
https://api.vercel.com), no fallback. - Dependency audit:
pnpm audit --prod→ 0 vulnerabilities. No install hooks in any dep. Noeval/new Function()/ base64 decoding in production code.
Architecture
┌────────────────────────────────────────────────┐
│ src/cli.ts + src/commands/*.ts │ commander + @inquirer/prompts + picocolors
│ parses args, runs prompts, renders output │
└──────────────────┬─────────────────────────────┘
│ calls
┌──────────────────▼─────────────────────────────┐
│ src/core/*.ts │ zod + native fetch (no other deps)
│ API client, domain types, filters, sort │ reusable by a future dashboard
└──────────────────┬─────────────────────────────┘
│ calls
┌──────────────────▼─────────────────────────────┐
│ Vercel REST API (https://api.vercel.com) │
│ GET /v2/user, GET /v9/projects, DELETE /v9/projects/:id │
└────────────────────────────────────────────────┘Layer separation is enforced by import direction: core/ imports nothing from commands/ or ui/, so the same core logic can later back a web dashboard without modification.
Directory layout
src/
cli.ts commander entry, global flags, default action → menu
commands/
login.ts prompt for token, validate, save
whoami.ts show current user
logout.ts remove local config (+ optional logs)
list.ts fetch → filter → sort → render table or JSON
delete.ts fetch → filter → sort → pick → review → confirm → delete
menu.ts interactive select routing to the above
core/
api.ts fetch wrapper, 429 + 5xx retry with backoff
user.ts getUser, validateToken
projects.ts listProjects (paginated), deleteProject
filters.ts applyFilters + parseDuration (pure)
sort.ts sortProjects (pure)
schemas.ts zod schemas for API responses
types.ts FilterOptions, SortOptions, domain types
config/
paths.ts configDir (env > local > home), auditLogPath
token.ts loadConfig, saveToken, resolveToken (async fs)
audit-log.ts writeAuditLog (async fs)
ui/
format.ts humanizeAge, formatDate, colors re-export
table.ts renderProjectTable
picker.ts @inquirer checkbox wrapper
confirm.ts numeric confirm prompt
review.ts post-pick review loop (proceed/refine/cancel)
errors.ts AuthError, RateLimitError, ApiError, ValidationError
test/
core/ unit tests (paths, token, audit-log, errors, schemas,
api, user, projects, filters, sort, format, table)
commands/ integration tests (login, whoami, logout, list, delete)
fixtures/ sample Vercel API responses
smoke.test.ts spawn built binary, assert --help worksDevelopment
vp install # install deps
vp exec tsx src/cli.ts --help # run in dev (no build)
vp run dev -- --help # same via npm script
vp test # full Vitest suite (81 tests across 19 files)
vp test --run test/core/filters.test.ts # single file
vp run typecheck # tsc --noEmit
vp check # lint + format + types
vp run build # bundle to dist/cli.mjs via tsdown
node dist/cli.mjs --help # smoke-test the bundleKey choices
- TypeScript
moduleResolution: "Bundler"— no.jsextensions on imports; bundler resolves at build time. noEmit: true—tsconly type-checks;vp pack(tsdown) emits the bundle.fs/promiseseverywhere that writes or reads config / audit logs — no event-loop blocking.- zod
.passthrough()on Vercel response schemas so upstream shape changes don't break the CLI; only required fields we actually use are validated.
Troubleshooting
The Vercel API returned a project with unexpected types. The CLI now shows which field(s) failed. If it's a required field we weren't expecting, file an issue with the zod error output.
No token resolvable from flag → env → local .vercel-bulk/ → ~/.vercel-bulk/. Run vercel-bulk-delete-projects login to save one.
Your Vercel token lacks permission on the project's team. The batch halts on the first 403 to avoid partial damage. Check the project's team scope vs. your token's team scope. Team-switching support is on the roadmap.
The CLI respects the Retry-After header automatically. If you're hitting frequently, reduce concurrency: --concurrency 3 (or lower). The default is 5.
Vercel deletion is permanent — there's no undo. The audit log at ~/.vercel-bulk/logs/deleted-<timestamp>.json records the project name, framework, and linked repo, so you can recreate the Vercel project and re-connect the git source. Redeployments recover.
Roadmap
Planned, not yet built:
- Team / scope support —
--scope <team>flag +vercel-bulk-delete-projects switchsubcommand. - Export before delete — snapshot project settings (env vars, domains, build config) to JSON before removal.
- Bulk env var ops — copy/delete env vars across many projects.
- Web dashboard — React + TanStack Start, importing
src/core/directly as a library.
Open an issue if you want one of these prioritized, or a feature not listed.
License
MIT (or whatever the repo owner chooses — update this line).
