fg-devkit 1.3.4 → 1.5.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.
package/README.md CHANGED
@@ -70,6 +70,50 @@ Run full consumer setup in one command:
70
70
  pnpm exec fgd full-consumer-setup
71
71
  ```
72
72
 
73
+ ## Dependency policy update workflow
74
+
75
+ Check outdated policy versions:
76
+
77
+ ```bash
78
+ pnpm exec fgd policy-outdated
79
+ ```
80
+
81
+ Check vulnerabilities for all policy packages together (including transitive dependencies):
82
+
83
+ ```bash
84
+ pnpm exec fgd policy-vulns
85
+ ```
86
+
87
+ Check vulnerabilities for all dependencies in the current repo:
88
+
89
+ ```bash
90
+ pnpm exec fgd repo-vulns
91
+ ```
92
+
93
+ Check vulnerabilities across all Vue repositories under the projects root:
94
+
95
+ ```bash
96
+ pnpm exec fgd fleet-vulns
97
+ ```
98
+
99
+ For summary-only mode (no full per-repo pnpm audit output):
100
+
101
+ ```bash
102
+ pnpm exec fgd fleet-vulns --summary-only
103
+ ```
104
+
105
+ If npm audit is rate-limited (429), slow the scan down:
106
+
107
+ ```bash
108
+ pnpm exec fgd fleet-vulns --delay-ms=8000
109
+ ```
110
+
111
+ Upgrade policy versions interactively:
112
+
113
+ ```bash
114
+ pnpm exec fgd policy-upgrade
115
+ ```
116
+
73
117
  ## tsconfig in consuming repos
74
118
 
75
119
  Use `fgd sync-project-configs` (or `fgd sync-tooling-configs`) to write the local `tsconfig.json` template from `fg-devkit`.
@@ -10,8 +10,8 @@
10
10
  "@sentry/tracing": "7.37.2",
11
11
  "@sentry/vue": "7.37.2",
12
12
  "@sentry/wasm": "7.67.0",
13
- "@tailwindcss/postcss": "4.2.2",
14
- "@tanstack/vue-query": "5.96.2",
13
+ "@tailwindcss/postcss": "4.2.3",
14
+ "@tanstack/vue-query": "5.99.2",
15
15
  "@types/leaflet": "1.5.7",
16
16
  "@unhead/vue": "2.0.5",
17
17
  "@vue-leaflet/vue-leaflet": "0.10.1",
@@ -27,7 +27,7 @@
27
27
  "imask": "7.6.1",
28
28
  "leaflet": "1.8.0",
29
29
  "lexical": "0.30.0",
30
- "lodash": "4.17.23",
30
+ "lodash": "4.18.1",
31
31
  "lodash-es": "4.18.1",
32
32
  "markdown-it": "14.1.1",
33
33
  "pinia": "3.0.4",
@@ -36,7 +36,7 @@
36
36
  "randexp": "0.5.3",
37
37
  "register-service-worker": "1.7.2",
38
38
  "vega-embed": "7.1.0",
39
- "vega-lite": "6.4.1",
39
+ "vega-lite": "6.4.2",
40
40
  "vue-chartjs": "5.3.3",
41
41
  "vue-i18n": "11.3.2",
42
42
  "vue-quilly": "1.0.5",
@@ -56,14 +56,14 @@
56
56
  "@storybook/addon-vitest": "10.2.12",
57
57
  "@storybook/vue3": "10.2.12",
58
58
  "@storybook/vue3-vite": "10.2.12",
59
- "@tanstack/vue-query-devtools": "6.1.13",
59
+ "@tanstack/vue-query-devtools": "6.1.18",
60
60
  "@tsconfig/node22": "22.0.5",
61
61
  "@types/lodash-es": "4.17.12",
62
62
  "@types/node": "22.16.5",
63
63
  "@vitest/browser": "3.2.4",
64
- "@vue/compiler-dom": "3.5.29",
64
+ "@vue/compiler-dom": "3.5.32",
65
65
  "@vue/eslint-config-standard": "8.0.1",
66
- "@vue/server-renderer": "3.5.29",
66
+ "@vue/server-renderer": "3.5.32",
67
67
  "@vue/test-utils": "2.4.6",
68
68
  "@vuepic/vue-datepicker": "6.0.3",
69
69
  "arg": "5.0.2",
@@ -71,26 +71,32 @@
71
71
  "eslint-plugin-storybook": "10.1.10",
72
72
  "globals": "15.11.0",
73
73
  "jsdom": "26.1.0",
74
- "postcss": "8.5.6",
74
+ "postcss": "8.5.10",
75
75
  "sass": "1.97.3",
76
76
  "start-server-and-test": "2.1.3",
77
77
  "storybook": "10.2.12",
78
78
  "tsx": "4.21.0",
79
79
  "vega": "6.2.0",
80
80
  "vite-plugin-dts": "4.5.4",
81
- "vite-plugin-vue-devtools": "8.0.7",
81
+ "vite-plugin-vue-devtools": "8.1.1",
82
82
  "vitest": "3.2.4",
83
83
  "yaml": "2.8.3"
84
84
  }
85
85
  },
86
86
  "managedAnywhere": {
87
- "@tailwindcss/vite": "4.2.2",
88
- "@vitejs/plugin-vue": "6.0.5",
89
- "axios": "1.15.0",
87
+ "@tailwindcss/vite": "4.2.3",
88
+ "@vitejs/plugin-vue": "6.0.6",
89
+ "axios": "1.15.1",
90
90
  "dotenv": "16.4.5",
91
91
  "qrcode.vue": "3.8.1",
92
- "tailwindcss": "4.2.2",
92
+ "tailwindcss": "4.2.3",
93
93
  "vite": "7.3.2",
94
94
  "vue": "3.5.32"
95
+ },
96
+ "overrides": {
97
+ "lodash@<=4.17.23": ">=4.18.0",
98
+ "lodash@>=4.0.0 <=4.17.22": ">=4.17.23",
99
+ "lodash@>=4.0.0 <=4.17.23": ">=4.18.0",
100
+ "tmp@<=0.2.3": ">=0.2.4"
95
101
  }
96
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.3.4",
3
+ "version": "1.5.0",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -28,6 +28,11 @@
28
28
  "./scripts/sync-consumer-scripts.mjs": "./scripts/sync-consumer-scripts.mjs",
29
29
  "./scripts/sync-consumer-lefthook.mjs": "./scripts/sync-consumer-lefthook.mjs",
30
30
  "./scripts/prune-consumer-deps.mjs": "./scripts/prune-consumer-deps.mjs",
31
+ "./scripts/policy-outdated.mjs": "./scripts/policy-outdated.mjs",
32
+ "./scripts/policy-vulns.mjs": "./scripts/policy-vulns.mjs",
33
+ "./scripts/repo-vulns.mjs": "./scripts/repo-vulns.mjs",
34
+ "./scripts/fleet-vulns.mjs": "./scripts/fleet-vulns.mjs",
35
+ "./scripts/policy-upgrade.mjs": "./scripts/policy-upgrade.mjs",
31
36
  "./scripts/check-dependency-policy.mjs": "./scripts/check-dependency-policy.mjs",
32
37
  "./scripts/sync-dependency-policy.mjs": "./scripts/sync-dependency-policy.mjs",
33
38
  "./scripts/resolve-dependency-policy-path.mjs": "./scripts/resolve-dependency-policy-path.mjs",
@@ -70,6 +75,7 @@
70
75
  "eslint-plugin-prettier": "5.5.5",
71
76
  "eslint-plugin-vue": "10.8.0",
72
77
  "lefthook": "2.1.6",
78
+ "npm-check-updates": "21.0.3",
73
79
  "prettier": "3.8.3",
74
80
  "typescript": "5.9.3",
75
81
  "vue-eslint-parser": "10.4.0",
@@ -21,6 +21,7 @@ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mj
21
21
 
22
22
  const cwd = process.cwd()
23
23
  const pkgPath = path.resolve(cwd, 'package.json')
24
+ const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
24
25
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
25
26
  const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
26
27
 
@@ -42,6 +43,30 @@ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
42
43
  const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
43
44
  const mismatches = []
44
45
 
46
+ function getOverridePackageName(overrideKey) {
47
+ const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
48
+ return match ? match[1] : overrideKey
49
+ }
50
+
51
+ function getRelevantPolicyOverrides(policyOverrides) {
52
+ const entries = Object.entries(policyOverrides ?? {})
53
+ if (entries.length === 0) {
54
+ return {}
55
+ }
56
+ if (!fs.existsSync(lockfilePath)) {
57
+ return Object.fromEntries(entries)
58
+ }
59
+ const lockfile = fs.readFileSync(lockfilePath, 'utf8')
60
+ const relevant = {}
61
+ for (const [name, version] of entries) {
62
+ const pkgName = getOverridePackageName(name)
63
+ if (lockfile.includes(`${pkgName}@`) || lockfile.includes(`/${pkgName}/`)) {
64
+ relevant[name] = version
65
+ }
66
+ }
67
+ return relevant
68
+ }
69
+
45
70
  /** True if this package+section is governed by the policy (so global pin checks skip it). */
46
71
  function isPolicyManagedInSection(section, name) {
47
72
  if (policy.managedAnywhere?.[name] !== undefined) {
@@ -56,7 +81,6 @@ for (const [section, dependencies] of Object.entries(policy.managedDependencies
56
81
  }
57
82
 
58
83
  mismatches.push(...collectPinViolations('[policy] managedAnywhere.', policy.managedAnywhere ?? {}))
59
- mismatches.push(...collectPinViolations('[policy] overrides.', policy.overrides ?? {}))
60
84
 
61
85
  // Intersection: only packages that exist in both policy and consumer are compared.
62
86
  for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
@@ -98,12 +122,10 @@ for (const [name, expected] of Object.entries(policy.managedAnywhere ?? {})) {
98
122
 
99
123
  // Optional: policy may define pnpm.overrides entries the consumer must mirror.
100
124
  const overrides = pkg.pnpm?.overrides ?? {}
101
- for (const [name, expected] of Object.entries(policy.overrides ?? {})) {
125
+ for (const [name, expected] of Object.entries(getRelevantPolicyOverrides(policy.overrides ?? {}))) {
102
126
  const actual = overrides[name]
103
127
  if (actual !== expected) {
104
128
  mismatches.push(`pnpm.overrides.${name}: expected ${expected}, got ${actual ?? '(missing)'}`)
105
- } else if (actual !== undefined && !isPinnedDependencyVersion(actual)) {
106
- mismatches.push(`pnpm.overrides.${name}: must be pinned, got ${JSON.stringify(actual)}`)
107
129
  }
108
130
  }
109
131
 
@@ -121,9 +143,6 @@ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies'
121
143
  }
122
144
  }
123
145
 
124
- // Consumer pnpm.overrides must be pinned (ranges reported even if not in policy).
125
- mismatches.push(...collectPinViolations('[pins] pnpm.overrides.', pkg.pnpm?.overrides ?? {}))
126
-
127
146
  if (mismatches.length > 0) {
128
147
  console.error('[check-deps] Dependency policy mismatches found:')
129
148
  for (const mismatch of mismatches) {
@@ -5,6 +5,9 @@
5
5
  module.exports = {
6
6
  prompter(cz, commit) {
7
7
  const defaultType = process.env.FG_DEVKIT_CZ_DEFAULT_TYPE;
8
+ const defaultPackages = process.env.FG_DEVKIT_CZ_DEFAULT_PACKAGES;
9
+ const defaultDepVersion = process.env.FG_DEVKIT_CZ_DEFAULT_DEP_VERSION;
10
+ const defaultDepChangeKind = process.env.FG_DEVKIT_CZ_DEFAULT_DEP_CHANGE_KIND;
8
11
  const types = [
9
12
  { value: "feat", name: "feat(scope): Added" },
10
13
  { value: "fix", name: "fix(scope): Fixed" },
@@ -36,6 +39,7 @@ module.exports = {
36
39
  name: "packages",
37
40
  when: (answers) => ["dep", "vuln"].includes(answers.change_type),
38
41
  message: "Packages:",
42
+ default: defaultType === "dep" && defaultPackages ? defaultPackages : undefined,
39
43
  },
40
44
  {
41
45
  type: "list",
@@ -64,12 +68,17 @@ module.exports = {
64
68
  { value: "update", name: "updated package" },
65
69
  { value: "remove", name: "removed package" },
66
70
  ],
71
+ default:
72
+ defaultType === "dep" && ["new", "update", "remove"].includes(defaultDepChangeKind)
73
+ ? defaultDepChangeKind
74
+ : "update",
67
75
  },
68
76
  {
69
77
  type: "input",
70
78
  name: "dep_version",
71
79
  when: (answers) => answers.change_type === "dep",
72
80
  message: "Version (x.y.z):",
81
+ default: defaultType === "dep" && defaultDepVersion ? defaultDepVersion : undefined,
73
82
  validate: (value) => (/^\d+\.\d+\.\d+$/.test(value) ? true : "Use format x.y.z"),
74
83
  },
75
84
  {
@@ -5,6 +5,11 @@
5
5
  * Non-interactive (for scripts and automation):
6
6
  * pnpm exec fg-devkit check-deps [policy-file]
7
7
  * pnpm exec fg-devkit sync-deps [policy-file]
8
+ * pnpm exec fg-devkit policy-outdated [policy-file]
9
+ * pnpm exec fg-devkit policy-vulns [policy-file]
10
+ * pnpm exec fg-devkit repo-vulns
11
+ * pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
12
+ * pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
8
13
  * pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
9
14
  * pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
10
15
  * pnpm exec fg-devkit sync-vscode-settings
@@ -38,6 +43,7 @@ import { createRequire } from 'node:module'
38
43
 
39
44
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
40
45
  const require = createRequire(import.meta.url)
46
+ const fgRoot = path.resolve(scriptDir, '..')
41
47
 
42
48
  /** Run another script in this package via Node; inherits stdio and returns the child exit code. */
43
49
  function runNode(scriptRelative, args = [], options = {}) {
@@ -153,6 +159,111 @@ function runFullConsumerSetup() {
153
159
  return runPnpmInstall()
154
160
  }
155
161
 
162
+ function stageFiles(paths = []) {
163
+ const existing = paths.filter((p) => fs.existsSync(path.join(process.cwd(), p)))
164
+ if (existing.length === 0) return 0
165
+ const r = spawnSync('git', ['add', '--', ...existing], {
166
+ stdio: 'inherit',
167
+ cwd: process.cwd(),
168
+ env: process.env,
169
+ })
170
+ if (r.error) {
171
+ console.error('[fg-devkit]', r.error.message)
172
+ return 1
173
+ }
174
+ return r.status ?? 1
175
+ }
176
+
177
+ function commitStaged(message) {
178
+ const r = spawnSync('git', ['commit', '-m', message], {
179
+ stdio: 'inherit',
180
+ cwd: process.cwd(),
181
+ env: process.env,
182
+ })
183
+ if (r.error) {
184
+ console.error('[fg-devkit]', r.error.message)
185
+ return 1
186
+ }
187
+ return r.status ?? 1
188
+ }
189
+
190
+ function collectPolicyVersionChanges(beforePolicy, afterPolicy) {
191
+ const before = {
192
+ ...(beforePolicy.managedDependencies?.dependencies ?? {}),
193
+ ...(beforePolicy.managedDependencies?.devDependencies ?? {}),
194
+ ...(beforePolicy.managedAnywhere ?? {}),
195
+ }
196
+ const after = {
197
+ ...(afterPolicy.managedDependencies?.dependencies ?? {}),
198
+ ...(afterPolicy.managedDependencies?.devDependencies ?? {}),
199
+ ...(afterPolicy.managedAnywhere ?? {}),
200
+ }
201
+ const changed = []
202
+ for (const [name, newVersion] of Object.entries(after)) {
203
+ const prev = before[name]
204
+ if (prev !== undefined && prev !== newVersion) {
205
+ changed.push({ name, from: prev, to: newVersion })
206
+ }
207
+ }
208
+ return changed
209
+ }
210
+
211
+ /** Maintainer flow: run policy-upgrade, stage policy file, then auto-commit dep(package): bump to version. */
212
+ function runMaintainerPolicyUpgradeAndCommit() {
213
+ const policyPath = path.join(process.cwd(), 'configs', 'dependency-policy.vue-workspace.json')
214
+ if (!fs.existsSync(policyPath)) {
215
+ console.error('[fg-devkit] Expected policy at configs/dependency-policy.vue-workspace.json')
216
+ return 1
217
+ }
218
+
219
+ let beforePolicy
220
+ try {
221
+ beforePolicy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
222
+ } catch (err) {
223
+ console.error(`[fg-devkit] Failed to read policy before upgrade: ${err.message}`)
224
+ return 1
225
+ }
226
+
227
+ const upgradeCode = runNode('policy-upgrade.mjs')
228
+ if (upgradeCode !== 0) return upgradeCode
229
+
230
+ let afterPolicy
231
+ try {
232
+ afterPolicy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
233
+ } catch (err) {
234
+ console.error(`[fg-devkit] Failed to read policy after upgrade: ${err.message}`)
235
+ return 1
236
+ }
237
+
238
+ const changed = collectPolicyVersionChanges(beforePolicy, afterPolicy)
239
+ if (changed.length === 0) {
240
+ console.log('[fg-devkit] No policy package version changes detected; skipping stage and commit flow.')
241
+ return 0
242
+ }
243
+ if (changed.length > 1) {
244
+ console.log('[fg-devkit] Multiple package changes detected in policy-upgrade:')
245
+ for (const item of changed) {
246
+ console.log(` - ${item.name}: ${item.from} -> ${item.to}`)
247
+ }
248
+ console.log('[fg-devkit] Please commit manually (or rerun and select a single package).')
249
+ return 1
250
+ }
251
+
252
+ const [only] = changed
253
+ const stageCode = stageFiles(['configs/dependency-policy.vue-workspace.json'])
254
+ if (stageCode !== 0) return stageCode
255
+
256
+ const commitMessage = `dep(${only.name}): bump to ${only.to}`
257
+ console.log(`[fg-devkit] Staged policy change for ${only.name} -> ${only.to}`)
258
+ console.log(`[fg-devkit] Auto-committing: ${commitMessage}`)
259
+ return commitStaged(commitMessage)
260
+ }
261
+
262
+ /** Maintainer flow: run full repo vulnerability audit in fg-devkit root. */
263
+ function runMaintainerAuditAll() {
264
+ return runNode('repo-vulns.mjs')
265
+ }
266
+
156
267
  /** Prompt release type in CLI before invoking release.sh to avoid a "stuck" terminal feeling. */
157
268
  async function promptReleaseType(rl) {
158
269
  console.log('\nSelect release type:')
@@ -293,6 +404,11 @@ function runBundledTypeScript(args = []) {
293
404
  const DIRECT_COMMANDS = {
294
405
  'check-deps': 'check-dependency-policy.mjs',
295
406
  'sync-deps': 'sync-dependency-policy.mjs',
407
+ 'policy-outdated': 'policy-outdated.mjs',
408
+ 'policy-vulns': 'policy-vulns.mjs',
409
+ 'repo-vulns': 'repo-vulns.mjs',
410
+ 'fleet-vulns': 'fleet-vulns.mjs',
411
+ 'policy-upgrade': 'policy-upgrade.mjs',
296
412
  'sync-project-configs': 'sync-project-configs.mjs',
297
413
  'sync-tooling-configs': 'sync-tooling-configs.mjs',
298
414
  'sync-vscode-settings': 'sync-vscode-settings.mjs',
@@ -308,6 +424,13 @@ function printHelp() {
308
424
  Commands:
309
425
  check-deps [policy-file] Validate package.json + pins against policy
310
426
  sync-deps [policy-file] Align declared versions to policy (existing keys only)
427
+ policy-outdated [policy-file] Show outdated policy packages via pnpm outdated
428
+ policy-vulns [policy-file] Check all policy packages together (including transitives) via pnpm audit
429
+ repo-vulns Check all repo dependencies for vulnerabilities via pnpm audit
430
+ fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
431
+ Run pnpm audit across Vue repos (full output + combined summary)
432
+ policy-upgrade [policy-file] [ncu-args...]
433
+ Upgrade policy packages via npm-check-updates (default interactive)
311
434
  sync-project-configs VS Code + tooling sync (overwrites config files)
312
435
  sync-tooling-configs Tooling sync only (overwrites config files)
313
436
  sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
@@ -381,12 +504,68 @@ const MENU = [
381
504
  },
382
505
  ]
383
506
 
507
+ const MAINTAINER_MENU = [
508
+ {
509
+ key: 'm1',
510
+ label: 'Maintainer: Show outdated dependency policy packages',
511
+ run: () => runNode('policy-outdated.mjs'),
512
+ },
513
+ {
514
+ key: 'm2',
515
+ label: 'Maintainer: Upgrade dependency policy (interactive)',
516
+ run: () => runNode('policy-upgrade.mjs'),
517
+ },
518
+ {
519
+ key: 'm3',
520
+ label: 'Maintainer: Refresh dependency policy from repos',
521
+ run: () => runNode('refresh-dependency-policy-from-repos.mjs'),
522
+ },
523
+ {
524
+ key: 'm4',
525
+ label: 'Maintainer: Upgrade one policy package, stage, and auto-commit',
526
+ run: () => runMaintainerPolicyUpgradeAndCommit(),
527
+ },
528
+ {
529
+ key: 'm5',
530
+ label: 'Maintainer: Check policy vulnerabilities (audit)',
531
+ run: () => runNode('policy-vulns.mjs'),
532
+ },
533
+ {
534
+ key: 'm6',
535
+ label: 'Maintainer: Check fg-devkit repo vulnerabilities',
536
+ run: () => runMaintainerAuditAll(),
537
+ },
538
+ {
539
+ key: 'm7',
540
+ label: 'Maintainer: Check vulnerabilities across all Vue repos',
541
+ run: () => runNode('fleet-vulns.mjs'),
542
+ },
543
+ ]
544
+
545
+ function isFgDevkitMaintainerContext() {
546
+ if (path.resolve(process.cwd()) !== fgRoot) {
547
+ return false
548
+ }
549
+ const pkgPath = path.join(process.cwd(), 'package.json')
550
+ if (!fs.existsSync(pkgPath)) {
551
+ return false
552
+ }
553
+ try {
554
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
555
+ return pkg?.name === 'fg-devkit'
556
+ } catch {
557
+ return false
558
+ }
559
+ }
560
+
384
561
  async function runInteractiveMenu() {
385
562
  const rl = readline.createInterface({ input, output })
563
+ const isMaintainer = isFgDevkitMaintainerContext()
564
+ const menu = isMaintainer ? [...MENU, ...MAINTAINER_MENU] : MENU
386
565
 
387
566
  console.log(`\nfg-devkit — choose a task (cwd: ${process.cwd()})\n`)
388
567
 
389
- for (const item of MENU) {
568
+ for (const item of menu) {
390
569
  console.log(` ${item.key}) ${item.label}`)
391
570
  }
392
571
  console.log(' 0) Exit\n')
@@ -398,7 +577,7 @@ async function runInteractiveMenu() {
398
577
  return 0
399
578
  }
400
579
 
401
- const item = MENU.find((m) => m.key === choice)
580
+ const item = menu.find((m) => m.key === choice)
402
581
  if (!item) {
403
582
  console.error('[fg-devkit] Unknown option. Run again and choose a number from the list.')
404
583
  rl.close()
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runs vulnerability audits across Vue repositories and prints both per-repo
4
+ * pnpm audit output and a combined summary.
5
+ *
6
+ * Usage:
7
+ * pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import { spawnSync } from 'node:child_process'
13
+ import { fileURLToPath } from 'node:url'
14
+
15
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
16
+ const cliArgs = process.argv.slice(2)
17
+ const summaryOnly = cliArgs.includes('--summary-only')
18
+ const projectsRootArg = cliArgs.find((a) => !a.startsWith('-'))
19
+ const projectsRoot = projectsRootArg ? path.resolve(process.cwd(), projectsRootArg) : path.resolve(scriptDir, '../..')
20
+ const delayArg = cliArgs.find((a) => a.startsWith('--delay-ms='))
21
+ const delayMs = Number(delayArg?.split('=')[1] ?? 2500)
22
+
23
+ function sleep(ms) {
24
+ return new Promise((resolve) => setTimeout(resolve, ms))
25
+ }
26
+
27
+ function walk(dir, acc = []) {
28
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
29
+ if (item.name === 'node_modules' || item.name === '.git') continue
30
+ const p = path.join(dir, item.name)
31
+ if (item.isDirectory()) walk(p, acc)
32
+ else if (item.name === 'package.json') acc.push(p)
33
+ }
34
+ return acc
35
+ }
36
+
37
+ function readJson(p) {
38
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
39
+ }
40
+
41
+ function isVueProject(pkg) {
42
+ return Boolean(pkg.dependencies?.vue || pkg.devDependencies?.vue)
43
+ }
44
+
45
+ function countTotal(severityMap = {}) {
46
+ return Object.values(severityMap).reduce((sum, n) => sum + Number(n || 0), 0)
47
+ }
48
+
49
+ const packageJsonFiles = walk(projectsRoot)
50
+ const repos = []
51
+ for (const packageJsonPath of packageJsonFiles) {
52
+ try {
53
+ const pkg = readJson(packageJsonPath)
54
+ const repoDir = path.dirname(packageJsonPath)
55
+ if (!isVueProject(pkg)) continue
56
+ if (pkg.name === 'fg-devkit') continue
57
+ repos.push(repoDir)
58
+ } catch {
59
+ // Ignore invalid package.json files.
60
+ }
61
+ }
62
+
63
+ const uniqueRepos = [...new Set(repos)].sort((a, b) => a.localeCompare(b))
64
+ if (uniqueRepos.length === 0) {
65
+ console.log(`[fleet-vulns] No Vue repositories found under ${projectsRoot}`)
66
+ process.exit(0)
67
+ }
68
+
69
+ console.log(`[fleet-vulns] projectsRoot=${projectsRoot}`)
70
+ console.log(`[fleet-vulns] repositories=${uniqueRepos.length}`)
71
+
72
+ const results = []
73
+ let hasFailures = false
74
+ let hasVulnerabilities = false
75
+
76
+ for (const repoDir of uniqueRepos) {
77
+ const repoName = path.basename(repoDir)
78
+ const lockfilePath = path.join(repoDir, 'pnpm-lock.yaml')
79
+ if (!fs.existsSync(lockfilePath)) {
80
+ console.log(`[fleet-vulns] SKIP ${repoName}: missing pnpm-lock.yaml`)
81
+ continue
82
+ }
83
+
84
+ console.log(`\n[fleet-vulns] Auditing ${repoName}`)
85
+ if (!summaryOnly) {
86
+ const prettyAudit = spawnSync('pnpm', ['audit'], {
87
+ cwd: repoDir,
88
+ env: process.env,
89
+ stdio: 'inherit',
90
+ })
91
+
92
+ if (prettyAudit.error) {
93
+ hasFailures = true
94
+ console.error(`[fleet-vulns] FAIL ${repoName}: ${prettyAudit.error.message}`)
95
+ continue
96
+ }
97
+ const prettyCode = prettyAudit.status ?? 1
98
+ if (prettyCode !== 0 && prettyCode !== 1) {
99
+ hasFailures = true
100
+ console.error(`[fleet-vulns] FAIL ${repoName}: pnpm audit exited with code ${prettyCode}`)
101
+ continue
102
+ }
103
+ }
104
+
105
+ const audit = spawnSync('pnpm', ['audit', '--json'], {
106
+ cwd: repoDir,
107
+ env: process.env,
108
+ encoding: 'utf8',
109
+ })
110
+
111
+ if (audit.error) {
112
+ hasFailures = true
113
+ console.error(`[fleet-vulns] FAIL ${repoName}: ${audit.error.message}`)
114
+ continue
115
+ }
116
+
117
+ let report = null
118
+ try {
119
+ report = JSON.parse(audit.stdout || '{}')
120
+ } catch {
121
+ report = null
122
+ }
123
+
124
+ if (!report || typeof report !== 'object') {
125
+ hasFailures = true
126
+ console.error(`[fleet-vulns] FAIL ${repoName}: could not parse pnpm audit --json output`)
127
+ continue
128
+ }
129
+
130
+ const vulnerabilities = report.metadata?.vulnerabilities ?? {}
131
+ const total = countTotal(vulnerabilities)
132
+ if (total > 0) hasVulnerabilities = true
133
+
134
+ results.push({
135
+ repoName,
136
+ vulnerabilities,
137
+ total,
138
+ dependencies: report.metadata?.totalDependencies ?? 0,
139
+ })
140
+
141
+ if (delayMs > 0) {
142
+ await sleep(delayMs)
143
+ }
144
+ }
145
+
146
+ console.log('\n[fleet-vulns] Summary')
147
+ for (const r of results) {
148
+ const v = r.vulnerabilities
149
+ console.log(
150
+ `- ${r.repoName}: total=${r.total} (critical=${v.critical ?? 0}, high=${v.high ?? 0}, moderate=${v.moderate ?? 0}, low=${v.low ?? 0}, info=${v.info ?? 0}) deps=${r.dependencies}`,
151
+ )
152
+ }
153
+
154
+ if (results.length === 0) {
155
+ console.log('[fleet-vulns] No repositories were audited.')
156
+ }
157
+
158
+ if (hasFailures) {
159
+ process.exit(1)
160
+ }
161
+ if (hasVulnerabilities) {
162
+ process.exit(1)
163
+ }
164
+ process.exit(0)
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shows outdated packages for dependency policy entries by projecting policy data
4
+ * into a temporary package.json and running `pnpm outdated` there.
5
+ *
6
+ * Usage:
7
+ * pnpm exec fg-devkit policy-outdated [policy-file]
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+ import { spawnSync } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
16
+
17
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
18
+ const policyPath = resolveDependencyPolicyPath(scriptDir, process.cwd(), process.argv)
19
+
20
+ function readJson(p) {
21
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
22
+ }
23
+
24
+ const policy = readJson(policyPath)
25
+ const deps = {
26
+ ...(policy.managedDependencies?.dependencies ?? {}),
27
+ ...(policy.managedAnywhere ?? {}),
28
+ }
29
+ const devDeps = {
30
+ ...(policy.managedDependencies?.devDependencies ?? {}),
31
+ }
32
+
33
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fgd-policy-outdated-'))
34
+ const pkgPath = path.join(tmpDir, 'package.json')
35
+ fs.writeFileSync(
36
+ pkgPath,
37
+ `${JSON.stringify(
38
+ {
39
+ name: 'fgd-policy-outdated-temp',
40
+ private: true,
41
+ version: '0.0.0',
42
+ dependencies: deps,
43
+ devDependencies: devDeps,
44
+ },
45
+ null,
46
+ 2,
47
+ )}\n`,
48
+ 'utf8',
49
+ )
50
+
51
+ console.log(`[policy-outdated] policy=${policyPath}`)
52
+ const install = spawnSync('pnpm', ['install', '--lockfile-only', '--ignore-scripts'], {
53
+ cwd: tmpDir,
54
+ stdio: 'inherit',
55
+ env: process.env,
56
+ })
57
+ if (install.error) {
58
+ console.error('[policy-outdated]', install.error.message)
59
+ process.exit(1)
60
+ }
61
+ if ((install.status ?? 1) !== 0) {
62
+ process.exit(install.status ?? 1)
63
+ }
64
+
65
+ const result = spawnSync('pnpm', ['outdated'], {
66
+ cwd: tmpDir,
67
+ stdio: 'inherit',
68
+ env: process.env,
69
+ })
70
+ if (result.error) {
71
+ console.error('[policy-outdated]', result.error.message)
72
+ process.exit(1)
73
+ }
74
+ // pnpm outdated returns exit code 1 when outdated packages are found.
75
+ if ((result.status ?? 1) === 1) {
76
+ process.exit(0)
77
+ }
78
+ process.exit(result.status ?? 1)
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Upgrades dependency policy package versions through npm-check-updates.
4
+ * It projects the policy into a temporary package.json, runs ncu, then writes
5
+ * selected versions back to the policy file.
6
+ *
7
+ * Usage:
8
+ * pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
9
+ *
10
+ * Defaults when no ncu args are given:
11
+ * --interactive --format group --upgrade --packageManager pnpm --install never
12
+ */
13
+
14
+ import fs from 'node:fs'
15
+ import os from 'node:os'
16
+ import path from 'node:path'
17
+ import { spawnSync } from 'node:child_process'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { createRequire } from 'node:module'
20
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
21
+
22
+ const require = createRequire(import.meta.url)
23
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
24
+
25
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
26
+ console.log(`Usage:
27
+ pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
28
+
29
+ Examples:
30
+ pnpm exec fg-devkit policy-upgrade
31
+ pnpm exec fg-devkit policy-upgrade configs/dependency-policy.vue-workspace.json --interactive --format group --upgrade`)
32
+ process.exit(0)
33
+ }
34
+
35
+ const argPolicy = process.argv[2]
36
+ const hasPolicyArg = Boolean(argPolicy) && !argPolicy.startsWith('-')
37
+ const policyPath = resolveDependencyPolicyPath(scriptDir, process.cwd(), process.argv)
38
+ const ncuArgs = hasPolicyArg ? process.argv.slice(3) : process.argv.slice(2)
39
+
40
+ function readJson(p) {
41
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
42
+ }
43
+
44
+ const policy = readJson(policyPath)
45
+
46
+ const depNames = new Set(Object.keys(policy.managedDependencies?.dependencies ?? {}))
47
+ const devNames = new Set(Object.keys(policy.managedDependencies?.devDependencies ?? {}))
48
+ const anyNames = new Set(Object.keys(policy.managedAnywhere ?? {}))
49
+
50
+ const deps = {
51
+ ...(policy.managedDependencies?.dependencies ?? {}),
52
+ ...(policy.managedAnywhere ?? {}),
53
+ }
54
+ const devDeps = {
55
+ ...(policy.managedDependencies?.devDependencies ?? {}),
56
+ }
57
+
58
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fgd-policy-upgrade-'))
59
+ const pkgPath = path.join(tmpDir, 'package.json')
60
+ fs.writeFileSync(
61
+ pkgPath,
62
+ `${JSON.stringify(
63
+ {
64
+ name: 'fgd-policy-upgrade-temp',
65
+ private: true,
66
+ version: '0.0.0',
67
+ dependencies: deps,
68
+ devDependencies: devDeps,
69
+ },
70
+ null,
71
+ 2,
72
+ )}\n`,
73
+ 'utf8',
74
+ )
75
+
76
+ let ncuBin
77
+ try {
78
+ const ncuPkgPath = require.resolve('npm-check-updates/package.json')
79
+ ncuBin = path.join(path.dirname(ncuPkgPath), 'build', 'cli.js')
80
+ } catch {
81
+ console.error('[policy-upgrade] Could not resolve npm-check-updates binary.')
82
+ process.exit(1)
83
+ }
84
+
85
+ const effectiveNcuArgs =
86
+ ncuArgs.length > 0
87
+ ? ncuArgs
88
+ : ['--interactive', '--format', 'group', '--upgrade', '--packageManager', 'pnpm', '--install', 'never']
89
+
90
+ console.log(`[policy-upgrade] policy=${policyPath}`)
91
+ const run = spawnSync(process.execPath, [ncuBin, ...effectiveNcuArgs], {
92
+ cwd: tmpDir,
93
+ stdio: 'inherit',
94
+ env: process.env,
95
+ })
96
+ if (run.error) {
97
+ console.error('[policy-upgrade]', run.error.message)
98
+ process.exit(1)
99
+ }
100
+ if ((run.status ?? 1) !== 0) {
101
+ process.exit(run.status ?? 1)
102
+ }
103
+
104
+ const updatedPkg = readJson(pkgPath)
105
+ const updatedDeps = updatedPkg.dependencies ?? {}
106
+ const updatedDevDeps = updatedPkg.devDependencies ?? {}
107
+
108
+ for (const [name, version] of Object.entries(updatedDeps)) {
109
+ if (anyNames.has(name)) policy.managedAnywhere[name] = version
110
+ else if (depNames.has(name)) policy.managedDependencies.dependencies[name] = version
111
+ }
112
+ for (const [name, version] of Object.entries(updatedDevDeps)) {
113
+ if (devNames.has(name)) policy.managedDependencies.devDependencies[name] = version
114
+ }
115
+
116
+ fs.writeFileSync(policyPath, `${JSON.stringify(policy, null, 2)}\n`, 'utf8')
117
+ console.log('[policy-upgrade] Updated dependency policy from selected ncu changes.')
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks vulnerabilities for all dependency policy entries together by projecting
4
+ * policy data into one temporary package.json and running `pnpm audit` there.
5
+ *
6
+ * Usage:
7
+ * pnpm exec fg-devkit policy-vulns [policy-file]
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+ import { spawnSync } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
16
+
17
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
18
+ const policyPath = resolveDependencyPolicyPath(scriptDir, process.cwd(), process.argv)
19
+
20
+ function readJson(p) {
21
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
22
+ }
23
+
24
+ const policy = readJson(policyPath)
25
+ console.log(`[policy-vulns] policy=${policyPath}`)
26
+ const deps = {
27
+ ...(policy.managedDependencies?.dependencies ?? {}),
28
+ ...(policy.managedAnywhere ?? {}),
29
+ }
30
+ const devDeps = {
31
+ ...(policy.managedDependencies?.devDependencies ?? {}),
32
+ }
33
+
34
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fgd-policy-vulns-'))
35
+ const pkgPath = path.join(tmpDir, 'package.json')
36
+ fs.writeFileSync(
37
+ pkgPath,
38
+ `${JSON.stringify(
39
+ {
40
+ name: 'fgd-policy-vulns-temp',
41
+ private: true,
42
+ version: '0.0.0',
43
+ dependencies: deps,
44
+ devDependencies: devDeps,
45
+ },
46
+ null,
47
+ 2,
48
+ )}\n`,
49
+ 'utf8',
50
+ )
51
+
52
+ const install = spawnSync('pnpm', ['install', '--ignore-scripts'], {
53
+ cwd: tmpDir,
54
+ stdio: 'inherit',
55
+ env: process.env,
56
+ })
57
+ if (install.error) {
58
+ console.error('[policy-vulns]', install.error.message)
59
+ process.exit(1)
60
+ }
61
+ if ((install.status ?? 1) !== 0) {
62
+ process.exit(install.status ?? 1)
63
+ }
64
+
65
+ const audit = spawnSync('pnpm', ['audit'], {
66
+ cwd: tmpDir,
67
+ stdio: 'inherit',
68
+ env: process.env,
69
+ })
70
+ if (audit.error) {
71
+ console.error('[policy-vulns]', audit.error.message)
72
+ process.exit(1)
73
+ }
74
+ process.exit(audit.status ?? 1)
@@ -39,6 +39,15 @@ const policyPath = process.argv[3]
39
39
  ? path.resolve(process.cwd(), process.argv[3])
40
40
  : path.join(fgDevkitRoot, 'configs/dependency-policy.vue-workspace.json')
41
41
 
42
+ let existingPolicy = {}
43
+ try {
44
+ if (fs.existsSync(policyPath)) {
45
+ existingPolicy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
46
+ }
47
+ } catch {
48
+ existingPolicy = {}
49
+ }
50
+
42
51
  /** Relative paths under projectsRoot whose package.json files are ignored (maintainer choice). */
43
52
  /** Relative path prefixes under projectsRoot to skip when scanning (add repo names as needed). */
44
53
  const SKIP_DIR_PREFIXES = []
@@ -180,6 +189,7 @@ const policy = {
180
189
  devDependencies: sortRecord(outDev),
181
190
  },
182
191
  managedAnywhere: sortRecord(outAny),
192
+ overrides: sortRecord(existingPolicy.overrides ?? {}),
183
193
  }
184
194
 
185
195
  const policyErrors = [
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks vulnerabilities for the full dependency graph in the current repository
4
+ * by running `pnpm audit` in the working directory.
5
+ *
6
+ * Usage:
7
+ * pnpm exec fg-devkit repo-vulns
8
+ */
9
+
10
+ import { spawnSync } from 'node:child_process'
11
+
12
+ console.log(`[repo-vulns] cwd=${process.cwd()}`)
13
+ const audit = spawnSync('pnpm', ['audit'], {
14
+ cwd: process.cwd(),
15
+ stdio: 'inherit',
16
+ env: process.env,
17
+ })
18
+ if (audit.error) {
19
+ console.error('[repo-vulns]', audit.error.message)
20
+ process.exit(1)
21
+ }
22
+ // pnpm audit returns exit code 1 when vulnerabilities are found.
23
+ if ((audit.status ?? 1) === 1) {
24
+ process.exit(0)
25
+ }
26
+ process.exit(audit.status ?? 1)
@@ -20,6 +20,7 @@ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mj
20
20
 
21
21
  const cwd = process.cwd()
22
22
  const pkgPath = path.resolve(cwd, 'package.json')
23
+ const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
23
24
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
24
25
  const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
25
26
 
@@ -43,7 +44,6 @@ const policyErrors = [
43
44
  collectPinViolations(`managedDependencies.${section}.`, deps),
44
45
  ),
45
46
  ...collectPinViolations('managedAnywhere.', policy.managedAnywhere ?? {}),
46
- ...collectPinViolations('overrides.', policy.overrides ?? {}),
47
47
  ]
48
48
 
49
49
  if (policyErrors.length > 0) {
@@ -57,6 +57,31 @@ if (policyErrors.length > 0) {
57
57
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
58
58
  const changes = []
59
59
 
60
+ function getOverridePackageName(overrideKey) {
61
+ const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
62
+ return match ? match[1] : overrideKey
63
+ }
64
+
65
+ function getRelevantPolicyOverrides(policyOverrides) {
66
+ const entries = Object.entries(policyOverrides ?? {})
67
+ if (entries.length === 0) {
68
+ return {}
69
+ }
70
+ if (!fs.existsSync(lockfilePath)) {
71
+ return Object.fromEntries(entries)
72
+ }
73
+ const lockfile = fs.readFileSync(lockfilePath, 'utf8')
74
+ const relevant = {}
75
+ for (const [name, version] of entries) {
76
+ const pkgName = getOverridePackageName(name)
77
+ // Conservative: only apply overrides for packages present in lockfile.
78
+ if (lockfile.includes(`${pkgName}@`) || lockfile.includes(`/${pkgName}/`)) {
79
+ relevant[name] = version
80
+ }
81
+ }
82
+ return relevant
83
+ }
84
+
60
85
  // Bump versions for managed dependencies / devDependencies where the key already exists.
61
86
  for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
62
87
  pkg[section] ||= {}
@@ -104,7 +129,7 @@ pkg.pnpm ||= {}
104
129
  pkg.pnpm.overrides ||= {}
105
130
 
106
131
  // Apply policy-defined override versions when present in the policy.
107
- for (const [name, version] of Object.entries(policy.overrides ?? {})) {
132
+ for (const [name, version] of Object.entries(getRelevantPolicyOverrides(policy.overrides ?? {}))) {
108
133
  if (pkg.pnpm.overrides[name] !== version) {
109
134
  changes.push(`pnpm.overrides.${name}: ${pkg.pnpm.overrides[name] ?? '(missing)'} -> ${version}`)
110
135
  pkg.pnpm.overrides[name] = version