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

@airmrcr/gitx

v0.1.0

Published

Wrangle many git repositories at once, in parallel, without losing your mind.

Readme

gitx

Build Status Downloads Release License

gitx is a command-line tool for people who don't have a repository — they have ninety of them, scattered across an org, each one slightly behind, two of them on a branch you don't remember creating, and one of them mysteriously dirty because of an .idea/ folder.

It clones them, updates them, prunes their dead branches, installs their dependencies, and tells you which CI runs are still chewing away in the background. All at the same time, with a live progress display.

$ gitx update --install --tidy
⊘ billing dirty working tree 102ms
⊘ legacy-soap on feature/never-merging 118ms
✔ api-gateway up to date 486ms
✔ auth-service updated 1.8s
✖ reporting fetch failed 2.1s
◐ design-system installing (pnpm)
◐ notifications fetching

Done!
Updated: 2 | Skipped: 2 | Failed: 1 | Installed: 1

Contents


Install

gitx needs Node 24 or newer and git. That's it — no forge CLI.

The commands that talk to GitHub or GitLab (runs, clone-all, list --all/--missing) need an API token. If you have ever authenticated git against the host, you already have one and there is nothing to do. See Talking to your forge.

$ npm install -g @airmrcr/gitx

Or, if you'd rather not commit:

$ npx @airmrcr/gitx update

From source:

$ git clone https://github.com/airmrcr/gitx.git
$ cd gitx
$ pnpm install
$ pnpm build
$ pnpm add -g .

Getting started

There is no setup step. Just run something:

$ gitx update

The first time, gitx asks which forge you use, on which host, where your repositories live and which owner they belong to, then remembers all of it in ~/.gitxconfig. The directory is created for you if it doesn't already exist. Cloning asks one more question the first time: HTTPS or SSH remotes.

Prefer to do it up front? Or running in CI, where nobody is around to answer questions?

$ gitx config set core.baseDir ~/dev
$ gitx config set remote.provider github
$ gitx config set remote.owner acme

Prompts are drawn on stderr, never stdout, so a question can never end up inside output you're piping somewhere. Pass --no-input and gitx will never prompt; it fails with a helpful message instead. Every prompted value can also be supplied per-invocation with --base-dir, --owner, --provider and --host.

$ gitx -o acme list                  # acme
$ gitx list -o widgets               # widgets
$ gitx -o acme list -o widgets       # widgets — the one next to the command

Which means a one-off peek at somebody else's estate never disturbs your config:

$ gitx list --all -o gitlab-org -P gitlab
$ gitx runs -o acme

Now grab everything:

$ gitx clone-all --install

And, while you're here, turn on tab completion:

$ source <(gitx completion bash)   # see `gitx completion --help` to make it permanent

Commands

Every multi-repo command accepts name globs, runs in parallel, and prints a summary at the end.

gitx clone

Clone one or more repositories into your workspace.

$ gitx clone api-gateway
$ gitx clone api-gateway auth-service --install
$ gitx clone someone-else/their-repo    # override the configured owner

The first clone asks whether you want HTTPS or SSH remotes and remembers the answer as remote.protocol. HTTPS is the default when nobody is around to ask, since it works without a key on the machine and gitx auth login has already given git the credential for it.

gitx clone-all

Clone everything the configured owner has. Already-cloned repositories are skipped, so it doubles as "did anything new appear?".

$ gitx clone-all
$ gitx clone-all 'platform-*' --install
$ gitx clone-all --visibility private --archived

gitx completion

Tab completion for bash, zsh and fish. The script is generated from the live command tree, so it's only as current as the last time you generated it:

$ gitx completion bash > ~/.local/share/bash-completion/completions/gitx
$ gitx completion zsh > "${fpath[1]}/_gitx"
$ gitx completion fish > ~/.config/fish/completions/gitx.fish

Or regenerate it every time a shell starts, so an upgrade can never leave it stale — a little slower to start a shell, but it can never drift either:

$ echo 'source <(gitx completion bash)' >> ~/.bashrc
$ echo 'eval "$(gitx completion zsh)"' >> ~/.zshrc     # after compinit
$ echo 'gitx completion fish | source' >> ~/.config/fish/config.fish

gitx config

Configuration lives in ~/.gitxconfig.

$ gitx config set core.baseDir ~/dev
$ gitx config get core.baseDir
$ gitx config set --add skip.install 'legacy-*'
$ gitx config list                   # what you've set
$ gitx config list --all --describe  # every key, its default and what it does
$ gitx config unset skip.install --all
$ gitx config path
$ gitx config edit

