fg-devkit 1.24.2 → 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,8 +95,10 @@ 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
103
  and other repos don't. When `.claude/settings.json` changed it commits that file on its own
102
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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.24.2",
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.2",
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, commit
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,9 +1091,9 @@ async function runInteractiveMenu() {
1091
1091
  description: 'Write lefthook.yml and install the git hooks',
1092
1092
  },
1093
1093
  {
1094
- name: autoCommit('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, install them for Claude Code and commit the file',
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',
@@ -2,9 +2,10 @@
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
@@ -16,19 +17,20 @@
16
17
  * every other repo that commits the same .claude/settings.json works without running it again.
17
18
  *
18
19
  * Usage:
19
- * pnpm exec fg-devkit setup-ai-rules
20
+ * pnpm exec fg-devkit setup-ai-rules [--plugins=fg-core,fg-frontend]
20
21
  */
21
22
 
22
23
  import { spawnSync } from 'node:child_process'
23
24
  import fs from 'node:fs'
24
25
  import path from 'node:path'
25
26
 
26
- import { fileDiffersFromHead, runGit } from './lib/common.mjs'
27
+ import { fileDiffersFromHead, readJson, runGit } from './lib/common.mjs'
27
28
 
28
29
  const TAG = '[setup-ai-rules]'
29
30
  const MARKETPLACE = 'fg'
30
31
  const MARKETPLACE_REPO = 'feelgood/fg-ai-rules'
31
- 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='))
32
34
 
33
35
  const projectRoot = fs.realpathSync(process.cwd())
34
36
  const SETTINGS_FILE = '.claude/settings.json'
@@ -66,26 +68,71 @@ function claudeJson(args) {
66
68
  }
67
69
  }
68
70
 
69
- /** Add the marketplace and enabled plugins to .claude/settings.json. Returns true when it changed. */
70
- function updateSettings() {
71
- let settings = {}
72
- if (fs.existsSync(settingsPath)) {
73
- try {
74
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
75
- } catch (err) {
76
- console.error(`${TAG} Could not parse ${settingsPath}: ${err.message}`)
77
- 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
78
105
  }
106
+ return chosen
79
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) {
80
126
  const before = JSON.stringify(settings)
81
127
  settings.extraKnownMarketplaces = {
82
128
  ...settings.extraKnownMarketplaces,
83
129
  [MARKETPLACE]: { source: { source: 'github', repo: MARKETPLACE_REPO }, autoUpdate: true },
84
130
  }
85
- settings.enabledPlugins = {
86
- ...settings.enabledPlugins,
87
- ...Object.fromEntries(PLUGINS.map((plugin) => [`${plugin}@${MARKETPLACE}`, true])),
88
- }
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
+ ])
89
136
  if (JSON.stringify(settings) === before) return false
90
137
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
91
138
  fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
@@ -106,43 +153,62 @@ function commitSettings() {
106
153
  return status
107
154
  }
108
155
 
109
- function printManualSteps() {
110
- console.log(`${TAG} Run these from the repo root, then restart Claude Code:`)
156
+ function printManualSteps(plugins) {
157
+ console.log(`${TAG} Run these, then restart Claude Code:`)
111
158
  console.log(` claude plugin marketplace add ${MARKETPLACE_REPO}`)
112
- for (const plugin of PLUGINS) {
159
+ for (const plugin of plugins) {
113
160
  console.log(` claude plugin install ${plugin}@${MARKETPLACE} --scope user`)
114
161
  console.log(` claude plugin disable ${plugin}@${MARKETPLACE} --scope user`)
115
162
  }
116
163
  }
117
164
 
118
- function main() {
165
+ async function main() {
119
166
  if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
120
167
  console.error(`${TAG} Run from the repo root (no package.json in ${projectRoot}).`)
121
168
  return 1
122
169
  }
123
170
 
124
- const settingsChanged = updateSettings()
125
- console.log(`${TAG} ${settingsChanged ? 'Updated' : 'Already up to date:'} .claude/settings.json`)
126
-
127
171
  const marketplaces = claudeJson(['plugin', 'marketplace', 'list'])
128
172
  if (marketplaces === null) {
129
173
  const missing = claude(['--version'], { capture: true }).missing
130
174
  console.error(`${TAG} ${missing ? 'Claude Code (claude) is not installed.' : 'Could not read Claude Code plugins.'}`)
131
- printManualSteps()
175
+ printManualSteps(DEFAULT_PLUGINS)
132
176
  return 1
133
177
  }
134
178
 
135
- 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 {
136
184
  console.log(`${TAG} Adding the ${MARKETPLACE} marketplace (${MARKETPLACE_REPO})…`)
137
185
  if (claude(['plugin', 'marketplace', 'add', MARKETPLACE_REPO]).status !== 0) {
138
186
  console.error(`${TAG} Could not add the marketplace. Check that you can clone ${MARKETPLACE_REPO} over SSH.`)
139
187
  return 1
140
188
  }
189
+ marketplace = claudeJson(['plugin', 'marketplace', 'list'])?.find((entry) => entry.name === MARKETPLACE)
141
190
  }
142
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
+
143
209
  const installed = claudeJson(['plugin', 'list']) ?? []
144
210
  let failed = false
145
- for (const plugin of PLUGINS) {
211
+ for (const plugin of plugins) {
146
212
  const id = `${plugin}@${MARKETPLACE}`
147
213
  if (installed.some((entry) => entry.id === id && entry.scope === 'user')) {
148
214
  console.log(`${TAG} ${plugin} is already installed.`)
@@ -161,7 +227,7 @@ function main() {
161
227
  }
162
228
  }
163
229
  if (failed) {
164
- printManualSteps()
230
+ printManualSteps(plugins)
165
231
  return 1
166
232
  }
167
233
 
@@ -170,4 +236,4 @@ function main() {
170
236
  return 0
171
237
  }
172
238
 
173
- process.exit(main())
239
+ process.exit(await main())