polyrepo-cli 1.0.0 → 1.0.1

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
@@ -414,11 +414,16 @@ polyrepo bump --packages vue-toast-kit,os-detect --yes
414
414
  they differ (genuinely unpublished) are pre-selected; already
415
415
  published ones are unchecked but still selectable (e.g. to
416
416
  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.
417
+ 3. After confirming, checks `npm whoami` once for the whole batch
418
+ (skipped under `--dry-run`) not logged in runs `npm login`
419
+ (a real terminal, so its browser-based OTP flow or credential
420
+ prompt works normally) before publishing anything, instead of
421
+ only finding out partway through the first package's `npm publish`.
422
+ 4. For each selected package, one at a time: `npm publish` (or
423
+ `npm publish --dry-run` with the `--dry-run` flag — npm's own dry
424
+ run, including the real build and pack step, not just printing a
425
+ plan). Runs with a real terminal, not captured — an npm 2FA/OTP
426
+ prompt works normally.
422
427
 
423
428
  **Options:**
424
429
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polyrepo-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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": {
@@ -5,19 +5,29 @@ import { loadConfig } from '../loadConfig.js'
5
5
  import { npm } from '../exec.js'
6
6
  import { fetchPublishedVersionAsync } from '../registry.js'
7
7
  import { selectPackages } from '../selectPackages.js'
8
+ import { filterByNames } from '../filterByNames.js'
8
9
  import { pMap } from '../pMap.js'
9
- import { heading, stepHeading, ok, fail, columnWidths, formatRow } from '../ui.js'
10
+ import { heading, stepHeading, ok, fail, warn, columnWidths, formatRow } from '../ui.js'
10
11
 
11
12
  export async function publishCommand({ configPath, packages, yes = false, dryRun = false } = {}) {
12
13
  const config = loadConfig({ configPath })
13
- const repos = (await inspectRepos(discoverRepos(config))).filter((r) => r.version)
14
- if (repos.length === 0) {
14
+ const allRepos = (await inspectRepos(discoverRepos(config))).filter((r) => r.version)
15
+ if (allRepos.length === 0) {
15
16
  console.log(pc.yellow('No repos found.'))
16
17
  return
17
18
  }
18
19
 
19
20
  heading('Publish to npm')
20
21
 
22
+ // Narrowed to --packages up front (a no-op when it wasn't given) — no
23
+ // reason to hit the registry for, or print the status of, packages
24
+ // nobody asked to publish.
25
+ const repos = filterByNames(allRepos, packages)
26
+ if (repos.length === 0) {
27
+ console.log(pc.dim('Nothing selected.'))
28
+ return
29
+ }
30
+
21
31
  console.log(pc.dim(`Checking ${repos.length} package(s) against the registry...`))
22
32
  // Each repo's registry lookup is a real network round trip — running them
23
33
  // all at once instead of one at a time is where this actually pays off.
@@ -34,7 +44,11 @@ export async function publishCommand({ configPath, packages, yes = false, dryRun
34
44
 
35
45
  const selected = await selectPackages({
36
46
  items: withRegistry,
37
- packages,
47
+ // repos above is already the exact --packages match — passing those
48
+ // same dir names back through here just skips the checkbox (as
49
+ // before) without filterByNames re-warning about anything, since
50
+ // there's nothing left for it to not find.
51
+ packages: packages ? withRegistry.map((r) => r.dir) : undefined,
38
52
  message: 'Pick packages to publish:',
39
53
  buildChoice: (all) => {
40
54
  const columns = [
@@ -68,6 +82,8 @@ export async function publishCommand({ configPath, packages, yes = false, dryRun
68
82
  }
69
83
  }
70
84
 
85
+ if (!dryRun && !(await ensureNpmLogin())) return
86
+
71
87
  let index = 0
72
88
  for (const repo of selected) {
73
89
  index += 1
@@ -80,3 +96,24 @@ export async function publishCommand({ configPath, packages, yes = false, dryRun
80
96
  else ok(`Published ${repo.name}@${repo.version}${dryRun ? ' (dry run).' : '.'}`)
81
97
  }
82
98
  }
99
+
100
+ // Without this, a batch of several packages would only find out about a
101
+ // missing/expired npm login at the very end of the first `npm publish` —
102
+ // after everything before it in the run (fetch, checkout, confirmation)
103
+ // already succeeded. Checked once for the whole batch, not per package,
104
+ // since npm auth isn't per-repo. `npm login` needs a real terminal — it
105
+ // can open a browser for npm's web-based OTP flow, or prompt directly.
106
+ async function ensureNpmLogin() {
107
+ if (npm('.', ['whoami'], { quiet: true }).ok) return true
108
+
109
+ warn('Not logged in to npm — running `npm login` first.')
110
+ npm('.', ['login'], { interactive: true })
111
+
112
+ if (npm('.', ['whoami'], { quiet: true }).ok) {
113
+ ok('Logged in to npm.')
114
+ return true
115
+ }
116
+
117
+ fail('npm login did not succeed — aborting before publishing anything.')
118
+ return false
119
+ }
@@ -6,19 +6,29 @@ import { tagName, tagExists } from '../tags.js'
6
6
  import { releaseExistsAsync, createRelease } from '../release.js'
7
7
  import { extractChangelogSection } from '../changelog.js'
8
8
  import { selectPackages } from '../selectPackages.js'
9
+ import { filterByNames } from '../filterByNames.js'
9
10
  import { pMap } from '../pMap.js'
10
11
  import { heading, stepHeading, ok, fail, columnWidths, formatRow } from '../ui.js'
11
12
 
12
13
  export async function releaseCommand({ configPath, packages, yes = false, dryRun = false } = {}) {
13
14
  const config = loadConfig({ configPath })
14
- const repos = (await inspectRepos(discoverRepos(config))).filter((r) => r.version)
15
- if (repos.length === 0) {
15
+ const allRepos = (await inspectRepos(discoverRepos(config))).filter((r) => r.version)
16
+ if (allRepos.length === 0) {
16
17
  console.log(pc.yellow('No repos found.'))
17
18
  return
18
19
  }
19
20
 
20
21
  heading('GitHub releases')
21
22
 
23
+ // Narrowed to --packages up front (a no-op when it wasn't given) — no
24
+ // reason to check, or print the tag/release status of, packages nobody
25
+ // asked about.
26
+ const repos = filterByNames(allRepos, packages)
27
+ if (repos.length === 0) {
28
+ console.log(pc.dim('Nothing selected.'))
29
+ return
30
+ }
31
+
22
32
  console.log(pc.dim(`Checking ${repos.length} package(s) for a tag and an existing release...`))
23
33
  // A release always targets `v<local version>` — the tag `polyrepo bump`
24
34
  // creates. No tag for the current version means there's nothing to
@@ -48,7 +58,10 @@ export async function releaseCommand({ configPath, packages, yes = false, dryRun
48
58
 
49
59
  const selected = await selectPackages({
50
60
  items: withStatus,
51
- packages,
61
+ // withStatus is already the exact --packages match — passing those
62
+ // same dir names back here just skips the checkbox without
63
+ // re-warning about anything (see publish.js for the same pattern).
64
+ packages: packages ? withStatus.map((r) => r.dir) : undefined,
52
65
  message: 'Pick packages to create a GitHub Release for:',
53
66
  buildChoice: (all) => {
54
67
  const columns = [
@@ -5,6 +5,7 @@ import { loadConfig } from '../loadConfig.js'
5
5
  import { syncMaster } from '../masterSync.js'
6
6
  import { tagName, tagExists, tagExistsAsync, createAndPushTag } from '../tags.js'
7
7
  import { selectPackages } from '../selectPackages.js'
8
+ import { filterByNames } from '../filterByNames.js'
8
9
  import { pMap } from '../pMap.js'
9
10
  import { heading, stepHeading, ok, fail, columnWidths, formatRow } from '../ui.js'
10
11
  import { releaseOne } from './release.js'
@@ -18,14 +19,23 @@ import { releaseOne } from './release.js'
18
19
  // release for it" are almost always the same reason to run this.
19
20
  export async function tagCommand({ configPath, packages, yes = false, dryRun = false, release = false } = {}) {
20
21
  const config = loadConfig({ configPath })
21
- const repos = (await inspectRepos(discoverRepos(config))).filter((r) => r.version)
22
- if (repos.length === 0) {
22
+ const allRepos = (await inspectRepos(discoverRepos(config))).filter((r) => r.version)
23
+ if (allRepos.length === 0) {
23
24
  console.log(pc.yellow('No repos found.'))
24
25
  return
25
26
  }
26
27
 
27
28
  heading('Tag current versions')
28
29
 
30
+ // Narrowed to --packages up front (a no-op when it wasn't given) — no
31
+ // reason to check, or print the tag status of, packages nobody asked
32
+ // about.
33
+ const repos = filterByNames(allRepos, packages)
34
+ if (repos.length === 0) {
35
+ console.log(pc.dim('Nothing selected.'))
36
+ return
37
+ }
38
+
29
39
  console.log(pc.dim(`Checking ${repos.length} package(s) for an existing tag...`))
30
40
  const withTag = await pMap(repos, async (r) => {
31
41
  const tag = tagName(r.version)
@@ -36,7 +46,10 @@ export async function tagCommand({ configPath, packages, yes = false, dryRun = f
36
46
 
37
47
  const selected = await selectPackages({
38
48
  items: withTag,
39
- packages,
49
+ // withTag is already the exact --packages match — passing those same
50
+ // dir names back here just skips the checkbox without re-warning
51
+ // about anything (see publish.js for the same pattern).
52
+ packages: packages ? withTag.map((r) => r.dir) : undefined,
40
53
  message: 'Pick packages to tag at their current version:',
41
54
  buildChoice: (all) => {
42
55
  const columns = [