polyrepo-cli 1.0.2 → 1.0.3

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
@@ -30,9 +30,11 @@ with `--dry-run` before anything actually changes.
30
30
  safe, non-destructive self-repairs along the way, and
31
31
  `--clean-branches` for an interactive local-branch cleanup), and
32
32
  cross-package dependency drift.
33
- - **`switch-master`** — fast-forward selected repos to their up-to-date
33
+ - **`switch-default`** — fast-forward selected repos to their up-to-date
34
34
  default branch, whatever it's actually named (`master`, `main`, or
35
- anything else — detected per repo, not assumed).
35
+ anything else — detected per repo, not assumed). `--force` hard-resets
36
+ a dirty repo to match origin; add `--clean` to also wipe untracked
37
+ files/directories.
36
38
  - **`bump`** — bump a package's version (patch by default, or
37
39
  `--minor`/`--major`) through a branch → PR → merge, then tag the
38
40
  release. Safe to re-run if a previous attempt was interrupted
@@ -284,8 +286,8 @@ few small, non-destructive self-repairs:
284
286
  3. **Remote sync** — two related repairs, both per-repo pointer
285
287
  refreshes that never touch a file, branch, or commit:
286
288
  - 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
+ same value `switch-default`/`bump`/`tag` all use — see
290
+ `switch-default` above) against what GitHub actually reports
289
291
  right now. Git never refreshes that local cache on its own, so
290
292
  renaming a repo's default branch on GitHub after it was cloned
291
293
  would otherwise go unnoticed by every other command forever —
@@ -302,7 +304,7 @@ few small, non-destructive self-repairs:
302
304
  4. **Branch sync** — fetches and compares each repo's local default
