fg-devkit 1.20.0 → 1.21.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.
@@ -1,16 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Fix one or more vulnerabilities via `pnpm audit --fix override -i`, prune unused
4
- * overrides, install, then auto-commit.
3
+ * Fix `pnpm audit` findings with the smallest possible change, verify, and auto-commit.
4
+ *
5
+ * 1. Audit and collect the vulnerable packages.
6
+ * 2. Refresh them in the lockfile (`pnpm update <names>`): newest version each parent's
7
+ * range already allows. No overrides, nothing to clean up later.
8
+ * 3. Audit again. Remaining findings with a patched version that a parent's range blocks
9
+ * get a tight override (`name@>=installed <patched`: lowest patched version) in
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.
12
+ * 4. Prune overrides that no longer change the audit (prune-overrides --fix).
13
+ * 5. If findings that are still reported have active <suppress> entries in fg-devkit's
14
+ * shared dependency-check-suppressions.xml, write the repo's copy with only those
15
+ * entries, so the Jenkins OWASP scan accepts exactly the risks this repo has.
16
+ * 6. Install and run the repo's checks (lint-check, lint:ts, test, test:unit, build).
17
+ * 7. Commit the refresh, the override changes and the suppressions file separately.
5
18
  *
6
19
  * Usage:
7
- * pnpm exec fg-devkit audit-override-commit [pnpm-audit-args...]
20
+ * pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
8
21
  */
9
22
 
10
23
  import fs from 'node:fs'
11
24
  import path from 'node:path'
12
25
  import { spawnSync } from 'node:child_process'
13
26
  import { fileURLToPath } from 'node:url'
