fg-devkit 1.22.0 → 1.24.0

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.
Files changed (69) hide show
  1. package/README.md +30 -7
  2. package/configs/dependency-policy.vue-workspace.json +28 -28
  3. package/package.json +12 -85
  4. package/scaffold/vue/package.json +13 -13
  5. package/scripts/audit-override-commit.mjs +167 -24
  6. package/scripts/bump-node-toolchain.mjs +8 -31
  7. package/scripts/bump-pnpm-toolchain.mjs +8 -29
  8. package/scripts/check-dependency-policy.mjs +10 -35
  9. package/scripts/fg-devkit.mjs +290 -337
  10. package/scripts/fleet-bump-fg-devkit.mjs +4 -5
  11. package/scripts/fleet-git-commit-push-scoped.mjs +6 -26
  12. package/scripts/fleet-git-discard-scoped.mjs +5 -25
  13. package/scripts/fleet-git-pull-scoped.mjs +5 -25
  14. package/scripts/fleet-vulns.mjs +4 -9
  15. package/scripts/generate-release-notes.mjs +1 -1
  16. package/scripts/{audit-fix-plan.mjs → lib/audit-fix-plan.mjs} +77 -4
  17. package/scripts/{audit-fix-plan.test.mjs → lib/audit-fix-plan.test.mjs} +50 -2
  18. package/scripts/lib/common.mjs +95 -0
  19. package/scripts/{dependency-policy-toolchain.mjs → lib/dependency-policy-toolchain.mjs} +3 -35
  20. package/scripts/{git-commit-push-fleet-common.mjs → lib/git-commit-push-fleet-common.mjs} +1 -1
  21. package/scripts/{git-discard-fleet-common.mjs → lib/git-discard-fleet-common.mjs} +1 -1
  22. package/scripts/{git-pull-fleet-common.mjs → lib/git-pull-fleet-common.mjs} +1 -1
  23. package/scripts/{override-changes.mjs → lib/override-changes.mjs} +20 -0
  24. package/scripts/{piwik-dimensions.test.mjs → lib/piwik-dimensions.test.mjs} +0 -5
  25. package/scripts/{release-notes-common.mjs → lib/release-notes-common.mjs} +5 -7
  26. package/scripts/{resolve-dependency-policy-path.mjs → lib/resolve-dependency-policy-path.mjs} +4 -3
  27. package/scripts/piwik.mjs +6 -6
  28. package/scripts/poeditor-download.mjs +1 -1
  29. package/scripts/poeditor-upload.mjs +1 -1
  30. package/scripts/policy-outdated.mjs +3 -8
  31. package/scripts/policy-upgrade-commit.mjs +5 -26
  32. package/scripts/policy-upgrade.mjs +6 -30
  33. package/scripts/policy-vulns.mjs +3 -8
  34. package/scripts/prune-overrides.mjs +5 -16
  35. package/scripts/prune-release-age-exclude.mjs +1 -1
  36. package/scripts/refresh-dependency-policy-from-repos.mjs +5 -7
  37. package/scripts/release.sh +2 -1
  38. package/scripts/scaffold-vue.mjs +1 -3
  39. package/scripts/setup-ai-rules.mjs +135 -0
  40. package/scripts/sync-consumer-lefthook.mjs +7 -24
  41. package/scripts/sync-dependency-check-suppressions-commit.mjs +7 -46
  42. package/scripts/sync-dependency-policy-commit.mjs +6 -38
  43. package/scripts/sync-dependency-policy.mjs +9 -35
  44. package/scripts/sync-node-toolchain.mjs +5 -8
  45. package/scripts/sync-pnpm-toolchain.mjs +5 -8
  46. package/scripts/sync-scaffold-vue-deps.mjs +6 -7
  47. package/scripts/sync-tooling-configs.mjs +8 -23
  48. package/scripts/sync-vite-plugin-vue-inspector.mjs +7 -39
  49. package/scripts/verify-release-notes.mjs +1 -1
  50. package/.cursor/rules/core-coding-standards.mdc +0 -291
  51. package/scaffold/vue/.gitignore +0 -13
  52. package/scripts/cz/.cz-config.cjs +0 -29
  53. package/scripts/fleet-git-commit-push.mjs +0 -75
  54. package/scripts/fleet-git-discard.mjs +0 -66
  55. package/scripts/fleet-git-pull.mjs +0 -65
  56. package/scripts/sync-core-coding-standards.mjs +0 -77
  57. package/scripts/sync-peers-from-dev.mjs +0 -31
  58. package/scripts/vuln-commit.mjs +0 -142
  59. /package/configs/{fallowrc.sync-stub.json → fallowrc.json} +0 -0
  60. /package/scripts/{dependency-policy-pins.mjs → lib/dependency-policy-pins.mjs} +0 -0
  61. /package/scripts/{fleet-project-repos.mjs → lib/fleet-project-repos.mjs} +0 -0
  62. /package/scripts/{fleet-repo-picker.mjs → lib/fleet-repo-picker.mjs} +0 -0
  63. /package/scripts/{fleet-vue-repos.mjs → lib/fleet-vue-repos.mjs} +0 -0
  64. /package/scripts/{git-fleet-common.mjs → lib/git-fleet-common.mjs} +0 -0
  65. /package/scripts/{node-toolchain-sync.mjs → lib/node-toolchain-sync.mjs} +0 -0
  66. /package/scripts/{piwik-common.mjs → lib/piwik-common.mjs} +0 -0
  67. /package/scripts/{piwik-dimensions.mjs → lib/piwik-dimensions.mjs} +0 -0
  68. /package/scripts/{pnpm-toolchain-sync.mjs → lib/pnpm-toolchain-sync.mjs} +0 -0
  69. /package/scripts/{poeditor-common.mjs → lib/poeditor-common.mjs} +0 -0
