fg-devkit 1.22.0 → 1.24.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.
Files changed (69) hide show
  1. package/README.md +30 -7
  2. package/configs/dependency-policy.vue-workspace.json +28 -28
  3. package/package.json +12 -85
  4. package/scaffold/vue/package.json +13 -13
  5. package/scripts/audit-override-commit.mjs +167 -24
  6. package/scripts/bump-node-toolchain.mjs +8 -31
  7. package/scripts/bump-pnpm-toolchain.mjs +8 -29
  8. package/scripts/check-dependency-policy.mjs +10 -35
  9. package/scripts/fg-devkit.mjs +290 -337
  10. package/scripts/fleet-bump-fg-devkit.mjs +4 -5
  11. package/scripts/fleet-git-commit-push-scoped.mjs +6 -26
  12. package/scripts/fleet-git-discard-scoped.mjs +5 -25
  13. package/scripts/fleet-git-pull-scoped.mjs +5 -25
  14. package/scripts/fleet-vulns.mjs +4 -9
  15. package/scripts/generate-release-notes.mjs +1 -1
  16. package/scripts/{audit-fix-plan.mjs → lib/audit-fix-plan.mjs} +77 -4
  17. package/scripts/{audit-fix-plan.test.mjs → lib/audit-fix-plan.test.mjs} +50 -2
  18. package/scripts/lib/common.mjs +95 -0
  19. package/scripts/{dependency-policy-toolchain.mjs → lib/dependency-policy-toolchain.mjs} +3 -35
  20. package/scripts/{git-commit-push-fleet-common.mjs → lib/git-commit-push-fleet-common.mjs} +1 -1
  21. package/scripts/{git-discard-fleet-common.mjs → lib/git-discard-fleet-common.mjs} +1 -1
  22. package/scripts/{git-pull-fleet-common.mjs → lib/git-pull-fleet-common.mjs} +1 -1
  23. package/scripts/{override-changes.mjs → lib/override-changes.mjs} +20 -0
  24. package/scripts/{piwik-dimensions.test.mjs → lib/piwik-dimensions.test.mjs} +0 -5
  25. package/scripts/{release-notes-common.mjs → lib/release-notes-common.mjs} +5 -7
  26. package/scripts/{resolve-dependency-policy-path.mjs → lib/resolve-dependency-policy-path.mjs} +4 -3
  27. package/scripts/piwik.mjs +6 -6
  28. package/scripts/poeditor-download.mjs +1 -1
  29. package/scripts/poeditor-upload.mjs +1 -1
  30. package/scripts/policy-outdated.mjs +3 -8
  31. package/scripts/policy-upgrade-commit.mjs +5 -26
  32. package/scripts/policy-upgrade.mjs +6 -30
  33. package/scripts/policy-vulns.mjs +3 -8
  34. package/scripts/prune-overrides.mjs +5 -16
  35. package/scripts/prune-release-age-exclude.mjs +1 -1
  36. package/scripts/refresh-dependency-policy-from-repos.mjs +5 -7
  37. package/scripts/release.sh +2 -1
  38. package/scripts/scaffold-vue.mjs +1 -3
  39. package/scripts/setup-ai-rules.mjs +135 -0
  40. package/scripts/sync-consumer-lefthook.mjs +7 -24
  41. package/scripts/sync-dependency-check-suppressions-commit.mjs +7 -46
  42. package/scripts/sync-dependency-policy-commit.mjs +6 -38
  43. package/scripts/sync-dependency-policy.mjs +9 -35
  44. package/scripts/sync-node-toolchain.mjs +5 -8
  45. package/scripts/sync-pnpm-toolchain.mjs +5 -8
  46. package/scripts/sync-scaffold-vue-deps.mjs +6 -7
  47. package/scripts/sync-tooling-configs.mjs +8 -23
  48. package/scripts/sync-vite-plugin-vue-inspector.mjs +7 -39
  49. package/scripts/verify-release-notes.mjs +1 -1
  50. package/.cursor/rules/core-coding-standards.mdc +0 -291
  51. package/scaffold/vue/.gitignore +0 -13
  52. package/scripts/cz/.cz-config.cjs +0 -29
  53. package/scripts/fleet-git-commit-push.mjs +0 -75
  54. package/scripts/fleet-git-discard.mjs +0 -66
  55. package/scripts/fleet-git-pull.mjs +0 -65
  56. package/scripts/sync-core-coding-standards.mjs +0 -77
  57. package/scripts/sync-peers-from-dev.mjs +0 -31
  58. package/scripts/vuln-commit.mjs +0 -142
  59. /package/configs/{fallowrc.sync-stub.json → fallowrc.json} +0 -0
  60. /package/scripts/{dependency-policy-pins.mjs → lib/dependency-policy-pins.mjs} +0 -0
  61. /package/scripts/{fleet-project-repos.mjs → lib/fleet-project-repos.mjs} +0 -0
  62. /package/scripts/{fleet-repo-picker.mjs → lib/fleet-repo-picker.mjs} +0 -0
  63. /package/scripts/{fleet-vue-repos.mjs → lib/fleet-vue-repos.mjs} +0 -0
  64. /package/scripts/{git-fleet-common.mjs → lib/git-fleet-common.mjs} +0 -0
  65. /package/scripts/{node-toolchain-sync.mjs → lib/node-toolchain-sync.mjs} +0 -0
  66. /package/scripts/{piwik-common.mjs → lib/piwik-common.mjs} +0 -0
  67. /package/scripts/{piwik-dimensions.mjs → lib/piwik-dimensions.mjs} +0 -0
  68. /package/scripts/{pnpm-toolchain-sync.mjs → lib/pnpm-toolchain-sync.mjs} +0 -0
  69. /package/scripts/{poeditor-common.mjs → lib/poeditor-common.mjs} +0 -0