303
305
  branch against `origin/<default>`: **diverged** (both ahead and
304
306
  behind — a fast-forward won't work, needs resolving by hand),
305
- **behind only** (safe to fast-forward with `switch-master`), or
307
+ **behind only** (safe to fast-forward with `switch-default`), or
306
308
  **ahead only** (local commits not yet pushed). Surfaces this
307
309
  before some other command trips over it mid-run instead of after.
308
310
  5. **Branch protection** — whether each repo's default branch
@@ -345,7 +347,7 @@ polyrepo doctor --clean-branches
345
347
  polyrepo doctor
346
348
  ```
347
349
 
348
- ### `polyrepo switch-master` (alias `sm`)
350
+ ### `polyrepo switch-default` (alias `sd`)
349
351
 
350
352
  Each repo's **default branch is detected per repo**, not assumed —
351
353
  GitHub itself defaults a new repo to `main`, and plenty of people
@@ -374,11 +376,15 @@ working tree is no longer skipped, and each repo gets
374
376
  `git checkout -f <default branch>` + `git reset --hard
375
377
  origin/<default branch>` instead of the safe fast-forward-only merge —
376
378
  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.
379
+ that branch are permanently discarded. Untracked files are still left
380
+ alone at this point (this isn't `git clean`) add `--clean` to also
381
+ run `git clean -fd` (removes untracked files/directories that aren't
382
+ gitignored; gitignored paths like `node_modules` are still left
383
+ alone, this doesn't use `-x`). `--clean` only means anything alongside
384
+ `--force` — without it there's nothing to discard in the first place.
385
+ The checkbox marks which selected repos would lose changes, and the
386
+ proceed confirmation says how many and defaults to "No" instead of
387
+ "Yes" whenever `--force` would actually discard something.
382
388
 
383
389
  **Options:**
384
390
 
@@ -387,15 +393,19 @@ actually discard something.
387
393
  | `--packages <a,b,c>` | Package list instead of the interactive checkbox. |
388
394
  | `--yes` | Skip the "proceed?" confirmation. |
389
395
  | `--force` | Discard uncommitted changes and local-only commits on the default branch, hard-resetting it to origin. |
396
+ | `--clean` | With `--force`, also remove untracked files/directories (`git clean -fd`). |
390
397
 
391
398
  ```bash
392
- polyrepo switch-master
399
+ polyrepo switch-default
393
400
 
394
401
  # no checkbox, specific repos, no confirmation — for scripts
395
- polyrepo switch-master --packages vue-toast-kit,os-detect --yes
402
+ polyrepo switch-default --packages vue-toast-kit,os-detect --yes
396
403
 
397
404
  # discard local changes on a repo you don't need anymore
398
- polyrepo switch-master --packages vue-toast-kit --force
405
+ polyrepo switch-default --packages vue-toast-kit --force
406
+
407
+ # same, but also wipe untracked build output etc.
408
+ polyrepo switch-default --packages vue-toast-kit --force --clean
399
409
  ```
400
410
 
401
411
  ### `polyrepo bump [options]`
@@ -415,7 +425,7 @@ polyrepo switch-master --packages vue-toast-kit --force
415
425
  1. `git fetch origin` → `git checkout <default branch>` →
416
426
  `git merge --ff-only origin/<default branch>` (the bump branch
417
427
  is always created from an up-to-date default branch — detected
418
- per repo, see `switch-master` above — not whatever branch the
428
+ per repo, see `switch-default` above — not whatever branch the
419
429
  repo happened to be on);
420
430
  2. **checks the state of a previous attempt** — is there already a
421
431
  merged PR, an open PR, or just a pushed branch named
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polyrepo-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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
@@ -6,7 +6,7 @@ const PREVIEW_LIMIT = 5
6
6
  // Summarizes what changed on the local default branch since the last `v*`
7
7
  // tag (or the last few commits if there's no tag yet), so the bump
8
8
  // checklist can show whether a package actually has anything worth
9
- // releasing. Reflects local repo state — run `polyrepo switch-master`
9
+ // releasing. Reflects local repo state — run `polyrepo switch-default`
10
10
  // first if it might be stale. Run through pMap across many repos at once
11
11
  // (see describeRecentChangesForAll) — this builds the bump checkbox's
12
12
  // preview, and waiting on 3 sequential git calls per repo, 17 times over,
@@ -6,7 +6,7 @@ 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'
9
- import { syncMaster } from '../masterSync.js'
9
+ import { syncDefaultBranch } from '../defaultBranchSync.js'
10
10
  import { detectBumpState, createPr, mergePr } from '../github.js'
11
11
  import { tagName, tagExists, createAndPushTag } from '../tags.js'
12
12
  import { describeRecentChangesForAll, formatRecentChanges, fullCommitLinesSince } from '../changes.js'
@@ -111,7 +111,7 @@ async function bumpOne(repo, { dryRun, waitChecks }) {
111
111
  return
112
112
  }
113
113
 
114
- const syncResult = syncMaster(repo)
114
+ const syncResult = syncDefaultBranch(repo)
115
115
  if (!syncResult.ok) return fail(syncResult.message)
116
116
  ok(`${repo.defaultBranch} is up to date.`)
117
117
 
@@ -193,7 +193,7 @@ async function bumpOne(repo, { dryRun, waitChecks }) {
193
193
  }
194
194
  ok(`Merged PR #${prNumber}.`)
195
195
 
196
- const resyncResult = syncMaster(repo)
196
+ const resyncResult = syncDefaultBranch(repo)
197
197
  if (!resyncResult.ok) return fail(resyncResult.message)
198
198
  ok(`Local ${repo.defaultBranch} synced to origin at ${repo.newVersion}.`)
199
199
  } else {
@@ -151,7 +151,7 @@ async function checkRemoteSync(repos) {
151
151
  }
152
152
 
153
153
  // Whether each repo's local default branch actually matches origin —
154
- // surfaced here proactively so it's known before a `switch-master` run
154
+ // surfaced here proactively so it's known before a `switch-default` run
155
155
  // fails partway on a fast-forward it can't do, or before `bump`/`tag`
156
156
  // build a branch from a base that isn't what it looks like locally.
157
157
  async function checkBranchSync(repos) {
@@ -186,7 +186,7 @@ async function checkBranchSync(repos) {
186
186
  }
187
187
  if (behindOnly.length > 0) {
188
188
  warn(
189
- `${behindOnly.length} repo(s) are behind origin (safe to fast-forward with \`polyrepo switch-master\`): ${behindOnly
189
+ `${behindOnly.length} repo(s) are behind origin (safe to fast-forward with \`polyrepo switch-default\`): ${behindOnly
190
190
  .map((r) => r.repo.dir)
191
191
  .join(', ')}`,
192
192
  )
@@ -2,12 +2,12 @@ import { confirm } from '@inquirer/prompts'
2
2
  import pc from 'picocolors'
3
3
  import { discoverRepos, inspectRepos } from '../repos.js'
4
4
  import { loadConfig } from '../loadConfig.js'
5
- import { syncMaster } from '../masterSync.js'
5
+ import { syncDefaultBranch } from '../defaultBranchSync.js'
6
6
  import { selectPackages } from '../selectPackages.js'
7
7
  import { heading, stepHeading, ok, fail, warn, columnWidths, formatRow } from '../ui.js'
8
8
  import { startSpinner } from '../spinner.js'
9
9
 
10
- export async function switchMasterCommand({ configPath, packages, yes = false, force = false } = {}) {
10
+ export async function switchDefaultCommand({ configPath, packages, yes = false, force = false, clean = false } = {}) {
11
11
  const config = loadConfig({ configPath })
12
12
  const discovered = discoverRepos(config)
13
13
  if (discovered.length === 0) {
@@ -24,7 +24,7 @@ export async function switchMasterCommand({ configPath, packages, yes = false, f
24
24
  const selected = await selectPackages({
25
25
  items: repos,
26
26
  packages,
27
- message: "Pick repos to switch to their default branch and update:",
27
+ message: 'Pick repos to switch to their default branch and update:',
28
28
  buildChoice: (all) => {
29
29
  const columns = [{ value: (r) => r.dir }]
30
30
  const widths = columnWidths(all, columns)
@@ -36,7 +36,7 @@ export async function switchMasterCommand({ configPath, packages, yes = false, f
36
36
  r.branch !== r.defaultBranch ? `, default: ${r.defaultBranch}` : ''
37
37
  })`,
38
38
  ) +
39
- (force && !r.clean ? pc.red(' will discard uncommitted changes') : ''),
39
+ (force && !r.clean ? pc.red(` will discard uncommitted changes${clean ? ' and untracked files' : ''}`) : ''),
40
40
  value: r,
41
41
  checked: r.branch !== r.defaultBranch,
42
42
  })
@@ -54,7 +54,9 @@ export async function switchMasterCommand({ configPath, packages, yes = false, f
54
54
  const proceed = await confirm({
55
55
  message:
56
56
  force && dirtyCount > 0
57
- ? `Switch ${selected.length} repo(s) to their default branch — ${dirtyCount} of them dirty, their uncommitted changes will be permanently discarded. Continue?`
57
+ ? `Switch ${selected.length} repo(s) to their default branch — ${dirtyCount} of them dirty, their uncommitted changes${
58
+ clean ? ' and untracked files' : ''
59
+ } will be permanently discarded. Continue?`
58
60
  : `Switch ${selected.length} repo(s) to their default branch and fast-forward?`,
59
61
  default: !force,
60
62
  })
@@ -74,7 +76,7 @@ export async function switchMasterCommand({ configPath, packages, yes = false, f
74
76
  continue
75
77
  }
76
78
 
77
- const result = syncMaster(repo, { force })
79
+ const result = syncDefaultBranch(repo, { force, clean })
78
80
  if (!result.ok) {
79
81
  fail(result.message)
80
82
  continue
@@ -82,7 +84,7 @@ export async function switchMasterCommand({ configPath, packages, yes = false, f
82
84
 
83
85
  ok(
84
86
  force && !repo.clean
85
- ? `Discarded local changes — now on ${repo.defaultBranch}, matching origin.`
87
+ ? `Discarded local changes${clean ? ' and untracked files' : ''} — now on ${repo.defaultBranch}, matching origin.`
86
88
  : `Now on ${repo.defaultBranch}, up to date with origin.`,
87
89
  )
88
90
  }
@@ -2,7 +2,7 @@ import { confirm } from '@inquirer/prompts'
2
2
  import pc from 'picocolors'
3
3
  import { discoverRepos, inspectRepos } from '../repos.js'
4
4
  import { loadConfig } from '../loadConfig.js'
5
- import { syncMaster } from '../masterSync.js'
5
+ import { syncDefaultBranch } from '../defaultBranchSync.js'
6
6
  import { tagName, tagExists, tagExistsAsync, createAndPushTag } from '../tags.js'
7
7
  import { selectPackages } from '../selectPackages.js'
8
8
  import { filterByNames } from '../filterByNames.js'
@@ -88,7 +88,7 @@ export async function tagCommand({ configPath, packages, yes = false, dryRun = f
88
88
  index += 1
89
89
  stepHeading(index, selected.length, `${repo.dir} ${repo.tag}`)
90
90
 
91
- const syncResult = syncMaster(repo)
91
+ const syncResult = syncDefaultBranch(repo)
92
92
  if (!syncResult.ok) {
93
93
  fail(syncResult.message)
94
94
  continue
@@ -7,14 +7,18 @@ import { git } from './exec.js'
7
7
  // (detected per repo — see repos.js's detectDefaultBranchAsync) rather
8
8
  // than assuming "master", since that isn't universal.
9
9
  //
10
- // `force: true` (only `switch-master --force` sets this) trades the safe
10
+ // `force: true` (only `switch-default --force` sets this) trades the safe
11
11
  // fast-forward-only merge for `checkout -f` + `reset --hard` — it discards
12
12
  // any uncommitted changes to tracked files and any local commits the
13
13
  // default branch has that origin doesn't, unconditionally. Untracked
14
- // files are left alone (this isn't `git clean`). Callers that don't pass
15
- // it keep the original safe behavior bump/tag rely on that, they never
16
- // force.
17
- export function syncMaster(repo, { force = false } = {}) {
14
+ // files are left alone unless `clean: true` is also given (only valid
15
+ // together with `force`)that additionally runs `git clean -fd`,
16
+ // removing untracked files/directories that aren't gitignored (build
17
+ // output that isn't in .gitignore is the common case). `-x` is
18
+ // deliberately not used — gitignored paths like node_modules are left
19
+ // alone even under --clean. Callers that don't pass `force` keep the
20
+ // original safe behavior — bump/tag rely on that, they never force.
21
+ export function syncDefaultBranch(repo, { force = false, clean = false } = {}) {
18
22
  const branch = repo.defaultBranch
19
23
  if (!git(repo.path, ['fetch', 'origin']).ok) {
20
24
  return { ok: false, message: 'git fetch origin failed.' }
@@ -26,6 +30,9 @@ export function syncMaster(repo, { force = false } = {}) {
26
30
  if (!git(repo.path, ['reset', '--hard', `origin/${branch}`]).ok) {
27
31
  return { ok: false, message: `git reset --hard origin/${branch} failed.` }
28
32
  }
33
+ if (clean && !git(repo.path, ['clean', '-fd']).ok) {
34
+ return { ok: false, message: 'git clean -fd failed.' }
35
+ }
29
36
  return { ok: true }
30
37
  }
31
38
  if (!git(repo.path, ['merge', '--ff-only', `origin/${branch}`]).ok) {
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from 'commander'
6
6
  import pc from 'picocolors'
7
7
  import { listCommand } from './commands/list.js'
8
8
  import { doctorCommand } from './commands/doctor.js'
9
- import { switchMasterCommand } from './commands/switchMaster.js'
9
+ import { switchDefaultCommand } from './commands/switchDefault.js'
10
10
  import { bumpCommand } from './commands/bump.js'
11
11
  import { publishCommand } from './commands/publish.js'
12
12
  import { tagCommand } from './commands/tag.js'
@@ -94,7 +94,7 @@ function wrapText(text, width) {
94
94
  program
95
95
  .name('polyrepo')
96
96
  .description(
97
- 'Manage local npm package repos: pick which directories to scan (setup), clone missing ones from GitHub (clone), see their state (list), outdated dependencies (outdated), or open PRs (prs), run a health check (doctor), keep them on an up-to-date default branch (switch-master), release a version through a PR (bump), publish to npm (publish), tag an already-current version (tag), create GitHub Releases (release), or run any command across every repo (exec).',
97
+ 'Manage local npm package repos: pick which directories to scan (setup), clone missing ones from GitHub (clone), see their state (list), outdated dependencies (outdated), or open PRs (prs), run a health check (doctor), keep them on an up-to-date default branch (switch-default), release a version through a PR (bump), publish to npm (publish), tag an already-current version (tag), create GitHub Releases (release), or run any command across every repo (exec).',
98
98
  )
99
99
  .version(CLI_VERSION)
100
100
  .option(
@@ -124,7 +124,7 @@ Examples:
124
124
  $ polyrepo list Show version + branch for every package
125
125
  $ polyrepo outdated Show outdated dependencies across every package
126
126
  $ polyrepo prs List open pull requests across every package
127
- $ polyrepo switch-master Update selected repos to their latest default branch
127
+ $ polyrepo switch-default Update selected repos to their latest default branch
128
128
  $ polyrepo bump --dry-run Preview a version bump, nothing is pushed
129
129
  $ polyrepo bump --minor --packages a,b --yes Bump specific packages' minor version, non-interactively
130
130
  $ polyrepo publish Publish packages that are ahead of the registry
@@ -322,7 +322,7 @@ non-destructive self-repair:
322
322
  Branch sync fetches and compares each repo's local default
323
323
  branch against origin: diverged (needs manual
324
324
  resolution), behind only (safe to fast-forward with
325
- \`switch-master\`), or ahead only (unpushed local
325
+ \`switch-default\`), or ahead only (unpushed local
326
326
  commits) — surfaced before a command trips over it.
327
327
  Branch protection whether each repo's default branch actually has
328
328
  GitHub branch protection enabled. Report-only —
@@ -355,8 +355,8 @@ Examples:
355
355
  )
356
356
 
357
357
  program
358
- .command('switch-master')
359
- .alias('sm')
358
+ .command('switch-default')
359
+ .alias('sd')
360
360
  .description('Pick repos and switch each to its up-to-date default branch.')
361
361
  .option(...PACKAGES_OPTION)
362
362
  .option(...YES_OPTION)
@@ -364,6 +364,10 @@ program
364
364
  '--force',
365
365
  "Discard uncommitted changes and any local-only commits on a repo's default branch, hard-resetting it to match origin.",
366
366
  )
367
+ .option(
368
+ '--clean',
369
+ 'With --force, also remove untracked files/directories (git clean -fd) — only valid together with --force.',
370
+ )
367
371
  .addHelpText(
368
372
  'after',
369
373
  `
@@ -380,25 +384,36 @@ to resolve by hand.
380
384
  selected repo gets \`git checkout -f <default branch>\` + \`git reset --hard
381
385
  origin/<default branch>\` instead of the safe fast-forward-only merge —
382
386
  uncommitted changes to tracked files and any local-only commits on that
383
- branch are permanently discarded (untracked files are left alone, this
384
- isn't \`git clean\`). The proceed confirmation says how many selected
385
- repos are dirty and defaults to "No" when --force would actually discard
386
- something.
387
+ branch are permanently discarded. Untracked files are still left alone
388
+ at this point (this isn't \`git clean\`) add --clean to also run
389
+ \`git clean -fd\` (removes untracked files/directories that aren't
390
+ gitignored; gitignored paths like node_modules are still left alone,
391
+ this doesn't use -x). --clean only means anything alongside --force —
392
+ without it there's nothing to discard in the first place. The proceed
393
+ confirmation says how many selected repos are dirty and defaults to
394
+ "No" when --force would actually discard something.
387
395
 
388
396
  Examples:
389
- $ polyrepo switch-master
390
- $ polyrepo sm --packages vue-toast-kit,os-detect --yes
391
- $ polyrepo sm --packages vue-toast-kit --force Discard its local changes and hard-reset to origin
397
+ $ polyrepo switch-default
398
+ $ polyrepo sd --packages vue-toast-kit,os-detect --yes
399
+ $ polyrepo sd --packages vue-toast-kit --force Discard its local changes and hard-reset to origin
400
+ $ polyrepo sd --packages vue-toast-kit --force --clean Also remove untracked build output etc.
392
401
  `,
393
402
  )
394
- .action((opts) =>
395
- switchMasterCommand({
403
+ .action((opts) => {
404
+ if (opts.clean && !opts.force) {
405
+ console.error('--clean only makes sense together with --force.')
406
+ process.exitCode = 1
407
+ return
408
+ }
409
+ return switchDefaultCommand({
396
410
  configPath: program.opts().config,
397
411
  packages: opts.packages ? opts.packages.split(',') : undefined,
398
412
  yes: Boolean(opts.yes),
399
413
  force: Boolean(opts.force),
400
- }),
401
- )
414
+ clean: Boolean(opts.clean),
415
+ })
416
+ })
402
417
 
403
418
  program
404
419
  .command('bump')
package/src/spinner.js CHANGED
@@ -9,7 +9,7 @@ const CLEAR_LINE = '\r\x1b[K'
9
9
 
10
10
  // Covers the "nothing is printed for several seconds" gap while a parallel
11
11
  // batch (pMap) is in flight and there's no per-item progress to show yet —
12
- // list's full mode, doctor, and the initial repo scan in bump/switch-master
12
+ // list's full mode, doctor, and the initial repo scan in bump/switch-default
13
13
  // all used to just sit there silently. Falls back to a single static line
14
14
  // when stdout isn't a real terminal (piped output, CI logs) — redrawing
15
15
  // with carriage returns there would just dump a stream of raw \r bytes
package/src/ui.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import pc from 'picocolors'
2
2
 
3
3
  // Column width/row-formatting is shared by `printTable` (the `list` output)
4
- // and by the checkbox choice labels in `bump`/`switch-master`/`publish` —
4
+ // and by the checkbox choice labels in `bump`/`switch-default`/`publish` —
5
5
  // so a package name column lines up the same way whether it's shown in a
6
6
  // static table or as a pickable list, instead of each command reinventing
7
7
  // its own ad-hoc padding (which is how columns used to drift out of line