Keys are validated on write, so a typo is caught immediately rather than silently ignored six weeks later:

$ gitx config set core.concurency 4
gitx: unknown configuration key: core.concurency
Run `gitx config list --all --describe` to see every supported key.

Some settings can be overridden for one repository. Pass --repo <name>, or just --repo to mean whichever repository your current directory is in:

$ gitx config set core.editor idea --repo legacy-monolith   # this repo only
$ gitx config set update.tidy false --repo                  # from inside that repo
$ gitx config get core.editor --repo legacy-monolith

This writes a subsection, so ~/.gitxconfig ends up with:

[core "legacy-monolith"]
	editor = idea

Not every key supports this — only ones where a per-repository override actually means something: core.editor, update.tidy, update.submodules, and the per-package-manager install.* settings (below). gitx config set remote.owner acme --repo x is rejected the same way an unknown key is, since an owner isn't a per-repository concept. A get without a matching override falls back to the plain value, so --repo only shows you something different where a repository actually has its own answer.

gitx default

Check out the remote's default branch — whatever it's actually called. Reads origin/HEAD, repairs it if it's stale, and falls back to main, master, develop or trunk.

$ gitx default

gitx list

What have you actually got?

$ gitx list                             # what's cloned
$ gitx list 'api-*'                     # what's cloned and matches
$ gitx list --all                       # cloned ✔ and not-yet-cloned ○, together
$ gitx list --missing                   # only what you haven't cloned
$ gitx list --missing --porcelain | xargs gitx clone

The default is purely local: no network, no token, no waiting. --all and --missing ask your forge what exists, so they need a token and accept the same --visibility, --archived and --limit options as clone-all.

gitx open

Open a cloned repository in your editor, from anywhere, by name.

$ gitx open api-gateway
$ gitx open api-gateway --editor 'code -n'   # just this once

The first run asks which command opens a repository and remembers it as core.editor. The repository's path is appended as the command's final argument; anything else you write, such as -n or --wait, is passed through in between. --editor overrides it for a single run without changing what's remembered.

gitx pull

Pull the current branch. Unknown flags are handed straight to git pull, so muscle memory still works:

$ gitx pull
$ gitx pull --rebase
$ gitx pull --tidy              # pull, then prune dead branches
$ gitx pull --install           # pull, then install dependencies
$ gitx pull --clean             # ...installing cleanly (implies --install)
$ gitx pull --depth 1 origin main

--install uses the same package-manager detection and install.* settings as gitx update, and respects skip.install. Unlike gitx update, it installs whether or not the pull actually brought anything down — you asked for it by name, in the one repository you're standing in. Which is also why there's no --force: there'd be nothing left for it to force.

gitx pwd

Resolve a cloned repository's path, by name, from anywhere:

$ gitx pwd api-gateway
$ cd "$(gitx pwd api-gateway)"

A child process can't change your shell's working directory for you, so this is what a cd wrapper function shells out to. Add one to your shell's startup file and you have gitx-aware jumping between repositories:

gcd() { cd "$(gitx pwd "$1")"; }

gitx runs

Which of your CI runs are still in flight? Workflow runs on GitHub, pipelines on GitLab — same command, same output.

$ gitx runs            # your runs, everywhere
$ gitx runs --all      # everyone's runs
$ gitx runs 'api-*'    # narrow it down

Repositories with nothing running say so and stay out of your way; the rest are listed with their workflow, status, age and branch.

The project each directory belongs to is read from its origin remote rather than assumed from its name, so renamed, forked and subgrouped repositories still resolve correctly.

gitx tidy

Fetch with --prune, then delete every local branch whose upstream has been deleted. The one you merged the PR for. And the eleven before it.

$ gitx tidy
$ gitx tidy --no-fetch

gitx update

Fetch and fast-forward every cloned repository.

$ gitx update                       # everything
$ gitx update 'api-*' auth-service  # only what matches
$ gitx update -j 4                  # four at a time
$ gitx update --install --tidy      # the full spring clean
$ gitx update --after m             # resume an interrupted sweep alphabetically
$ gitx update --dirty               # update even repositories with local changes

A repository is skipped — never failed — when it's on a non-default branch, has a dirty working tree (unless --dirty is passed), or matches skip.update. That's the point: a sweep across ninety repositories shouldn't stop because you left one mid-refactor.