@@ -7,6 +7,7 @@
7
7
 
8
8
  import fs from 'node:fs'
9
9
  import path from 'node:path'
10
+ import { fgDevkitRoot } from './common.mjs'
10
11
 
11
12
  const DEFAULT_POLICY = 'dependency-policy.vue-workspace.json'
12
13
 
@@ -16,12 +17,12 @@ const DEFAULT_POLICY = 'dependency-policy.vue-workspace.json'
16
17
  * - Absolute path: used as-is
17
18
  * - Relative path: if it exists under cwd, use it; else fg-devkit/configs/<arg>
18
19
  */
19
- export function resolveDependencyPolicyPath(scriptDir, cwd, argv) {
20
+ export function resolveDependencyPolicyPath(cwd, argv) {
20
21
  // Optional policy path is argv[2] only when it is not an option flag
21
22
  // (e.g. `policy-upgrade --interactive` must use the default policy file).
22
23
  const arg = argv[2]
23
24
  if (!arg || arg.startsWith('-')) {
24
- return path.resolve(scriptDir, '../configs', DEFAULT_POLICY)
25
+ return path.join(fgDevkitRoot, 'configs', DEFAULT_POLICY)
25
26
  }
26
27
  if (path.isAbsolute(arg)) {
27
28
  return arg
@@ -30,5 +31,5 @@ export function resolveDependencyPolicyPath(scriptDir, cwd, argv) {
30
31
  if (fs.existsSync(fromCwd)) {
31
32
  return fromCwd
32
33
  }
33
- return path.resolve(scriptDir, '../configs', arg)
34
+ return path.join(fgDevkitRoot, 'configs', arg)
34
35
  }
package/scripts/piwik.mjs CHANGED
@@ -37,14 +37,14 @@ import {
37
37
  planLocalPull,
38
38
  renderTable,
39
39
  rewriteDimensionNumbers,
40
- } from './piwik-dimensions.mjs'
40
+ } from './lib/piwik-dimensions.mjs'
41
41
  import {
42
42
  assertApiConfig,
43
43
  createPiwikClient,
44
44
  loadLocalDimensions,
45
45
  resolveEnvironmentPair,
46
46
  resolvePiwikConfig,
47
- } from './piwik-common.mjs'
47
+ } from './lib/piwik-common.mjs'
48
48
 
49
49
  const COMMANDS = new Set(['list', 'diff', 'add', 'update', 'sync', 'pull', 'compare', 'migrate'])
50
50
 
@@ -83,7 +83,7 @@ slot is not the local slot.`)
83
83
  }
84
84
 
85
85
  /**
86
- * @param {import('./piwik-dimensions.mjs').LocalDimension[]} locals
86
+ * @param {import('./lib/piwik-dimensions.mjs').LocalDimension[]} locals
87
87
  */
88
88
  function printLocal(locals) {
89
89
  for (const scope of SCOPES) {
@@ -100,7 +100,7 @@ function printLocal(locals) {
100
100
  }
101
101
 
102
102
  /**
103
- * @param {import('./piwik-dimensions.mjs').RemoteDimension[]} dimensions
103
+ * @param {import('./lib/piwik-dimensions.mjs').RemoteDimension[]} dimensions
104
104
  * @param {Record<string, { used?: number, available?: number, left?: number }>} slots
105
105
  */
106
106
  function printRemote(dimensions, slots) {
@@ -149,7 +149,7 @@ async function confirmApply(message, config) {
149
149
  /**
150
150
  * @param {ReturnType<typeof createPiwikClient>} client
151
151
  * @param {ReturnType<typeof resolvePiwikConfig>} config
152
- * @param {import('./piwik-dimensions.mjs').LocalDimension[]} creates
152
+ * @param {import('./lib/piwik-dimensions.mjs').LocalDimension[]} creates
153
153
  */
154
154
  async function applyCreates(client, config, creates) {
155
155
  for (const local of creates) {
@@ -179,7 +179,7 @@ async function applyCreates(client, config, creates) {
179
179
  /**
180
180
  * @param {ReturnType<typeof createPiwikClient>} client
181
181
  * @param {string} websiteId
182
- * @param {{ local: import('./piwik-dimensions.mjs').LocalDimension, remote: import('./piwik-dimensions.mjs').RemoteDimension }[]} updates
182
+ * @param {{ local: import('./lib/piwik-dimensions.mjs').LocalDimension, remote: import('./lib/piwik-dimensions.mjs').RemoteDimension }[]} updates
183
183
  */
184
184
  async function applyUpdates(client, websiteId, updates) {
185
185
  for (const { local, remote } of updates) {
@@ -18,7 +18,7 @@ import {
18
18
  loadPoeditorConfig,
19
19
  parseFlags,
20
20
  resolvePathFromCwd,
21
- } from './poeditor-common.mjs'
21
+ } from './lib/poeditor-common.mjs'
22
22
 
23
23
  async function exportLanguage({ token, projectId, language, format }) {
24
24
  const params = new URLSearchParams({
@@ -13,7 +13,7 @@
13
13
  import fs from 'node:fs'
14
14
  import path from 'node:path'
15
15
 
16
- import { getFlagValue, getPoeditorToken, loadPoeditorConfig, parseFlags, resolvePathFromCwd } from './poeditor-common.mjs'
16
+ import { getFlagValue, getPoeditorToken, loadPoeditorConfig, parseFlags, resolvePathFromCwd } from './lib/poeditor-common.mjs'
17
17
 
18
18
  const MIN_UPLOAD_INTERVAL_MS = 20_000
19
19
  const MAX_UPLOAD_RETRIES = 3
@@ -11,15 +11,10 @@ import fs from 'node:fs'
11
11
  import os from 'node:os'
12
12
  import path from 'node:path'
13
13
  import { spawnSync } from 'node:child_process'
14
- import { fileURLToPath } from 'node:url'
15
- import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
14
+ import { resolveDependencyPolicyPath } from './lib/resolve-dependency-policy-path.mjs'
15
+ import { readJson } from './lib/common.mjs'
16
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
- }
17
+ const policyPath = resolveDependencyPolicyPath(process.cwd(), process.argv)
23
18
 
24
19
  const policy = readJson(policyPath)
25
20
  const deps = {
@@ -17,10 +17,12 @@ import {
17
17
  collectOverrideChanges,
18
18
  collectOverrides,
19
19
  formatOverrideValue,
20
- } from './override-changes.mjs'
20
+ } from './lib/override-changes.mjs'
21
+ import { readJson, runGit, stageExistingFiles } from './lib/common.mjs'
21
22
 
22
23
  const require = createRequire(import.meta.url)
23
24
  const cwd = process.cwd()
25
+ const GIT = { cwd, tag: '[policy-upgrade-commit]' }
24
26
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
25
27
  const packageJsonPath = path.join(cwd, 'package.json')
26
28
  const PRUNE_FLAG = '--prune-overrides'
@@ -59,19 +61,6 @@ function runNcu(args = []) {
59
61
  return result.status ?? 1
60
62
  }
61
63
 
62
- function runGit(args) {
63
- const result = spawnSync('git', args, {
64
- stdio: 'inherit',
65
- cwd,
66
- env: process.env,
67
- })
68
- if (result.error) {
69
- console.error('[policy-upgrade-commit]', result.error.message)
70
- return 1
71
- }
72
- return result.status ?? 1
73
- }
74
-
75
64
  function runPruneOverridesFix() {
76
65
  console.log('[policy-upgrade-commit] Pruning overrides that no longer affect pnpm audit…')
77
66
  const result = spawnSync(process.execPath, [path.join(scriptDir, 'prune-overrides.mjs'), '--fix'], {
@@ -86,16 +75,6 @@ function runPruneOverridesFix() {
86
75
  return result.status ?? 1
87
76
  }
88
77
 
89
- function stageExistingFiles(files) {
90
- const existingFiles = files.filter((filePath) => fs.existsSync(path.join(cwd, filePath)))
91
- if (existingFiles.length === 0) return 0
92
- return runGit(['add', '--', ...existingFiles])
93
- }
94
-
95
- function readJson(jsonPath) {
96
- return JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
97
- }
98
-
99
78
  function collectPackageVersionChanges(beforePackage, afterPackage) {
100
79
  const before = {
101
80
  ...(beforePackage.dependencies ?? {}),
@@ -144,7 +123,7 @@ function buildCommitMessage(changed) {
144
123
  }
145
124
 
146
125
  function commitChanges(prefix, subject, body) {
147
- const stageExitCode = stageExistingFiles(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'])
126
+ const stageExitCode = stageExistingFiles(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'], GIT)
148
127
  if (stageExitCode !== 0) {
149
128
  return stageExitCode
150
129
  }
@@ -153,7 +132,7 @@ function commitChanges(prefix, subject, body) {
153
132
  console.log(body)
154
133
  }
155
134
  const commitArgs = body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject]
156
- return runGit(commitArgs)
135
+ return runGit(commitArgs, GIT).status
157
136
  }
158
137
 
159
138
  function pruneAndCommitOverrides() {
@@ -15,14 +15,13 @@ import fs from 'node:fs'
15
15
  import os from 'node:os'
16
16
  import path from 'node:path'
17
17
  import { spawnSync } from 'node:child_process'
18
- import { fileURLToPath } from 'node:url'
19
18
  import { createRequire } from 'node:module'
20
- import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
19
+ import { resolveDependencyPolicyPath } from './lib/resolve-dependency-policy-path.mjs'
21
20
  import { syncScaffoldVueDepsFromPolicy } from './sync-scaffold-vue-deps.mjs'
21
+ import { fgDevkitRoot, readJson, runGit, stageExistingFiles } from './lib/common.mjs'
22
22
 
23
23
  const require = createRequire(import.meta.url)
24
- const scriptDir = path.dirname(fileURLToPath(import.meta.url))
25
- const fgDevkitRoot = path.resolve(scriptDir, '..')
24
+ const GIT = { cwd: fgDevkitRoot, tag: '[policy-upgrade]' }
26
25
 
27
26
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
28
27
  console.log(`Usage:
@@ -36,13 +35,9 @@ Examples:
36
35
 
37
36
  const argPolicy = process.argv[2]
38
37
  const hasPolicyArg = Boolean(argPolicy) && !argPolicy.startsWith('-')
39
- const policyPath = resolveDependencyPolicyPath(scriptDir, process.cwd(), process.argv)
38
+ const policyPath = resolveDependencyPolicyPath(process.cwd(), process.argv)
40
39
  const ncuArgs = hasPolicyArg ? process.argv.slice(3) : process.argv.slice(2)
41
40
 
42
- function readJson(p) {
43
- return JSON.parse(fs.readFileSync(p, 'utf8'))
44
- }
45
-
46
41
  function collectPolicyVersions(policy) {
47
42
  return {
48
43
  ...(policy.managedDependencies?.dependencies ?? {}),
@@ -72,25 +67,6 @@ function buildCommitMessage(changed) {
72
67
  return { subject: `dep(): bump ${changed.length} packages`, body }
73
68
  }
74
69
 
75
- function runGit(args) {
76
- const result = spawnSync('git', args, {
77
- stdio: 'inherit',
78
- cwd: fgDevkitRoot,
79
- env: process.env,
80
- })
81
- if (result.error) {
82
- console.error('[policy-upgrade]', result.error.message)
83
- return 1
84
- }
85
- return result.status ?? 1
86
- }
87
-
88
- function stageExistingFiles(files) {
89
- const existingFiles = files.filter((filePath) => fs.existsSync(path.join(fgDevkitRoot, filePath)))
90
- if (existingFiles.length === 0) return 0
91
- return runGit(['add', '--', ...existingFiles])
92
- }
93
-
94
70
  function toRepoRelative(absolutePath) {
95
71
  return path.relative(fgDevkitRoot, absolutePath)
96
72
  }
@@ -188,7 +164,7 @@ const stageExitCode = stageExistingFiles([
188
164
  toRepoRelative(policyPath),
189
165
  'scaffold/vue/package.json',
190
166
  'scaffold/vue/Dockerfile.local',
191
- ])
167
+ ], GIT)
192
168
  if (stageExitCode !== 0) {
193
169
  process.exit(stageExitCode)
194
170
  }
@@ -199,4 +175,4 @@ if (body) {
199
175
  console.log(body)
200
176
  }
201
177
  const commitArgs = body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject]