27
+ import { parseArgs } from 'node:util'
28
+ import {
29
+ DEFAULT_MINIMUM_RELEASE_AGE_MINUTES,
30
+ activeSuppressedGhsas,
31
+ buildRefreshCommitMessage,
32
+ filterSuppressions,
33
+ diffLockfileVersions,
34
+ isReleaseAgeExcluded,
35
+ normalizeAdvisories,
36
+ parseAuditJson,
37
+ planOverrides,
38
+ upsertWorkspaceOverrides,
39
+ } from './audit-fix-plan.mjs'
14
40
  import {
15
41
  buildOverrideCommitMessage,
16
42
  collectOverrideChanges,
@@ -18,120 +44,393 @@ import {
18
44
  formatOverrideValue,
19
45
  } from './override-changes.mjs'
20
46
 
47
+ const TAG = '[audit-override-commit]'
21
48
  const cwd = process.cwd()
22
49
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
23
- const packageJsonPath = path.join(cwd, 'package.json')
50
+ const TRACKED_FILES = ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']
51
+ const VERIFY_SCRIPTS = ['lint-check', 'lint:ts', 'test', 'test:unit', 'build']
52
+ const SUPPRESSIONS_FILE = 'dependency-check-suppressions.xml'
53
+ const SUPPRESSIONS_COMMIT_MESSAGE = 'chore(repo): sync dependency-check suppressions'
54
+ let suppressionsWritten = false
55
+
56
+ const { values: args } = parseArgs({
57
+ options: {
58
+ 'no-verify': { type: 'boolean', default: false },
59
+ 'no-commit': { type: 'boolean', default: false },
60
+ help: { type: 'boolean', short: 'h', default: false },
61
+ },
62
+ strict: true,
63
+ })
64
+
65
+ if (args.help) {
66
+ console.log(`Usage: pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
67
+
68
+ Refreshes vulnerable packages in the lockfile within their existing ranges, adds a
69
+ tight override only where a parent's range blocks the patched version, prunes unused
70
+ overrides, syncs dependency-check-suppressions.xml when a remaining finding is
71
+ suppressed there, runs the repo's checks, and commits each kind of change separately.
72
+
73
+ --no-verify Skip lint/typecheck/test/build (git hooks still run on commit)
74
+ --no-commit Leave the changes in the working tree`)
75
+ process.exit(0)
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Process helpers
80
+ // ---------------------------------------------------------------------------
24
81
 
25
- function runPnpm(args) {
26
- const result = spawnSync('pnpm', args, {
27
- stdio: 'inherit',
82
+ function run(command, commandArgs, { capture = false, env = {}, input } = {}) {
83
+ const result = spawnSync(command, commandArgs, {
28
84
  cwd,
29
- env: process.env,
85
+ env: { ...process.env, ...env },
86
+ encoding: 'utf8',
87
+ stdio: capture ? ['pipe', 'pipe', 'pipe'] : ['inherit', 'inherit', 'inherit'],
88
+ input,
89
+ maxBuffer: 50 * 1024 * 1024,
30
90
  })
31
- if (result.error) {
32
- console.error('[audit-override-commit]', result.error.message)
33
- return 1
91
+ if (result.error) throw result.error
92
+ return result
93
+ }
94
+
95
+ function runOrFail(command, commandArgs, options) {
96
+ const result = run(command, commandArgs, options)
97
+ if (result.status !== 0) {
98
+ const detail = options?.capture ? `\n${(result.stderr || result.stdout || '').trim()}` : ''
99
+ throw new Error(`${command} ${commandArgs.join(' ')} failed (exit ${result.status})${detail}`)
34
100
  }
35
- return result.status ?? 1
101
+ return result
36
102
  }
37
103
 
38
- function runGit(args) {
39
- const result = spawnSync('git', args, {
40
- stdio: 'inherit',
41
- cwd,
42
- env: process.env,
43
- })
44
- if (result.error) {
45
- console.error('[audit-override-commit]', result.error.message)
46
- return 1
104
+ function readFile(file) {
105
+ const full = path.join(cwd, file)
106
+ return fs.existsSync(full) ? fs.readFileSync(full, 'utf8') : undefined
107
+ }
108
+
109
+ function snapshot() {
110
+ return Object.fromEntries(TRACKED_FILES.map((file) => [file, readFile(file)]))
111
+ }
112
+
113
+ function isWorkspaceWithPackages() {
114
+ return /^packages:/m.test(readFile('pnpm-workspace.yaml') ?? '')
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // pnpm
119
+ // ---------------------------------------------------------------------------
120
+
121
+ function audit() {
122
+ const result = run('pnpm', ['audit', '--json'], { capture: true })
123
+ if (result.status !== 0 && result.status !== 1) {
124
+ throw new Error(`pnpm audit failed (exit ${result.status})\n${(result.stderr || result.stdout || '').trim()}`)
47
125
  }
48
- return result.status ?? 1
126
+ return normalizeAdvisories(parseAuditJson(result.stdout))
49
127
  }
50
128
 
51
- function runPruneOverridesFix() {
52
- console.log('[audit-override-commit] Pruning overrides that no longer affect pnpm audit…')
53
- const result = spawnSync(process.execPath, [path.join(scriptDir, 'prune-overrides.mjs'), '--fix'], {
54
- stdio: 'inherit',
55
- cwd,
56
- env: process.env,
57
- })
58
- if (result.error) {
59
- console.error('[audit-override-commit]', result.error.message)
60
- return 1
129
+ function readPnpmConfig() {
130
+ const result = run('pnpm', ['config', 'list', '--json'], { capture: true })
131
+ if (result.status !== 0) return {}
132
+ try {
133
+ return parseAuditJson(result.stdout)
134
+ } catch {
135
+ return {}
136
+ }
137
+ }
138
+
139
+ function publishTimes(name) {
140
+ const result = run('pnpm', ['view', name, 'time', '--json'], { capture: true })
141
+ if (result.status !== 0) return {}
142
+ try {
143
+ return parseAuditJson(result.stdout)
144
+ } catch {
145
+ return {}
61
146
  }
62
- return result.status ?? 1
63
147
  }
64
148
 
65
- function stageExistingFiles(files) {
66
- const existingFiles = files.filter((filePath) => fs.existsSync(path.join(cwd, filePath)))
67
- if (existingFiles.length === 0) return 0
68
- return runGit(['add', '--', ...existingFiles])
149
+ function directDependencies() {
150
+ const pkg = JSON.parse(readFile('package.json'))
151
+ return new Set(
152
+ ['dependencies', 'devDependencies', 'optionalDependencies'].flatMap((section) => Object.keys(pkg[section] ?? {})),
153
+ )
69
154
  }
70
155
 
71
- function main() {
72
- if (!fs.existsSync(packageJsonPath)) {
73
- console.error(`[audit-override-commit] package.json not found at ${packageJsonPath}`)
156
+ // ---------------------------------------------------------------------------
157
+ // git
158
+ // ---------------------------------------------------------------------------
159
+
160
+ function assertCleanGitState() {
161
+ const files = [...TRACKED_FILES, SUPPRESSIONS_FILE]
162
+ const status = runOrFail('git', ['status', '--porcelain', '--', ...files], { capture: true }).stdout.trim()
163
+ const problem = status
164
+ ? `Commit or stash your changes to ${files.join(', ')} first:\n${status}`
165
+ : run('git', ['diff', '--cached', '--quiet'], { capture: true }).status !== 0
166
+ ? 'There are staged changes. Commit or unstage them first.'
167
+ : undefined
168
+ if (problem) {
169
+ console.error(`${TAG} ${problem}`)
74
170
  process.exit(1)
75
171
  }
172
+ }
76
173
 
77
- let beforeOverrides
78
- try {
79
- beforeOverrides = collectOverrides(cwd)
80
- } catch (err) {
81
- console.error(`[audit-override-commit] Failed to read overrides before audit fix: ${err.message}`)
82
- process.exit(1)
174
+ function headContent(file) {
175
+ const result = run('git', ['show', `HEAD:${file}`], { capture: true })
176
+ return result.status === 0 ? result.stdout : undefined
177
+ }
178
+
179
+ /** Stage given file contents (not necessarily what is on disk) without touching the working tree. */
180
+ function stageContents(contents) {
181
+ let staged = 0
182
+ for (const [file, content] of Object.entries(contents)) {
183
+ if (content === undefined || content === headContent(file)) continue
184
+ const sha = runOrFail('git', ['hash-object', '-w', '--stdin', `--path=${file}`], {
185
+ capture: true,
186
+ input: content,
187
+ }).stdout.trim()
188
+ runOrFail('git', ['update-index', '--add', '--cacheinfo', `100644,${sha},${file}`])
189
+ staged += 1
83
190
  }
191
+ return staged
192
+ }
193
+
194
+ let commitsMade = 0
195
+
196
+ function commit({ subject, body }) {
197
+ commitsMade += 1
198
+ console.log(`${TAG} Committing: ${subject}`)
199
+ runOrFail('git', body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject])
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Steps
204
+ // ---------------------------------------------------------------------------
84
205
 
85
- const auditArgs =
86
- process.argv.length > 2 ? process.argv.slice(2) : ['audit', '--fix', 'override', '-i']
206
+ function refreshLockfile(advisories) {
207
+ const names = [...new Set(advisories.map((advisory) => advisory.name))].sort()
208
+ console.log(`${TAG} Refreshing within existing ranges: ${names.join(', ')}`)
209
+ const recursive = isWorkspaceWithPackages() ? ['--recursive'] : []
210
+ runOrFail('pnpm', ['update', ...recursive, ...names, '--lockfile-only'])
211
+ }
87
212
 
88
- const auditExitCode = runPnpm(auditArgs)
89
- if (auditExitCode !== 0) {
90
- process.exit(auditExitCode)
213
+ /** Split planned overrides into ones we apply and ones held back by minimumReleaseAge. */
214
+ function applyReleaseAgeGate(overrides) {
215
+ const config = readPnpmConfig()
216
+ const minutes = Number(config.minimumReleaseAge ?? DEFAULT_MINIMUM_RELEASE_AGE_MINUTES)
217
+ if (!minutes) return { allowed: overrides, heldBack: [] }
218
+ const excluded = config.minimumReleaseAgeExclude ?? []
219
+ const allowed = []
220
+ const heldBack = []
221
+ const timesByName = new Map()
222
+ for (const override of overrides) {
223
+ if (isReleaseAgeExcluded(excluded, override.name, override.value)) {
224
+ allowed.push(override)
225
+ continue
226
+ }
227
+ if (!timesByName.has(override.name)) timesByName.set(override.name, publishTimes(override.name))
228
+ const published = Date.parse(timesByName.get(override.name)[override.value] ?? '')
229
+ const ageMinutes = (Date.now() - published) / 60000
230
+ if (Number.isFinite(ageMinutes) && ageMinutes < minutes) {
231
+ heldBack.push({ ...override, waitHours: Math.ceil((minutes - ageMinutes) / 60) })
232
+ } else {
233
+ allowed.push(override)
234
+ }
91
235
  }
236
+ return { allowed, heldBack }
237
+ }
92
238
 
93
- const installExitCode = runPnpm(['install'])
94
- if (installExitCode !== 0) {
95
- process.exit(installExitCode)
239
+ function writeOverrides(overrides) {
240
+ const file = path.join(cwd, 'pnpm-workspace.yaml')
241
+ const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
242
+ fs.writeFileSync(file, upsertWorkspaceOverrides(before, overrides))
243
+ for (const override of overrides) {
244
+ console.log(`${TAG} Override ${override.key}: ${override.value} (${override.ghsas.join(', ')})`)
96
245
  }
246
+ runOrFail('pnpm', ['install', '--lockfile-only', '--ignore-scripts', '--no-frozen-lockfile'])
247
+ }
248
+
249
+ function pruneOverrides() {
250
+ if (!/^overrides:/m.test(readFile('pnpm-workspace.yaml') ?? '')) return
251
+ console.log(`${TAG} Pruning overrides that no longer affect pnpm audit…`)
252
+ runOrFail(process.execPath, [path.join(scriptDir, 'prune-overrides.mjs'), '--fix'])
253
+ }
97
254
 
98
- const pruneExitCode = runPruneOverridesFix()
99
- if (pruneExitCode !== 0) {
100
- process.exit(pruneExitCode)
255
+ /**
256
+ * Write the repo's dependency-check-suppressions.xml with only the entries from fg-devkit's
257
+ * shared file that match a finding still reported here. Untouched when nothing matches.
258
+ */
259
+ function syncSuppressions(remaining) {
260
+ const sourcePath = path.resolve(scriptDir, '..', 'configs', SUPPRESSIONS_FILE)
261
+ if (!fs.existsSync(sourcePath)) return { covered: [], uncovered: remaining }
262
+ const shared = fs.readFileSync(sourcePath, 'utf8')
263
+ const active = activeSuppressedGhsas(shared)
264
+ const covered = remaining
265
+ .filter((advisory) => active.has(advisory.ghsa))
266
+ .map((advisory) => ({ ...advisory, until: active.get(advisory.ghsa) }))
267
+ const uncovered = remaining.filter((advisory) => !active.has(advisory.ghsa))
268
+ if (covered.length === 0) return { covered, uncovered }
269
+ const content = filterSuppressions(
270
+ shared,
271
+ covered.map((advisory) => advisory.ghsa),
272
+ )
273
+ if (readFile(SUPPRESSIONS_FILE) !== content) {
274
+ fs.writeFileSync(path.join(cwd, SUPPRESSIONS_FILE), content)
275
+ suppressionsWritten = true
276
+ console.log(`${TAG} Wrote ${SUPPRESSIONS_FILE} with the entries that apply to this repo.`)
101
277
  }
278
+ return { covered, uncovered }
279
+ }
102
280
 
103
- let afterOverrides
104
- try {
105
- afterOverrides = collectOverrides(cwd)
106
- } catch (err) {
107
- console.error(`[audit-override-commit] Failed to read overrides after audit fix: ${err.message}`)
108
- process.exit(1)
281
+ function verify() {
282
+ console.log(`${TAG} Installing…`)
283
+ runOrFail('pnpm', ['install', '--no-frozen-lockfile'])
284
+ if (args['no-verify']) return
285
+ const scripts = JSON.parse(readFile('package.json')).scripts ?? {}
286
+ const toRun = VERIFY_SCRIPTS.filter((name) => scripts[name])
287
+ if (toRun.length === 0) {
288
+ console.log(`${TAG} No verification scripts (${VERIFY_SCRIPTS.join(', ')}) found; skipping checks.`)
289
+ return
109
290
  }
291
+ for (const name of toRun) {
292
+ console.log(`${TAG} Verifying: pnpm run ${name}`)
293
+ runOrFail('pnpm', ['run', name], { env: { CI: 'true' } })
294
+ }
295
+ }
110
296
 
111
- const changed = collectOverrideChanges(beforeOverrides, afterOverrides)
112
- if (changed.length === 0) {
113
- console.log('[audit-override-commit] No override changes detected.')
114
- process.exit(0)
297
+ function printSummary({ fixedByRefresh, plan, heldBack, applied, remaining, suppressions }) {
298
+ const suppressed = new Set(suppressions.covered.map((advisory) => advisory.ghsa))
299
+ const unique = (items) => [...new Set(items)]
300
+ const lines = []
301
+ const section = (title, items) => {
302
+ if (items.length === 0) return
303
+ lines.push(title, ...items.map((item) => ` - ${item}`))
115
304
  }
305
+ section('Fixed by lockfile refresh:', fixedByRefresh)
306
+ section(
307
+ 'Overridden (parent range blocked the patch):',
308
+ applied.map((item) => `${item.key} -> ${item.value} (${item.ghsas.join(', ')})`),
309
+ )
310
+ section(
311
+ 'Held back by minimumReleaseAge (re-run later, or add to minimumReleaseAgeExclude):',
312
+ heldBack.map((item) => `${item.name}@${item.value}, ~${item.waitHours}h left (${item.ghsas.join(', ')})`),
313
+ )
314
+ section(
315
+ 'Direct dependency, bump it in package.json (policy-upgrade-commit):',
316
+ plan.direct.map((item) => `${item.name} ${item.versions.join(', ')} (${item.ghsas.join(', ')})`),
317
+ )
318
+ section(
319
+ 'Patch only in a new major, upgrade the parent instead:',
320
+ plan.crossMajor.map((item) => `${item.name} ${item.from} -> ${item.to} (${item.ghsas.join(', ')})`),
321
+ )
322
+ section(
323
+ 'No patched version and not suppressed, add a <suppress> entry to fg-devkit configs/dependency-check-suppressions.xml if the risk is accepted:',
324
+ plan.noPatch
325
+ .filter((item) => !item.ghsas.every((ghsa) => suppressed.has(ghsa)))
326
+ .map((item) => `${item.name} ${item.versions.join(', ')} (${item.ghsas.join(', ')})`),
327
+ )
328
+ section(
329
+ `Accepted in ${SUPPRESSIONS_FILE}${suppressionsWritten ? ' (updated from fg-devkit)' : ' (already up to date)'}:`,
330
+ unique(
331
+ suppressions.covered.map(
332
+ (advisory) => `${advisory.name} ${advisory.ghsa}${advisory.until ? `, until ${advisory.until}` : ''}`,
333
+ ),
334
+ ),
335
+ )
336
+ section(
337
+ 'Could not read the patched range, check manually:',
338
+ plan.unparseable.map((item) => `${item.name}: ${item.patched} (${item.ghsas.join(', ')})`),
339
+ )
340
+ if (remaining.length > 0) {
341
+ lines.push(`Still reported by pnpm audit: ${remaining.length} advisory range(s).`)
342
+ } else {
343
+ lines.push('pnpm audit is clean.')
344
+ }
345
+ console.log(`\n${TAG} Summary\n${lines.join('\n')}`)
346
+ }
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // Main
350
+ // ---------------------------------------------------------------------------
351
+
352
+ function main() {
353
+ if (!readFile('package.json')) throw new Error(`package.json not found in ${cwd}`)
354
+ if (!readFile('pnpm-lock.yaml')) throw new Error(`pnpm-lock.yaml not found in ${cwd}`)
355
+ const shouldCommit = !args['no-commit']
356
+ if (shouldCommit) assertCleanGitState()
357
+
358
+ const original = snapshot()
359
+ const overridesBefore = collectOverrides(cwd)
116
360
 
117
- console.log('[audit-override-commit] Override changes detected:')
118
- for (const item of changed) {
119
- const fromLabel = item.from ?? '(missing)'
120
- console.log(` - ${item.name}: ${fromLabel} -> ${formatOverrideValue(item.to)}`)
361
+ console.log(`${TAG} Running pnpm audit…`)
362
+ const initial = audit()
363
+ if (initial.length === 0) {
364
+ console.log(`${TAG} pnpm audit is clean. Nothing to do.`)
365
+ return 0
121
366
  }
367
+ console.log(`${TAG} ${initial.length} advisory range(s) in ${new Set(initial.map((a) => a.name)).size} package(s).`)
122
368
 
123
- const stageExitCode = stageExistingFiles(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'])
124
- if (stageExitCode !== 0) {
125
- process.exit(stageExitCode)
369
+ // 2. Refresh within ranges.
370
+ refreshLockfile(initial)
371
+ const refreshed = snapshot()
372
+ const refreshChanges = diffLockfileVersions(original['pnpm-lock.yaml'], refreshed['pnpm-lock.yaml'])
373
+
374
+ // 3. Plan overrides for what is left.
375
+ const afterRefresh = audit()
376
+ const remainingIds = new Set(afterRefresh.map((advisory) => advisory.id))
377
+ const fixedByRefresh = [
378
+ ...new Set(initial.filter((advisory) => !remainingIds.has(advisory.id)).map((a) => `${a.name} (${a.ghsa})`)),
379
+ ]
380
+ const plan = planOverrides(afterRefresh, { directDependencies: directDependencies() })
381
+ const { allowed, heldBack } = applyReleaseAgeGate(plan.overrides)
382
+ if (allowed.length > 0) writeOverrides(allowed)
383
+
384
+ // 4. Prune, 5. sync suppressions, 6. verify.
385
+ pruneOverrides()
386
+ const remaining = allowed.length > 0 ? audit() : afterRefresh
387
+ const suppressions = syncSuppressions(remaining)
388
+ printSummary({ fixedByRefresh, plan, heldBack, applied: allowed, remaining, suppressions })
389
+ verify()
390
+
391
+ // 7. Commit.
392
+ const final = snapshot()
393
+ const overrideChanges = collectOverrideChanges(overridesBefore, collectOverrides(cwd))
394
+ const nothingChanged = !suppressionsWritten && TRACKED_FILES.every((file) => final[file] === original[file])
395
+ if (nothingChanged) {
396
+ console.log(`${TAG} No file changes.`)
397
+ return 0
398
+ }
399
+ if (!shouldCommit) {
400
+ console.log(`${TAG} --no-commit: changes left in the working tree.`)
401
+ return 0
126
402
  }
127
403
 
128
- const { subject, body } = buildOverrideCommitMessage(changed)
129
- console.log(`[audit-override-commit] Auto-committing: ${subject}`)
130
- if (body) {
131
- console.log(body)
404
+ if (refreshChanges.length > 0 && stageContents(refreshed) > 0) {
405
+ commit(buildRefreshCommitMessage(refreshChanges))
132
406
  }
133
- const commitArgs = body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject]
134
- process.exit(runGit(commitArgs))
407
+ if (overrideChanges.length > 0) {
408
+ for (const item of overrideChanges) {
409
+ console.log(` - ${item.name}: ${item.from ?? '(missing)'} -> ${formatOverrideValue(item.to)}`)
410
+ }
411
+ }
412
+ if (stageContents(final) > 0) {
413
+ commit(
414
+ overrideChanges.length > 0
415
+ ? buildOverrideCommitMessage(overrideChanges)
416
+ : buildRefreshCommitMessage(diffLockfileVersions(refreshed['pnpm-lock.yaml'], final['pnpm-lock.yaml'])),
417
+ )
418
+ }
419
+ if (suppressionsWritten) {
420
+ runOrFail('git', ['add', '--', SUPPRESSIONS_FILE])
421
+ commit({ subject: SUPPRESSIONS_COMMIT_MESSAGE })
422
+ }
423
+ return 0
135
424
  }
136
425
 
137
- main()
426
+ try {
427
+ process.exit(main())
428
+ } catch (error) {
429
+ console.error(`${TAG} ${error instanceof Error ? error.message : error}`)
430
+ console.error(
431
+ commitsMade > 0
432
+ ? `${TAG} ${commitsMade} commit(s) were made before the failure. Check git log and git status.`
433
+ : `${TAG} Nothing was committed. Inspect the changes with git status and git diff.`,
434
+ )
435
+ process.exit(1)
436
+ }
@@ -16,11 +16,11 @@
16
16
  * pnpm exec fg-devkit bump-node-toolchain [nvmrc-version] [projects-root] [--dry-run] [--skip-fleet] [--no-engines]
17
17
  * pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
18
18
  * pnpm exec fg-devkit policy-upgrade-commit [--prune-overrides] [ncu-args...]
19
- * pnpm exec fg-devkit audit-override-commit [pnpm-audit-args...]
19
+ * pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
20
20
  * pnpm exec fg-devkit prune-overrides [--fix] [--commit]
21
21
  * pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
22
22
  * pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
23
- * pnpm exec fg-devkit sync-dependency-check-suppressions-commit
23
+ * pnpm exec fg-devkit sync-dependency-check-suppressions-commit [--all]
24
24
  * pnpm exec fg-devkit sync-vscode-settings
25
25
  * pnpm exec fg-devkit sync-core-coding-standards
26
26
  * pnpm exec fg-devkit sync-consumer-scripts
@@ -542,14 +542,14 @@ Commands:
542
542
  policy-upgrade-commit [--prune-overrides] [ncu-args...]
543
543
  Upgrade consumer package.json packages and auto-commit
544
544
  --prune-overrides also drops unused workspace overrides in a second commit
545
- audit-override-commit [pnpm-audit-args...]
546
- Fix vulnerability overrides via pnpm audit, prune unused ones, auto-commit
545
+ audit-override-commit [--no-verify] [--no-commit]
546
+ Fix pnpm audit findings: refresh lockfile first, override only when a range blocks the fix, verify, auto-commit
547
547
  prune-overrides [--fix] [--commit]
548
548
  Report unused workspace overrides; --fix removes them; --commit also auto-commits
549
549
  sync-project-configs VS Code + tooling sync (overwrites config files)
550
550
  sync-tooling-configs Tooling sync only (overwrites config files)
551
- sync-dependency-check-suppressions-commit
552
- Copy dependency-check-suppressions.xml and auto-commit
551
+ sync-dependency-check-suppressions-commit [--all]
552
+ Write the dependency-check suppressions that match pnpm audit findings and auto-commit (--all: copy every entry)
553
553
  sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
554
554
  sync-core-coding-standards Copy .cursor core coding standards with consumer package name
555
555
  sync-vite-plugin-vue-inspector Add/pin vite-plugin-vue-inspector, wire vite.config, install, commit