@@ -8,7 +8,11 @@
8
8
  * 3. Audit again. Remaining findings with a patched version that a parent's range blocks
9
9
  * get a tight override (`name@>=installed <patched`: lowest patched version) in
10
10
  * pnpm-workspace.yaml. Findings with no patch, a patch in another major, a patch
11
- * younger than minimumReleaseAge, or on a direct dependency are reported, not overridden.
11
+ * younger than minimumReleaseAge, or on a direct dependency are not fixed automatically:
12
+ * in a terminal you are asked per package whether to force it (override across the
13
+ * major, allow the young release via minimumReleaseAgeExclude, bump the direct
14
+ * dependency in package.json). Without a terminal they are reported unless the matching
15
+ * --allow-major / --allow-young / --bump-direct flag is passed.
12
16
  * 4. Prune overrides that no longer change the audit (prune-overrides --fix).
13
17
  * 5. Prune minimumReleaseAgeExclude entries that are no longer needed
14
18
  * (prune-release-age-exclude --fix). Runs even when the audit is clean.
@@ -16,11 +20,12 @@
16
20
  * shared dependency-check-suppressions.xml, write the repo's copy with only those
17
21
  * entries, so OWASP Dependency-Check accepts exactly the risks this repo has.
18
22
  * 7. Install and run the repo's checks (lint-check, lint:ts, test, test:unit, build).
19
- * 8. Commit the refresh, the overrides, the release-age excludes and the suppressions
20
- * file separately.
23
+ * 8. Commit the refresh, the direct bumps, the overrides, the release-age excludes and
24
+ * the suppressions file separately.
21
25
  *
22
26
  * Usage:
23
27
  * pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
28
+ * [--allow-major] [--allow-young] [--bump-direct]
24
29
  */
25
30
 
26
31
  import fs from 'node:fs'
@@ -31,23 +36,27 @@ import { parseArgs } from 'node:util'
31
36
  import {
32
37
  DEFAULT_MINIMUM_RELEASE_AGE_MINUTES,
33
38
  activeSuppressedGhsas,
39
+ addReleaseAgeExcludes,
34
40
  buildRefreshCommitMessage,
41
+ bumpDependencySpec,
35
42
  filterSuppressions,
36
43
  diffLockfileVersions,
37
44
  isReleaseAgeExcluded,
45
+ isSameReleaseLine,
38
46
  normalizeAdvisories,
39
47
  parseAuditJson,
40
48
  parseReleaseAgeExcludeEntry,
41
49
  planOverrides,
42
50
  readWorkspaceList,
43
51
  upsertWorkspaceOverrides,
44
- } from './audit-fix-plan.mjs'
52
+ } from './lib/audit-fix-plan.mjs'
45
53
  import {
46
54
  buildOverrideCommitMessage,
47
55
  collectOverrideChanges,
48
56
  collectOverrides,
49
57
  formatOverrideValue,
50
- } from './override-changes.mjs'
58
+ } from './lib/override-changes.mjs'
59
+ import { fgDevkitRoot } from './lib/common.mjs'
51
60
 
52
61
  const TAG = '[audit-override-commit]'
53
62
  const cwd = process.cwd()
