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

@kova1/pullr

v0.9.0

Published

Safe, fast-forward pull every git repository under a directory

Readme

pullr

CI npm Homebrew License: MIT

Safe, fast-forward pull every git repository under a directory.

Built for workspaces made of many independent repositories, where keeping everything current means cd-ing into each one.

$ pullr ~/work
+ api (main, 3 new commits)
+ web (main, 1 new commit, local changes reapplied)
~ docs (main, 2 commits held back because local changes conflict with them)
- scratch (main has no upstream)
x infra (main)
    diverged from upstream (behind 2, ahead 1): cannot fast-forward

Repos: 5  updated: 2  up to date: 0  dirty: 1  skipped: 1  failed: 1
Dirty: docs
Failed: infra

Install

# Homebrew
brew install kova1max/tap/pullr

# npm
npm install -g @kova1/pullr

To try it without installing anything:

npx @kova1/pullr --dry-run ~/work

Every release is also mirrored to GitHub Packages as @kova1max/pullr (GitHub requires the repo owner as scope). Installing from there needs a GitHub token, so the npm registry above is the easier choice.

What it will never do

pullr is built to be safe to run across a whole workspace without looking first. It will never:

  • Create a merge commit. Pulls are fast-forward only.
  • Touch a branch that has diverged from its upstream. It is reported as failed and left exactly as it was. Rebasing it is opt-in with --rebase, and a rebase that hits a conflict is aborted, leaving the repository as it was.
  • Leave your changes half-merged, or lose them. When local edits or untracked files are in the way of an update, pullr stashes them, updates, and re-applies them on top. If they conflict with the incoming commits, it puts the repository back exactly as it was - same commit, same files, same staged and unstaged changes, no stash left behind - and reports it as dirty. Edits that don't touch incoming files are never stashed at all. --no-autostash skips the stash and just reports the repository as dirty.
  • Touch a repository on a detached HEAD, or a branch with no upstream. Both are skipped.
  • Enter a nested repository. Repositories inside another repository are left to their parent.
  • Move a submodule off its branch. With --submodules, a submodule is only ever fast-forwarded on the branch it is on. (If you set git's submodule.recurse, pullr does what git pull does with it: checks submodules out at the recorded commit.)
  • Follow symlinked directories.

Usage

pullr [options] [DIR]

Short options can be combined: -rs is -r -s, and -j8 or -rj 8 set the number of jobs.

| Option                       | Default | Meaning | | :--- | :--- | :--- | | DIR | current directory | Where to look for repositories | | -j, --jobs N | 8 | Work on up to N repositories in parallel. Output is buffered per repository and printed in discovery order, so it reads the same as a sequential run. With N > 1 git never prompts for credentials; use --jobs 1 for repositories that need an interactive login. | | --max-depth N | 2 | How many directory levels below DIR to search. 0 = only DIR itself, 1 = DIR and its direct children, ... | | -n, --dry-run | | Show what a run would do without pulling, see below | | -r, --rebase | off | Rebase local commits onto the upstream instead of refusing a diverged branch (git pull --rebase). A rebase that hits a conflict is aborted and the repository is left as it was. | | --no-autostash | | Don't stash local changes that are in the way of an update; report the repository as dirty instead. Autostash is on by default, see "What it will never do". | | -s, --submodules | off | Also fast-forward each submodule that is checked out on a branch, see below | | --no-color | | Disable colored output. Also disabled when the NO_COLOR environment variable is set, or when output is not a terminal. | | -V, --version | | Print the version | | -h, --help | | Show help |

Output

Each repository gets one line: + updated, = already up to date, ~ dirty (local changes conflict with the update, which is held back), - skipped, x failed (with the reason indented below it). Exits 1 if any pull failed, 2 on invalid arguments, 0 otherwise; a dirty repository is not a failure.

Dry run

pullr --dry-run shows what a run would do, without pulling:

$ pullr --dry-run ~/work
Dry run: nothing will be pulled.
+ api (main, 3 new commits)
= web (main)
x infra (main)
    diverged from upstream (behind 2, ahead 1): cannot fast-forward
- scratch (main has no upstream)

Repos: 4  would update: 1  up to date: 1  dirty: 0  skipped: 1  would fail: 1
Failed: infra

It runs git fetch in each repository first, so the answer reflects the remote as it is now, then prints the same lines a real run would. An update that would need to stash local changes shows as + ... , local changes to autostash, and with --rebase a diverged repository shows as + ... , 2 to rebase instead of x; whether re-applying or rebasing would conflict is only known by running it. Fetching only updates remote-tracking refs: the working tree and current branch are never modified. Exit codes match a real run.

Submodules

By default pullr updates a repository the way git pull does: submodules stay where they are, unless you set git's submodule.recurse, in which case they are checked out at the commits the parent now records, as git pull would.

--submodules is for working inside submodules. Each submodule that is checked out on a branch is treated as a repository of its own - fetched, fast-forwarded with the same rules, and listed under its parent:

$ pullr --submodules ~/work
+ app (main, 2 new commits)
  + app/lib/core (main, 5 new commits)
  - app/vendor/sdk (detached HEAD)

Submodules on a detached HEAD (git's default after git submodule update) are skipped, and uninitialized ones are left out.

How it compares

| | pullr | gita | gitup | myrepos (mr) | mani | | :--- | :--- | :--- | :--- | :--- | :--- | | Needs | bash and git | Python 3.8+ | Python 3.10+ | Perl | Go binary | | Finds repos | scans a directory | register first (gita add) | scans a directory | register first (~/.mrconfig) | register first (mani.yaml) | | Pulls in parallel | 8 at a time by default | all at once, no limit | no | opt-in | opt-in | | Update | fetch + fast‑forward | git pull | fetch + fast‑forward | git pull | no built-in pull | | Merge commits | never | depends on your git config | never | depends on your git config | depends on your command | | Local changes in the way | stashed and reapplied; rolled back on conflict | git's error | skipped | git's error | - | | Dry run | yes | no | no | no | prints commands |

Why "8 at a time" and not "all at once". Many self-hosted git servers limit how many SSH connections can be opening at the same time; OpenSSH's default (MaxStartups 10:30:100) starts refusing them past 10. Fetching every repository at once then fails for reasons that have nothing to do with your repositories. On a self-hosted GitLab with 61 repositories, 16 parallel fetches produced 6-9 of those spurious failures and 32 produced 10-14, while 8 produced none. GitHub itself handled 64 at once without errors, so raise -j freely there.

License

MIT