Skip to content

Polyrepo ​

An interactive CLI for managing a folder of local npm package repos: version bumps through a pull request, npm publishing, releases, and cross-package dependency drift — all from one tool, all reviewable with --dry-run before anything actually changes. Works with GitHub and GitLab (including self-hosted), autodetected per repo — a folder can freely mix both. A repo that's itself a pnpm workspace is auto-detected and expanded into one entry per publishable member, each versioned, tagged, and released independently.

Features ​

  • setup — add, edit, or remove the package directories the CLI scans, right from the terminal. Also where a self-hosted GitLab instance's hostname is registered (gitlab.com itself needs no setup).
  • clone — diff a GitHub org's (or, with --provider gitlab, a GitLab group's) repos against what's already cloned locally, and clone whatever's missing.
  • list — one table per package: local version, branch, git status, the version on origin's default branch (independent of whatever's checked out locally), git tag, release status, npm registry version, and cross-package dependency drift. --quick skips the network checks for an instant version/branch/git-only view; --output saves the same table as Markdown, JSON, CSV, HTML, or plain text.
  • outdated — one table of outdated dependencies across every package, grouped by package, with the Latest column colored by how far behind each one is (major/minor/patch).
  • audit — one table of npm security vulnerabilities across every package, grouped by package, with severity colored critical/high/moderate/low and whether a fix is available.
  • prs — one table of every open pull/merge request across every package.
  • doctor — a health check with a few safe self-repairs along the way: environment (Node/git/npm, plus gh/glab — whichever your repos actually use), default-branch drift from the host, stale remote-tracking refs, divergence from origin, detached HEAD, missing branch protection, and leftover branches from past releases — checked both locally and on origin, --clean-branches/--clean-remote-branches turn each into its own interactive cleanup.
  • switch-default — fast-forward selected repos to their default branch, whatever it's actually named (master, main, or anything else — detected per repo, not assumed). --force/--clean are there for a full reset when a repo needs one.
  • bump — bump a package's version (patch by default; --minor/--major for those; --prerelease/--preid to start or advance a prerelease; --custom-version for an exact version) through a branch → PR/MR → merge, then tag the release. Safe to re-run if a previous attempt was interrupted partway — it picks up from wherever it left off. Drafts a CHANGELOG.md entry when the package already has one. Tags as v<version>, or <name>@<version> for a pnpm workspace member, so two packages in the same repo bumped to the same version number never collide — tag/release follow the same convention. --publish also publishes each package right after it's tagged (asked once at the end if you leave the flag off) — see publish below.
  • publish — run npm publish (or pnpm publish for a pnpm workspace member, so a workspace:* dependency on a sibling package resolves correctly) for the packages that are actually ahead of the registry, checking (and fixing, if needed) npm login before anything is published. A private package is never offered. A prerelease version (e.g. 2.0.0-beta.0) publishes under the next dist-tag automatically instead of npm's own default latest — --dist-tag overrides this for any version. Warns (without blocking) when a selected package's local version differs from origin's, or the repo isn't on its default branch, or has uncommitted changes.
  • tag — tag a package at its current version without bumping again, for when the version was already moved forward some other way. Fast-forwards to the default branch first, then re-reads the version so the tag always names what's actually being tagged — warns if that changed the tag name. Offers to create a release right after.
  • release — create a release from a tag, with notes pulled from the matching CHANGELOG.md section when there is one.
  • sync-deps — the write side of the "Deps" column in list/doctor: updates a local package's declared range on another local package to match that package's current version (keeping the range's own ^/~/exact style), for the ones you pick. Only ever edits files on disk — no commit, no push.
  • exec — run any command across every selected package, one at a time, with a real terminal.
  • commit — commit what npm audit fix, npm update or sync-deps left in the working tree without stumbling over branch rules: on a feature branch it commits there; on the default branch it reads the host's rules and either commits and pushes, or creates a branch, pushes it and opens a pull/merge request. A direct push the host refuses anyway is moved to a branch for you.
  • ui — a local web interface for all of the above: one table of every package with the problems called out and monorepos grouped into blocks, a form for every command with a summary (and a dry-run preview) before anything changes, a Release wizard, live runs with a report and a searchable log, suggestions for what to do after every outcome (including a guided fix for npm audit), a history, and a Settings page for the config.
  • Every command that touches multiple repos supports --packages and --yes for fully non-interactive use in scripts.

How It Works ​

Every command reads the same list of repos from one config file (roots — folders whose subfolders are each a package; packages — individual repo folders; gitlabHosts — self-hosted GitLab hostnames) — set up once via setup, after which everything else just works against the same list. Each repo's host (GitHub or GitLab) is detected from its origin remote, not configured per repo.

A repo and an npm package aren't always the same thing — a pnpm workspace repo holds several. Commands that deal with individual packages (list, outdated, audit, bump, publish, tag, release, sync-deps, exec) expand a workspace repo into one entry per member automatically; commands that deal with the repo itself (switch-default, clone, prs, and most of doctor) still run once per physical repo. See Config & Discovery for the detection rules.

Read-only checks (git status, tags, releases, registry state, PR state) run in parallel across every repo, so a full list or doctor across many packages is fast rather than one repo waiting on the last. Anything that actually changes state — a commit, a push, a merge, a tag — runs one repo at a time instead, printing each step as it happens. bump in particular tracks exactly how far a previous attempt got (branch pushed? PR opened? PR merged?) so re-running it after an interruption resumes from there instead of failing on "branch already exists" or opening a duplicate PR.