fg-devkit 1.24.1 → 1.24.3

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
@@ -95,11 +95,13 @@ To use the shared AI rules from [feelgood/fg-ai-rules](https://github.com/feelgo
95
95
  pnpm exec fgd setup-ai-rules
96
96
  ```
97
97
 
98
- It enables the `fg-core` and `fg-frontend` plugins in `.claude/settings.json`, adds the `fg` marketplace
99
- to Claude Code if it is missing, and installs the plugins once for the user, disabled at user scope. The
98
+ It adds the `fg` marketplace to Claude Code if it is missing and asks which of its plugins the repo
99
+ should use (the repo's current choice, or `fg-core` and `fg-frontend`, is preselected; pass
100
+ `--plugins=fg-core,fg-backend` to skip the question). It enables those in `.claude/settings.json` and
101
+ installs them once for the user, disabled at user scope. The
100
102
  repo's `.claude/settings.json` then switches them on, so every repo that commits that file gets the rules
101
- and other repos don't. Each developer runs it once on their machine; commit `.claude/settings.json` in
102
- each repo. Needs Claude Code and SSH access to `feelgood/fg-ai-rules`.
103
+ and other repos don't. When `.claude/settings.json` changed it commits that file on its own
104
+ (`chore(repo): enable fg-ai-rules Claude Code plugins`). Each developer runs it once on their machine. Needs Claude Code and SSH access to `feelgood/fg-ai-rules`.
103
105
 
104
106
  Run full consumer setup in one command:
105
107
 
@@ -108,7 +110,7 @@ pnpm exec fgd full-consumer-setup
108
110
  ```
109
111
 
110
112
  It syncs the consumer scripts, Lefthook, configs and dependency policy, runs `pnpm install`, and finishes with
111
- `setup-ai-rules`. Without Claude Code that last step only prints a warning.
113
+ `setup-ai-rules`, which commits `.claude/settings.json`. Without Claude Code that last step only prints a warning.
112
114
 
113
115
  ## Dependency policy update workflow
114
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.24.1",
3
+ "version": "1.24.3",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -32,7 +32,7 @@
32
32
  "@intlify/unplugin-vue-i18n": "11.2.6",
33
33
  "@tanstack/vue-query-devtools": "6.3.3",
34
34
  "@types/node": "22.16.5",
35
- "fg-devkit": "1.24.1",
35
+ "fg-devkit": "1.24.3",
36
36
  "@vitejs/plugin-vue": "6.0.10",
37
37
  "tailwindcss": "4.3.3",
38
38
  "tsx": "4.23.15",
@@ -577,7 +577,7 @@ Commands:
577
577
  sync-vite-plugin-vue-inspector Add/pin vite-plugin-vue-inspector, wire vite.config, install, commit
578
578
  sync-consumer-scripts Enforce standard fgd scripts in consumer package.json
579
579
  setup-lefthook Write lefthook.yml and install the git hooks
580
- setup-ai-rules Enable the fg-ai-rules Claude Code plugins in this repo, install them once per machine
580
+ setup-ai-rules [--plugins=a,b] Pick the fg-ai-rules Claude Code plugins for this repo, install them once per machine, commit
581
581
  full-consumer-setup Run: scripts -> lefthook -> step 3 -> step 2 -> pnpm install -> AI rules
582
582
  create-release [release|candidate] [patch|minor|major] [--skip-release-notes]
583
583
  Stable release or candidate (X.Y.Z-rc.N); omit = interactive menu
@@ -1091,14 +1091,14 @@ async function runInteractiveMenu() {
1091
1091
  description: 'Write lefthook.yml and install the git hooks',
1092
1092
  },
1093
1093
  {
1094
- name: 'AI rules',
1094
+ name: tagged('AI rules', { interactive: true, autoCommit: true }),
1095
1095
  value: 'ai-rules',
1096
- description: 'Enable the fg-ai-rules plugins in .claude/settings.json and install them for Claude Code',
1096
+ description: 'Pick the fg-ai-rules plugins for this repo, install them for Claude Code and commit .claude/settings.json',
1097
1097
  },
1098
1098
  {
1099
1099
  name: 'Full setup',
1100
1100
  value: 'full-consumer-setup',
1101
- description: 'Sync scripts, Lefthook, configs and dependency policy, pnpm install, then set up AI rules',
1101
+ description: 'Sync scripts, Lefthook, configs and dependency policy, pnpm install, then set up AI rules (commits .claude/settings.json)',
1102
1102
  },
1103
1103
  { name: 'Cancel', value: 'cancel' },
1104
1104
  ],
