# polyrepo-cli — AI Reference A command-line tool (binary `polyrepo`) for managing a FOLDER OF LOCAL NPM PACKAGE REPOS as one set: see the state of every package at once, release a version through a branch -> PR/MR -> merge -> tag flow, publish to npm, create releases, find stale dependency ranges between the local packages, and run any command in every repo. Works with GitHub (via `gh`) and GitLab, including self-hosted (via `glab`), detected per repo. Version 1.3.1. This document is hand-written for AI agents and other tools that drive this CLI: every flag, default and behavior note below is verified against the source (not summarized from prose docs). It covers what `--help` does not make obvious: which command to pick, which commands block waiting for input, what changes on disk or on the remote, and the traps. For narrative docs, see the site: - Full docs (EN): https://npm.vuecraft.ru/en/packages/polyrepo-cli/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/polyrepo-cli/guide/overview - GitHub: https://github.com/macrulezru/polyrepo-cli - npm: https://www.npmjs.com/package/polyrepo-cli Links starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Read this first — five rules that prevent most mistakes 1. **`--yes` does NOT make a command non-interactive. `--packages` does.** Every selecting command (`bump`, `publish`, `tag`, `release`, `sync-deps`, `commit`, `switch-default`, `exec`, `clone`) shows an interactive checkbox unless `--packages a,b` is given. `--yes` only skips the final "proceed?" question. Fully non-interactive = `--packages --yes`. Without a terminal, a command that needs a checkbox hangs or fails. 2. **Exit codes are NOT a success signal.** Only these exit non-zero: three flag checks (`commit` with an unknown `--scope`/`--mode` (2), `bump` with more than one shape flag, `bump --custom-version` without exactly one `--packages`, `switch-default --clean` without `--force`), `ui` when its port is invalid (2) or taken (1), `exec` when at least one package's command failed, and an uncaught exception — all exit 1. A failed step of `bump`, `publish`, `tag`, `release`, `doctor` or `list` is printed (a red line starting with the failure text) but the process still exits 0. After a mutating command, verify the outcome with `polyrepo list`, `polyrepo prs`, or the output text. 3. **Always preview a mutating command with `--dry-run` first** (`bump`, `tag`, `release`, `publish`, `sync-deps`, `commit`, `clone` support it). Nothing is pushed, opened, merged, tagged or written in dry-run. 4. **Package names for `--packages` are directory names, not npm names**, matched case-insensitively, comma-separated, no spaces required (they are trimmed). For a pnpm workspace member the name is `/`, e.g. `inview/packages/core`. An unknown name is ignored with a yellow "Unknown package, ignoring: x" line — NOT an error. If every name is unknown the command simply finds nothing to do. 5. **Run `polyrepo doctor` before blaming anything else.** It checks Node, git, npm, `gh`/`glab` authentication, branch health, and stale dependency ranges. --- ## 2. Choosing a command | Goal | Command | Writes? | |---|---|---| | Tell the tool where the repos live | `setup` (interactive menu) or edit the config JSON by hand | config file only | | Clone repos of an org/group that are not local yet | `clone --org X [--provider gitlab]` | clones | | See version, branch, git state, tag, release, npm, dep drift of everything | `list` (`--quick` = local only, no network) | no (optionally a file via `--output`) | | Outdated dependencies, all packages | `outdated` | no | | npm security vulnerabilities, all packages | `audit` | no | | Open PRs/MRs, all packages | `prs` | no | | Health check + safe self-repairs | `doctor` | pointer-only fixes; deletes branches only with the two `--clean-*` flags | | Get repos onto an up-to-date default branch | `switch-default` | checkout / fast-forward; `--force` discards work | | Release a new version | `bump` (branch -> PR/MR -> merge -> tag [-> publish]) | git + host + (optionally) npm | | Tag the CURRENT version without bumping | `tag` | git tag + push | | Create a GitHub/GitLab release from an existing tag | `release` | host | | Publish to npm what is ahead of the registry | `publish` | npm | | Fix stale local dependency ranges | `sync-deps` | package.json on disk only (no commit) | | Commit dependency changes (after `npm audit fix`, `npm update`, `sync-deps`) the way the repo's branch rules allow | `commit` | git commit; push; a branch and a PR/MR when the default branch refuses direct commits | | Run any command in every repo | `exec -- ` | whatever the command does | | Let a human drive all of the above from a browser | `ui` (a long-running local server) | nothing by itself; every change is confirmed in the page | Read-only (safe at any time): `list`, `outdated`, `audit`, `prs`, `doctor` (without `--clean-*`). Typical release flow: `doctor` -> `bump --dry-run --packages p` -> `bump --packages p --yes` -> (`publish` if not already done via `--publish`) -> `release --packages p --yes` -> verify with `polyrepo list` and read the row of p (`list` has no `--packages` filter). --- ## 3. Configuration Config file: JSON, resolved in this order — `--config ` (a program-level option, written before the subcommand: `polyrepo --config x.json list`), then env `POLYREPO_CONFIG`, then `polyrepo.config.json` NEXT TO THE CLI's own package folder (not the current directory, not the home directory). ```json { "roots": ["/path/to/folder-of-repos"], "packages": ["/path/to/one-off-repo"], "gitlabHosts": ["gitlab.company.com"] } ``` - All three keys are optional and additive. Relative paths are resolved against the CONFIG FILE's directory, never the working directory. - `roots`: each entry's DIRECT subfolders are candidate repos. `packages`: each entry is one repo folder. A candidate counts only if it contains BOTH `package.json` and `.git`; others are silently skipped (a missing configured folder prints a yellow note). - `gitlabHosts`: hostnames of self-hosted GitLab. `gitlab.com` needs no entry. Any remote host not recognized as GitLab is treated as GitHub. - Env `POLYREPO_ROOT` replaces ALL configured `roots` for that run (explicit `packages` entries stay). - If there is no config and no env, every command prints "No repos found." — the fix is `polyrepo setup` or creating the JSON above. - A repo containing `pnpm-workspace.yaml` is expanded automatically into one entry per publishable member (the workspace root, usually private, is not offered). Each member is versioned, tagged (`@`) and published (`pnpm publish --no-git-checks`) on its own. A plain repo is one package, tagged `v`. - Repos are sorted by directory name. Git host is read from the `origin` remote of each repo; a repo without `origin` cannot be bumped/tagged/released. Requirements: Node >= 20, `git`; `gh` (authenticated) for GitHub repos; `glab` (authenticated) for GitLab repos; `npm` (authenticated) only for `publish`. --- ## 4. Interactivity matrix | Command | No-prompt form | What still prompts | |---|---|---| | `list`, `outdated`, `audit`, `prs` | always non-interactive | — | | `doctor` | no flags | `--clean-branches` / `--clean-remote-branches` show a checkbox | | `setup` | none — always an interactive menu | everything | | `clone` | `--org X --packages a,b --yes` | checkbox of missing repos without `--packages` | | `switch-default` | `--packages a,b --yes` | checkbox / confirm | | `bump` | `--packages a,b --yes` (+ `--publish`) | checkbox; end-of-run "publish now?" unless `--publish` or `--yes` | | `tag` | `--packages a,b --yes --release` | checkbox; "release now?" unless `--release` or `--yes` | | `release`, `publish`, `sync-deps` | `--packages a,b --yes` | checkbox / confirm | | `commit` | `--packages a,b --yes [-m "msg"]` | checkbox, message prompt, confirm per package | | `exec` | `--packages a,b --yes -- cmd` | checkbox (all pre-checked) / confirm | | `ui` | `--no-open` (still never returns) | runs until Ctrl+C | `publish` and `npm login` also hand the terminal to npm (2FA/OTP prompt); there is no way to supply an OTP non-interactively from the CLI. (The Publish form of `polyrepo ui` takes a one-time password and passes it to npm as `--otp`; it is never stored. Because npm offers its web sign-in (a link, a security key, a passkey) ONLY on a real terminal, a publish that fails in the UI offers "Publish in a terminal": `POST /api/terminal/publish {dir, distTag?}` opens a system terminal window in the package folder running `npm publish [--tag ]` (`pnpm publish --no-git-checks` for a pnpm workspace member; Windows `cmd`, macOS Terminal, Linux the first of x-terminal-emulator, gnome-terminal, konsole, xfce4-terminal, xterm; none: the command is copied). The server accepts only a package name and a dist-tag, never a command line.) Ctrl+C at any prompt prints "Cancelled." and exits 0. --- ## 5. Commands Global option: `--config `. Program name `polyrepo`. `-V/--version`, `-h/--help`. Aliases: `ls` = `list`, `sd` = `switch-default`. ### 5.1 `setup` Interactive menu to view/add/edit/remove `roots`, `packages`, `gitlabHosts`. Nothing is written until "Save and exit". `--config` picks another file (created if absent). ### 5.2 `clone --org [options]` Lists repos of a GitHub org/user (`gh repo list`) or GitLab group (`glab repo list --group`), compares with directory names already under the target root, offers to clone the missing ones. - `--org ` REQUIRED. `--provider ` default `github` — it cannot be autodetected because the repo does not exist locally yet. - `--root ` default: the config's FIRST root. `--include-archived` (archived repos skipped by default). `--packages a,b` = only offer these repo names. `--yes`, `--dry-run`. ### 5.3 `list` (alias `ls`) Table per package. Columns: `Package` (directory name; for workspace members `repo/rel`), [`Path` with `--path`], `Local` (version in package.json on disk), `Branch` (current; shown yellow when it is not the default branch), `Git` (`clean`/`dirty`). Without `--quick` also: `Origin` (version in package.json on origin's DEFAULT branch, independent of the checked-out branch), `Tag` (`v` / `@` or `—` if the current version is not tagged on origin), `Release` (`✓`, `✗`, or `—` when untagged), `npm` (registry version, `unpublished`, or `private`), `Deps` (`✓` or `⚠ N` = N stale local dependency references to this package). - `--quick`: only Package/Local/Branch/Git, no network. `--path`. - `--output ` also saves the SAME table (same columns, honors `--quick`/`--path`). `--format md|json|csv|html|txt` (guessed from extension when omitted: `.json`, `.md`/`.markdown`, `.csv`, `.html`/`.htm`, anything else = txt). `--format` without `--output` prints an error ("nothing to export it to"), runs nothing, and still exits 0. An unknown `--format` value behaves the same. - JSON export: array of objects keyed by column label, all values strings EXCEPT `"✓"` -> `true` and `"✗"` -> `false`. Other cells stay text (`"⚠ 2"`, `"—"`, `"unpublished"`). This is the machine-readable way to read repo state: `polyrepo list --output state.json` (the table is still printed to stdout). ### 5.4 `outdated`, `audit`, `prs` (all read-only; `--packages` narrows) - `outdated`: runs `npm outdated --json` per package in parallel; one flat table package / dependency / current / wanted / latest. Packages with nothing add no rows. - `audit`: `npm audit --json` per package; rows: package, dependency, severity (critical/high/moderate/low), direct or transitive, fix available (with the version when the fix is a semver-major bump of a top-level dependency). Changes nothing. - `prs`: `gh pr list` / `glab mr list`; rows: package, number, title, branch, draft. Useful after an interrupted `bump` to see what is still waiting for a merge. ### 5.5 `doctor [--clean-branches] [--clean-remote-branches]` Seven sections: Environment, Config, Remote sync, Branch sync, Branch protection, Stale bump branches, Cross-package deps. Self-repairs, all pointer-only and safe: `git remote set-head origin --auto` when the cached default-branch name drifted from the host, and `git remote prune origin`. Branch protection is REPORT-ONLY. - `--clean-branches`: checkbox of LOCAL bump branches whose PR is merged; deletes the chosen ones with `git branch -d` (refuses unmerged ones, never forces). - `--clean-remote-branches`: same for the copy on origin (`git push origin --delete`) — separate opt-in because it is shared state. Bump branches are recognized by the pattern `-version-bump` (workspace members: `--version-bump`, `@` dropped and `/` -> `-`). ### 5.6 `switch-default [--packages] [--yes] [--force] [--clean]` Per repo: `git fetch origin`, checkout the DEFAULT branch (detected per repo from origin — never assumed to be main or master), `git merge --ff-only`. A dirty repo is skipped with a warning; a diverged local default branch is reported and left alone. - `--force`: `checkout -f` + `reset --hard origin/` — PERMANENTLY discards uncommitted changes to tracked files and local-only commits on that branch. The confirmation defaults to "No" when something would be discarded. - `--clean`: only valid WITH `--force` (else prints an error, exit 1); also runs `git clean -fd` (untracked files/directories; ignored paths such as node_modules stay). ### 5.7 `bump [shape] [--packages] [--yes] [--dry-run] [--wait-checks] [--publish]` Releases a version. Shape flags are MUTUALLY EXCLUSIVE (more than one -> error, exit 1): default patch, `--minor`, `--major`, `--prerelease`, `--custom-version `. - `--preid ` names the prerelease id (default `alpha`). `--prerelease` alone advances/starts a prerelease (1.2.9 -> 1.2.10-alpha.0, 1.2.10-alpha.0 -> 1.2.10-alpha.1). `--major --preid beta` -> premajor (2.0.0-beta.0); `--minor --preid x` -> preminor. `--preid` alone behaves like `--prerelease`. - `--custom-version ` requires `--packages` naming EXACTLY ONE package (else error, exit 1). Per selected package, in order (a dirty working tree = skipped with a warning): 1. fetch + fast-forward the default branch; 2. branch `-version-bump` (or `--version-bump` for a workspace member) — reused if it already exists locally or on origin; 3. if package.json is not yet at the new version: edit the version, and if a `CHANGELOG.md` exists add a draft Keep-a-Changelog entry `## [x.y.z] - ` with `### Changed` bullets from the commit log since the last tag (merge commits dropped), commit `chore: bump version to x.y.z`; 4. push, open a PR/MR (title `chore: bump version to x.y.z`), optionally wait for CI (`--wait-checks`; no CI configured = nothing to wait for; GitLab waits on the branch pipeline), MERGE it, re-sync local default branch; 5. create and push the tag (`v` or `@`), unless it exists. It is RESUMABLE: it detects an existing branch / open PR / already-merged PR and continues from there, so re-running after an interruption is the right recovery. It never force-pushes. At the end it lists other local packages whose dependency ranges no longer match (it changes nothing) — follow with `sync-deps`. Publishing: `--publish` publishes the just-tagged packages without asking; without it you are asked once at the end unless `--yes` is set (then it does NOT publish). Only packages tagged in this run are offered. Prerelease versions publish under the `next` dist-tag. ### 5.8 `publish [--packages] [--yes] [--dry-run] [--dist-tag ]` Compares local version with the npm registry (parallel). The checkbox pre-selects only packages whose version differs from the registry. Private packages are never offered. A workspace member uses `pnpm publish --no-git-checks` (after `pnpm install` in its repo); others use `npm publish`. Checks `npm whoami` and runs `npm login` if needed (not for `--dry-run`). - Dist-tag: a version containing `-` publishes under `next`; `--dist-tag` overrides for every selected package (also useful to put a stable release on another tag). - **With `--packages`, the named packages are published even if they are already up to date** (the pre-selection logic is bypassed) — npm will then reject a duplicate version. Do not pass a package that is not ahead. - It never checks git state: publishes what is ON DISK; warns (does not block) when a package is off its default branch, dirty, or its local version differs from origin. - `--dry-run` = `npm publish --dry-run` (full pack, nothing published). ### 5.9 `tag [--packages] [--yes] [--dry-run] [--release]` Tags the CURRENT version (no bump, no PR): fast-forwards the default branch, re-reads the version, creates the annotated tag, pushes it. Already-tagged packages are shown unchecked. A tag created locally but never pushed is recovered (only pushed). `--release` creates the releases right after, without asking. ### 5.10 `release [--packages] [--yes] [--dry-run]` Creates a release for the package's CURRENT version's tag. Only packages that already have that tag are selectable (run `bump` or `tag` first). Release notes: the matching section of `CHANGELOG.md` if present, else GitHub's generated notes, else (GitLab) a commit list since the previous tag. GitHub: `--verify-tag` is passed to `gh`. ### 5.11 `sync-deps [--packages] [--yes] [--dry-run]` Finds local packages whose declared range on ANOTHER LOCAL package no longer matches that package's current version (the `Deps ⚠` column), previews before/after keeping each range's style (`^` stays `^`, `~` stays `~`, exact stays exact; unusual ones fall back to `^`), edits `package.json` files on disk. NO commit, NO push, NO PR — commit it with `polyrepo commit` (it follows the repo's branch rules). `--packages` limits which DEPENDENT packages are offered. ### 5.12 `exec [--packages] [--yes] [--bail] -- ` Runs the command once per selected package (all pre-checked), sequentially, with the terminal attached (output and prompts show normally; nothing is captured). The command MUST come after a literal `--`. A failing package is reported and the run continues unless `--bail`; at the end a summary lists failures and the process exits 1 if any failed. Together with `commit`, the only command whose exit code reflects per-package failures. The working directory is the PACKAGE directory (a workspace member's own folder). ### 5.13 `ui [--port ] [--host
] [--no-open] [--token ]` Starts a local web interface and keeps running until Ctrl+C. It prints `polyrepo ui is running at http://127.0.0.1:/?token=` and opens the browser unless `--no-open`. It never returns on its own, so do not run it from an agent or a script unless the user asked for the interface: it blocks the shell. Defaults: `--port 0` (a free port), `--host 127.0.0.1`. Anything but `127.0.0.1` exposes it to the network over plain HTTP, protected only by the token. The Publish form and the Release wizard list the version on npm next to the local one (`GET /api/packages/registry`, cached for a minute; filter "Only ahead of npm"), rows are tinted by status (red dirty, yellow ahead of npm, accent not on npm) and the start button is pinned to the bottom of the page. The page runs the same commands as the terminal (one form per command, a Release wizard, a Packages overview, a report and a log per run, a history, Settings for the config) and every command that changes a repo needs an explicit confirmation before it starts. Runs are kept in `~/.polyrepo/runs/` (move with `POLYREPO_HOME`); saved package sets and the monorepo color range in `~/.polyrepo/ui.json`. Exit codes: invalid port 2, port in use or another start failure 1, Ctrl+C 0. The built interface lives in `ui-dist/` of the package; if it is missing the page says to run `npm run build:ui`. ### 5.14 `commit [-m ] [--scope manifest|all] [--mode auto|direct|branch] [--branch ] [--no-push] [--no-pr] [--stay] [--dry-run] [--packages] [--yes]` Commits what `npm audit fix`, `npm update` or `sync-deps` left in the working tree, without hitting branch rules. Offers only repos with uncommitted changes in `package.json` or the lock files (`--scope all`: every tracked change in the package folder; untracked files are never added). Message default: `chore: update dependencies` (used with `--yes` and no `-m`); it is also the PR/MR title. Route per repo: on a feature branch (not the default) the commit goes there and is pushed. On the default branch, `--mode auto` (default) reads the host's rules — GitHub rulesets (pull request / status checks / update restrictions) and classic branch protection (needs admin to read), GitLab protected-branch push level vs your role — and (a) commits and pushes when direct commits are allowed, or (b) when a PR/MR is required OR the rules cannot be read ("unknown" always means branch): creates `chore/-` (or `--branch`), commits there, pushes it, opens a PR/MR into the default branch, and checks the default branch out again, untouched (`--stay` keeps the new branch). `--mode direct` forces a direct commit and push; if the host refuses it ("protected branch", GH006/GH013, rule violations, pre-receive hook declined) and that commit is the only one the local default branch has ahead of origin, the commit is moved to a branch and the PR/MR is opened anyway. `--mode branch` always branches. `--no-push` stops after the commit; `--no-pr` pushes the branch but opens no PR/MR. The PR/MR is NEVER merged by this command; afterwards run `switch-default`. Exit: nothing to commit → 0; a failed git step → 1 after trying the other packages; invalid `--scope`/`--mode` → 2. In `polyrepo ui` the same flow is the "Commit…" dialog and the catalog form "Commit changes". --- ## 6. Host behavior (GitHub vs GitLab) | | GitHub | GitLab | |---|---|---| | CLI | `gh` | `glab` | | Detected by | `origin` host not in the GitLab set | host is `gitlab.com` or listed in `gitlabHosts` | | Term | PR | MR | | `--wait-checks` | `gh pr checks --watch` | pipeline of the branch (`glab ci status --branch --wait`) | | Release notes fallback | generated notes | plain commit list | A repo whose host cannot be determined (no `origin`) is skipped by the PR/tag steps with "Could not determine a git host". --- ## 7. Recipes Non-interactive patch release of two packages and a release, with a safety preview: ``` polyrepo doctor polyrepo bump --dry-run --packages a,b polyrepo bump --packages a,b --yes --publish polyrepo release --packages a,b --yes polyrepo list ``` Release a prerelease under `next`: `polyrepo bump --packages a --prerelease --preid beta --yes --publish` Exact version for one package: `polyrepo bump --packages a --custom-version 3.0.0-hotfix.1 --yes` Recover from an interrupted bump: `polyrepo prs` to see the open PR/MR, then simply re-run the same `bump` command — it resumes. Machine-readable status: `polyrepo list --output state.json` (see 5.3), or `polyrepo list --quick --output state.csv --format csv`. Run tests everywhere and stop at the first failure: `polyrepo exec --yes --bail --packages a,b -- npm test` (exit code 1 if any failed). --- ## 8. Pitfalls - `--yes` alone still opens a checkbox (rule 1). In a script always pair it with `--packages`. - A failed `bump`/`publish`/`tag`/`release` exits 0 (rule 2). Read the output or run `polyrepo list` afterwards. - `--packages` takes directory names. For a workspace member use the full `repo/relative/path` form shown in the `Package` column. - `bump` skips a DIRTY repo silently except for one warning line; commit or stash first. `switch-default` also skips dirty repos unless `--force`. - `switch-default --force` destroys uncommitted work and local-only commits. Never add it without an explicit request. - `publish --packages x` publishes x even when the registry already has that version. - `Origin` in `list` is the version on origin's default branch; `Local` is the version on whatever branch is checked out. They legitimately differ on a feature branch. - `list` and `bump` need the network (GitHub/GitLab, npm registry). `list --quick` is the only network-free view. - The config default location is next to the CLI installation, so a globally linked CLI used from another directory still reads the SAME config. Use `--config` or `POLYREPO_CONFIG` for a per-project file. - Tags: `v` for a plain repo, `@` for a workspace member. Do not create tags by hand in a different shape; `release` looks for exactly these names. - There is no `--json` on commands other than via `list --output`. - `commit` never merges its PR/MR and never touches untracked files; with `--mode direct` a refused push is recovered into a branch, so the default branch is never left ahead of origin. Without `gh`/`glab` access to the rules the route is a branch and a PR/MR. - `polyrepo ui` is a server, not a command that finishes. Start it only when asked, with `--no-open` if no browser should open, and stop it with Ctrl+C.