fg-devkit 1.0.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.
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fg-devkit CLI: dependency policy, editor/tooling config sync, and VS Code settings.
4
+ *
5
+ * Non-interactive (for scripts and automation):
6
+ * pnpm exec fg-devkit check-deps [policy-file]
7
+ * pnpm exec fg-devkit sync-deps [policy-file]
8
+ * pnpm exec fg-devkit sync-project-configs (VS Code + Prettier + ESLint stub + tsconfig app template)
9
+ * pnpm exec fg-devkit sync-tooling-configs (Prettier + ESLint stub + tsconfig app template only)
10
+ * pnpm exec fg-devkit sync-vscode-settings
11
+ * pnpm exec fg-devkit create-release [patch|minor|major]
12
+ * pnpm exec fg-devkit commit [cz-args...] (Commitizen; uses package.json config.commitizen.path)
13
+ * pnpm exec fg-devkit commit-vuln (staged package.json version bumps → vuln() commit)
14
+ * Interactive menu (no subcommand):
15
+ * pnpm exec fg-devkit
16
+ *
17
+ * Maintainer-only policy regeneration is not exposed here; run
18
+ * node node_modules/fg-devkit/scripts/refresh-dependency-policy-from-repos.mjs from a checkout when needed.
19
+ */
20
+
21
+ import { spawnSync } from 'node:child_process'
22
+ import fs from 'node:fs'
23
+ import readline from 'node:readline/promises'
24
+ import path from 'node:path'
25
+ import { fileURLToPath } from 'node:url'
26
+ import { stdin as input, stdout as output } from 'node:process'
27
+
28
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
29
+
30
+ /** Run another script in this package via Node; inherits stdio and returns the child exit code. */
31
+ function runNode(scriptRelative, args = [], options = {}) {
32
+ const scriptPath = path.join(scriptDir, scriptRelative)
33
+ if (!fs.existsSync(scriptPath)) {
34
+ console.error(`[fg-devkit] Missing script: ${scriptPath}`)
35
+ return 1
36
+ }
37
+ const r = spawnSync(process.execPath, [scriptPath, ...args], {
38
+ stdio: 'inherit',
39
+ cwd: options.cwd ?? process.cwd(),
40
+ env: process.env,
41
+ })
42
+ if (r.error) {
43
+ console.error('[fg-devkit]', r.error.message)
44
+ return 1
45
+ }
46
+ return r.status ?? 1
47
+ }
48
+
49
+ /**
50
+ * Run release.sh (pnpm version bump, git push, gh release). Uses consumer cwd.
51
+ * Optional first arg: patch | minor | major (otherwise the script prompts interactively).
52
+ */
53
+ function runReleaseScript(args = []) {
54
+ const shPath = path.join(scriptDir, 'release.sh')
55
+ if (!fs.existsSync(shPath)) {
56
+ console.error(`[fg-devkit] Missing release.sh: ${shPath}`)
57
+ return 1
58
+ }
59
+ const r = spawnSync('bash', [shPath, ...args], {
60
+ stdio: 'inherit',
61
+ cwd: process.cwd(),
62
+ env: process.env,
63
+ })
64
+ if (r.error) {
65
+ console.error('[fg-devkit]', r.error.message)
66
+ return 1
67
+ }
68
+ return r.status ?? 1
69
+ }
70
+
71
+ /**
72
+ * Run Commitizen in the consumer repo (reads config.commitizen.path from package.json).
73
+ * Tries pnpm exec cz, then pnpm exec git-cz, then npx commitizen git-cz.
74
+ */
75
+ function runCommitizen(args = []) {
76
+ const cwd = process.cwd()
77
+ const attempts = [
78
+ () => spawnSync('pnpm', ['exec', 'cz', ...args], { stdio: 'inherit', cwd, env: process.env }),
79
+ () => spawnSync('pnpm', ['exec', 'git-cz', ...args], { stdio: 'inherit', cwd, env: process.env }),
80
+ () =>
81
+ spawnSync('npx', ['--yes', 'commitizen', 'git-cz', ...args], {
82
+ stdio: 'inherit',
83
+ cwd,
84
+ env: process.env,
85
+ }),
86
+ ]
87
+ for (const run of attempts) {
88
+ const r = run()
89
+ if (r.error) {
90
+ if (r.error.code === 'ENOENT') {
91
+ continue
92
+ }
93
+ console.error('[fg-devkit]', r.error.message)
94
+ return 1
95
+ }
96
+ return r.status ?? 1
97
+ }
98
+ console.error(
99
+ '[fg-devkit] Commitizen not found. Add devDependency "commitizen", set config.commitizen.path, then retry.',
100
+ )
101
+ return 1
102
+ }
103
+
104
+ /** Map subcommand name → script file (extra argv after subcommand is forwarded). */
105
+ const DIRECT_COMMANDS = {
106
+ 'check-deps': 'check-dependency-policy.mjs',
107
+ 'sync-deps': 'sync-dependency-policy.mjs',
108
+ 'sync-project-configs': 'sync-project-configs.mjs',
109
+ 'sync-tooling-configs': 'sync-tooling-configs.mjs',
110
+ 'sync-vscode-settings': 'sync-vscode-settings.mjs',
111
+ }
112
+
113
+ function printHelp() {
114
+ console.log(`Usage:
115
+ pnpm exec fg-devkit [command] [args...]
116
+
117
+ Commands:
118
+ check-deps [policy-file] Validate package.json + pins against policy
119
+ sync-deps [policy-file] Align declared versions to policy (existing keys only)
120
+ sync-project-configs VS Code settings + Prettier + ESLint + tsconfig (app template)
121
+ sync-tooling-configs Prettier + ESLint + tsconfig (app template; no VS Code)
122
+ sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
123
+ create-release [bump] Version bump, git push, GitHub release (patch|minor|major; omit = prompt)
124
+ (alias: createRelease)
125
+ commit [args...] Commitizen (git-cz); uses config.commitizen.path in package.json
126
+ commit-vuln Commit staged dependency bumps as vuln() (alias: vuln-commit)
127
+
128
+ With no command, opens an interactive menu.
129
+
130
+ Options:
131
+ -h, --help Show this help`)
132
+ }
133
+
134
+ /** Numbered actions shown in the interactive menu (keys must match user input). */
135
+ const MENU = [
136
+ {
137
+ key: '1',
138
+ label: 'Check dependency policy against package.json (check-deps)',
139
+ run: () => runNode('check-dependency-policy.mjs'),
140
+ },
141
+ {
142
+ key: '2',
143
+ label: 'Sync dependency versions from policy (sync-deps)',
144
+ run: () => runNode('sync-dependency-policy.mjs'),
145
+ },
146
+ {
147
+ key: '3',
148
+ label: 'Sync project configs (VS Code + Prettier + ESLint + tsconfig app template)',
149
+ run: () => runNode('sync-project-configs.mjs'),
150
+ },
151
+ {
152
+ key: '4',
153
+ label: 'Create release (version bump, push, gh release — run from package repo root)',
154
+ run: () => runReleaseScript(),
155
+ },
156
+ {
157
+ key: '5',
158
+ label: 'Commit (Commitizen — interactive conventional commit)',
159
+ run: () => runCommitizen(),
160
+ },
161
+ {
162
+ key: '6',
163
+ label: 'Commit vulnerability bumps (staged package.json → vuln() message)',
164
+ run: () => runNode('vuln-commit.mjs'),
165
+ },
166
+ ]
167
+
168
+ async function runInteractiveMenu() {
169
+ const rl = readline.createInterface({ input, output })
170
+
171
+ console.log(`\nfg-devkit — choose a task (cwd: ${process.cwd()})\n`)
172
+
173
+ for (const item of MENU) {
174
+ console.log(` ${item.key}) ${item.label}`)
175
+ }
176
+ console.log(' 0) Exit\n')
177
+
178
+ const choice = (await rl.question('Number: ')).trim()
179
+
180
+ if (choice === '0' || choice === '') {
181
+ rl.close()
182
+ return 0
183
+ }
184
+
185
+ const item = MENU.find((m) => m.key === choice)
186
+ if (!item) {
187
+ console.error('[fg-devkit] Unknown option. Run again and choose a number from the list.')
188
+ rl.close()
189
+ return 1
190
+ }
191
+
192
+ let exitCode = 0
193
+ try {
194
+ const out = item.run()
195
+ if (typeof out === 'object' && out !== null && typeof out.then === 'function') {
196
+ exitCode = await out
197
+ } else if (typeof out === 'number') {
198
+ exitCode = out
199
+ }
200
+ } finally {
201
+ rl.close()
202
+ }
203
+ return exitCode
204
+ }
205
+
206
+ async function main() {
207
+ const [, , sub, ...rest] = process.argv
208
+
209
+ if (sub === '--help' || sub === '-h') {
210
+ printHelp()
211
+ return
212
+ }
213
+
214
+ if (sub) {
215
+ if (sub === 'create-release' || sub === 'createRelease') {
216
+ process.exitCode = runReleaseScript(rest)
217
+ return
218
+ }
219
+ if (sub === 'commit') {
220
+ process.exitCode = runCommitizen(rest)
221
+ return
222
+ }
223
+ if (sub === 'commit-vuln' || sub === 'vuln-commit' || sub === 'commit:vuln') {
224
+ process.exitCode = runNode('vuln-commit.mjs', rest)
225
+ return
226
+ }
227
+ const script = DIRECT_COMMANDS[sub]
228
+ if (!script) {
229
+ console.error(`[fg-devkit] Unknown command: ${sub}`)
230
+ console.error('[fg-devkit] Run with --help for usage.')
231
+ process.exitCode = 1
232
+ return
233
+ }
234
+ process.exitCode = runNode(script, rest)
235
+ return
236
+ }
237
+
238
+ const code = await runInteractiveMenu()
239
+ if (code) {
240
+ process.exitCode = code
241
+ }
242
+ }
243
+
244
+ main().catch((err) => {
245
+ console.error('[fg-devkit]', err)
246
+ process.exitCode = 1
247
+ })
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rebuilds dependency-policy.vue-workspace.json from the union of all npm packages
4
+ * declared in Vue projects under a given projects root (not limited to keys that
5
+ * already exist in the policy).
6
+ *
7
+ * Classification:
8
+ * - Package appears only as dependencies everywhere → managedDependencies.dependencies
9
+ * - Package appears only as devDependencies → managedDependencies.devDependencies
10
+ * - Package appears as both (across or within repos) → managedAnywhere
11
+ *
12
+ * Version per package: highest semver among all parseable version strings (ignores
13
+ * file:, workspace:, link:). Packages with no parseable semver are skipped (e.g. only file:).
14
+ *
15
+ * Rules:
16
+ * - A package.json is considered a Vue project only if it lists "vue" in dependencies or devDependencies.
17
+ *
18
+ * Runs entirely on the local filesystem (no registry or network).
19
+ *
20
+ * Usage:
21
+ * node scripts/refresh-dependency-policy-from-repos.mjs [projectsRoot] [policyFile]
22
+ */
23
+
24
+ import fs from 'node:fs'
25
+ import path from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ import { collectPinViolations } from './dependency-policy-pins.mjs'
29
+
30
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
31
+ const fgDevkitRoot = path.resolve(scriptDir, '..')
32
+
33
+ const projectsRoot = process.argv[2]
34
+ ? path.resolve(process.cwd(), process.argv[2])
35
+ : path.resolve(scriptDir, '../..')
36
+
37
+ const policyPath = process.argv[3]
38
+ ? path.resolve(process.cwd(), process.argv[3])
39
+ : path.join(fgDevkitRoot, 'configs/dependency-policy.vue-workspace.json')
40
+
41
+ /** Relative paths under projectsRoot whose package.json files are ignored (maintainer choice). */
42
+ /** Relative path prefixes under projectsRoot to skip when scanning (add repo names as needed). */
43
+ const SKIP_DIR_PREFIXES = []
44
+
45
+ /** Recursively collect every package.json path under dir, skipping node_modules and .git. */
46
+ function walk(dir, acc = []) {
47
+ for (const name of fs.readdirSync(dir, { withFileTypes: true })) {
48
+ if (name.name === 'node_modules' || name.name === '.git') continue
49
+ const p = path.join(dir, name.name)
50
+ if (name.isDirectory()) walk(p, acc)
51
+ else if (name.name === 'package.json') acc.push(p)
52
+ }
53
+ return acc
54
+ }
55
+
56
+ function shouldSkipPackageJson(relDir) {
57
+ for (const prefix of SKIP_DIR_PREFIXES) {
58
+ if (relDir === prefix || relDir.startsWith(`${prefix}${path.sep}`)) return true
59
+ }
60
+ return false
61
+ }
62
+
63
+ function isVueProject(j) {
64
+ const d = { ...j.dependencies, ...j.devDependencies }
65
+ return Boolean(d.vue)
66
+ }
67
+
68
+ /**
69
+ * Parse a version range string into [major, minor, patch] for comparison, or null if not usable.
70
+ * Strips common range prefixes; returns null for file/workspace/link/local paths.
71
+ */
72
+ function coerceParts(spec) {
73
+ if (typeof spec !== 'string') return null
74
+ const s = spec.trim()
75
+ if (/^(file:|workspace:|link:|\.\/|\.\.\/)/.test(s)) return null
76
+ let t = s.replace(/^>=|^<=|^>|^</, '').trim()
77
+ t = t.replace(/^[\^~]/, '')
78
+ const base = t.split(/[-+]/)[0]
79
+ let m = base.match(/^(\d+)\.(\d+)\.(\d+)/)
80
+ if (m) return [+m[1], +m[2], +m[3]]
81
+ m = base.match(/^(\d+)\.(\d+)$/)
82
+ if (m) return [+m[1], +m[2], 0]
83
+ m = base.match(/^(\d+)$/)
84
+ if (m) return [+m[0], 0, 0]
85
+ return null
86
+ }
87
+
88
+ /** Lexicographic semver compare on numeric [major, minor, patch] tuples. */
89
+ function compareParts(a, b) {
90
+ for (let i = 0; i < 3; i++) {
91
+ if (a[i] !== b[i]) return a[i] - b[i]
92
+ }
93
+ return 0
94
+ }
95
+
96
+ /** Stable sort keys alphabetically for readable JSON diffs. */
97
+ function sortRecord(obj) {
98
+ return Object.keys(obj)
99
+ .sort((a, b) => a.localeCompare(b))
100
+ .reduce((o, k) => {
101
+ o[k] = obj[k]
102
+ return o
103
+ }, {})
104
+ }
105
+
106
+ const pkgPaths = walk(projectsRoot).filter((pkgPath) => {
107
+ const rel = path.relative(projectsRoot, path.dirname(pkgPath))
108
+ return !shouldSkipPackageJson(rel || '.')
109
+ })
110
+
111
+ /**
112
+ * Per package name: whether it appears in dependencies and/or devDependencies, and all parseable semver tuples.
113
+ * @type {Map<string, { hasDep: boolean, hasDev: boolean, partsList: number[][] }>}
114
+ */
115
+ const usage = new Map()
116
+
117
+ // Aggregate usage across all Vue package.json files under projectsRoot.
118
+ for (const pkgPath of pkgPaths) {
119
+ let j
120
+ try {
121
+ j = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
122
+ } catch {
123
+ continue
124
+ }
125
+ if (!isVueProject(j)) continue
126
+
127
+ const deps = j.dependencies ?? {}
128
+ const devs = j.devDependencies ?? {}
129
+ const names = new Set([...Object.keys(deps), ...Object.keys(devs)])
130
+
131
+ for (const name of names) {
132
+ if (!usage.has(name)) {
133
+ usage.set(name, { hasDep: false, hasDev: false, partsList: [] })
134
+ }
135
+ const u = usage.get(name)
136
+ if (deps[name] !== undefined) u.hasDep = true
137
+ if (devs[name] !== undefined) u.hasDev = true
138
+ const v = deps[name] ?? devs[name]
139
+ const parts = coerceParts(v)
140
+ if (parts) u.partsList.push(parts)
141
+ }
142
+ }
143
+
144
+ const outDeps = {}
145
+ const outDev = {}
146
+ const outAny = {}
147
+
148
+ // Pick highest semver per package and assign to dependencies, devDependencies, or managedAnywhere.
149
+ for (const [name, u] of usage) {
150
+ if (u.partsList.length === 0) {
151
+ continue
152
+ }
153
+ let best = u.partsList[0]
154
+ for (const p of u.partsList) {
155
+ if (compareParts(p, best) > 0) best = p
156
+ }
157
+ const pinned = `${best[0]}.${best[1]}.${best[2]}`
158
+ const mixed = u.hasDep && u.hasDev
159
+ if (mixed) outAny[name] = pinned
160
+ else if (u.hasDep) outDeps[name] = pinned
161
+ else outDev[name] = pinned
162
+ }
163
+
164
+ const policy = {
165
+ managedDependencies: {
166
+ dependencies: sortRecord(outDeps),
167
+ devDependencies: sortRecord(outDev),
168
+ },
169
+ managedAnywhere: sortRecord(outAny),
170
+ }
171
+
172
+ const policyErrors = [
173
+ ...Object.entries(policy.managedDependencies).flatMap(([section, deps]) =>
174
+ collectPinViolations(`managedDependencies.${section}.`, deps),
175
+ ),
176
+ ...collectPinViolations('managedAnywhere.', policy.managedAnywhere),
177
+ ]
178
+
179
+ if (policyErrors.length > 0) {
180
+ console.error('[refresh-policy] Generated policy has invalid pins:')
181
+ for (const e of policyErrors) console.error(`- ${e}`)
182
+ process.exit(1)
183
+ }
184
+
185
+ fs.writeFileSync(policyPath, `${JSON.stringify(policy, null, 2)}\n`)
186
+
187
+ console.log(`[refresh-policy] projectsRoot=${projectsRoot}`)
188
+ console.log(`[refresh-policy] policy=${policyPath}`)
189
+ console.log(
190
+ `[refresh-policy] wrote union: ${Object.keys(outDeps).length} dependencies, ${Object.keys(outDev).length} devDependencies, ${Object.keys(outAny).length} managedAnywhere (${usage.size} package names seen)`,
191
+ )
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Interactive or scripted release for this package: bumps version with pnpm, pushes commits/tags,
4
+ # and creates a GitHub Release with generated notes (requires gh CLI, authenticated).
5
+ #
6
+ # Usage: ./scripts/release.sh [major|minor|patch]
7
+ # With no argument, prompts for patch/minor/major.
8
+
9
+ set -euo pipefail
10
+
11
+ bump_type="${1:-}"
12
+
13
+ if [[ -z "$bump_type" ]]; then
14
+ echo "Select release type:"
15
+ echo " 1) patch"
16
+ echo " 2) minor"
17
+ echo " 3) major"
18
+ read -r -p "Enter choice [1-3] (default: 1): " release_choice
19
+ release_choice="${release_choice:-1}"
20
+ case "$release_choice" in
21
+ 1) bump_type="patch" ;;
22
+ 2) bump_type="minor" ;;
23
+ 3) bump_type="major" ;;
24
+ *)
25
+ echo "Invalid choice: $release_choice"
26
+ exit 1
27
+ ;;
28
+ esac
29
+ fi
30
+
31
+ if [[ "$bump_type" != "major" && "$bump_type" != "minor" && "$bump_type" != "patch" ]]; then
32
+ echo "Usage: ./scripts/release.sh [major|minor|patch]"
33
+ exit 1
34
+ fi
35
+
36
+ if ! command -v gh >/dev/null 2>&1; then
37
+ echo "GitHub CLI (gh) is required but not installed."
38
+ exit 1
39
+ fi
40
+
41
+ if ! gh auth status >/dev/null 2>&1; then
42
+ echo "GitHub CLI is not authenticated. Run: gh auth login"
43
+ exit 1
44
+ fi
45
+
46
+ if [[ -n "$(git status --porcelain)" ]]; then
47
+ echo "Git working tree is not clean. Commit or stash changes first."
48
+ exit 1
49
+ fi
50
+
51
+ previous_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)"
52
+
53
+ pnpm version "$bump_type"
54
+
55
+ new_version="$(node -p "require('./package.json').version")"
56
+ new_tag="v${new_version}"
57
+
58
+ git push
59
+ git push --tags
60
+
61
+ if [[ -n "$previous_tag" ]]; then
62
+ gh release create "$new_tag" \
63
+ --latest \
64
+ --generate-notes \
65
+ --notes-start-tag "$previous_tag"
66
+ else
67
+ gh release create "$new_tag" \
68
+ --latest \
69
+ --generate-notes
70
+ fi
71
+
72
+ echo "Release created: ${new_tag}"
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolves which dependency policy JSON file check/sync scripts should load.
3
+ *
4
+ * Used by check-dependency-policy.mjs and sync-dependency-policy.mjs so both tools share one rule:
5
+ * optional argv[2] override, otherwise the packaged default next to this package.
6
+ */
7
+
8
+ import fs from 'node:fs'
9
+ import path from 'node:path'
10
+
11
+ const DEFAULT_POLICY = 'dependency-policy.vue-workspace.json'
12
+
13
+ /**
14
+ * First CLI argument selects the policy file (optional).
15
+ * - Omitted: fg-devkit/configs/dependency-policy.vue-workspace.json
16
+ * - Absolute path: used as-is
17
+ * - Relative path: if it exists under cwd, use it; else fg-devkit/configs/<arg>
18
+ */
19
+ export function resolveDependencyPolicyPath(scriptDir, cwd, argv) {
20
+ const arg = argv[2]
21
+ if (!arg) {
22
+ return path.resolve(scriptDir, '../configs', DEFAULT_POLICY)
23
+ }
24
+ if (path.isAbsolute(arg)) {
25
+ return arg
26
+ }
27
+ const fromCwd = path.resolve(cwd, arg)
28
+ if (fs.existsSync(fromCwd)) {
29
+ return fromCwd
30
+ }
31
+ return path.resolve(scriptDir, '../configs', arg)
32
+ }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rewrites version fields in the current package.json so they match the shared dependency policy.
4
+ *
5
+ * Only updates keys that already exist in package.json (does not add new dependencies from the policy).
6
+ * Aligns managedDependencies, managedAnywhere (only where the package is already declared), optional
7
+ * policy overrides into pnpm.overrides, and logs each change.
8
+ *
9
+ * Refuses to run if the policy file contains any non-pinned versions.
10
+ *
11
+ * Policy path: first CLI argument, or default under fg-devkit/configs/.
12
+ */
13
+
14
+ import fs from 'node:fs'
15
+ import path from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ import { collectPinViolations } from './dependency-policy-pins.mjs'
19
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
20
+
21
+ const cwd = process.cwd()
22
+ const pkgPath = path.resolve(cwd, 'package.json')
23
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
24
+ const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
25
+
26
+ if (!fs.existsSync(pkgPath)) {
27
+ console.error('[sync-deps] Could not find package.json in current working directory')
28
+ process.exit(1)
29
+ }
30
+
31
+ if (!fs.existsSync(policyPath)) {
32
+ console.error(`[sync-deps] Could not find dependency policy: ${policyPath}`)
33
+ console.error(
34
+ '[sync-deps] Usage: node …/sync-dependency-policy.mjs [policy-file]\n' +
35
+ ' policy-file: absolute path, path relative to cwd, or filename under fg-devkit/configs/ (default: dependency-policy.vue-workspace.json)',
36
+ )
37
+ process.exit(1)
38
+ }
39
+
40
+ const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
41
+ const policyErrors = [
42
+ ...Object.entries(policy.managedDependencies ?? {}).flatMap(([section, deps]) =>
43
+ collectPinViolations(`managedDependencies.${section}.`, deps),
44
+ ),
45
+ ...collectPinViolations('managedAnywhere.', policy.managedAnywhere ?? {}),
46
+ ...collectPinViolations('overrides.', policy.overrides ?? {}),
47
+ ]
48
+
49
+ if (policyErrors.length > 0) {
50
+ console.error('[sync-deps] Policy file contains non-pinned versions:')
51
+ for (const err of policyErrors) {
52
+ console.error(`- ${err}`)
53
+ }
54
+ process.exit(1)
55
+ }
56
+
57
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
58
+ const changes = []
59
+
60
+ // Bump versions for managed dependencies / devDependencies where the key already exists.
61
+ for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
62
+ pkg[section] ||= {}
63
+
64
+ for (const [name, version] of Object.entries(dependencies)) {
65
+ if (pkg[section][name] === undefined) {
66
+ continue
67
+ }
68
+ if (pkg[section][name] !== version) {
69
+ changes.push(`${section}.${name}: ${pkg[section][name]} -> ${version}`)
70
+ pkg[section][name] = version
71
+ }
72
+ }
73
+ }
74
+
75
+ // Same for packages that may live in either dependencies or devDependencies.
76
+ for (const [name, version] of Object.entries(policy.managedAnywhere ?? {})) {
77
+ pkg.dependencies ||= {}
78
+ pkg.devDependencies ||= {}
79
+ const inDep = pkg.dependencies[name] !== undefined
80
+ const inDev = pkg.devDependencies[name] !== undefined
81
+ if (inDep && inDev) {
82
+ if (pkg.dependencies[name] !== version) {
83
+ changes.push(`dependencies.${name}: ${pkg.dependencies[name]} -> ${version}`)
84
+ pkg.dependencies[name] = version
85
+ }
86
+ if (pkg.devDependencies[name] !== version) {
87
+ changes.push(`devDependencies.${name}: ${pkg.devDependencies[name]} -> ${version}`)
88
+ pkg.devDependencies[name] = version
89
+ }
90
+ } else if (inDep) {
91
+ if (pkg.dependencies[name] !== version) {
92
+ changes.push(`dependencies.${name}: ${pkg.dependencies[name] ?? '(missing)'} -> ${version}`)
93
+ pkg.dependencies[name] = version
94
+ }
95
+ } else if (inDev) {
96
+ if (pkg.devDependencies[name] !== version) {
97
+ changes.push(`devDependencies.${name}: ${pkg.devDependencies[name] ?? '(missing)'} -> ${version}`)
98
+ pkg.devDependencies[name] = version
99
+ }
100
+ }
101
+ }
102
+
103
+ pkg.pnpm ||= {}
104
+ pkg.pnpm.overrides ||= {}
105
+
106
+ // Apply policy-defined override versions when present in the policy.
107
+ for (const [name, version] of Object.entries(policy.overrides ?? {})) {
108
+ if (pkg.pnpm.overrides[name] !== version) {
109
+ changes.push(`pnpm.overrides.${name}: ${pkg.pnpm.overrides[name] ?? '(missing)'} -> ${version}`)
110
+ pkg.pnpm.overrides[name] = version
111
+ }
112
+ }
113
+
114
+ if (changes.length === 0) {
115
+ console.log('[sync-deps] package.json already matches dependency policy')
116
+ process.exit(0)
117
+ }
118
+
119
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
120
+ console.log('[sync-deps] Updated package.json to match dependency policy')
121
+ for (const change of changes) {
122
+ console.log(`- ${change}`)
123
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * For library packages: copies versions from devDependencies/dependencies into peerDependencies
4
+ * for every key already listed in peerDependencies, so declared peers stay aligned with local installs.
5
+ *
6
+ * Does not add new peer keys; warns and skips peers that have no matching dev/dependency entry.
7
+ */
8
+
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+
12
+ const pkgPath = path.resolve(process.cwd(), 'package.json')
13
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
14
+
15
+ const sources = { ...pkg.devDependencies, ...pkg.dependencies }
16
+
17
+ pkg.peerDependencies = pkg.peerDependencies || {}
18
+ const peerKeys = Object.keys(pkg.peerDependencies)
19
+
20
+ for (const name of peerKeys) {
21
+ const version = sources[name]
22
+ if (version) {
23
+ pkg.peerDependencies[name] = version
24
+ } else {
25
+ // If missing locally, leave existing peer version untouched
26
+ console.warn(`[sync-peers] Skipping ${name} (not found in devDependencies/dependencies)`)
27
+ }
28
+ }
29
+
30
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
31
+ console.warn('[sync-peers] peerDependencies updated from devDependencies')