Talking to your forge

gitx speaks to GitHub and GitLab directly over their REST APIs. There is no gh or glab to install.

Choosing a provider

The first command that needs the network asks which forge you use and on which host, then remembers both:

$ gitx config set remote.provider gitlab
$ gitx config set gitlab.host gitlab.acme.com

The host is per-provider, so GitHub Enterprise Server and gitlab.com can happily coexist in one config. Provider, host and owner can each be overridden for a single command, without touching ~/.gitxconfig:

$ gitx list --all --provider gitlab --owner gitlab-org
$ gitx clone-all -H github.acme.com -o platform

clone, clone-all, list, runs and update all take -o/--owner, -P/--provider and -H/--host; gitx auth takes the latter two, since signing in is per-host rather than per-owner.

Where the token comes from

In order, first hit wins:

  1. $GITX_TOKEN, for a one-off override.
  2. $GH_TOKEN / $GITHUB_TOKEN, or $GITLAB_TOKEN / $GITLAB_ACCESS_TOKEN — the same variables the official CLIs use, so a token you've already exported just works.
  3. Your git credential helper: the macOS Keychain, libsecret, Windows Credential Manager, whatever credential.helper points at.

That third one is the good bit. If you have ever authenticated git against the host over HTTPS, gitx is already authenticated too. Nothing to set up, and no second copy of your token lying around.

gitx auth

$ gitx auth status                 # can I authenticate, and with what?
$ gitx auth login                  # store a token
$ gitx auth login --with-token     # ...from stdin, for scripts
$ gitx auth logout                 # forget it again

gitx auth login verifies the token against the API before storing anything — a paste error fails there and then, rather than on your next command — and writes it through git credential approve. It lands in whatever credential.helper names, under the account the provider says the token belongs to. gitx never writes a secret to ~/.gitxconfig.

Because it's the same store git push uses, gitx auth logout also logs plain git out of HTTPS pushes to that host — it says so when it happens. It doesn't revoke anything: the token still exists on the server until you delete it there.

Tokens need enough scope to read repositories and CI: repo + workflow on GitHub, read_api on GitLab.

Adding another forge

Gitea? Bitbucket? Implement the Provider interface in src/provider/ and add an entry to the registry. No command knows the difference — see CONTRIBUTING.md.


Aliases

Aliases work exactly like git's, because that's the muscle memory you already have.

$ gitx config set alias.ll 'list --all'
$ gitx ll                        # → gitx list --all
$ gitx ll 'api-*'                # → gitx list --all 'api-*'

Anything you type after the alias is appended, so an alias can set up the boring flags and leave the interesting arguments to you. Aliases can point at other aliases — with loop detection — and quoting inside the value is honoured, so alias.api = "list 'api-* web-*'" passes one argument, not two.

Shell aliases

Start the value with ! and the rest is handed to /bin/sh, which is where the fun is:

$ gitx config set alias.fresh '!gitx update --install && gitx runs'
$ gitx config set alias.grep '!f() { gitx list --porcelain | xargs -I{} sh -c "cd ~/dev/{} && grep -Hn \"$1\" -r ."; }; f'

As in git, arguments are appended as "$@", so the !f() { …; }; f function idiom is the reliable way to get at them:

$ gitx config set alias.hello '!f() { echo "hi $1"; }; f'
$ gitx hello world
hi world

$GITX_PREFIX holds the directory you ran from, stdin/stdout/stderr are inherited (so pagers and prompts work), and the script's exit code becomes gitx's.


Configuration

core

| Key | Default | Description | | ------------------ | ------------ | ------------------------------------------------------------------ | | core.baseDir | (prompted) | Directory all repositories are cloned beneath | | core.layout | nested | nested → <baseDir>/<owner>/<repo>, flat → <baseDir>/<repo> | | core.concurrency | auto | Parallel repositories. auto = CPU count, 0 = unlimited | | core.color | auto | auto, always or never | | core.outputLines | 1 | Trailing output lines shown per in-flight task | | core.editor | (prompted) | Command that opens a repository, e.g. code, idea or vim |

core.editor can be set per repository with --repo; see gitx config.

remote, github and gitlab

