npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

branchtidy

v0.1.0

Published

Find and delete merged & stale git branches, safely. Dry-run by default, never touches main/master/develop/current, handles local + remote. Zero dependencies.

Readme

branchtidy

Delete merged & stale git branches — safely. branchtidy finds the local (and optionally remote) branches that are already merged, or haven't seen a commit in N days, previews them, and deletes them in one batch. It is dry-run by default, refuses to touch main / master / develop / your current branch, and won't nuke unmerged work unless you explicitly ask.

Zero dependencies. Zero config. No daemon, no account, nothing to set up.

npx branchtidy
branchtidy  local branches  ·  default main  ·  stale > 90d

  BRANCH            LAST COMMIT   MERGED   ACTION
  feature/login     12d ago       yes      delete (merged)
  feature/old-poc   210d ago      no       delete (stale 210d)
  main              2d ago        no       keep (protected)
  feature/wip       3d ago        no       keep (active)

Dry run. 2 branch(es) WOULD be deleted. Re-run with --delete to apply.

Nothing was deleted. That's the point — you read the table, then decide.

Why another branch cleaner?

Everyone reinvents this as a throwaway git branch --merged | grep -v ... | xargs one-liner, and those one-liners are exactly how people delete branches they wanted. branchtidy's whole pitch is safety + zero config:

  • Dry-run is the default. No flags → it only prints what it would do.
  • Real deletion is gated twice: --delete and an interactive confirm (skip the prompt only with --yes).
  • Protected branches are never candidates: main, master, develop, the current HEAD, plus anything you pass to --protect.
  • Merged vs unmerged is respected. Merged branches use the safe git branch -d. Unmerged branches are only deletable with an explicit --force (which maps to git branch -D).
  • Remote deletion is double-gated: it requires --remote --delete and its own confirmation, and uses git push <remote> --delete.

When in doubt, branchtidy keeps the branch.

Install

npx branchtidy            # no install, run on demand
npm i -g branchtidy       # or install the `branchtidy` command globally

There's an identical Python build too: pipx run branchtidy / pip install branchtidy (see branchtidy-py). Both ports share one selection-vector table, so they make byte-for-byte identical decisions.

Usage

branchtidy [options]            # dry-run preview (default — deletes nothing)
branchtidy --delete             # actually delete, after a confirm

| Option | Description | | --- | --- | | --delete | Perform deletion. Without it, branchtidy only previews. | | --yes | Skip the interactive confirm (use with --delete, e.g. in scripts). | | --stale <dur> | Staleness threshold. Default 90d. Accepts 30d, 2w, 12h, 45m, 30s, or a bare number (days). | | --merged-only | Only delete merged branches; never delete on age alone. | | --remote [name] | Operate on remote-tracking branches (default remote: origin). | | --protect <a,b> | Extra branch names to never delete (comma-separated). | | --force | Allow deleting unmerged branches (maps to git branch -D). | | --json | Machine-readable output; never prompts, never deletes (preview only). | | --no-color | Disable ANSI colors. | | -h, --help | Show help. | | -v, --version | Print version. |

Exit codes: 0 success/clean, 1 one or more deletions failed, 2 usage or environment error (e.g. not a git repo).

Examples

# what WOULD be cleaned up, right now?
branchtidy

# stricter window, only merged branches, do it (with a confirm)
branchtidy --stale 30d --merged-only --delete

# clean up gone-stale remote branches on origin (double-gated + confirm)
branchtidy --remote origin --delete

# delete unmerged stale branches too — you have to ask for it
branchtidy --stale 180d --delete --force

# protect a couple of long-lived branches by name
branchtidy --protect release/v1,staging --delete

# pipe the plan somewhere
branchtidy --json | jq '.toDelete'

How it decides

For each branch branchtidy looks at: is it the current HEAD? is it protected? is it merged into the default branch? how old is its last commit? Then:

  1. current branch → keep (current)
  2. protected (default set or --protect) → keep (protected)
  3. merged → delete (merged)
  4. otherwise, if older than --staledelete (stale <N>d)
  5. otherwise → keep (active)

In --merged-only mode, step 4 is skipped entirely — age never causes a deletion.

The default branch is resolved from origin/HEAD when available, otherwise it falls back to main, then master.

Design notes

  • One pure function at the core. selectBranches(branches, policy, nowMs) has no git, no fs, no clock — it's a pure data→data transform that returns {toDelete, toKeep} with a reason on every branch. The CLI is a thin git wrapper around it. That's what makes the Node and Python ports verifiably identical: they run the same vector table.
  • Time is integer math. Ages are computed from committerdate:unix against a single captured now in milliseconds — no Date/datetime parity to worry about between languages.
  • Safe by construction. Protected and current branches are filtered out before any staleness logic runs, the staleness test is a strict > (a branch exactly at the threshold is kept), and deletion always passes through the safe git branch -d unless you opt into -D with --force.

License

MIT