polyrepo-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,12 +23,16 @@ with `--dry-run` before anything actually changes.
23
23
  - **`prs`** — one table of every open pull request across every
24
24
  package (`gh pr list`) — useful after an interrupted `bump` run to
25
25
  see what's still waiting to be merged.
26
- - **`doctor`** — a one-command health check: is the environment set up
27
- correctly (Node/git/gh/npm, authentication), and does any local
28
- package still depend on an incompatible version of another local
29
- package.
30
- - **`switch-master`** — fast-forward selected repos to an up-to-date
31
- `master`.
26
+ - **`doctor`** — a one-command health check: environment (Node/git/gh/npm,
27
+ authentication), branch health (default-branch name drift, stale
28
+ remote-tracking refs, divergence from origin, detached `HEAD`,
29
+ missing branch protection, leftover `bump` branches — with a few
30
+ safe, non-destructive self-repairs along the way, and
31
+ `--clean-branches` for an interactive local-branch cleanup), and
32
+ cross-package dependency drift.
33
+ - **`switch-master`** — fast-forward selected repos to their up-to-date
34
+ default branch, whatever it's actually named (`master`, `main`, or
35
+ anything else — detected per repo, not assumed).
32
36
  - **`bump`** — bump a package's version (patch by default, or
33
37
  `--minor`/`--major`) through a branch → PR → merge, then tag the
34
38
  release. Safe to re-run if a previous attempt was interrupted
@@ -265,21 +269,77 @@ polyrepo prs
265
269
  polyrepo prs --packages vue-toast-kit,os-detect
266
270
  ```
267
271
 
268
- ### `polyrepo doctor`
272
+ ### `polyrepo doctor [options]`
269
273
 
270
- A read-only health check in three sections:
274
+ A health check in seven sections — mostly read-only diagnosis, plus a
275
+ few small, non-destructive self-repairs:
271
276
 
272
277
  1. **Environment** — Node.js version (20+ required), whether
273
278
  `git`/`gh`/`npm` are on `PATH`, and whether `gh`/`npm` are
274
279
  authenticated (npm auth is only a warning — it's only needed for
275
280
  `publish`).
276
281
  2. **Config** — how many packages the current config actually
277
- resolves to, and which repos are dirty or off `master`.
278
- 3. **Cross-package dependencies** the same dependency-drift check
282
+ resolves to, and which repos are dirty, in a detached `HEAD` state,
283
+ or off their default branch.
284
+ 3. **Remote sync** — two related repairs, both per-repo pointer
285
+ refreshes that never touch a file, branch, or commit:
286
+ - compares each repo's locally cached default-branch name (the
287
+ same value `switch-master`/`bump`/`tag` all use — see
288
+ `switch-master` above) against what GitHub actually reports
289
+ right now. Git never refreshes that local cache on its own, so
290
+ renaming a repo's default branch on GitHub after it was cloned
291
+ would otherwise go unnoticed by every other command forever —
292
+ wherever it's drifted, this fixes it with `git remote set-head
293
+ origin --auto`;
294
+ - runs `git remote prune origin` on every repo, dropping local
295
+ `remotes/origin/x` refs left over for branches already deleted
296
+ on GitHub (these tend to accumulate — every PR branch a `bump`
297
+ or manual workflow ever created and later got deleted on GitHub
298
+ leaves one behind locally until pruned).
299
+
300
+ Both are skipped for a repo GitHub can't be reached for (offline,
301
+ or `gh` not authenticated).
302
+ 4. **Branch sync** — fetches and compares each repo's local default
303
+ branch against `origin/<default>`: **diverged** (both ahead and
304
+ behind — a fast-forward won't work, needs resolving by hand),
305
+ **behind only** (safe to fast-forward with `switch-master`), or
306
+ **ahead only** (local commits not yet pushed). Surfaces this
307
+ before some other command trips over it mid-run instead of after.
308
+ 5. **Branch protection** — whether each repo's default branch
309
+ actually has GitHub branch protection enabled right now.
310
+ Report-only; enabling protection is a policy decision, not
311
+ something this fixes on your behalf.
312
+ 6. **Stale bump branches** — `bump` merges through a PR with the
313
+ branch intentionally left on origin (`--delete-branch=false`, see
314
+ `bump` above), so every completed bump leaves a local branch copy
315
+ behind too, forever. This reports how many local branches match
316
+ `<version>-version-bump` **and** already have a merged PR.
317
+ `--clean-branches` turns that into a checkbox — pick which ones to
318
+ delete locally (`git branch -d`, which refuses instead of forcing
319
+ if a branch somehow isn't actually fully merged locally; the
320
+ branch on origin is never touched, deleting that is out of scope
321
+ here — it's more sensitive shared state).
322
+ 7. **Cross-package dependencies** — the same dependency-drift check
279
323
  that runs at the end of `bump`, available on demand without
280
324
  bumping anything.
281
325
 
282
- Worth running first if any other command is behaving unexpectedly.
326
+ Worth running first if any other command is behaving unexpectedly, any
327
+ time you rename a default branch on GitHub, or just periodically to
328
+ catch accumulated cruft (stale remote-tracking refs, leftover bump
329
+ branches) before it piles up.
330
+
331
+ **Options:**
332
+
333
+ | Flag | What it does |
334
+ | --- | --- |
335
+ | `--clean-branches` | After scanning, show a checkbox of local bump branches whose PR is already merged; delete the ones you pick. |
336
+
337
+ ```bash
338
+ polyrepo doctor
339
+
340
+ # also review and clean up leftover local bump branches
341
+ polyrepo doctor --clean-branches
342
+ ```
283
343
 
284
344
  ```bash