@@ -2,32 +2,41 @@
2
2
  /**
3
3
  * Set up the shared AI rules from feelgood/fg-ai-rules for Claude Code in the current repo:
4
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.
5
+ * 1. Add the `fg` marketplace to Claude Code on this machine if it is missing, or refresh it.
6
+ * 2. Ask which of its plugins the repo should use (a checklist; the repo's current choice, or
7
+ * fg-core and fg-frontend, is preselected) and write them to .claude/settings.json, keeping
8
+ * every other setting. --plugins=fg-core,fg-backend picks them without asking.
8
9
  * 3. Install the plugins once for the user (`--scope user`) and disable them at user scope.
9
10
  * Claude Code doesn't install plugins from a repo's settings file on its own, and a
10
11
  * project-scope install only covers one repo. A user-scope install covers every repo, and
11
12
  * disabling it at user scope leaves the repo's .claude/settings.json to switch it on.
12
13
  *
14
+ * 4. Commit .claude/settings.json when it changed (only that file).
15
+ *
13
16
  * Safe to run again: it only changes what is missing. Once a developer has run it in any repo,
14
17
  * every other repo that commits the same .claude/settings.json works without running it again.
15
18
  *
16
19
  * Usage:
17
- * pnpm exec fg-devkit setup-ai-rules
20
+ * pnpm exec fg-devkit setup-ai-rules [--plugins=fg-core,fg-frontend]
18
21
  */
19
22
 
20
23
  import { spawnSync } from 'node:child_process'
21
24
  import fs from 'node:fs'
22
25
  import path from 'node:path'
23
26
 
27
+ import { fileDiffersFromHead, readJson, runGit } from './lib/common.mjs'
28
+
24
29
  const TAG = '[setup-ai-rules]'
25
30
  const MARKETPLACE = 'fg'
26
31
  const MARKETPLACE_REPO = 'feelgood/fg-ai-rules'
27
- const PLUGINS = ['fg-core', 'fg-frontend']
32
+ const DEFAULT_PLUGINS = ['fg-core', 'fg-frontend']
33
+ const pluginsArg = process.argv.slice(2).find((arg) => arg.startsWith('--plugins='))
28
34
 
29
35
  const projectRoot = fs.realpathSync(process.cwd())
30
- const settingsPath = path.join(projectRoot, '.claude', 'settings.json')
36
+ const SETTINGS_FILE = '.claude/settings.json'
37
+ const COMMIT_MESSAGE = 'chore(repo): enable fg-ai-rules Claude Code plugins'
38
+ const settingsPath = path.join(projectRoot, SETTINGS_FILE)
39
+ const GIT = { cwd: projectRoot, tag: TAG }
31
40
 
