fg-devkit 1.9.0 → 1.12.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.
@@ -10,8 +10,10 @@
10
10
  * pnpm exec fg-devkit repo-vulns
11
11
  * pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
12
12
  * pnpm exec fg-devkit fleet-bump-fg-devkit [projects-root] [--dry-run]
13
+ * pnpm exec fg-devkit bump-pnpm-toolchain [version] [projects-root] [--dry-run] [--skip-fleet] [--skip-install]
13
14
  * pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
14
15
  * pnpm exec fg-devkit policy-upgrade-commit [ncu-args...]
16
+ * pnpm exec fg-devkit audit-override-commit [pnpm-audit-args...]
15
17
  * pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
16
18
  * pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
17
19
  * pnpm exec fg-devkit sync-vscode-settings
@@ -20,7 +22,10 @@
20
22
  * pnpm exec fg-devkit setup-lefthook
21
23
  * pnpm exec fg-devkit prune-consumer-deps [--dry-run]
22
24
  * pnpm exec fg-devkit full-consumer-setup
23
- * pnpm exec fg-devkit create-release [release|candidate] [patch|minor|major]
25
+ * pnpm exec fg-devkit create-release [release|candidate] [patch|minor|major] [--skip-release-notes]
26
+ * pnpm exec fg-devkit generate-release-notes [--tag=vX.Y.Z]
27
+ * pnpm exec fg-devkit verify-release-notes
28
+ * pnpm exec fg-devkit sync-release-notes-config
24
29
  * pnpm exec fg-devkit commit [cz-args...] (czg/cz-git with fg-devkit bundled config)
25
30
  * pnpm exec fg-devkit commit-vuln (staged package.json version bumps → vuln() commit)
26
31
  * pnpm exec fg-devkit lint [eslint-args...] (run ESLint with fg-devkit bundled packages)