285
345
  polyrepo doctor
@@ -287,22 +347,55 @@ polyrepo doctor
287
347
 
288
348
  ### `polyrepo switch-master` (alias `sm`)
289
349
 
290
- 1. Shows a checkbox list of every repo with its current branch; repos
291
- not currently on `master` are pre-selected.
350
+ Each repo's **default branch is detected per repo**, not assumed
351
+ GitHub itself defaults a new repo to `main`, and plenty of people
352
+ rename it (`master` included), so a mixed folder of repos can easily
353
+ have some on `master` and some on `main`. Detection prefers the
354
+ locally cached `origin/HEAD` ref (no network — set by `git clone`),
355
+ falls back to asking origin directly (`git ls-remote --symref`,
356
+ read-only), then to whichever of `master`/`main` exists as a local
357
+ branch, and finally to `main` (GitHub's own default) if nothing else
358
+ could tell it.
359
+
360
+ 1. Shows a checkbox list of every repo with its current branch (and
361
+ its default branch, when the two differ); repos not currently on
362
+ their default branch are pre-selected.
292
363
  2. After confirming, for each selected repo, one at a time (each
293
364
  step's result prints immediately, not after the whole batch):
294
365
  - a dirty working tree is skipped with a warning, untouched;
295
- - otherwise: `git fetch origin` → `git checkout master`
296
- `git merge --ff-only origin/master`.
297
- 3. If local `master` has diverged from `origin/master` (fast-forward
366
+ - otherwise: `git fetch origin` → `git checkout <default branch>`
367
+ `git merge --ff-only origin/<default branch>`.
368
+ 3. If the local default branch has diverged from origin (fast-forward
298
369
  isn't possible), that repo is reported and left alone to resolve by
299
- hand — no `--force`/`reset --hard` is ever used.
370
+ hand.
371
+
372
+ `--force` changes step 2 and 3 for every selected repo: a dirty
373
+ working tree is no longer skipped, and each repo gets
374
+ `git checkout -f <default branch>` + `git reset --hard
375
+ origin/<default branch>` instead of the safe fast-forward-only merge —
376
+ uncommitted changes to tracked files and any local-only commits on
377
+ that branch are permanently discarded (untracked files are left
378
+ alone, this isn't `git clean`). The checkbox marks which selected
379
+ repos would lose changes, and the proceed confirmation says how many
380
+ and defaults to "No" instead of "Yes" whenever `--force` would
381
+ actually discard something.
382
+
383
+ **Options:**
384
+
385
+ | Flag | What it does |
386
+ | --- | --- |
387
+ | `--packages <a,b,c>` | Package list instead of the interactive checkbox. |
388
+ | `--yes` | Skip the "proceed?" confirmation. |
389
+ | `--force` | Discard uncommitted changes and local-only commits on the default branch, hard-resetting it to origin. |
300
390
 
301
391
  ```bash
302
392
  polyrepo switch-master
303
393
 
304
394
  # no checkbox, specific repos, no confirmation — for scripts
305
395
  polyrepo switch-master --packages vue-toast-kit,os-detect --yes
396
+
397
+ # discard local changes on a repo you don't need anymore
398
+ polyrepo switch-master --packages vue-toast-kit --force
306
399
  ```
307
400
 
308
401
  ### `polyrepo bump [options]`
@@ -313,15 +406,17 @@ polyrepo switch-master --packages vue-toast-kit,os-detect --yes
313
406
  the parts below it to `0`, same as any semver tool). Packages
314
407
  with a dirty working tree are marked — they'll be skipped. The
315
408
  highlighted package's description shows what's actually changed
316
- since the last git tag (`git log <tag>..master`) — if that's empty,
317
- there's probably nothing worth bumping. These previews are computed
318
- for every package in parallel, not one at a time.
409
+ since the last git tag (`git log <tag>..<default branch>`) — if
410
+ that's empty, there's probably nothing worth bumping. These
411
+ previews are computed for every package in parallel, not one at a
412
+ time.
319
413
  2. After confirming, for each selected package, one at a time, with
320
414
  live progress:
321
- 1. `git fetch origin` → `git checkout master`
322
- `git merge --ff-only origin/master` (the bump branch is always
323
- created from an up-to-date master, not whatever branch the repo
324
- happened to be on);
415
+ 1. `git fetch origin` → `git checkout <default branch>`
416
+ `git merge --ff-only origin/<default branch>` (the bump branch
417
+ is always created from an up-to-date default branch detected
418
+ per repo, see `switch-master` above — not whatever branch the
419
+ repo happened to be on);
325
420
  2. **checks the state of a previous attempt** — is there already a
326
421
  merged PR, an open PR, or just a pushed branch named
327
422
  `<new-version>-version-bump` (e.g. `1.2.10-version-bump` — the
@@ -329,8 +424,8 @@ polyrepo switch-master --packages vue-toast-kit,os-detect --yes
329
424
  different bumps never collide). Depending on what's found, it
330
425
  resumes from the right place instead of failing on "branch
331
426
  already exists" or opening a duplicate PR:
332
- - **already merged** — nothing to do (master was already synced
333
- in step 1), go straight to tagging;
427
+ - **already merged** — nothing to do (the default branch was
428
+ already synced in step 1), go straight to tagging;
334
429
  - **open PR exists** — merge that one, don't open a new one;
335
430
  - **branch pushed, no PR** — reuse the branch, open a PR;
336
431
  - **nothing exists** — the full flow from scratch.
@@ -344,7 +439,8 @@ polyrepo switch-master --packages vue-toast-kit,os-detect --yes
344
439
  review, not a finished changelog. Packages without a
345
440
  `CHANGELOG.md` don't get one created. Both files are committed
346
441
  together;
347
- 4. `gh pr create` against `master` (if there isn't one already);
442
+ 4. `gh pr create` against the default branch (if there isn't one
443
+ already);
348
444
  5. with `--wait-checks`: wait for the PR's CI checks via
349
445
  `gh pr checks --watch` (with a real terminal, live-updating). No
350
446
  checks configured isn't an error — there's just nothing to wait
@@ -352,9 +448,9 @@ polyrepo switch-master --packages vue-toast-kit,os-detect --yes
352
448
  skip the merge;
353
449
  6. `gh pr merge --merge` — through a PR, not a direct push, since
354
450
  these repos require it;
355
- 7. `git checkout master` → `git fetch origin` →
356
- `git merge --ff-only origin/master` — local master is synced to
357
- the just-merged PR;
451
+ 7. `git checkout <default branch>` → `git fetch origin` →
452
+ `git merge --ff-only origin/<default branch>` — local default
453
+ branch is synced to the just-merged PR;
358
454
  8. **git tag** `v<new-version>` (e.g. `v1.2.10`) is created and
359
455
  pushed if it doesn't already exist (idempotent, like everything
360
456
  else here — a re-run won't try to create it twice).
@@ -414,11 +510,16 @@ polyrepo bump --packages vue-toast-kit,os-detect --yes
414
510
  they differ (genuinely unpublished) are pre-selected; already
415
511
  published ones are unchecked but still selectable (e.g. to
416
512
  republish after an unpublish).
417
- 3. After confirming, for each selected package, one at a time:
418
- `npm publish` (or `npm publish --dry-run` with the `--dry-run`
419
- flag npm's own dry run, including the real build and pack step,
420
- not just printing a plan). Runs with a real terminal, not captured
421
- an npm 2FA/OTP prompt works normally.
513
+ 3. After confirming, checks `npm whoami` once for the whole batch
514
+ (skipped under `--dry-run`) not logged in runs `npm login`
515
+ (a real terminal, so its browser-based OTP flow or credential
516
+ prompt works normally) before publishing anything, instead of
517
+ only finding out partway through the first package's `npm publish`.
518
+ 4. For each selected package, one at a time: `npm publish` (or
519
+ `npm publish --dry-run` with the `--dry-run` flag — npm's own dry
520
+ run, including the real build and pack step, not just printing a
521
+ plan). Runs with a real terminal, not captured — an npm 2FA/OTP
522
+ prompt works normally.
422
523
 
423
524
  **Options:**
424
525
 
@@ -453,9 +554,9 @@ it again or opening a PR:
453
554
  pre-selected; already-tagged ones can still be picked manually
454
555
  (harmless — it just confirms the tag is there).
455
556
  3. After confirming, for each selected package, one at a time:
456
- `git fetch`/`checkout master`/`merge --ff-only` (tags an up-to-date
457
- master, same as `bump`), then creates and pushes the tag if it's
458
- missing.
557
+ `git fetch`/`checkout <default branch>`/`merge --ff-only` (tags an
558
+ up-to-date default branch, same as `bump`), then creates and pushes
559
+ the tag if it's missing.
459
560
  4. If at least one package was actually tagged (and it wasn't a
460
561
  `--dry-run`), it asks: "Create a GitHub Release for the N
461
562
  package(s) just tagged?" — answering yes runs the same process as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polyrepo-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Interactive CLI for managing a folder of local npm package repos: version bumps through a PR, npm publish, GitHub releases, and cross-package dependency drift checks — all from one tool.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/changes.js CHANGED
@@ -1,21 +1,21 @@
1
- import { MASTER_BRANCH } from './config.js'
2
1
  import { git, gitAsync } from './exec.js'
3
2
  import { pMap } from './pMap.js'
4
3
 
5
4
  const PREVIEW_LIMIT = 5
6
5
 
7
- // Summarizes what changed on local master since the last `v*` tag (or the
8
- // last few commits if there's no tag yet), so the bump checklist can show
9
- // whether a package actually has anything worth releasing. Reflects local
10
- // repo state — run `polyrepo switch-master` first if it might be stale. Run
11
- // through pMap across many repos at once (see describeRecentChangesForAll)
12
- // — this builds the bump checkbox's preview, and waiting on 3 sequential
13
- // git calls per repo, 17 times over, added up to a real pause before the
14
- // prompt even appeared.
6
+ // Summarizes what changed on the local default branch since the last `v*`
7
+ // tag (or the last few commits if there's no tag yet), so the bump
8
+ // checklist can show whether a package actually has anything worth
9
+ // releasing. Reflects local repo state — run `polyrepo switch-master`
10
+ // first if it might be stale. Run through pMap across many repos at once
11
+ // (see describeRecentChangesForAll) — this builds the bump checkbox's
12
+ // preview, and waiting on 3 sequential git calls per repo, 17 times over,
13
+ // added up to a real pause before the prompt even appeared.
15
14
  async function describeRecentChangesAsync(repo) {
16
- const tagResult = await gitAsync(repo.path, ['describe', '--tags', '--abbrev=0', '--match', 'v*', MASTER_BRANCH])
15
+ const branch = repo.defaultBranch
16
+ const tagResult = await gitAsync(repo.path, ['describe', '--tags', '--abbrev=0', '--match', 'v*', branch])
17
17
  const sinceTag = tagResult.ok ? tagResult.stdout : null
18
- const range = sinceTag ? `${sinceTag}..${MASTER_BRANCH}` : MASTER_BRANCH
18
+ const range = sinceTag ? `${sinceTag}..${branch}` : branch
19
19
 
20
20
  const [countResult, logResult] = await Promise.all([
21
21
  gitAsync(repo.path, ['rev-list', '--count', range]),
@@ -34,9 +34,9 @@ export function describeRecentChangesForAll(repos, concurrency) {
34
34
  // Uncapped commit list from the last `v*` tag to HEAD — used right before
35
35
  // committing a version bump (see bump.js) to draft a CHANGELOG.md entry, so
36
36
  // unlike describeRecentChanges (capped for a preview) this needs the whole
37
- // list. Uses HEAD rather than MASTER_BRANCH because it's called from
38
- // exactly where that matters: after checking out the bump branch, whose
39
- // HEAD is master's tip at that point anyway.
37
+ // list. Uses HEAD rather than the repo's default branch because it's
38
+ // called from exactly where that matters: after checking out the bump
39
+ // branch, whose HEAD is the default branch's tip at that point anyway.
40
40
  export function fullCommitLinesSince(repo) {
41
41
  const tagResult = git(repo.path, ['describe', '--tags', '--abbrev=0', '--match', 'v*', 'HEAD'], { quiet: true })
42
42
  const sinceTag = tagResult.ok ? tagResult.stdout : null
@@ -2,7 +2,7 @@ import fs from 'node:fs'
2
2
  import { confirm } from '@inquirer/prompts'
3
3
  import pc from 'picocolors'
4
4
  import { discoverRepos, inspectRepos, readPackageJson } from '../repos.js'
5
- import { MASTER_BRANCH, bumpBranchName } from '../config.js'
5
+ import { bumpBranchName } from '../config.js'
6
6
  import { loadConfig } from '../loadConfig.js'
7
7
  import { bumpVersion, replaceVersionInText } from '../version.js'
8
8
  import { git } from '../exec.js'
@@ -84,7 +84,7 @@ export async function bumpCommand({
84
84
 
85
85
  if (!yes) {
86
86
  const proceed = await confirm({
87
- message: `Bump ${selected.length} package(s), open a PR, and merge each into ${MASTER_BRANCH}?${
87
+ message: `Bump ${selected.length} package(s), open a PR, and merge each into its default branch?${
88
88
  dryRun ? ' (dry run — no changes will actually be pushed)' : ''
89
89
  }`,
90
90
  default: true,
@@ -113,7 +113,7 @@ async function bumpOne(repo, { dryRun, waitChecks }) {
113
113
 
114
114
  const syncResult = syncMaster(repo)
115
115
  if (!syncResult.ok) return fail(syncResult.message)
116
- ok(`${MASTER_BRANCH} is up to date.`)
116
+ ok(`${repo.defaultBranch} is up to date.`)
117
117
 
118
118
  const branchName = bumpBranchName(repo.newVersion)
119
119
  const state = detectBumpState(repo, branchName)
@@ -172,7 +172,7 @@ async function bumpOne(repo, { dryRun, waitChecks }) {
172
172
 
173
173
  if (state.status !== 'open') {
174
174
  const prResult = createPr(repo, {
175
- base: MASTER_BRANCH,
175
+ base: repo.defaultBranch,
176
176
  branch: branchName,
177
177
  title: `chore: bump version to ${repo.newVersion}`,
178
178
  body: `Bump version: ${repo.version} → ${repo.newVersion}.`,
@@ -195,9 +195,9 @@ async function bumpOne(repo, { dryRun, waitChecks }) {
195
195
 
196
196
  const resyncResult = syncMaster(repo)
197
197
  if (!resyncResult.ok) return fail(resyncResult.message)
198
- ok(`Local ${MASTER_BRANCH} synced to origin at ${repo.newVersion}.`)
198
+ ok(`Local ${repo.defaultBranch} synced to origin at ${repo.newVersion}.`)
199
199
  } else {
200
- ok(`Already merged as PR #${state.pr.number} — ${MASTER_BRANCH} already has it.`)
200
+ ok(`Already merged as PR #${state.pr.number} — ${repo.defaultBranch} already has it.`)
201
201
  }
202
202
 
203
203
  const tag = tagName(repo.newVersion)
@@ -213,8 +213,9 @@ async function bumpOne(repo, { dryRun, waitChecks }) {
213
213
  // Reuse a local branch left over from a previous attempt if there is one,
214
214
  // otherwise track the remote branch if a previous attempt got as far as
215
215
  // pushing it, otherwise create it fresh from the current (just-synced)
216
- // master. Trying "reuse" first before falling back is what makes this safe
217
- // to call again after any partial failure without any state bookkeeping.
216
+ // default branch. Trying "reuse" first before falling back is what makes
217
+ // this safe to call again after any partial failure without any state
218
+ // bookkeeping.
218
219
  function checkoutBumpBranch(repo, branchName) {
219
220
  if (git(repo.path, ['checkout', branchName], { quiet: true }).ok) {
220
221
  return { ok: true, reused: true }