32
41
  function claude(args, { capture = false } = {}) {
33
42
  const result = spawnSync('claude', args, {
@@ -59,69 +68,147 @@ function claudeJson(args) {
59
68
  }
60
69
  }
61
70
 
62
- /** Add the marketplace and enabled plugins to .claude/settings.json. Returns true when it changed. */
63
- function updateSettings() {
64
- let settings = {}
65
- if (fs.existsSync(settingsPath)) {
66
- try {
67
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
68
- } catch (err) {
69
- console.error(`${TAG} Could not parse ${settingsPath}: ${err.message}`)
70
- process.exit(1)
71
+ function readSettings() {
72
+ if (!fs.existsSync(settingsPath)) return {}
73
+ try {
74
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
75
+ } catch (err) {
76
+ console.error(`${TAG} Could not parse ${settingsPath}: ${err.message}`)
77
+ process.exit(1)
78
+ }
79
+ }
80
+
81
+ /** Plugins this repo enables from the marketplace. */
82
+ function enabledInRepo(settings) {
83
+ return Object.entries(settings.enabledPlugins ?? {})
84
+ .filter(([id, enabled]) => enabled && id.endsWith(`@${MARKETPLACE}`))
85
+ .map(([id]) => id.slice(0, -`@${MARKETPLACE}`.length))
86
+ }
87
+
88
+ /** Plugins listed in the marketplace on this machine: `[{ name, description }]`. */
89
+ function marketplacePlugins(installLocation) {
90
+ try {
91
+ return readJson(path.join(installLocation, '.claude-plugin', 'marketplace.json')).plugins ?? []
92
+ } catch {
93
+ return []
94
+ }
95
+ }
96
+
97
+ async function choosePlugins(available, settings) {
98
+ const names = available.map((plugin) => plugin.name)
99
+ if (pluginsArg) {
100
+ const chosen = pluginsArg.slice('--plugins='.length).split(',').filter(Boolean)
101
+ const unknown = chosen.filter((name) => !names.includes(name))
102
+ if (unknown.length > 0) {
103
+ console.error(`${TAG} Unknown plugin(s): ${unknown.join(', ')}. Available: ${names.join(', ')}`)
104
+ return null
71
105
  }
106
+ return chosen
72
107
  }
108
+ const current = enabledInRepo(settings).filter((name) => names.includes(name))
109
+ const preselected = current.length > 0 ? current : DEFAULT_PLUGINS.filter((name) => names.includes(name))
110
+ if (!process.stdin.isTTY) return preselected
111
+ const checkbox = (await import('@inquirer/checkbox')).default
112
+ return checkbox({
113
+ message: 'Which fg-ai-rules plugins should this repo use?',
114
+ choices: available.map((plugin) => ({
115
+ name: plugin.name,
116
+ value: plugin.name,
117
+ description: plugin.description,
118
+ checked: preselected.includes(plugin.name),
119
+ })),
120
+ required: true,
121
+ })
122
+ }
123
+
124
+ /** Enable the chosen plugins in .claude/settings.json and drop the other fg ones. Returns true when it changed. */
125
+ function updateSettings(settings, plugins) {
73
126
  const before = JSON.stringify(settings)
74
127
  settings.extraKnownMarketplaces = {
75
128
  ...settings.extraKnownMarketplaces,
76
129
  [MARKETPLACE]: { source: { source: 'github', repo: MARKETPLACE_REPO }, autoUpdate: true },
77
130
  }
78
- settings.enabledPlugins = {
79
- ...settings.enabledPlugins,
80
- ...Object.fromEntries(PLUGINS.map((plugin) => [`${plugin}@${MARKETPLACE}`, true])),
81
- }
131
+ const others = Object.entries(settings.enabledPlugins ?? {}).filter(([id]) => !id.endsWith(`@${MARKETPLACE}`))
132
+ settings.enabledPlugins = Object.fromEntries([
133
+ ...others,
134
+ ...plugins.map((plugin) => [`${plugin}@${MARKETPLACE}`, true]),
135
+ ])
82
136
  if (JSON.stringify(settings) === before) return false
83
137
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
84
138
  fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
85
139
  return true
86
140
  }
87
141
 
88
- function printManualSteps() {
89
- console.log(`${TAG} Run these from the repo root, then restart Claude Code:`)
142
+ /** Commit .claude/settings.json on its own when it differs from HEAD. */
143
+ function commitSettings() {
144
+ if (runGit(['rev-parse', '--is-inside-work-tree'], { ...GIT, capture: true }).status !== 0) {
145
+ console.log(`${TAG} Not a git repository; commit ${SETTINGS_FILE} yourself.`)
146
+ return 0
147
+ }
148
+ if (!fileDiffersFromHead(SETTINGS_FILE, GIT)) return 0
149
+ const status =
150
+ runGit(['add', '--', SETTINGS_FILE], GIT).status ||
151
+ runGit(['commit', '-m', COMMIT_MESSAGE, '--', SETTINGS_FILE], GIT).status
152
+ if (status !== 0) console.error(`${TAG} Could not commit ${SETTINGS_FILE}.`)
153
+ return status
154
+ }
155
+
156
+ function printManualSteps(plugins) {
157
+ console.log(`${TAG} Run these, then restart Claude Code:`)
90
158
  console.log(` claude plugin marketplace add ${MARKETPLACE_REPO}`)
91
- for (const plugin of PLUGINS) {
159
+ for (const plugin of plugins) {
92
160
  console.log(` claude plugin install ${plugin}@${MARKETPLACE} --scope user`)
93
161
  console.log(` claude plugin disable ${plugin}@${MARKETPLACE} --scope user`)
94
162
  }
95
163
  }
96
164
 
97
- function main() {
165
+ async function main() {
98
166
  if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
99
167
  console.error(`${TAG} Run from the repo root (no package.json in ${projectRoot}).`)
100
168
  return 1
101
169
  }
102
170
 
103
- const settingsChanged = updateSettings()
104
- console.log(`${TAG} ${settingsChanged ? 'Updated' : 'Already up to date:'} .claude/settings.json`)
105
-
106
171
  const marketplaces = claudeJson(['plugin', 'marketplace', 'list'])
107
172
  if (marketplaces === null) {
108
173
  const missing = claude(['--version'], { capture: true }).missing
109
174
  console.error(`${TAG} ${missing ? 'Claude Code (claude) is not installed.' : 'Could not read Claude Code plugins.'}`)
110
- printManualSteps()
175
+ printManualSteps(DEFAULT_PLUGINS)
111
176
  return 1
112
177
  }
113
178
 
114
- if (!marketplaces.some((marketplace) => marketplace.name === MARKETPLACE)) {
179
+ let marketplace = marketplaces.find((entry) => entry.name === MARKETPLACE)
180
+ if (marketplace) {
181
+ // Refresh so the checklist shows plugins added to fg-ai-rules since the last update.
182
+ claude(['plugin', 'marketplace', 'update', MARKETPLACE], { capture: true })
183
+ } else {
115
184
  console.log(`${TAG} Adding the ${MARKETPLACE} marketplace (${MARKETPLACE_REPO})…`)
116
185
  if (claude(['plugin', 'marketplace', 'add', MARKETPLACE_REPO]).status !== 0) {
117
186
  console.error(`${TAG} Could not add the marketplace. Check that you can clone ${MARKETPLACE_REPO} over SSH.`)
118
187
  return 1
119
188
  }
189
+ marketplace = claudeJson(['plugin', 'marketplace', 'list'])?.find((entry) => entry.name === MARKETPLACE)
120
190
  }
121
191
 
192
+ const available = marketplacePlugins(marketplace?.installLocation ?? '')
193
+ if (available.length === 0) {
194
+ console.error(`${TAG} Could not read the plugins in the ${MARKETPLACE} marketplace.`)
195
+ return 1
196
+ }
197
+
198
+ const settings = readSettings()
199
+ const plugins = await choosePlugins(available, settings)
200
+ if (!plugins) return 1
201
+ if (plugins.length === 0) {
202
+ console.error(`${TAG} No plugins chosen.`)
203
+ return 1
204
+ }
205
+
206
+ const settingsChanged = updateSettings(settings, plugins)
207
+ console.log(`${TAG} ${settingsChanged ? 'Updated' : 'Already up to date:'} ${SETTINGS_FILE} (${plugins.join(', ')})`)
208
+
122
209
  const installed = claudeJson(['plugin', 'list']) ?? []
123
210
  let failed = false
124
- for (const plugin of PLUGINS) {
211
+ for (const plugin of plugins) {
125
212
  const id = `${plugin}@${MARKETPLACE}`
126
213
  if (installed.some((entry) => entry.id === id && entry.scope === 'user')) {
127
214
  console.log(`${TAG} ${plugin} is already installed.`)
@@ -140,13 +227,13 @@ function main() {
140
227
  }
141
228
  }
142
229
  if (failed) {
143
- printManualSteps()
230
+ printManualSteps(plugins)
144
231
  return 1
145
232
  }
146
233
 
234
+ if (commitSettings() !== 0) return 1
147
235
  console.log(`${TAG} Done. Restart Claude Code to load the rules.`)
148
- if (settingsChanged) console.log(`${TAG} Commit .claude/settings.json so other repos and teammates get the same plugins.`)
149
236
  return 0
150
237
  }
151
238
 
152
- process.exit(main())
239
+ process.exit(await main())