| Key | Default | Description | | ------------------------ | -------------- | ------------------------------------------- | | remote.provider | (prompted) | github or gitlab | | remote.owner | (prompted) | Organisation, group or user repos come from | | remote.user | (your login) | Login used to filter CI runs | | remote.protocol | (prompted) | https or ssh, for cloning | | remote.visibility | all | all, public, private or internal | | remote.limit | 1000 | Maximum repositories to list | | remote.includeArchived | false | Include archived repositories | | github.host | github.com | GitHub host, for Enterprise Server | | gitlab.host | gitlab.com | GitLab host, for self-managed instances |

clone and update

| Key | Default | Description | | -------------------- | -------- | --------------------------------------------- | | clone.install | false | Install dependencies after cloning | | update.install | false | Install dependencies after updating | | update.clean | false | Prefer clean installs when updating | | update.force | false | Install even when already up to date | | update.tidy | true | Prune dead branches after pulling | | update.submodules | true | Update submodules | | update.prune | true | Pass --prune when fetching | | update.ignoreDirty | .idea/ | Paths that don't count as "dirty". Repeatable |

update.tidy and update.submodules can be set per repository with --repo.

install

| Key | Default | Description | | ------------------------- | ------- | -------------------------------------------------------------------- | | install.restoreLockfile | true | Restore the lock file so a sync never leaves a repo dirty | | install.frozen | true | Use frozen/immutable installs when a lock file exists | | install.frozenFallback | true | Retry unfrozen when a stale lock file breaks a frozen install | | install.volta | auto | auto, always (wrap in volta run) or never (VOLTA_BYPASS=1) |

Every one of these can be overridden per package manager, and a package manager can be switched off entirely:

[install]
	yarnEnabled = false
	pnpmFrozen = false
	pnpmArgs = --ignore-scripts

| Key | Description | | ---------------------------------- | ------------------------------------------------- | | install.<manager>Enabled | Disable a package manager entirely | | install.<manager>Frozen | Per-manager override of install.frozen | | install.<manager>RestoreLockfile | Per-manager override of install.restoreLockfile | | install.<manager>Args | Extra arguments appended to the install command |

<manager>Enabled, <manager>Frozen and <manager>RestoreLockfile (not <manager>Args, which is a list) can also be set per repository with --repo, taking priority over both the per-manager and plain settings:

[install "legacy-monolith"]
	yarnFrozen = false

skip

All optional. All glob-aware (* and ?). All repeatable.

| Key | Description | | -------------- | ------------------------------------ | | skip.install | Never install dependencies for these | | skip.update | Never update these |

Skipping is deliberately narrow: it controls what gitx does to a repository, never whether the repository exists as far as gitx is concerned. gitx list shows everything, gitx clone-all clones everything, and gitx runs reports on everything — then skip.install and skip.update decide what gets left alone.

runs

| Key | Default | Description | | ------------ | ------- | -------------------------------- | | runs.limit | 10 | CI runs inspected per repository |

alias

| Key | Default | Description | | -------------- | ------- | ------------------------------ | | alias.<name> | — | Shorthand for a longer command |

See Aliases for what you can put in one.

A complete example

[core]
	baseDir = ~/dev
	concurrency = 8

[remote]
	provider = github
	owner = acme

[update]
	install = true
	ignoreDirty = .idea/
	ignoreDirty = .vscode/

[install]
	yarnEnabled = false

[core "legacy-monolith"]
	editor = webstorm

[skip]
	install = docs-site
	update = archived-*

[alias]
	ll = list --all
	sweep = update --install --tidy
	web = !gitx update 'web-*' && echo 'web estate is fresh'

Extending it

Node is the only ecosystem implemented today, but nothing about the design assumes it. An ecosystem is a small object that answers two questions — "is this one of mine?" and "how do I install it?":

export const goEcosystem: Ecosystem = {
  name: 'go',
  async detect(dir) {
    if (!(await exists(path.join(dir, 'go.mod')))) return undefined;
    return {
      ecosystem: 'go',
      manager: 'go',
      available: hasExecutable('go'),
      hasLockFile: await exists(path.join(dir, 'go.sum')),
      lockFile: 'go.sum',
      plan: (options) => ({ command: 'go', args: ['mod', 'download', ...options.extraArgs] }),
    };
  },
};

Register it, and every existing flag — skip.install, install.<manager>.args, lock file restoration, the parallel runner, the live display — works for Go repositories with no further changes. CONTRIBUTING.md has the step-by-step version.


Contributing

Pull requests and bug reports are welcome. See CONTRIBUTING.md for the project layout, the testing approach, and how to add an ecosystem, a configuration key or a command.


Licence

MIT © airmrcr