202
- process.exit(runGit(commitArgs))
178
+ process.exit(runGit(commitArgs, GIT).status)
@@ -11,15 +11,10 @@ import fs from 'node:fs'
11
11
  import os from 'node:os'
12
12
  import path from 'node:path'
13
13
  import { spawnSync } from 'node:child_process'
14
- import { fileURLToPath } from 'node:url'
15
- import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
14
+ import { resolveDependencyPolicyPath } from './lib/resolve-dependency-policy-path.mjs'
15
+ import { readJson } from './lib/common.mjs'
16
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
- }
17
+ const policyPath = resolveDependencyPolicyPath(process.cwd(), process.argv)
23
18
 
24
19
  const policy = readJson(policyPath)
25
20
  console.log(`[policy-vulns] policy=${policyPath}`)
@@ -23,9 +23,11 @@ import {
23
23
  collectOverrideChanges,
24
24
  collectOverrides,
25
25
  formatOverrideValue,
26
- } from './override-changes.mjs'
26
+ } from './lib/override-changes.mjs'
27
+ import { runGit } from './lib/common.mjs'
27
28
 
28
29
  const ROOT = process.cwd()
30
+ const GIT = { cwd: ROOT, tag: '[prune-overrides]' }
29
31
  const WORKSPACE_FILE = 'pnpm-workspace.yaml'
