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.comitself 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.--quickskips the network checks for an instant version/branch/git-only view;--outputsaves the same table as Markdown, JSON, CSV, HTML, or plain text.outdated— one table of outdated dependencies across every package, grouped by package, with theLatestcolumn 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, plusgh/glab— whichever your repos actually use), default-branch drift from the host, stale remote-tracking refs, divergence from origin, detachedHEAD, missing branch protection, and leftover branches from past releases — checked both locally and on origin,--clean-branches/--clean-remote-branchesturn 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/--cleanare there for a full reset when a repo needs one.bump— bump a package's version (patch by default;--minor/--majorfor those;--prerelease/--preidto start or advance a prerelease;--custom-versionfor 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 aCHANGELOG.mdentry when the package already has one. Tags asv<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/releasefollow the same convention.--publishalso publishes each package right after it's tagged (asked once at the end if you leave the flag off) — seepublishbelow.publish— runnpm publish(orpnpm publishfor a pnpm workspace member, so aworkspace:*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 thenextdist-tag automatically instead of npm's own defaultlatest—--dist-tagoverrides 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 matchingCHANGELOG.mdsection when there is one.sync-deps— the write side of the "Deps" column inlist/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 whatnpm audit fix,npm updateorsync-depsleft 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 fornpm audit), a history, and a Settings page for the config.- Every command that touches multiple repos supports
--packagesand--yesfor 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.