fg-devkit 1.9.1 → 1.12.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.
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fix one vulnerability via `pnpm audit --fix override -i`, install, then auto-commit.
4
+ *
5
+ * Usage:
6
+ * pnpm exec fg-devkit audit-override-commit [pnpm-audit-args...]
7
+ */
8
+
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import { spawnSync } from 'node:child_process'
12
+
13
+ const cwd = process.cwd()
14
+ const packageJsonPath = path.join(cwd, 'package.json')
15
+ const workspaceYamlPath = path.join(cwd, 'pnpm-workspace.yaml')
16
+
17
+ function getOverridePackageName(overrideKey) {
18
+ const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
19
+ return match ? match[1] : overrideKey
20
+ }
21
+
22
+ /** Read flat `overrides:` entries from pnpm-workspace.yaml (no YAML dependency). */
23
+ function readWorkspaceOverrides(yamlPath) {
24
+ if (!fs.existsSync(yamlPath)) return {}
25
+ const overrides = {}
26
+ let inOverrides = false
27
+ for (const line of fs.readFileSync(yamlPath, 'utf8').split('\n')) {
28
+ if (/^overrides:\s*$/.test(line)) {
29
+ inOverrides = true
30
+ continue
31
+ }
32
+ if (!inOverrides) continue
33
+ if (/^\S/.test(line)) break
34
+ const match = line.match(/^ ([^:]+):\s*(.+?)\s*$/)
35
+ if (match) {
36
+ overrides[match[1].trim()] = match[2].trim()
37
+ }
38
+ }
39
+ return overrides
40
+ }
41
+
42
+ function readJson(jsonPath) {
43
+ return JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
44
+ }
45
+
46
+ function collectOverrides() {
47
+ const overrides = {}
48
+
49
+ if (fs.existsSync(packageJsonPath)) {
50
+ const pkg = readJson(packageJsonPath)
51
+ Object.assign(overrides, pkg.overrides ?? {})
52
+ Object.assign(overrides, pkg.pnpm?.overrides ?? {})
53
+ }
54
+
55
+ Object.assign(overrides, readWorkspaceOverrides(workspaceYamlPath))
56
+ return overrides
57
+ }
58
+
59
+ function collectOverrideChanges(before, after) {
60
+ const changed = []
61
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)])
62
+ for (const key of keys) {
63
+ const from = before[key]
64
+ const to = after[key]
65
+ if (to !== undefined && from !== to) {
66
+ changed.push({ key, name: getOverridePackageName(key), from, to })
67
+ }
68
+ }
69
+ return changed
70
+ }
71
+
72
+ function runPnpm(args) {
73
+ const result = spawnSync('pnpm', args, {
74
+ stdio: 'inherit',
75
+ cwd,
76
+ env: process.env,
77
+ })
78
+ if (result.error) {
79
+ console.error('[audit-override-commit]', result.error.message)
80
+ return 1
81
+ }
82
+ return result.status ?? 1
83
+ }
84
+
85
+ function runGit(args) {
86
+ const result = spawnSync('git', args, {
87
+ stdio: 'inherit',
88
+ cwd,
89
+ env: process.env,
90
+ })
91
+ if (result.error) {
92
+ console.error('[audit-override-commit]', result.error.message)
93
+ return 1
94
+ }
95
+ return result.status ?? 1
96
+ }
97
+
98
+ function stageExistingFiles(files) {
99
+ const existingFiles = files.filter((filePath) => fs.existsSync(path.join(cwd, filePath)))
100
+ if (existingFiles.length === 0) return 0
101
+ return runGit(['add', '--', ...existingFiles])
102
+ }
103
+
104
+ function main() {
105
+ if (!fs.existsSync(packageJsonPath)) {
106
+ console.error(`[audit-override-commit] package.json not found at ${packageJsonPath}`)
107
+ process.exit(1)
108
+ }
109
+
110
+ let beforeOverrides
111
+ try {
112
+ beforeOverrides = collectOverrides()
113
+ } catch (err) {
114
+ console.error(`[audit-override-commit] Failed to read overrides before audit fix: ${err.message}`)
115
+ process.exit(1)
116
+ }
117
+
118
+ const auditArgs =
119
+ process.argv.length > 2 ? process.argv.slice(2) : ['audit', '--fix', 'override', '-i']
120
+
121
+ const auditExitCode = runPnpm(auditArgs)
122
+ if (auditExitCode !== 0) {
123
+ process.exit(auditExitCode)
124
+ }
125
+
126
+ let afterOverrides
127
+ try {
128
+ afterOverrides = collectOverrides()
129
+ } catch (err) {
130
+ console.error(`[audit-override-commit] Failed to read overrides after audit fix: ${err.message}`)
131
+ process.exit(1)
132
+ }
133
+
134
+ const changed = collectOverrideChanges(beforeOverrides, afterOverrides)
135
+ if (changed.length === 0) {
136
+ console.log('[audit-override-commit] No override changes detected.')
137
+ process.exit(0)
138
+ }
139
+ if (changed.length > 1) {
140
+ console.log('[audit-override-commit] Multiple override changes detected:')
141
+ for (const item of changed) {
142
+ const fromLabel = item.from ?? '(missing)'
143
+ console.log(` - ${item.name}: ${fromLabel} -> ${item.to}`)
144
+ }
145
+ console.log('[audit-override-commit] Auto-commit supports exactly one override change. Commit manually.')
146
+ process.exit(1)
147
+ }
148
+
149
+ const installExitCode = runPnpm(['install'])
150
+ if (installExitCode !== 0) {
151
+ process.exit(installExitCode)
152
+ }
153
+
154
+ const [only] = changed
155
+ const stageExitCode = stageExistingFiles(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'])
156
+ if (stageExitCode !== 0) {
157
+ process.exit(stageExitCode)
158
+ }
159
+
160
+ const commitMessage = `vuln(${only.name}): override ${only.to}`
161
+ console.log(`[audit-override-commit] Staged override for ${only.name} -> ${only.to}`)
162
+ console.log(`[audit-override-commit] Auto-committing: ${commitMessage}`)
163
+ process.exit(runGit(['commit', '-m', commitMessage]))
164
+ }
165
+
166
+ main()
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Maintainer: bump pnpm to the latest (or given) version in fg-devkit and sync toolchain everywhere.
4
+ *
5
+ * Steps in fg-devkit:
6
+ * 1. Resolve target pnpm version (CLI arg or `npm view pnpm version`)
7
+ * 2. Set package.json packageManager
8
+ * 3. Sync dependency policy toolchain, scaffold/vue, and Dockerfile.local
9
+ * 4. corepack prepare + pnpm install
10
+ *
11
+ * Steps per Vue consumer repo under projects-root (unless --skip-fleet):
12
+ * 1. sync-deps against this checkout's policy file (works before fg-devkit is published)
13
+ * 2. corepack prepare + pnpm install
14
+ * 3. check-deps against the same policy file
15
+ *
16
+ * Usage (from fg-devkit repository root):
17
+ * pnpm exec fg-devkit bump-pnpm-toolchain [version] [projects-root] [options]
18
+ *
19
+ * Options:
20
+ * --dry-run Print planned steps without changing files or running install
21
+ * --skip-fleet Only update fg-devkit (policy, scaffold, local install)
22
+ * --skip-install Skip corepack prepare and pnpm install
23
+ *
24
+ * Examples:
25
+ * pnpm exec fg-devkit bump-pnpm-toolchain
26
+ * pnpm exec fg-devkit bump-pnpm-toolchain 12.0.0
27
+ * pnpm exec fg-devkit bump-pnpm-toolchain --dry-run
28
+ * pnpm exec fg-devkit bump-pnpm-toolchain --skip-fleet
29
+ */
30
+
31
+ import fs from 'node:fs'
32
+ import path from 'node:path'
33
+ import { spawnSync } from 'node:child_process'
34
+ import { fileURLToPath } from 'node:url'
35
+
36
+ import { formatPnpmPackageManager } from './dependency-policy-toolchain.mjs'
37
+ import { collectFleetVueRepos } from './fleet-vue-repos.mjs'
38
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
39
+ import { syncScaffoldVueDepsFromPolicy } from './sync-scaffold-vue-deps.mjs'
40
+
41
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
42
+ const fgDevkitRoot = path.resolve(scriptDir, '..')
43
+ const fgDevkitPkgPath = path.join(fgDevkitRoot, 'package.json')
44
+ const policyPath = resolveDependencyPolicyPath(scriptDir, fgDevkitRoot, [])
45
+
46
+ const cliArgs = process.argv.slice(2)
47
+ const dryRun = cliArgs.includes('--dry-run')
48
+ const skipFleet = cliArgs.includes('--skip-fleet')
49
+ const skipInstall = cliArgs.includes('--skip-install')
50
+ const positionalArgs = cliArgs.filter((a) => !a.startsWith('-'))
51
+
52
+ function readJson(jsonPath) {
53
+ return JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
54
+ }
55
+
56
+ function assertFgDevkitMaintainerContext() {
57
+ if (path.resolve(process.cwd()) !== fgDevkitRoot) {
58
+ console.error('[bump-pnpm-toolchain] Run from the fg-devkit repository root.')
59
+ process.exit(1)
60
+ }
61
+ try {
62
+ const pkg = readJson(fgDevkitPkgPath)
63
+ if (pkg?.name !== 'fg-devkit') {
64
+ console.error('[bump-pnpm-toolchain] package.json name must be fg-devkit.')
65
+ process.exit(1)
66
+ }
67
+ } catch {
68
+ console.error('[bump-pnpm-toolchain] Could not read fg-devkit package.json.')
69
+ process.exit(1)
70
+ }
71
+ }
72
+
73
+ function resolveTargetVersion(versionArg) {
74
+ if (versionArg) {
75
+ return formatPnpmPackageManager(versionArg)
76
+ }
77
+ const lookup = spawnSync('npm', ['view', 'pnpm', 'version'], {
78
+ encoding: 'utf8',
79
+ env: process.env,
80
+ })
81
+ if (lookup.error) {
82
+ console.error('[bump-pnpm-toolchain]', lookup.error.message)
83
+ process.exit(1)
84
+ }
85
+ if ((lookup.status ?? 1) !== 0) {
86
+ console.error('[bump-pnpm-toolchain] Failed to resolve latest pnpm version via npm view.')
87
+ process.exit(lookup.status ?? 1)
88
+ }
89
+ const latest = lookup.stdout.trim()
90
+ if (!latest) {
91
+ console.error('[bump-pnpm-toolchain] npm view pnpm version returned empty output.')
92
+ process.exit(1)
93
+ }
94
+ return formatPnpmPackageManager(latest)
95
+ }
96
+
97
+ function runStep(label, fn) {
98
+ console.log(`\n[bump-pnpm-toolchain] ${label}`)
99
+ return fn()
100
+ }
101
+
102
+ function runCorepackPrepare(packageManager, cwd) {
103
+ const result = spawnSync('corepack', ['prepare', packageManager, '--activate'], {
104
+ cwd,
105
+ stdio: 'inherit',
106
+ env: process.env,
107
+ })
108
+ if (result.error) {
109
+ console.error('[bump-pnpm-toolchain]', result.error.message)
110
+ return 1
111
+ }
112
+ return result.status ?? 1
113
+ }
114
+
115
+ function runPnpmInstall(cwd) {
116
+ const result = spawnSync('pnpm', ['install'], {
117
+ cwd,
118
+ stdio: 'inherit',
119
+ env: process.env,
120
+ })
121
+ if (result.error) {
122
+ console.error('[bump-pnpm-toolchain]', result.error.message)
123
+ return 1
124
+ }
125
+ return result.status ?? 1
126
+ }
127
+
128
+ function runNodeScript(scriptName, args, cwd) {
129
+ const scriptPath = path.join(scriptDir, scriptName)
130
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
131
+ cwd,
132
+ stdio: 'inherit',
133
+ env: process.env,
134
+ })
135
+ if (result.error) {
136
+ console.error('[bump-pnpm-toolchain]', result.error.message)
137
+ return 1
138
+ }
139
+ return result.status ?? 1
140
+ }
141
+
142
+ function updateFgDevkitPackageManager(packageManager) {
143
+ const pkg = readJson(fgDevkitPkgPath)
144
+ if (pkg.packageManager === packageManager) {
145
+ console.log(`[bump-pnpm-toolchain] fg-devkit packageManager already ${packageManager}`)
146
+ return false
147
+ }
148
+ pkg.packageManager = packageManager
149
+ fs.writeFileSync(fgDevkitPkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
150
+ console.log(`[bump-pnpm-toolchain] Set fg-devkit packageManager -> ${packageManager}`)
151
+ return true
152
+ }
153
+
154
+ function syncFleetRepo(repoDir, repoName, packageManager) {
155
+ if (dryRun) {
156
+ console.log(`[bump-pnpm-toolchain] DRY RUN ${repoName}: sync-deps ${policyPath}`)
157
+ console.log(`[bump-pnpm-toolchain] DRY RUN ${repoName}: corepack prepare ${packageManager}`)
158
+ console.log(`[bump-pnpm-toolchain] DRY RUN ${repoName}: pnpm install`)
159
+ console.log(`[bump-pnpm-toolchain] DRY RUN ${repoName}: check-deps ${policyPath}`)
160
+ return 0
161
+ }
162
+
163
+ const syncCode = runNodeScript('sync-dependency-policy.mjs', [policyPath], repoDir)
164
+ if (syncCode !== 0) {
165
+ console.error(`[bump-pnpm-toolchain] FAIL ${repoName}: sync-deps exited with code ${syncCode}`)
166
+ return syncCode
167
+ }
168
+
169
+ if (!skipInstall) {
170
+ const corepackCode = runCorepackPrepare(packageManager, repoDir)
171
+ if (corepackCode !== 0) {
172
+ console.error(`[bump-pnpm-toolchain] FAIL ${repoName}: corepack prepare exited with code ${corepackCode}`)
173
+ return corepackCode
174
+ }
175
+
176
+ const installCode = runPnpmInstall(repoDir)
177
+ if (installCode !== 0) {
178
+ console.error(`[bump-pnpm-toolchain] FAIL ${repoName}: pnpm install exited with code ${installCode}`)
179
+ return installCode
180
+ }
181
+ }
182
+
183
+ const checkCode = runNodeScript('check-dependency-policy.mjs', [policyPath], repoDir)
184
+ if (checkCode !== 0) {
185
+ console.error(`[bump-pnpm-toolchain] FAIL ${repoName}: check-deps exited with code ${checkCode}`)
186
+ return checkCode
187
+ }
188
+
189
+ console.log(`[bump-pnpm-toolchain] OK ${repoName}`)
190
+ return 0
191
+ }
192
+
193
+ function main() {
194
+ assertFgDevkitMaintainerContext()
195
+
196
+ const versionArg = positionalArgs.find((a) => /^(pnpm@)?\d/.test(a))
197
+ const projectsRootArg = positionalArgs.find((a) => a !== versionArg)
198
+ const projectsRoot = projectsRootArg
199
+ ? path.resolve(process.cwd(), projectsRootArg)
200
+ : path.resolve(fgDevkitRoot, '..')
201
+
202
+ const packageManager = resolveTargetVersion(versionArg)
203
+
204
+ console.log(`[bump-pnpm-toolchain] target=${packageManager}`)
205
+ console.log(`[bump-pnpm-toolchain] policy=${policyPath}`)
206
+ if (!skipFleet) {
207
+ console.log(`[bump-pnpm-toolchain] projectsRoot=${projectsRoot}`)
208
+ }
209
+
210
+ if (dryRun) {
211
+ console.log('[bump-pnpm-toolchain] DRY RUN fg-devkit: update packageManager')
212
+ console.log('[bump-pnpm-toolchain] DRY RUN fg-devkit: sync-scaffold-vue-deps')
213
+ if (!skipInstall) {
214
+ console.log(`[bump-pnpm-toolchain] DRY RUN fg-devkit: corepack prepare ${packageManager}`)
215
+ console.log('[bump-pnpm-toolchain] DRY RUN fg-devkit: pnpm install')
216
+ }
217
+ } else {
218
+ runStep('Update fg-devkit package.json', () => {
219
+ updateFgDevkitPackageManager(packageManager)
220
+ })
221
+
222
+ runStep('Sync policy, scaffold, and Dockerfile', () => {
223
+ syncScaffoldVueDepsFromPolicy({ fgDevkitRoot, policyPath })
224
+ })
225
+
226
+ if (!skipInstall) {
227
+ const corepackCode = runStep('Activate pnpm via corepack (fg-devkit)', () =>
228
+ runCorepackPrepare(packageManager, fgDevkitRoot),
229
+ )
230
+ if (corepackCode !== 0) {
231
+ process.exit(corepackCode)
232
+ }
233
+
234
+ const installCode = runStep('pnpm install (fg-devkit)', () => runPnpmInstall(fgDevkitRoot))
235
+ if (installCode !== 0) {
236
+ process.exit(installCode)
237
+ }
238
+ }
239
+ }
240
+
241
+ if (skipFleet) {
242
+ console.log('\n[bump-pnpm-toolchain] Done (--skip-fleet). Commit fg-devkit changes and release when ready.')
243
+ process.exit(0)
244
+ }
245
+
246
+ if (!fs.existsSync(projectsRoot)) {
247
+ console.error(`[bump-pnpm-toolchain] projects root not found: ${projectsRoot}`)
248
+ process.exit(1)
249
+ }
250
+
251
+ const repos = collectFleetVueRepos(projectsRoot)
252
+ if (repos.length === 0) {
253
+ console.log(`[bump-pnpm-toolchain] No Vue repositories with fg-devkit under ${projectsRoot}`)
254
+ process.exit(0)
255
+ }
256
+
257
+ console.log(`\n[bump-pnpm-toolchain] Fleet repositories=${repos.length}`)
258
+
259
+ let failures = 0
260
+ for (const repoDir of repos) {
261
+ const repoName = path.basename(repoDir)
262
+ console.log(`\n[bump-pnpm-toolchain] Fleet: ${repoName}`)
263
+ const code = syncFleetRepo(repoDir, repoName, packageManager)
264
+ if (code !== 0) {
265
+ failures += 1
266
+ }
267
+ }
268
+
269
+ console.log(`\n[bump-pnpm-toolchain] Done: fleet ok=${repos.length - failures}, failed=${failures}`)
270
+ if (failures > 0) {
271
+ process.exit(1)
272
+ }
273
+
274
+ if (!dryRun) {
275
+ console.log(
276
+ '[bump-pnpm-toolchain] Commit changes in fg-devkit and each updated consumer repo, then release fg-devkit.',
277
+ )
278
+ }
279
+ }
280
+
281
+ main()
@@ -6,6 +6,7 @@
6
6
  * - For each dependency listed in the policy, checks the consumer’s declared version matches.
7
7
  * - For managedAnywhere packages, allows either dependencies or devDependencies but requires one consistent version.
8
8
  * - Optionally checks policy-defined pnpm overrides against the consumer’s package.json.
9
+ * - When the policy defines toolchain.packageManager, requires the consumer to use the same value.
9
10
  * - Enforces pinned semver for any other dependencies/devDependencies/optionalDependencies and consumer pnpm.overrides.
10
11
  *
11
12
  * Policy path: first CLI argument, or default policy under fg-devkit/configs/ (see resolve-dependency-policy-path.mjs).
@@ -17,6 +18,10 @@ import path from 'node:path'
17
18
  import { fileURLToPath } from 'node:url'
18
19
 
19
20
  import { collectPinViolations, isPinnedDependencyVersion } from './dependency-policy-pins.mjs'
21
+ import {
22
+ collectToolchainPolicyViolations,
23
+ resolveExpectedPackageManager,
24
+ } from './dependency-policy-toolchain.mjs'
20
25
  import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
21
26
 
22
27
  const cwd = process.cwd()
@@ -81,6 +86,17 @@ for (const [section, dependencies] of Object.entries(policy.managedDependencies
81
86
  }
82
87
 
83
88
  mismatches.push(...collectPinViolations('[policy] managedAnywhere.', policy.managedAnywhere ?? {}))
89
+ mismatches.push(...collectToolchainPolicyViolations(policy))
90
+
91
+ const expectedPackageManager = resolveExpectedPackageManager(policy, path.resolve(scriptDir, '..'))
92
+ if (expectedPackageManager !== undefined) {
93
+ const actualPackageManager = pkg.packageManager
94
+ if (actualPackageManager !== expectedPackageManager) {
95
+ mismatches.push(
96
+ `packageManager: expected ${expectedPackageManager}, got ${actualPackageManager ?? '(missing)'}`,
97
+ )
98
+ }
99
+ }
84
100
 
85
101
  // Intersection: only packages that exist in both policy and consumer are compared.
86
102
  for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared helpers for fleet-wide pnpm / Corepack toolchain policy.
3
+ */
4
+
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+
8
+ const PNPM_PACKAGE_MANAGER_PATTERN = /^pnpm@\d+\.\d+\.\d+(-[\w.]+)?$/i
9
+
10
+ /** @param {unknown} value */
11
+ export function isValidPnpmPackageManager(value) {
12
+ return typeof value === 'string' && PNPM_PACKAGE_MANAGER_PATTERN.test(value)
13
+ }
14
+
15
+ /**
16
+ * @param {string} fgDevkitRoot
17
+ * @returns {string | undefined}
18
+ */
19
+ export function readFgDevkitPackageManager(fgDevkitRoot) {
20
+ const pkgPath = path.join(fgDevkitRoot, 'package.json')
21
+ if (!fs.existsSync(pkgPath)) {
22
+ return undefined
23
+ }
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
25
+ return typeof pkg.packageManager === 'string' ? pkg.packageManager : undefined
26
+ }
27
+
28
+ /**
29
+ * @param {Record<string, unknown>} policy
30
+ * @param {string} fgDevkitRoot
31
+ * @returns {{ policy: Record<string, unknown>; changed: boolean }}
32
+ */
33
+ export function syncPolicyToolchainFromFgDevkit(policy, fgDevkitRoot) {
34
+ const fromFgDevkit = readFgDevkitPackageManager(fgDevkitRoot)
35
+ if (!fromFgDevkit || !isValidPnpmPackageManager(fromFgDevkit)) {
36
+ return { policy, changed: false }
37
+ }
38
+ const next = { ...policy, toolchain: { ...(policy.toolchain ?? {}) } }
39
+ if (next.toolchain.packageManager === fromFgDevkit) {
40
+ return { policy: next, changed: false }
41
+ }
42
+ next.toolchain.packageManager = fromFgDevkit
43
+ return { policy: next, changed: true }
44
+ }
45
+
46
+ /**
47
+ * Expected `packageManager` for consumers: policy.toolchain first, then fg-devkit package.json.
48
+ *
49
+ * @param {Record<string, unknown>} policy
50
+ * @param {string} [fgDevkitRoot]
51
+ * @returns {string | undefined}
52
+ */
53
+ export function resolveExpectedPackageManager(policy, fgDevkitRoot) {
54
+ const fromPolicy = policy?.toolchain?.packageManager
55
+ if (typeof fromPolicy === 'string') {
56
+ return fromPolicy
57
+ }
58
+ if (fgDevkitRoot) {
59
+ return readFgDevkitPackageManager(fgDevkitRoot)
60
+ }
61
+ return undefined
62
+ }
63
+
64
+ /**
65
+ * @param {Record<string, unknown>} policy
66
+ * @param {string} [labelPrefix]
67
+ * @returns {string[]}
68
+ */
69
+ export function collectToolchainPolicyViolations(policy, labelPrefix = '[policy] toolchain.') {
70
+ const packageManager = policy?.toolchain?.packageManager
71
+ if (packageManager === undefined) {
72
+ return []
73
+ }
74
+ if (!isValidPnpmPackageManager(packageManager)) {
75
+ return [
76
+ `${labelPrefix}packageManager: must be pnpm@x.y.z, got ${JSON.stringify(packageManager)}`,
77
+ ]
78
+ }
79
+ return []
80
+ }
81
+
82
+ /**
83
+ * @param {string} version Bare semver (e.g. 11.7.0) or packageManager string (pnpm@11.7.0).
84
+ * @returns {string}
85
+ */
86
+ export function formatPnpmPackageManager(version) {
87
+ const bare = version.trim().replace(/^pnpm@/i, '')
88
+ if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(bare)) {
89
+ throw new Error(`Invalid pnpm version: ${JSON.stringify(version)}`)
90
+ }
91
+ return `pnpm@${bare}`
92
+ }
93
+
94
+ /**
95
+ * @param {string} packageManager e.g. pnpm@11.7.0
96
+ * @returns {string | undefined}
97
+ */
98
+ export function extractPnpmVersion(packageManager) {
99
+ const match = packageManager.match(/^pnpm@(.+)$/i)
100
+ return match ? match[1] : undefined
101
+ }