30
32
  const COPIED_FILES = ['package.json', 'pnpm-lock.yaml', WORKSPACE_FILE, '.npmrc']
31
33
 
@@ -49,19 +51,6 @@ Pass --commit to apply the deletions and auto-commit.`)
49
51
 
50
52
  const shouldFix = args.fix || args.commit
51
53
 
52
- function runGit(gitArgs) {
53
- const result = spawnSync('git', gitArgs, {
54
- stdio: 'inherit',
55
- cwd: ROOT,
56
- env: process.env,
57
- })
58
- if (result.error) {
59
- console.error('[prune-overrides]', result.error.message)
60
- return 1
61
- }
62
- return result.status ?? 1
63
- }
64
-
65
54
  function commitOverrideChanges(beforeOverrides, afterOverrides) {
66
55
  const changed = collectOverrideChanges(beforeOverrides, afterOverrides)
67
56
  if (changed.length === 0) {
@@ -83,7 +72,7 @@ function commitOverrideChanges(beforeOverrides, afterOverrides) {
83
72
  return 1
84
73
  }
85
74
 
86
- const stageExitCode = runGit(['add', '--', ...files])
75
+ const stageExitCode = runGit(['add', '--', ...files], GIT).status
87
76
  if (stageExitCode !== 0) {
88
77
  return stageExitCode
89
78
  }
@@ -94,7 +83,7 @@ function commitOverrideChanges(beforeOverrides, afterOverrides) {
94
83
  console.log(body)
95
84
  }
96
85
  const commitArgs = body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject]
97
- return runGit(commitArgs)
86
+ return runGit(commitArgs, GIT).status
98
87
  }
99
88
 
100
89
  function runPnpm(cwd, pnpmArgs, { allowFailure = false } = {}) {
@@ -24,7 +24,7 @@ import {
24
24
  planReleaseAgeExcludePrune,
25
25
  readWorkspaceList,
26
26
  rewriteWorkspaceList,
27
- } from './audit-fix-plan.mjs'
27
+ } from './lib/audit-fix-plan.mjs'
28
28
 
29
29
  const TAG = '[prune-release-age-exclude]'
30
30
  const KEY = 'minimumReleaseAgeExclude'
@@ -26,19 +26,17 @@
26
26
 
27
27
  import fs from 'node:fs'
28
28
  import path from 'node:path'
29
- import { fileURLToPath } from 'node:url'
30
29
 
31
- import { collectPinViolations } from './dependency-policy-pins.mjs'
32
- import { collectToolchainPolicyViolations } from './dependency-policy-toolchain.mjs'
30
+ import { collectPinViolations } from './lib/dependency-policy-pins.mjs'
31
+ import { collectToolchainPolicyViolations } from './lib/dependency-policy-toolchain.mjs'
33
32
  import { syncScaffoldVueDepsFromPolicy } from './sync-scaffold-vue-deps.mjs'
33
+ import { fgDevkitRoot } from './lib/common.mjs'
34
34
 
35
- const scriptDir = path.dirname(fileURLToPath(import.meta.url))
36
- const fgDevkitRoot = path.resolve(scriptDir, '..')
37
35
  const fgDevkitPackageJsonPath = path.join(fgDevkitRoot, 'package.json')
38
36
 
39
37
  const projectsRoot = process.argv[2]
40
38
  ? path.resolve(process.cwd(), process.argv[2])
41
- : path.resolve(scriptDir, '../..')
39
+ : path.resolve(fgDevkitRoot, '..')
42
40
 
43
41
  const policyPath = process.argv[3]
44
42
  ? path.resolve(process.cwd(), process.argv[3])
@@ -56,7 +54,7 @@ try {
56
54
  /** Relative paths under projectsRoot whose package.json files are ignored (maintainer choice). */
57
55
  /** Relative path prefixes under projectsRoot to skip when scanning (add repo names as needed). */
58
56
  const SKIP_DIR_PREFIXES = []
59
- const EXTRA_TOOLING_EXCLUDES = new Set(['husky', 'lint-staged', '@rushstack/eslint-patch', 'jsonc-eslint-parser', 'yaml-eslint-parser'])
57
+ const EXTRA_TOOLING_EXCLUDES = new Set(['@rushstack/eslint-patch', 'jsonc-eslint-parser', 'yaml-eslint-parser'])
60
58
  const EXCLUDED_POLICY_PACKAGES = (() => {
61
59
  try {
62
60
  const pkg = JSON.parse(fs.readFileSync(fgDevkitPackageJsonPath, 'utf8'))
@@ -79,7 +79,8 @@ is_fg_devkit_repo="$(
79
79
  sync_fg_devkit_package_files() {
80
80
  if [[ "$is_fg_devkit_repo" == "yes" && -f "./scripts/sync-scaffold-vue-deps.mjs" && -f "./scaffold/vue/package.json" ]]; then
81
81
  node ./scripts/sync-scaffold-vue-deps.mjs
82
- git add package.json scaffold/vue/package.json
82
+ # Stage everything sync-scaffold-vue-deps can write, or the release leaves it uncommitted.
83
+ git add package.json scaffold/vue/package.json configs/dependency-policy.vue-workspace.json
83
84
  else
84
85
  git add package.json
85
86
  fi
@@ -26,10 +26,8 @@
26
26
 
27
27
  import fs from 'node:fs'
28
28
  import path from 'node:path'
29
- import { fileURLToPath } from 'node:url'
29
+ import { fgDevkitRoot } from './lib/common.mjs'
30
30
 
31
- const scriptDir = path.dirname(fileURLToPath(import.meta.url))
32
- const fgDevkitRoot = path.resolve(scriptDir, '..')
33
31
  const scaffoldRoot = path.join(fgDevkitRoot, 'scaffold/vue')
34
32
 
35
33
  /** True if the directory does not exist, is empty, or only contains a fresh clone (.git). */
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Set up the shared AI rules from feelgood/fg-ai-rules for Claude Code in the current repo:
4
+ *
5
+ * 1. Add the `fg` marketplace and enable fg-core and fg-frontend in .claude/settings.json,
6
+ * keeping every other setting.
7
+ * 2. Add the marketplace to Claude Code on this machine if it is missing.
8
+ * 3. Install the plugins for this repo (`claude plugin install … --scope project`). Claude Code
9
+ * does not install them from the settings file on its own, and without the install they don't
10
+ * load at session start.
11
+ *
12
+ * Safe to run again: it only changes what is missing. Commit .claude/settings.json afterwards so
13
+ * teammates get the same plugins; each of them still runs this once.
14
+ *
15
+ * Usage:
16
+ * pnpm exec fg-devkit setup-ai-rules
17
+ */
18
+
19
+ import { spawnSync } from 'node:child_process'
20
+ import fs from 'node:fs'
21
+ import path from 'node:path'
22
+
23
+ const TAG = '[setup-ai-rules]'
24
+ const MARKETPLACE = 'fg'
25
+ const MARKETPLACE_REPO = 'feelgood/fg-ai-rules'
26
+ const PLUGINS = ['fg-core', 'fg-frontend']
27
+
28
+ const projectRoot = fs.realpathSync(process.cwd())
29
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json')
30
+
31
+ function claude(args, { capture = false } = {}) {
32
+ const result = spawnSync('claude', args, {
33
+ cwd: projectRoot,
34
+ encoding: 'utf8',
35
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
36
+ })
37
+ if (result.error) return { status: 1, stdout: '', missing: result.error.code === 'ENOENT' }
38
+ return { status: result.status ?? 1, stdout: result.stdout ?? '' }
39
+ }
40
+
41
+ function claudeJson(args) {
42
+ const result = claude([...args, '--json'], { capture: true })
43
+ if (result.status !== 0) return null
44
+ try {
45
+ return JSON.parse(result.stdout)
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+
51
+ /** Add the marketplace and enabled plugins to .claude/settings.json. Returns true when it changed. */
52
+ function updateSettings() {
53
+ let settings = {}
54
+ if (fs.existsSync(settingsPath)) {
55
+ try {
56
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
57
+ } catch (err) {
58
+ console.error(`${TAG} Could not parse ${settingsPath}: ${err.message}`)
59
+ process.exit(1)
60
+ }
61
+ }
62
+ const before = JSON.stringify(settings)
63
+ settings.extraKnownMarketplaces = {
64
+ ...settings.extraKnownMarketplaces,
65
+ [MARKETPLACE]: { source: { source: 'github', repo: MARKETPLACE_REPO }, autoUpdate: true },
66
+ }
67
+ settings.enabledPlugins = {
68
+ ...settings.enabledPlugins,
69
+ ...Object.fromEntries(PLUGINS.map((plugin) => [`${plugin}@${MARKETPLACE}`, true])),
70
+ }
71
+ if (JSON.stringify(settings) === before) return false
72
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
73
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
74
+ return true
75
+ }
76
+
77
+ function printManualSteps() {
78
+ console.log(`${TAG} Run these from the repo root, then restart Claude Code:`)
79
+ console.log(` claude plugin marketplace add ${MARKETPLACE_REPO}`)
80
+ for (const plugin of PLUGINS) console.log(` claude plugin install ${plugin}@${MARKETPLACE} --scope project`)
81
+ }
82
+
83
+ function main() {
84
+ if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
85
+ console.error(`${TAG} Run from the repo root (no package.json in ${projectRoot}).`)
86
+ return 1
87
+ }
88
+
89
+ const settingsChanged = updateSettings()
90
+ console.log(`${TAG} ${settingsChanged ? 'Updated' : 'Already up to date:'} .claude/settings.json`)
91
+
92
+ const marketplaces = claudeJson(['plugin', 'marketplace', 'list'])
93
+ if (marketplaces === null) {
94
+ const missing = claude(['--version'], { capture: true }).missing
95
+ console.error(`${TAG} ${missing ? 'Claude Code (claude) is not installed.' : 'Could not read Claude Code plugins.'}`)
96
+ printManualSteps()
97
+ return 1
98
+ }
99
+
100
+ if (!marketplaces.some((marketplace) => marketplace.name === MARKETPLACE)) {
101
+ console.log(`${TAG} Adding the ${MARKETPLACE} marketplace (${MARKETPLACE_REPO})…`)
102
+ if (claude(['plugin', 'marketplace', 'add', MARKETPLACE_REPO]).status !== 0) {
103
+ console.error(`${TAG} Could not add the marketplace. Check that you can clone ${MARKETPLACE_REPO} over SSH.`)
104
+ return 1
105
+ }
106
+ }
107
+
108
+ const installed = claudeJson(['plugin', 'list']) ?? []
109
+ const isInstalledHere = (plugin) =>
110
+ installed.some(
111
+ (entry) =>
112
+ entry.id === `${plugin}@${MARKETPLACE}` &&
113
+ (entry.scope === 'user' || (entry.projectPath && fs.realpathSync(entry.projectPath) === projectRoot)),
114
+ )
115
+
116
+ let failed = false
117
+ for (const plugin of PLUGINS) {
118
+ if (isInstalledHere(plugin)) {
119
+ console.log(`${TAG} ${plugin} is already installed for this repo.`)
120
+ continue
121
+ }
122
+ console.log(`${TAG} Installing ${plugin}…`)
123
+ if (claude(['plugin', 'install', `${plugin}@${MARKETPLACE}`, '--scope', 'project']).status !== 0) failed = true
124
+ }
125
+ if (failed) {
126
+ printManualSteps()
127
+ return 1
128
+ }
129
+
130
+ console.log(`${TAG} Done. Restart Claude Code to load the rules.`)
131
+ if (settingsChanged) console.log(`${TAG} Commit .claude/settings.json so teammates get the same plugins.`)
132
+ return 0
133
+ }
134
+
135
+ process.exit(main())
@@ -2,9 +2,7 @@
2
2
  /**
3
3
  * Syncs Lefthook setup into a consuming repo:
4
4
  * - writes lefthook.yml from fg-devkit template
5
- * - removes legacy .husky directory (if present)
6
- * - removes legacy lint-staged config block from package.json
7
- * - removes legacy prepare hook scripts for husky/lefthook bootstrap
5
+ * - removes a legacy `prepare` script that bootstrapped lefthook (hooks are installed by setup-lefthook)
8
6
  *
9
7
  * Usage:
10
8
  * node node_modules/fg-devkit/scripts/sync-consumer-lefthook.mjs
@@ -13,15 +11,12 @@
13
11
 
14
12
  import fs from 'node:fs'
15
13
  import path from 'node:path'
16
- import { fileURLToPath } from 'node:url'
14
+ import { fgDevkitRoot } from './lib/common.mjs'
17
15
 
18
16
  const projectRoot = process.cwd()
19
- const scriptDir = path.dirname(fileURLToPath(import.meta.url))
20
- const fgRoot = path.resolve(scriptDir, '..')
21
17
 
22
- const lefthookTemplate = path.join(fgRoot, 'configs', 'lefthook.yml')
18
+ const lefthookTemplate = path.join(fgDevkitRoot, 'configs', 'lefthook.yml')
23
19
  const lefthookTarget = path.join(projectRoot, 'lefthook.yml')
24
- const huskyDir = path.join(projectRoot, '.husky')
25
20
  const packageJsonPath = path.join(projectRoot, 'package.json')
26
21
 
27
22
  if (!fs.existsSync(lefthookTemplate)) {
@@ -42,25 +37,13 @@ try {
42
37
  process.exit(1)
43
38
  }
44
39
 
45
- pkg.scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
46
- if (
47
- pkg.scripts.prepare &&
48
- (pkg.scripts.prepare.includes('husky') || pkg.scripts.prepare.includes('setup-lefthook'))
49
- ) {
50
- delete pkg.scripts.prepare
51
- }
52
-
53
- if (pkg['lint-staged'] !== undefined) {
54
- delete pkg['lint-staged']
55
- }
40
+ const removePrepare = pkg.scripts?.prepare?.includes('setup-lefthook')
41
+ if (removePrepare) delete pkg.scripts.prepare
56
42
 
57
43
  try {
58
44
  fs.copyFileSync(lefthookTemplate, lefthookTarget)
59
- fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
60
- if (fs.existsSync(huskyDir)) {
61
- fs.rmSync(huskyDir, { recursive: true, force: true })
62
- }
63
- console.log('[sync-consumer-lefthook] Synced lefthook.yml and removed .husky/lint-staged legacy setup.')
45
+ if (removePrepare) fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
46
+ console.log('[sync-consumer-lefthook] Synced lefthook.yml.')
64
47
  } catch (err) {
65
48
  console.error(`[sync-consumer-lefthook] ${err.message}`)
66
49
  process.exit(1)