@@ -62,6 +71,9 @@ const { values: args } = parseArgs({
62
71
  options: {
63
72
  'no-verify': { type: 'boolean', default: false },
64
73
  'no-commit': { type: 'boolean', default: false },
74
+ 'allow-major': { type: 'boolean', default: false },
75
+ 'allow-young': { type: 'boolean', default: false },
76
+ 'bump-direct': { type: 'boolean', default: false },
65
77
  help: { type: 'boolean', short: 'h', default: false },
66
78
  },
67
79
  strict: true,
@@ -69,14 +81,21 @@ const { values: args } = parseArgs({
69
81
 
70
82
  if (args.help) {
71
83
  console.log(`Usage: pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
84
+ [--allow-major] [--allow-young] [--bump-direct]
72
85
 
73
86
  Refreshes vulnerable packages in the lockfile within their existing ranges, adds a
74
87
  tight override only where a parent's range blocks the patched version, prunes unused
75
88
  overrides, syncs dependency-check-suppressions.xml when a remaining finding is
76
89
  suppressed there, runs the repo's checks, and commits each kind of change separately.
77
90
 
78
- --no-verify Skip lint/typecheck/test/build (git hooks still run on commit)
79
- --no-commit Leave the changes in the working tree`)
91
+ Findings that need a decision are asked about one package at a time in a terminal.
92
+ Without a terminal they are only reported, unless a flag below says yes for them.
93
+
94
+ --no-verify Skip lint/typecheck/test/build (git hooks still run on commit)
95
+ --no-commit Leave the changes in the working tree
96
+ --allow-major Override even when the patch is only in a new major
97
+ --allow-young Allow patches younger than minimumReleaseAge (adds minimumReleaseAgeExclude)
98
+ --bump-direct Bump vulnerable direct dependencies in package.json`)
80
99
  process.exit(0)
81
100
  }
82
101
 
@@ -151,11 +170,11 @@ function publishTimes(name) {
151
170
  }
152
171
  }
153
172
 
173
+ const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies']
174
+
154
175
  function directDependencies() {
155
176
  const pkg = JSON.parse(readFile('package.json'))
156
- return new Set(
157
- ['dependencies', 'devDependencies', 'optionalDependencies'].flatMap((section) => Object.keys(pkg[section] ?? {})),
158
- )
177
+ return new Set(DEPENDENCY_SECTIONS.flatMap((section) => Object.keys(pkg[section] ?? {})))
159
178
  }
160
179
 
161
180
  // ---------------------------------------------------------------------------
@@ -215,7 +234,7 @@ function refreshLockfile(advisories) {
215
234
  runOrFail('pnpm', ['update', ...recursive, ...names, '--lockfile-only'])
216
235
  }
217
236
 
218
- /** Split planned overrides into ones we apply and ones held back by minimumReleaseAge. */
237
+ /** Split planned changes (`{ name, value }`) into allowed ones and ones held back by minimumReleaseAge. */
219
238
  function applyReleaseAgeGate(overrides) {
220
239
  const config = readPnpmConfig()
221
240
  const minutes = Number(config.minimumReleaseAge ?? DEFAULT_MINIMUM_RELEASE_AGE_MINUTES)
@@ -241,6 +260,92 @@ function applyReleaseAgeGate(overrides) {
241
260
  return { allowed, heldBack }
242
261
  }
243
262
 
263
+ /** Yes when the flag is passed, else ask in a terminal, else no. */
264
+ let confirmPrompt
265
+ async function decide(message, flag) {
266
+ if (args[flag]) return true
267
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false
268
+ confirmPrompt ??= (await import('@inquirer/confirm')).default
269
+ return confirmPrompt({ message, default: false })
270
+ }
271
+
272
+ /** First few advisory ids, so prompts stay on one line. */
273
+ const ghsaList = (item) =>
274
+ item.ghsas.length > 3 ? `${item.ghsas.slice(0, 3).join(', ')} +${item.ghsas.length - 3} more` : item.ghsas.join(', ')
275
+
276
+ /**
277
+ * Ask about the findings the automatic pass will not fix. Returns what to force:
278
+ * overrides across a major, bumps of direct dependencies, and which of the young
279
+ * releases to allow via minimumReleaseAgeExclude.
280
+ */
281
+ async function chooseForcedFixes(plan) {
282
+ const blocked = [...plan.crossMajor, ...plan.direct.filter((item) => item.to)]
283
+ if (blocked.length === 0) return { overrides: [], bumps: [] }
284
+ if (process.stdin.isTTY) console.log(`\n${TAG} Remaining findings that need a decision:`)
285
+
286
+ const overrides = []
287
+ for (const item of plan.crossMajor) {
288
+ const message = `${item.name} ${item.from} → ${item.to} (new major, ${ghsaList(item)}). Override?`
289
+ if (await decide(message, 'allow-major')) {
290
+ overrides.push({ ...item, key: `${item.name}@>=${item.from} <${item.to}`, value: item.to, forced: true })
291
+ }
292
+ }
293
+
294
+ const pkg = JSON.parse(readFile('package.json'))
295
+ const bumps = []
296
+ for (const item of plan.direct) {
297
+ if (!item.to) continue
298
+ const sections = DEPENDENCY_SECTIONS.filter((section) => pkg[section]?.[item.name])
299
+ const specs = sections.map((section) => bumpDependencySpec(pkg[section][item.name], item.to))
300
+ if (sections.length === 0 || specs.includes(null)) continue
301
+ const major = item.versions.some((version) => !isSameReleaseLine(version, item.to)) ? 'new major, ' : ''
302
+ const message = `${item.name} ${item.versions.join(', ')} → ${item.to} (direct dependency, ${major}${ghsaList(item)}). Bump in package.json?`
303
+ if (await decide(message, 'bump-direct')) bumps.push({ ...item, value: item.to, kind: 'bump' })
304
+ }
305
+ return { overrides, bumps }
306
+ }
307
+
308
+ /** Ask, per release held back by minimumReleaseAge, whether to allow it anyway. */
309
+ async function chooseYoungReleases(heldBack) {
310
+ const allowed = []
311
+ const declined = []
312
+ for (const item of heldBack) {
313
+ const message = `${item.name} ${item.value} is younger than minimumReleaseAge (~${item.waitHours}h left, ${ghsaList(item)}). Allow it via minimumReleaseAgeExclude?`
314
+ if (await decide(message, 'allow-young')) allowed.push({ ...item, excluded: true })
315
+ else declined.push(item)
316
+ }
317
+ return { allowed, declined }
318
+ }
319
+
320
+ function writeReleaseAgeExcludes(items) {
321
+ if (items.length === 0) return
322
+ const file = path.join(cwd, 'pnpm-workspace.yaml')
323
+ const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
324
+ fs.writeFileSync(file, addReleaseAgeExcludes(before, items.map(({ name, value }) => ({ name, version: value }))))
325
+ for (const item of items) console.log(`${TAG} minimumReleaseAgeExclude += ${item.name}@${item.value}`)
326
+ }
327
+
328
+ function writeDirectBumps(bumps) {
329
+ const file = path.join(cwd, 'package.json')
330
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf8'))
331
+ for (const bump of bumps) {
332
+ for (const section of DEPENDENCY_SECTIONS) {
333
+ if (!pkg[section]?.[bump.name]) continue
334
+ pkg[section][bump.name] = bumpDependencySpec(pkg[section][bump.name], bump.value)
335
+ }
336
+ console.log(`${TAG} Bump ${bump.name} -> ${bump.value} (${ghsaList(bump)})`)
337
+ }
338
+ const indent = fs.readFileSync(file, 'utf8').match(/^\{\n([ \t]+)/)?.[1] ?? ' '
339
+ fs.writeFileSync(file, `${JSON.stringify(pkg, null, indent)}\n`)
340
+ runOrFail('pnpm', ['install', '--lockfile-only', '--ignore-scripts', '--no-frozen-lockfile'])
341
+ }
342
+
343
+ function buildBumpCommitMessage(bumps) {
344
+ const describe = (bump) => `- ${bump.name}: ${bump.versions.join(', ')} -> ${bump.value}`
345
+ if (bumps.length === 1) return { subject: `vuln(${bumps[0].name}): bump to ${bumps[0].value}`, body: describe(bumps[0]) }
346
+ return { subject: `vuln(): bump ${bumps.length} dependencies`, body: bumps.map(describe).join('\n') }
347
+ }
348
+
244
349
  function writeOverrides(overrides) {
245
350
  const file = path.join(cwd, 'pnpm-workspace.yaml')
246
351
  const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
@@ -300,7 +405,7 @@ function pruneOverrides() {
300
405
  * shared file that match a finding still reported here. Untouched when nothing matches.
301
406
  */
302
407
  function syncSuppressions(remaining) {
303
- const sourcePath = path.resolve(scriptDir, '..', 'configs', SUPPRESSIONS_FILE)
408
+ const sourcePath = path.join(fgDevkitRoot, 'configs', SUPPRESSIONS_FILE)
304
409
  if (!fs.existsSync(sourcePath)) return { covered: [], uncovered: remaining }
305
410
  const shared = fs.readFileSync(sourcePath, 'utf8')
306
411
  const active = activeSuppressedGhsas(shared)
@@ -337,7 +442,7 @@ function verify() {
337
442
  }
338
443
  }
339
444
 
340
- function printSummary({ fixedByRefresh, plan, heldBack, applied, remaining, suppressions, excludeChanges }) {
445
+ function printSummary({ fixedByRefresh, plan, forced, heldBack, applied, remaining, suppressions, excludeChanges }) {
341
446
  const suppressed = new Set(suppressions.covered.map((advisory) => advisory.ghsa))
342
447
  const unique = (items) => [...new Set(items)]
343
448
  const lines = []
@@ -346,21 +451,40 @@ function printSummary({ fixedByRefresh, plan, heldBack, applied, remaining, supp
346
451
  lines.push(title, ...items.map((item) => ` - ${item}`))
347
452
  }
348
453
  section('Fixed by lockfile refresh:', fixedByRefresh)
454
+ const youngNote = (item) => (item.excluded ? ', young release allowed via minimumReleaseAgeExclude' : '')
349
455
  section(
350
456
  'Overridden (parent range blocked the patch):',
351
- applied.map((item) => `${item.key} -> ${item.value} (${item.ghsas.join(', ')})`),
457
+ applied
458
+ .filter((item) => !item.forced)
459
+ .map((item) => `${item.key} -> ${item.value} (${item.ghsas.join(', ')}${youngNote(item)})`),
460
+ )
461
+ section(
462
+ 'Overridden across a major (forced, verify covers it):',
463
+ applied
464
+ .filter((item) => item.forced)
465
+ .map((item) => `${item.key} -> ${item.value} (${item.ghsas.join(', ')}${youngNote(item)})`),
352
466
  )
353
467
  section(
354
- 'Held back by minimumReleaseAge (re-run later, or add to minimumReleaseAgeExclude):',
468
+ 'Bumped in package.json:',
469
+ forced.bumps.map(
470
+ (item) => `${item.name} ${item.versions.join(', ')} -> ${item.value} (${item.ghsas.join(', ')}${youngNote(item)})`,
471
+ ),
472
+ )
473
+ section(
474
+ 'Held back by minimumReleaseAge (re-run later, or pass --allow-young):',
355
475
  heldBack.map((item) => `${item.name}@${item.value}, ~${item.waitHours}h left (${item.ghsas.join(', ')})`),
356
476
  )
357
477
  section(
358
- 'Direct dependency, bump it in package.json (policy-upgrade-commit):',
359
- plan.direct.map((item) => `${item.name} ${item.versions.join(', ')} (${item.ghsas.join(', ')})`),
478
+ 'Direct dependency, not bumped (pass --bump-direct, or use policy-upgrade-commit):',
479
+ plan.direct
480
+ .filter((item) => !forced.bumps.some((bump) => bump.name === item.name))
481
+ .map((item) => `${item.name} ${item.versions.join(', ')} (${item.ghsas.join(', ')})`),
360
482
  )
361
483
  section(
362
- 'Patch only in a new major, upgrade the parent instead:',
363
- plan.crossMajor.map((item) => `${item.name} ${item.from} -> ${item.to} (${item.ghsas.join(', ')})`),
484
+ 'Patch only in a new major, not overridden (pass --allow-major, or upgrade the parent):',
485
+ plan.crossMajor
486
+ .filter((item) => !forced.overrides.some((override) => override.name === item.name && override.from === item.from))
487
+ .map((item) => `${item.name} ${item.from} -> ${item.to} (${item.ghsas.join(', ')})`),
364
488
  )
365
489
  section(
366
490
  'No patched version and not suppressed, add a <suppress> entry to fg-devkit configs/dependency-check-suppressions.xml if the risk is accepted:',
@@ -393,7 +517,7 @@ function printSummary({ fixedByRefresh, plan, heldBack, applied, remaining, supp
393
517
  // Main
394
518
  // ---------------------------------------------------------------------------
395
519
 
396
- function main() {
520
+ async function main() {
397
521
  if (!readFile('package.json')) throw new Error(`package.json not found in ${cwd}`)
398
522
  if (!readFile('pnpm-lock.yaml')) throw new Error(`pnpm-lock.yaml not found in ${cwd}`)
399
523
  const shouldCommit = !args['no-commit']
@@ -408,9 +532,11 @@ function main() {
408
532
  let refreshChanges = []
409
533
  let fixedByRefresh = []
410
534
  let plan = { overrides: [], noPatch: [], crossMajor: [], direct: [], unparseable: [] }
535
+ let forced = { overrides: [], bumps: [] }
411
536
  let heldBack = []
412
537
  let allowed = []
413
538
  let remaining = []
539
+ let afterBumps
414
540
 
415
541
  if (initial.length === 0) {
416
542
  console.log(`${TAG} pnpm audit is clean.`)
@@ -429,19 +555,33 @@ function main() {
429
555
  ...new Set(initial.filter((advisory) => !remainingIds.has(advisory.id)).map((a) => `${a.name} (${a.ghsa})`)),
430
556
  ]
431
557
  plan = planOverrides(afterRefresh, { directDependencies: directDependencies() })
432
- ;({ allowed, heldBack } = applyReleaseAgeGate(plan.overrides))
558
+ forced = await chooseForcedFixes(plan)
559
+ const gated = applyReleaseAgeGate([...plan.overrides, ...forced.overrides, ...forced.bumps])
560
+ const young = await chooseYoungReleases(gated.heldBack)
561
+ heldBack = young.declined
562
+ const passed = [...gated.allowed, ...young.allowed]
563
+ forced.bumps = passed.filter((item) => item.kind === 'bump')
564
+ allowed = passed.filter((item) => item.kind !== 'bump')
565
+
566
+ // Direct bumps first, in their own commit.
567
+ if (forced.bumps.length > 0) {
568
+ writeReleaseAgeExcludes(forced.bumps.filter((item) => item.excluded))
569
+ writeDirectBumps(forced.bumps)
570
+ afterBumps = snapshot()
571
+ }
572
+ writeReleaseAgeExcludes(allowed.filter((item) => item.excluded))
433
573
  if (allowed.length > 0) writeOverrides(allowed)
434
574
 
435
575
  // 4. Prune overrides.
436
576
  pruneOverrides()
437
- remaining = allowed.length > 0 ? audit() : afterRefresh
577
+ remaining = allowed.length > 0 || forced.bumps.length > 0 ? audit() : afterRefresh
438
578
  }
439
579
  const afterOverrides = snapshot()
440
580
 
441
581
  // 5. Release-age excludes, 6. suppressions, 7. verify.
442
582
  const excludeChanges = pruneReleaseAgeExclude()
443
583
  const suppressions = syncSuppressions(remaining)
444
- printSummary({ fixedByRefresh, plan, heldBack, applied: allowed, remaining, suppressions, excludeChanges })
584
+ printSummary({ fixedByRefresh, plan, forced, heldBack, applied: allowed, remaining, suppressions, excludeChanges })
445
585
 
446
586
  const final = snapshot()
447
587
  const nothingChanged = !suppressionsWritten && TRACKED_FILES.every((file) => final[file] === original[file])
@@ -460,6 +600,9 @@ function main() {
460
600
  if (refreshChanges.length > 0 && stageContents(refreshed) > 0) {
461
601
  commit(buildRefreshCommitMessage(refreshChanges))
462
602
  }
603
+ if (afterBumps && stageContents(afterBumps) > 0) {
604
+ commit(buildBumpCommitMessage(forced.bumps))
605
+ }
463
606
  for (const item of overrideChanges) {
464
607
  console.log(` - ${item.name}: ${item.from ?? '(missing)'} -> ${formatOverrideValue(item.to)}`)
465
608
  }
@@ -487,7 +630,7 @@ function main() {
487
630
  }
488
631
 
489
632
  try {
490
- process.exit(main())
633
+ process.exit(await main())
491
634
  } catch (error) {
492
635
  console.error(`${TAG} ${error instanceof Error ? error.message : error}`)
493
636
  console.error(
@@ -31,22 +31,20 @@
31
31
 
32
32
  import fs from 'node:fs'
33
33
  import path from 'node:path'
34
- import { fileURLToPath } from 'node:url'
35
34
 
36
- import { collectFleetProjectRepos, toolchainExclude } from './fleet-project-repos.mjs'
37
- import { parseFleetSelectionFlags, resolveFleetRepos } from './fleet-repo-picker.mjs'
35
+ import { collectFleetProjectRepos, toolchainExclude } from './lib/fleet-project-repos.mjs'
36
+ import { parseFleetSelectionFlags, resolveFleetRepos } from './lib/fleet-repo-picker.mjs'
38
37
  import {
39
38
  formatNodeNvmrc,
40
39
  readFgDevkitNodePolicy,
41
40
  resolveExpectedNodePolicy,
42
- } from './dependency-policy-toolchain.mjs'
43
- import { syncNodeToolchainInRepo } from './node-toolchain-sync.mjs'
44
- import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
41
+ } from './lib/dependency-policy-toolchain.mjs'
42
+ import { syncNodeToolchainInRepo } from './lib/node-toolchain-sync.mjs'
43
+ import { resolveDependencyPolicyPath } from './lib/resolve-dependency-policy-path.mjs'
44
+ import { assertFgDevkitMaintainerContext, fgDevkitRoot, readJson } from './lib/common.mjs'
45
45
 
46
- const scriptDir = path.dirname(fileURLToPath(import.meta.url))
47
- const fgDevkitRoot = path.resolve(scriptDir, '..')
48
46
  const fgDevkitPkgPath = path.join(fgDevkitRoot, 'package.json')
49
- const policyPath = resolveDependencyPolicyPath(scriptDir, fgDevkitRoot, [])
47
+ const policyPath = resolveDependencyPolicyPath(fgDevkitRoot, [])
50
48
 
51
49
  const cliArgs = process.argv.slice(2)
52
50
  const dryRun = cliArgs.includes('--dry-run')
@@ -55,27 +53,6 @@ const noEngines = cliArgs.includes('--no-engines')
55
53
  const enginesArg = cliArgs.find((a) => a.startsWith('--engines='))?.slice('--engines='.length)
56
54
  const { pick, all, only, filteredArgs: positionalArgs } = parseFleetSelectionFlags(cliArgs)
57
55
 
58
- function readJson(jsonPath) {
59
- return JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
60
- }
61
-
62
- function assertFgDevkitMaintainerContext() {
63
- if (path.resolve(process.cwd()) !== fgDevkitRoot) {
64
- console.error('[bump-node-toolchain] Run from the fg-devkit repository root.')
65
- process.exit(1)
66
- }
67
- try {
68
- const pkg = readJson(fgDevkitPkgPath)
69
- if (pkg?.name !== 'fg-devkit') {
70
- console.error('[bump-node-toolchain] package.json name must be fg-devkit.')
71
- process.exit(1)
72
- }
73
- } catch {
74
- console.error('[bump-node-toolchain] Could not read fg-devkit package.json.')
75
- process.exit(1)
76
- }
77
- }
78
-
79
56
  function runStep(label, fn) {
80
57
  console.log(`\n[bump-node-toolchain] ${label}`)
81
58
  return fn()
@@ -185,7 +162,7 @@ function syncFleetRepo(repoDir, projectsRoot, policy) {
185
162
  }
186
163
 
187
164
  async function main() {
188
- assertFgDevkitMaintainerContext()
165
+ assertFgDevkitMaintainerContext('[bump-node-toolchain]')
189
166
 
190
167
  const nvmrcArg = positionalArgs.find((a) => /^\d/.test(a))
191
168
  const projectsRootArg = positionalArgs.find((a) => a !== nvmrcArg)
@@ -35,20 +35,20 @@ import path from 'node:path'
35
35
  import { spawnSync } from 'node:child_process'
36
36
  import { fileURLToPath } from 'node:url'
37
37
 
38
- import { formatPnpmPackageManager } from './dependency-policy-toolchain.mjs'
39
- import { collectFleetPnpmRepos } from './fleet-project-repos.mjs'
40
- import { parseFleetSelectionFlags, resolveFleetRepos } from './fleet-repo-picker.mjs'
38
+ import { formatPnpmPackageManager } from './lib/dependency-policy-toolchain.mjs'
39
+ import { collectFleetPnpmRepos } from './lib/fleet-project-repos.mjs'
40
+ import { parseFleetSelectionFlags, resolveFleetRepos } from './lib/fleet-repo-picker.mjs'
41
41
  import {
42
42
  isVueFgDevkitConsumerRepo,
43
43
  syncPnpmToolchainInRepo,
44
- } from './pnpm-toolchain-sync.mjs'
45
- import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
44
+ } from './lib/pnpm-toolchain-sync.mjs'
45
+ import { resolveDependencyPolicyPath } from './lib/resolve-dependency-policy-path.mjs'
46
46
  import { syncScaffoldVueDepsFromPolicy } from './sync-scaffold-vue-deps.mjs'
47
+ import { assertFgDevkitMaintainerContext, fgDevkitRoot, readJson } from './lib/common.mjs'
47
48
 
48
49
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
49
- const fgDevkitRoot = path.resolve(scriptDir, '..')
50
50
  const fgDevkitPkgPath = path.join(fgDevkitRoot, 'package.json')
51
- const policyPath = resolveDependencyPolicyPath(scriptDir, fgDevkitRoot, [])
51
+ const policyPath = resolveDependencyPolicyPath(fgDevkitRoot, [])
52
52
 
53
53
  const cliArgs = process.argv.slice(2)
54
54
  const dryRun = cliArgs.includes('--dry-run')
@@ -57,27 +57,6 @@ const skipInstall = cliArgs.includes('--skip-install')
57
57
  const { pick, all, only, filteredArgs: positionalArgs } = parseFleetSelectionFlags(cliArgs)
58
58
  let projectsRoot = path.resolve(fgDevkitRoot, '..')
59
59
 
60
- function readJson(jsonPath) {
61
- return JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
62
- }
63
-
64
- function assertFgDevkitMaintainerContext() {
65
- if (path.resolve(process.cwd()) !== fgDevkitRoot) {
66
- console.error('[bump-pnpm-toolchain] Run from the fg-devkit repository root.')
67
- process.exit(1)
68
- }
69
- try {
70
- const pkg = readJson(fgDevkitPkgPath)
71
- if (pkg?.name !== 'fg-devkit') {
72
- console.error('[bump-pnpm-toolchain] package.json name must be fg-devkit.')
73
- process.exit(1)
74
- }
75
- } catch {
76
- console.error('[bump-pnpm-toolchain] Could not read fg-devkit package.json.')
77
- process.exit(1)
78
- }
79
- }
80
-
81
60
  function resolveTargetVersion(versionArg) {
82
61
  if (versionArg) {
83
62
  return formatPnpmPackageManager(versionArg)
@@ -218,7 +197,7 @@ function syncFleetRepo(repoDir, repoName, packageManager) {
218
197
  }
219
198
 
220
199
  async function main() {
221
- assertFgDevkitMaintainerContext()
200
+ assertFgDevkitMaintainerContext('[bump-pnpm-toolchain]')
222
201
 
223
202
  const versionArg = positionalArgs.find((a) => /^(pnpm@)?\d/.test(a))
224
203
  const projectsRootArg = positionalArgs.find((a) => a !== versionArg)
@@ -15,22 +15,22 @@
15
15
 
16
16
  import fs from 'node:fs'
17
17
  import path from 'node:path'
18
- import { fileURLToPath } from 'node:url'
19
18
 
20
- import { collectPinViolations, isPinnedDependencyVersion } from './dependency-policy-pins.mjs'
19
+ import { collectPinViolations, isPinnedDependencyVersion } from './lib/dependency-policy-pins.mjs'
21
20
  import {
22
21
  collectToolchainPolicyViolations,
23
22
  resolveExpectedPackageManager,
24
- } from './dependency-policy-toolchain.mjs'
25
- import { findPnpmVersionMismatchesInRepo } from './pnpm-toolchain-sync.mjs'
26
- import { collectNodeToolchainViolations } from './node-toolchain-sync.mjs'
27
- import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
23
+ } from './lib/dependency-policy-toolchain.mjs'
24
+ import { findPnpmVersionMismatchesInRepo } from './lib/pnpm-toolchain-sync.mjs'
25
+ import { collectNodeToolchainViolations } from './lib/node-toolchain-sync.mjs'
26
+ import { resolveDependencyPolicyPath } from './lib/resolve-dependency-policy-path.mjs'
27
+ import { getRelevantPolicyOverrides } from './lib/override-changes.mjs'
28
+ import { fgDevkitRoot } from './lib/common.mjs'
28
29
 
29
30
  const cwd = process.cwd()
30
31
  const pkgPath = path.resolve(cwd, 'package.json')
31
32
  const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
32
- const scriptDir = path.dirname(fileURLToPath(import.meta.url))
33
- const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
33
+ const policyPath = resolveDependencyPolicyPath(cwd, process.argv)
34
34
 
35
35
  if (!fs.existsSync(pkgPath)) {
36
36
  console.error('[check-deps] Could not find package.json in current working directory')
@@ -50,30 +50,6 @@ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
50
50
  const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
51
51
  const mismatches = []
52
52
 
53
- function getOverridePackageName(overrideKey) {
54
- const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
55
- return match ? match[1] : overrideKey
56
- }
57
-
58
- function getRelevantPolicyOverrides(policyOverrides) {
59
- const entries = Object.entries(policyOverrides ?? {})
60
- if (entries.length === 0) {
61
- return {}
62
- }
63
- if (!fs.existsSync(lockfilePath)) {
64
- return Object.fromEntries(entries)
65
- }
66
- const lockfile = fs.readFileSync(lockfilePath, 'utf8')
67
- const relevant = {}
68
- for (const [name, version] of entries) {
69
- const pkgName = getOverridePackageName(name)
70
- if (lockfile.includes(`${pkgName}@`) || lockfile.includes(`/${pkgName}/`)) {
71
- relevant[name] = version
72
- }
73
- }
74
- return relevant
75
- }
76
-
77
53
  /** True if this package+section is governed by the policy (so global pin checks skip it). */
78
54
  function isPolicyManagedInSection(section, name) {
79
55
  if (policy.managedAnywhere?.[name] !== undefined) {
@@ -90,7 +66,7 @@ for (const [section, dependencies] of Object.entries(policy.managedDependencies
90
66
  mismatches.push(...collectPinViolations('[policy] managedAnywhere.', policy.managedAnywhere ?? {}))
91
67
  mismatches.push(...collectToolchainPolicyViolations(policy))
92
68
 
93
- const expectedPackageManager = resolveExpectedPackageManager(policy, path.resolve(scriptDir, '..'))
69
+ const expectedPackageManager = resolveExpectedPackageManager(policy, fgDevkitRoot)
94
70
  if (expectedPackageManager !== undefined) {
95
71
  const actualPackageManager = pkg.packageManager
96
72
  if (actualPackageManager !== expectedPackageManager) {
@@ -100,7 +76,6 @@ if (expectedPackageManager !== undefined) {
100
76
  }
101
77
  }
102
78
 
103
- const fgDevkitRoot = path.resolve(scriptDir, '..')
104
79
  mismatches.push(...collectNodeToolchainViolations(cwd, policy, fgDevkitRoot))
105
80
  if (expectedPackageManager !== undefined) {
106
81
  mismatches.push(...findPnpmVersionMismatchesInRepo(cwd, expectedPackageManager))
@@ -146,7 +121,7 @@ for (const [name, expected] of Object.entries(policy.managedAnywhere ?? {})) {
146
121
 
147
122
  // Optional: policy may define pnpm.overrides entries the consumer must mirror.
148
123
  const overrides = pkg.pnpm?.overrides ?? {}
149
- for (const [name, expected] of Object.entries(getRelevantPolicyOverrides(policy.overrides ?? {}))) {
124
+ for (const [name, expected] of Object.entries(getRelevantPolicyOverrides(policy.overrides, lockfilePath))) {
150
125
  const actual = overrides[name]
151
126
  if (actual !== expected) {
152
127
  mismatches.push(`pnpm.overrides.${name}: expected ${expected}, got ${actual ?? '(missing)'}`)