@@ -153,6 +158,9 @@ function runFullConsumerSetup() {
153
158
  const scriptsStep = runNode('sync-consumer-scripts.mjs')
154
159
  if (scriptsStep !== 0) return scriptsStep
155
160
 
161
+ const releaseNotesStep = runNode('sync-release-notes-config.mjs')
162
+ if (releaseNotesStep !== 0) return releaseNotesStep
163
+
156
164
  const hooksStep = runSetupLefthook()
157
165
  if (hooksStep !== 0) return hooksStep
158
166
 
@@ -453,13 +461,18 @@ const DIRECT_COMMANDS = {
453
461
  'repo-vulns': 'repo-vulns.mjs',
454
462
  'fleet-vulns': 'fleet-vulns.mjs',
455
463
  'fleet-bump-fg-devkit': 'fleet-bump-fg-devkit.mjs',
464
+ 'bump-pnpm-toolchain': 'bump-pnpm-toolchain.mjs',
456
465
  'policy-upgrade': 'policy-upgrade.mjs',
457
466
  'policy-upgrade-commit': 'policy-upgrade-commit.mjs',
467
+ 'audit-override-commit': 'audit-override-commit.mjs',
458
468
  'sync-project-configs': 'sync-project-configs.mjs',
459
469
  'sync-tooling-configs': 'sync-tooling-configs.mjs',
460
470
  'sync-vscode-settings': 'sync-vscode-settings.mjs',
461
471
  'sync-core-coding-standards': 'sync-core-coding-standards.mjs',
462
472
  'sync-consumer-scripts': 'sync-consumer-scripts.mjs',
473
+ 'sync-release-notes-config': 'sync-release-notes-config.mjs',
474
+ 'generate-release-notes': 'generate-release-notes.mjs',
475
+ 'verify-release-notes': 'verify-release-notes.mjs',
463
476
  'setup-lefthook': 'setup-lefthook',
464
477
  'prune-consumer-deps': 'prune-consumer-deps.mjs',
465
478
  'sync-scaffold-vue-deps': 'sync-scaffold-vue-deps.mjs',
@@ -472,8 +485,8 @@ function printHelp() {
472
485
  pnpm exec fg-devkit [command] [args...]
473
486
 
474
487
  Commands:
475
- check-deps [policy-file] Validate package.json + pins against policy
476
- sync-deps [policy-file] Align declared versions to policy (existing keys only)
488
+ check-deps [policy-file] Validate package.json + pins + toolchain against policy
489
+ sync-deps [policy-file] Align declared versions and packageManager to policy
477
490
  policy-outdated [policy-file] Show outdated policy packages via pnpm outdated
478
491
  policy-vulns [policy-file] Check all policy packages together (including transitives) via pnpm audit
479
492
  repo-vulns Check all repo dependencies for vulnerabilities via pnpm audit
@@ -481,10 +494,15 @@ Commands:
481
494
  Run pnpm audit across Vue repos (full output + combined summary)
482
495
  fleet-bump-fg-devkit [projects-root] [--dry-run]
483
496
  Bump fg-devkit, run sync-deps, then pnpm install across Vue repos
497
+ bump-pnpm-toolchain [version] [projects-root] [options]
498
+ Maintainer: bump pnpm to latest (or version), sync fg-devkit + fleet
499
+ Options: --dry-run, --skip-fleet, --skip-install
484
500
  policy-upgrade [policy-file] [ncu-args...]
485
501
  Upgrade policy packages via npm-check-updates (default interactive)
486
502
  policy-upgrade-commit [ncu-args...]
487
503
  Upgrade one consumer package.json package and auto-commit
504
+ audit-override-commit [pnpm-audit-args...]
505
+ Fix one vulnerability override via pnpm audit and auto-commit
488
506
  sync-project-configs VS Code + tooling sync (overwrites config files)
489
507
  sync-tooling-configs Tooling sync only (overwrites config files)
490
508
  sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
@@ -493,9 +511,13 @@ Commands:
493
511
  setup-lefthook Setup lefthook and migrate from husky/lint-staged
494
512
  prune-consumer-deps [--dry-run] Remove consumer tooling deps bundled by fg-devkit
495
513
  full-consumer-setup Run: step 7 -> scripts -> lefthook -> step 3 -> step 2 -> pnpm install
496
- create-release [release|candidate] [patch|minor|major]
514
+ create-release [release|candidate] [patch|minor|major] [--skip-release-notes]
497
515
  Stable release or candidate (X.Y.Z-rc.N); omit = interactive menu
498
516
  (alias: createRelease)
517
+ generate-release-notes [--tag=vX.Y.Z]
518
+ Generate CHANGELOG.md and user changelog via git-cliff
519
+ verify-release-notes Interactive review step before committing release notes
520
+ sync-release-notes-config Copy cliff configs and generate:release-notes script when missing
499
521
  commit [args...] czg/cz-git with fg-devkit bundled config
500
522
  commit-vuln Commit staged dependency bumps as vuln() (alias: vuln-commit)
501
523
  lint [eslint-args...] Run ESLint using fg-devkit bundled ESLint stack
@@ -648,6 +670,18 @@ async function runInteractiveMenu() {
648
670
  })
649
671
  if (releaseKind === 'cancel') continue
650
672
 
673
+ const releaseMode = await selectPrompt({
674
+ message: 'Create release',
675
+ choices: [
676
+ { name: 'Release + release notes', value: 'with-notes' },
677
+ { name: 'Release', value: 'without-notes' },
678
+ { name: 'Cancel', value: 'cancel' },
679
+ ],
680
+ default: 'with-notes',
681
+ loop: false,
682
+ })
683
+ if (releaseMode === 'cancel') continue
684
+
651
685
  const bumpChoice = await selectPrompt({
652
686
  message: releaseKind === 'candidate' ? 'Candidate' : 'Release',
653
687
  choices: [
@@ -663,6 +697,11 @@ async function runInteractiveMenu() {
663
697
 
664
698
  const releaseArgs =
665
699
  releaseKind === 'candidate' ? [bumpChoice, 'candidate'] : [bumpChoice]
700
+
701
+ if (releaseMode === 'without-notes') {
702
+ releaseArgs.push('--skip-release-notes')
703
+ }
704
+
666
705
  return runSelectedAction(() => runReleaseScript(releaseArgs))
667
706
  }
668
707
 
@@ -710,6 +749,7 @@ async function runInteractiveMenu() {
710
749
  { name: 'Check', value: 'check' },
711
750
  { name: 'Sync', value: 'sync' },
712
751
  { name: 'Upgrade one package.json package and auto-commit', value: 'upgrade-commit' },
752
+ { name: 'Fix one vulnerability override and auto-commit', value: 'audit-override-commit' },
713
753
  { name: 'Cancel', value: 'cancel' },
714
754
  ],
715
755
  default: 'check',
@@ -718,6 +758,9 @@ async function runInteractiveMenu() {
718
758
  if (choice === 'cancel') continue
719
759
  if (choice === 'check') return runSelectedAction(() => runNode('check-dependency-policy.mjs'))
720
760
  if (choice === 'sync') return runSelectedAction(() => runNode('sync-dependency-policy.mjs'))
761
+ if (choice === 'audit-override-commit') {
762
+ return runSelectedAction(() => runNode('audit-override-commit.mjs'))
763
+ }
721
764
  return runSelectedAction(() => runNode('policy-upgrade-commit.mjs'))
722
765
  }
723
766
 
@@ -781,6 +824,7 @@ async function runInteractiveMenu() {
781
824
  { name: 'Check fg-devkit repo vulnerabilities', value: 'm5' },
782
825
  { name: 'Check vulnerabilities across all Vue repos', value: 'm6' },
783
826
  { name: 'Bump fg-devkit in all Vue repos, run sync-deps, and pnpm install', value: 'm7' },
827
+ { name: 'Bump pnpm toolchain (latest)', value: 'm8' },
784
828
  { name: 'Cancel', value: 'cancel' },
785
829
  ],
786
830
  default: 'm1',
@@ -796,6 +840,24 @@ async function runInteractiveMenu() {
796
840
  if (choice === 'm5') return runSelectedAction(() => runMaintainerAuditAll())
797
841
  if (choice === 'm6') return runSelectedAction(() => runNode('fleet-vulns.mjs'))
798
842
  if (choice === 'm7') return runSelectedAction(() => runNode('fleet-bump-fg-devkit.mjs'))
843
+ if (choice === 'm8') {
844
+ const scope = await selectPrompt({
845
+ message: 'Bump pnpm toolchain',
846
+ choices: [
847
+ { name: 'fg-devkit only (policy, scaffold, install)', value: 'fg-devkit' },
848
+ { name: 'fg-devkit + all Vue repos under projects root', value: 'fleet' },
849
+ { name: 'Cancel', value: 'cancel' },
850
+ ],
851
+ default: 'fg-devkit',
852
+ loop: false,
853
+ })
854
+ if (scope === 'cancel') continue
855
+ const bumpArgs = []
856
+ if (scope === 'fg-devkit') {
857
+ bumpArgs.push('--skip-fleet')
858
+ }
859
+ return runSelectedAction(() => runNode('bump-pnpm-toolchain.mjs', bumpArgs))
860
+ }
799
861
  return 1
800
862
  }
801
863
  }
@@ -875,6 +937,11 @@ async function main() {
875
937
  process.exitCode = 1
876
938
  return
877
939
  }
940
+ if (sub === 'bump-pnpm-toolchain' && !isFgDevkitMaintainerContext()) {
941
+ console.error('[fg-devkit] bump-pnpm-toolchain is only available in the fg-devkit repository root.')
942
+ process.exitCode = 1
943
+ return
944
+ }
878
945
  process.exitCode = runNode(script, rest)
879
946
  return
880
947
  }
@@ -6,68 +6,19 @@
6
6
  * pnpm exec fg-devkit fleet-bump-fg-devkit [projects-root] [--dry-run]
7
7
  */
8
8
 
9
- import fs from 'node:fs'
10
9
  import path from 'node:path'
11
10
  import { spawnSync } from 'node:child_process'
12
11
  import { fileURLToPath } from 'node:url'
13
12
 
13
+ import { collectFleetVueRepos } from './fleet-vue-repos.mjs'
14
+
14
15
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
15
16
  const cliArgs = process.argv.slice(2)
16
17
  const dryRun = cliArgs.includes('--dry-run')
17
18
  const projectsRootArg = cliArgs.find((a) => !a.startsWith('-'))
18
19
  const projectsRoot = projectsRootArg ? path.resolve(process.cwd(), projectsRootArg) : path.resolve(scriptDir, '../..')
19
20
 
20
- function shouldSkipPackageJsonDir(relDir) {
21
- const normalized = relDir.split(path.sep).join('/')
22
- if (normalized === 'scaffold/vue' || normalized.endsWith('/scaffold/vue')) {
23
- return true
24
- }
25
- return false
26
- }
27
-
28
- function walk(dir, acc = []) {
29
- for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
30
- if (item.name === 'node_modules' || item.name === '.git') continue
31
- const p = path.join(dir, item.name)
32
- if (item.isDirectory()) walk(p, acc)
33
- else if (item.name === 'package.json') acc.push(p)
34
- }
35
- return acc
36
- }
37
-
38
- function readJson(p) {
39
- return JSON.parse(fs.readFileSync(p, 'utf8'))
40
- }
41
-
42
- function isVueProject(pkg) {
43
- return Boolean(pkg.dependencies?.vue || pkg.devDependencies?.vue)
44
- }
45
-
46
- function hasFgDevkit(pkg) {
47
- return Boolean(pkg.dependencies?.['fg-devkit'] || pkg.devDependencies?.['fg-devkit'])
48
- }
49
-
50
- function isFgDevkitRepo(pkg) {
51
- return pkg?.name === 'fg-devkit'
52
- }
53
-
54
- const packageJsonFiles = walk(projectsRoot)
55
- const repos = []
56
- for (const packageJsonPath of packageJsonFiles) {
57
- try {
58
- const relDir = path.relative(projectsRoot, path.dirname(packageJsonPath))
59
- if (shouldSkipPackageJsonDir(relDir)) continue
60
- const pkg = readJson(packageJsonPath)
61
- if (!isVueProject(pkg)) continue
62
- if (isFgDevkitRepo(pkg)) continue
63
- if (!hasFgDevkit(pkg)) continue
64
- repos.push(path.dirname(packageJsonPath))
65
- } catch {
66
- // Ignore invalid package.json files.
67
- }
68
- }
69
-
70
- const uniqueRepos = [...new Set(repos)].sort((a, b) => a.localeCompare(b))
21
+ const uniqueRepos = collectFleetVueRepos(projectsRoot)
71
22
  if (uniqueRepos.length === 0) {
72
23
  console.log(`[fleet-bump-fg-devkit] No Vue repositories with fg-devkit found under ${projectsRoot}`)
73
24
  process.exit(0)
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Discover Vue repositories under a projects root that use fg-devkit.
3
+ */
4
+
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+
8
+ /**
9
+ * @param {string} projectsRoot
10
+ * @param {{ skipScaffoldVue?: boolean }} [options]
11
+ * @returns {string[]} Absolute repo directory paths, sorted.
12
+ */
13
+ export function collectFleetVueRepos(projectsRoot, options = {}) {
14
+ const { skipScaffoldVue = true } = options
15
+
16
+ function shouldSkipPackageJsonDir(relDir) {
17
+ if (!skipScaffoldVue) {
18
+ return false
19
+ }
20
+ const normalized = relDir.split(path.sep).join('/')
21
+ return normalized === 'scaffold/vue' || normalized.endsWith('/scaffold/vue')
22
+ }
23
+
24
+ function walk(dir, acc = []) {
25
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
26
+ if (item.name === 'node_modules' || item.name === '.git') continue
27
+ const p = path.join(dir, item.name)
28
+ if (item.isDirectory()) walk(p, acc)
29
+ else if (item.name === 'package.json') acc.push(p)
30
+ }
31
+ return acc
32
+ }
33
+
34
+ function readJson(p) {
35
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
36
+ }
37
+
38
+ const repos = []
39
+ for (const packageJsonPath of walk(projectsRoot)) {
40
+ try {
41
+ const relDir = path.relative(projectsRoot, path.dirname(packageJsonPath))
42
+ if (shouldSkipPackageJsonDir(relDir)) continue
43
+ const pkg = readJson(packageJsonPath)
44
+ if (pkg?.name === 'fg-devkit') continue
45
+ if (!pkg.dependencies?.vue && !pkg.devDependencies?.vue) continue
46
+ if (!pkg.dependencies?.['fg-devkit'] && !pkg.devDependencies?.['fg-devkit']) continue
47
+ repos.push(path.dirname(packageJsonPath))
48
+ } catch {
49
+ // Ignore invalid package.json files.
50
+ }
51
+ }
52
+
53
+ return [...new Set(repos)].sort((a, b) => a.localeCompare(b))
54
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate technical and user-facing changelogs via git-cliff, then sync user changelog.
4
+ *
5
+ * Usage:
6
+ * node node_modules/fg-devkit/scripts/generate-release-notes.mjs [--tag=vX.Y.Z]
7
+ * pnpm exec fg-devkit generate-release-notes [--tag=vX.Y.Z]
8
+ */
9
+
10
+ import { spawnSync } from 'node:child_process'
11
+ import path from 'node:path'
12
+ import {
13
+ hasReleaseNotesConfig,
14
+ resolveCliffConfig,
15
+ resolveGitCliffBin,
16
+ runSyncUserChangelog,
17
+ } from './release-notes-common.mjs'
18
+ const cwd = process.cwd()
19
+ const tagArg = process.argv.find((arg) => arg.startsWith('--tag='))?.slice('--tag='.length)
20
+
21
+ function runGitCliff(configPath, outputPath) {
22
+ const gitCliffBin = resolveGitCliffBin()
23
+ if (!gitCliffBin) {
24
+ console.error('[fg-devkit] Could not resolve git-cliff. Reinstall fg-devkit and retry.')
25
+ return 1
26
+ }
27
+
28
+ const args = ['-c', configPath, '-o', outputPath]
29
+ if (tagArg) {
30
+ args.push('--tag', tagArg)
31
+ }
32
+
33
+ const r = spawnSync(gitCliffBin, args, {
34
+ stdio: 'inherit',
35
+ cwd,
36
+ env: process.env,
37
+ })
38
+
39
+ if (r.error) {
40
+ console.error('[fg-devkit]', r.error.message)
41
+ return 1
42
+ }
43
+
44
+ return r.status ?? 1
45
+ }
46
+
47
+ function main() {
48
+ if (!hasReleaseNotesConfig(cwd)) {
49
+ console.error(
50
+ '[fg-devkit] No release-notes config found. Run "pnpm exec fgd sync-release-notes-config" first.',
51
+ )
52
+ return 1
53
+ }
54
+
55
+ const technicalConfig = resolveCliffConfig('cliff.toml', cwd)
56
+ if (!technicalConfig) {
57
+ console.error('[fg-devkit] Missing cliff.toml in repo and fg-devkit configs.')
58
+ return 1
59
+ }
60
+
61
+ const userConfig = resolveCliffConfig('cliff.user.toml', cwd)
62
+
63
+ const technicalCode = runGitCliff(technicalConfig, path.join(cwd, 'CHANGELOG.md'))
64
+ if (technicalCode !== 0) {
65
+ return technicalCode
66
+ }
67
+
68
+ if (userConfig) {
69
+ const userCode = runGitCliff(userConfig, path.join(cwd, 'CHANGELOG.user.draft.md'))
70
+ if (userCode !== 0) {
71
+ return userCode
72
+ }
73
+
74
+ const syncCode = runSyncUserChangelog(cwd)
75
+ if (syncCode !== 0) {
76
+ return syncCode
77
+ }
78
+ }
79
+
80
+ console.log('[fg-devkit] Release notes generated.')
81
+ return 0
82
+ }
83
+
84
+ process.exitCode = main()
@@ -29,6 +29,7 @@ import path from 'node:path'
29
29
  import { fileURLToPath } from 'node:url'
30
30
 
31
31
  import { collectPinViolations } from './dependency-policy-pins.mjs'
32
+ import { collectToolchainPolicyViolations } from './dependency-policy-toolchain.mjs'
32
33
  import { syncScaffoldVueDepsFromPolicy } from './sync-scaffold-vue-deps.mjs'
33
34
 
34
35
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
@@ -188,6 +189,18 @@ for (const [name, u] of usage) {
188
189
  }
189
190
 
190
191
  const policy = {
192
+ toolchain:
193
+ existingPolicy.toolchain ??
194
+ (() => {
195
+ try {
196
+ const fgDevkitPkg = JSON.parse(fs.readFileSync(fgDevkitPackageJsonPath, 'utf8'))
197
+ return fgDevkitPkg.packageManager
198
+ ? { packageManager: fgDevkitPkg.packageManager }
199
+ : undefined
200
+ } catch {
201
+ return undefined
202
+ }
203
+ })(),
191
204
  managedDependencies: {
192
205
  dependencies: sortRecord(outDeps),
193
206
  devDependencies: sortRecord(outDev),
@@ -201,6 +214,7 @@ const policyErrors = [
201
214
  collectPinViolations(`managedDependencies.${section}.`, deps),
202
215
  ),
203
216
  ...collectPinViolations('managedAnywhere.', policy.managedAnywhere),
217
+ ...collectToolchainPolicyViolations(policy),
204
218
  ]
205
219
 
206
220
  if (policyErrors.length > 0) {
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared helpers for release-notes scripts.
4
+ */
5
+
6
+ import { spawnSync } from 'node:child_process'
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { createRequire } from 'node:module'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
13
+ const require = createRequire(import.meta.url)
14
+
15
+ export const fgRoot = path.resolve(scriptDir, '..')
16
+
17
+ /** True when the consumer repo has opted into git-cliff release notes. */
18
+ export function hasReleaseNotesConfig(cwd = process.cwd()) {
19
+ return (
20
+ fs.existsSync(path.join(cwd, 'cliff.toml')) ||
21
+ fs.existsSync(path.join(cwd, 'cliff.user.toml')) ||
22
+ fs.existsSync(path.join(cwd, 'CHANGELOG.md')) ||
23
+ fs.existsSync(path.join(cwd, 'CHANGELOG.user.md'))
24
+ )
25
+ }
26
+
27
+ export function resolveCliffConfig(name, cwd = process.cwd()) {
28
+ const local = path.join(cwd, name)
29
+ if (fs.existsSync(local)) {
30
+ return local
31
+ }
32
+
33
+ const bundled = path.join(fgRoot, 'configs', name)
34
+ if (fs.existsSync(bundled)) {
35
+ return bundled
36
+ }
37
+
38
+ return null
39
+ }
40
+
41
+ export function resolveGitCliffBin() {
42
+ const candidates = [
43
+ path.join(fgRoot, 'node_modules', 'git-cliff', 'lib', 'cli', 'cli.js'),
44
+ path.join(process.cwd(), 'node_modules', 'git-cliff', 'lib', 'cli', 'cli.js'),
45
+ path.join(process.cwd(), 'node_modules', 'fg-devkit', 'node_modules', 'git-cliff', 'lib', 'cli', 'cli.js'),
46
+ ]
47
+
48
+ for (const candidate of candidates) {
49
+ if (fs.existsSync(candidate)) {
50
+ return candidate
51
+ }
52
+ }
53
+
54
+ return null
55
+ }
56
+
57
+ export function resolveTsxBin() {
58
+ const candidates = [
59
+ path.join(fgRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs'),
60
+ path.join(fgRoot, 'node_modules', 'tsx', 'cli.mjs'),
61
+ path.join(process.cwd(), 'node_modules', 'tsx', 'dist', 'cli.mjs'),
62
+ path.join(process.cwd(), 'node_modules', 'fg-devkit', 'node_modules', 'tsx', 'dist', 'cli.mjs'),
63
+ ]
64
+
65
+ for (const candidate of candidates) {
66
+ if (fs.existsSync(candidate)) {
67
+ return candidate
68
+ }
69
+ }
70
+
71
+ try {
72
+ return require.resolve('tsx/cli.mjs', { paths: [fgRoot, process.cwd()] })
73
+ } catch {
74
+ return null
75
+ }
76
+ }
77
+
78
+ export function runSyncUserChangelog(cwd = process.cwd()) {
79
+ const tsxBin = resolveTsxBin()
80
+ const syncScript = path.join(fgRoot, 'release-notes', 'sync-user-changelog.ts')
81
+
82
+ if (!tsxBin) {
83
+ console.error('[fg-devkit] Could not resolve tsx. Reinstall fg-devkit and retry.')
84
+ return 1
85
+ }
86
+
87
+ const r = spawnSync(process.execPath, [tsxBin, syncScript], {
88
+ stdio: 'inherit',
89
+ cwd,
90
+ env: { ...process.env, FG_DEVKIT_CWD: cwd },
91
+ })
92
+
93
+ if (r.error) {
94
+ console.error('[fg-devkit]', r.error.message)
95
+ return 1
96
+ }
97
+
98
+ return r.status ?? 1
99
+ }
100
+
101
+ export function readPreviewFiles(cwd = process.cwd()) {
102
+ const files = ['CHANGELOG.user.draft.md', 'CHANGELOG.user.md', 'CHANGELOG.md']
103
+ const previews = []
104
+
105
+ for (const file of files) {
106
+ const filePath = path.join(cwd, file)
107
+ if (!fs.existsSync(filePath)) {
108
+ continue
109
+ }
110
+ previews.push({ file, content: fs.readFileSync(filePath, 'utf8') })
111
+ }
112
+
113
+ return previews
114
+ }