@cregis-dev/cckit 0.6.6 → 0.6.8

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 (54) hide show
  1. package/LICENSE +21 -21
  2. package/bin/cckit.js +3 -3
  3. package/package.json +53 -53
  4. package/registry.json +145 -145
  5. package/src/cli.js +79 -79
  6. package/src/commands/init.js +174 -174
  7. package/src/commands/status.js +125 -125
  8. package/src/commands/update.js +192 -192
  9. package/src/core/config.js +82 -75
  10. package/src/core/orchestrator.js +79 -79
  11. package/src/core/registry.js +60 -60
  12. package/src/steps/add-plugin.js +148 -116
  13. package/src/steps/configure-user.js +181 -181
  14. package/src/steps/enable-plugins.js +97 -97
  15. package/src/steps/install-bmad.js +85 -85
  16. package/src/steps/install-mcp.js +70 -70
  17. package/src/steps/install-rules.js +69 -69
  18. package/src/steps/install-skills.js +56 -56
  19. package/src/utils/compare-versions.js +106 -106
  20. package/src/utils/fs.js +33 -33
  21. package/src/utils/logger.js +16 -16
  22. package/src/utils/manifest.js +101 -101
  23. package/src/utils/prompt.js +41 -41
  24. package/templates/mcp/claude-code/.mcp.json +40 -40
  25. package/templates/rules/README.md +103 -103
  26. package/templates/rules/common/agents.md +49 -49
  27. package/templates/rules/common/coding-style.md +48 -48
  28. package/templates/rules/common/development-workflow.md +37 -37
  29. package/templates/rules/common/git-workflow.md +24 -24
  30. package/templates/rules/common/hooks.md +30 -30
  31. package/templates/rules/common/patterns.md +31 -31
  32. package/templates/rules/common/performance.md +55 -55
  33. package/templates/rules/common/security.md +29 -29
  34. package/templates/rules/common/testing.md +29 -29
  35. package/templates/rules/golang/coding-style.md +32 -32
  36. package/templates/rules/golang/hooks.md +17 -17
  37. package/templates/rules/golang/patterns.md +45 -45
  38. package/templates/rules/golang/security.md +34 -34
  39. package/templates/rules/golang/testing.md +31 -31
  40. package/templates/rules/python/coding-style.md +42 -42
  41. package/templates/rules/python/hooks.md +19 -19
  42. package/templates/rules/python/patterns.md +39 -39
  43. package/templates/rules/python/security.md +30 -30
  44. package/templates/rules/python/testing.md +38 -38
  45. package/templates/rules/swift/coding-style.md +47 -47
  46. package/templates/rules/swift/hooks.md +20 -20
  47. package/templates/rules/swift/patterns.md +66 -66
  48. package/templates/rules/swift/security.md +33 -33
  49. package/templates/rules/swift/testing.md +45 -45
  50. package/templates/rules/typescript/coding-style.md +65 -65
  51. package/templates/rules/typescript/hooks.md +22 -22
  52. package/templates/rules/typescript/patterns.md +52 -52
  53. package/templates/rules/typescript/security.md +28 -28
  54. package/templates/rules/typescript/testing.md +18 -18
@@ -1,181 +1,181 @@
1
- /**
2
- * Step: Configure user-level settings in ~/.claude/settings.json.
3
- *
4
- * Writes API URL, API key (ANTHROPIC_AUTH_TOKEN), and model env vars.
5
- * User-level settings are separate from project-level .claude/settings.json.
6
- */
7
-
8
- import path from 'node:path'
9
- import os from 'node:os'
10
- import fse from 'fs-extra'
11
- import { askQuestion } from '../utils/prompt.js'
12
-
13
- /**
14
- * @param {object} opts
15
- * @param {string} opts.apiUrl - API base URL (default from registry)
16
- * @param {string} [opts.apiKey] - API key from CLI flag
17
- * @param {boolean} [opts.forceApiKey] - Force overwrite existing key
18
- * @param {string} opts.modelId - Resolved model ID (e.g. "MiniMax-M2.5")
19
- * @param {string[]} opts.modelEnvKeys - Env keys to set for model
20
- * @param {boolean} [opts.yes] - Non-interactive mode
21
- * @param {boolean} opts.skip - Skip this step
22
- * @param {object} logger
23
- * @param {object} [_deps] - Injectable dependencies for testing
24
- * @returns {Promise<import('../core/orchestrator.js').StepResult>}
25
- */
26
- export async function configureUser(opts, logger, _deps = {}) {
27
- if (opts.skip) {
28
- return { stepId: 'configure-user', name: 'Configure User Settings', success: true, skipped: true, details: {} }
29
- }
30
-
31
- const { askFn = askQuestion } = _deps
32
- const settingsPath = _deps.settingsPath || path.join(os.homedir(), '.claude', 'settings.json')
33
- const claudeJsonPath = _deps.claudeJsonPath || path.join(os.homedir(), '.claude.json')
34
-
35
- // Read existing user settings
36
- let existing = {}
37
- try {
38
- const raw = await fse.readFile(settingsPath, 'utf8')
39
- existing = JSON.parse(raw)
40
- } catch (err) {
41
- if (err.code === 'ENOENT') {
42
- // File doesn't exist — start fresh
43
- } else {
44
- // Other error (permission, corrupt, etc.) — log and start fresh
45
- logger.warn(`Failed to read ${settingsPath}: ${err.message}, using defaults`)
46
- }
47
- }
48
-
49
- const existingEnv = existing.env || {}
50
-
51
- // 1. API URL — always prompt for confirmation when not in --yes mode
52
- let apiUrl = opts.apiUrl
53
- if (!opts.yes) {
54
- const input = await askFn(' Enter API URL (default: ' + opts.apiUrl + '): ')
55
- if (input && input.trim()) {
56
- apiUrl = input.trim()
57
- }
58
- }
59
-
60
- // 2. API Key — prompt when not in --yes mode
61
- let apiKey = existingEnv.ANTHROPIC_AUTH_TOKEN || ''
62
- const hasExistingKey = Boolean(apiKey)
63
-
64
- if (!opts.yes) {
65
- // Use CLI value if provided, otherwise prompt
66
- if (opts.apiKey) {
67
- apiKey = opts.apiKey
68
- } else {
69
- const prompt = hasExistingKey
70
- ? ' API Key already exists. Enter new key to replace (empty to keep): '
71
- : ' Enter your API Key (empty to skip): '
72
- const input = await askFn(prompt)
73
- if (input) {
74
- apiKey = input
75
- } else if (!hasExistingKey) {
76
- logger.warn('No API Key provided. You can set it later with `cckit install --api-key <key>`')
77
- }
78
- }
79
- } else if (opts.apiKey) {
80
- // --yes mode: use CLI flag if provided
81
- apiKey = opts.apiKey
82
- }
83
- // --yes mode without --api-key: keep existing
84
-
85
- // 3. Model — always prompt for confirmation when not in --yes mode
86
- let modelId = opts.modelId
87
- if (!opts.yes) {
88
- const input = await askFn(' Enter model (default: ' + opts.modelId + '): ')
89
- if (input && input.trim()) {
90
- modelId = input.trim()
91
- }
92
- }
93
-
94
- // 4. Model env vars
95
- const modelEnvEntries = {}
96
- for (const key of opts.modelEnvKeys) {
97
- modelEnvEntries[key] = modelId
98
- }
99
-
100
- // 5. Merge and write settings.json
101
- const merged = {
102
- ...existing,
103
- env: {
104
- ...existingEnv,
105
- ANTHROPIC_BASE_URL: apiUrl,
106
- ...(apiKey ? { ANTHROPIC_AUTH_TOKEN: apiKey } : {}),
107
- ...modelEnvEntries,
108
- },
109
- }
110
-
111
- await fse.ensureDir(path.dirname(settingsPath))
112
- await fse.writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8')
113
-
114
- // 6. Check and prompt for hasCompletedOnboarding in ~/.claude.json
115
- if (!opts.yes) {
116
- await checkAndPromptOnboarding(claudeJsonPath, logger, askFn)
117
- }
118
-
119
- const maskedKey = apiKey ? `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}` : '(not set)'
120
-
121
- logger.info(` API URL: ${apiUrl}`)
122
- logger.info(` API Key: ${maskedKey}`)
123
- logger.info(` Model: ${modelId}`)
124
- logger.info(` Settings: ${settingsPath}`)
125
-
126
- return {
127
- stepId: 'configure-user',
128
- name: 'Configure User Settings',
129
- success: true,
130
- skipped: false,
131
- details: {
132
- apiUrl,
133
- hasApiKey: Boolean(apiKey),
134
- modelId,
135
- settingsPath,
136
- },
137
- }
138
- }
139
-
140
- /**
141
- * Check if ~/.claude.json has hasCompletedOnboarding and prompt if missing.
142
- *
143
- * @param {string} claudeJsonPath - Path to ~/.claude.json
144
- * @param {object} logger
145
- * @param {function} askFn
146
- * @returns {Promise<void>}
147
- */
148
- async function checkAndPromptOnboarding(claudeJsonPath, logger, askFn) {
149
- let claudeJson = {}
150
- let fileExisted = false
151
-
152
- try {
153
- const raw = await fse.readFile(claudeJsonPath, 'utf8')
154
- claudeJson = JSON.parse(raw)
155
- fileExisted = true
156
- } catch (err) {
157
- if (err.code !== 'ENOENT') {
158
- logger.warn(`Failed to read ${claudeJsonPath}: ${err.message}`)
159
- return
160
- }
161
- // File doesn't exist - will create new
162
- }
163
-
164
- // Check if hasCompletedOnboarding already exists
165
- if ('hasCompletedOnboarding' in claudeJson) {
166
- return
167
- }
168
-
169
- // Prompt user
170
- const input = await askFn(' Add hasCompletedOnboarding: true to ~/.claude.json? (Y/n): ')
171
- const answer = (input || '').trim().toLowerCase()
172
-
173
- // Default to yes if empty input
174
- if (answer === '' || answer === 'y' || answer === 'yes') {
175
- claudeJson.hasCompletedOnboarding = true
176
- await fse.writeFile(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + '\n', 'utf8')
177
- logger.info(` Added hasCompletedOnboarding: true to ~/.claude.json`)
178
- } else {
179
- logger.info(` Skipped adding hasCompletedOnboarding`)
180
- }
181
- }
1
+ /**
2
+ * Step: Configure user-level settings in ~/.claude/settings.json.
3
+ *
4
+ * Writes API URL, API key (ANTHROPIC_AUTH_TOKEN), and model env vars.
5
+ * User-level settings are separate from project-level .claude/settings.json.
6
+ */
7
+
8
+ import path from 'node:path'
9
+ import os from 'node:os'
10
+ import fse from 'fs-extra'
11
+ import { askQuestion } from '../utils/prompt.js'
12
+
13
+ /**
14
+ * @param {object} opts
15
+ * @param {string} opts.apiUrl - API base URL (default from registry)
16
+ * @param {string} [opts.apiKey] - API key from CLI flag
17
+ * @param {boolean} [opts.forceApiKey] - Force overwrite existing key
18
+ * @param {string} opts.modelId - Resolved model ID (e.g. "MiniMax-M2.5")
19
+ * @param {string[]} opts.modelEnvKeys - Env keys to set for model
20
+ * @param {boolean} [opts.yes] - Non-interactive mode
21
+ * @param {boolean} opts.skip - Skip this step
22
+ * @param {object} logger
23
+ * @param {object} [_deps] - Injectable dependencies for testing
24
+ * @returns {Promise<import('../core/orchestrator.js').StepResult>}
25
+ */
26
+ export async function configureUser(opts, logger, _deps = {}) {
27
+ if (opts.skip) {
28
+ return { stepId: 'configure-user', name: 'Configure User Settings', success: true, skipped: true, details: {} }
29
+ }
30
+
31
+ const { askFn = askQuestion } = _deps
32
+ const settingsPath = _deps.settingsPath || path.join(os.homedir(), '.claude', 'settings.json')
33
+ const claudeJsonPath = _deps.claudeJsonPath || path.join(os.homedir(), '.claude.json')
34
+
35
+ // Read existing user settings
36
+ let existing = {}
37
+ try {
38
+ const raw = await fse.readFile(settingsPath, 'utf8')
39
+ existing = JSON.parse(raw)
40
+ } catch (err) {
41
+ if (err.code === 'ENOENT') {
42
+ // File doesn't exist — start fresh
43
+ } else {
44
+ // Other error (permission, corrupt, etc.) — log and start fresh
45
+ logger.warn(`Failed to read ${settingsPath}: ${err.message}, using defaults`)
46
+ }
47
+ }
48
+
49
+ const existingEnv = existing.env || {}
50
+
51
+ // 1. API URL — always prompt for confirmation when not in --yes mode
52
+ let apiUrl = opts.apiUrl
53
+ if (!opts.yes) {
54
+ const input = await askFn(' Enter API URL (default: ' + opts.apiUrl + '): ')
55
+ if (input && input.trim()) {
56
+ apiUrl = input.trim()
57
+ }
58
+ }
59
+
60
+ // 2. API Key — prompt when not in --yes mode
61
+ let apiKey = existingEnv.ANTHROPIC_AUTH_TOKEN || ''
62
+ const hasExistingKey = Boolean(apiKey)
63
+
64
+ if (!opts.yes) {
65
+ // Use CLI value if provided, otherwise prompt
66
+ if (opts.apiKey) {
67
+ apiKey = opts.apiKey
68
+ } else {
69
+ const prompt = hasExistingKey
70
+ ? ' API Key already exists. Enter new key to replace (empty to keep): '
71
+ : ' Enter your API Key (empty to skip): '
72
+ const input = await askFn(prompt)
73
+ if (input) {
74
+ apiKey = input
75
+ } else if (!hasExistingKey) {
76
+ logger.warn('No API Key provided. You can set it later with `cckit install --api-key <key>`')
77
+ }
78
+ }
79
+ } else if (opts.apiKey) {
80
+ // --yes mode: use CLI flag if provided
81
+ apiKey = opts.apiKey
82
+ }
83
+ // --yes mode without --api-key: keep existing
84
+
85
+ // 3. Model — always prompt for confirmation when not in --yes mode
86
+ let modelId = opts.modelId
87
+ if (!opts.yes) {
88
+ const input = await askFn(' Enter model (default: ' + opts.modelId + '): ')
89
+ if (input && input.trim()) {
90
+ modelId = input.trim()
91
+ }
92
+ }
93
+
94
+ // 4. Model env vars
95
+ const modelEnvEntries = {}
96
+ for (const key of opts.modelEnvKeys) {
97
+ modelEnvEntries[key] = modelId
98
+ }
99
+
100
+ // 5. Merge and write settings.json
101
+ const merged = {
102
+ ...existing,
103
+ env: {
104
+ ...existingEnv,
105
+ ANTHROPIC_BASE_URL: apiUrl,
106
+ ...(apiKey ? { ANTHROPIC_AUTH_TOKEN: apiKey } : {}),
107
+ ...modelEnvEntries,
108
+ },
109
+ }
110
+
111
+ await fse.ensureDir(path.dirname(settingsPath))
112
+ await fse.writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8')
113
+
114
+ // 6. Check and prompt for hasCompletedOnboarding in ~/.claude.json
115
+ if (!opts.yes) {
116
+ await checkAndPromptOnboarding(claudeJsonPath, logger, askFn)
117
+ }
118
+
119
+ const maskedKey = apiKey ? `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}` : '(not set)'
120
+
121
+ logger.info(` API URL: ${apiUrl}`)
122
+ logger.info(` API Key: ${maskedKey}`)
123
+ logger.info(` Model: ${modelId}`)
124
+ logger.info(` Settings: ${settingsPath}`)
125
+
126
+ return {
127
+ stepId: 'configure-user',
128
+ name: 'Configure User Settings',
129
+ success: true,
130
+ skipped: false,
131
+ details: {
132
+ apiUrl,
133
+ hasApiKey: Boolean(apiKey),
134
+ modelId,
135
+ settingsPath,
136
+ },
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Check if ~/.claude.json has hasCompletedOnboarding and prompt if missing.
142
+ *
143
+ * @param {string} claudeJsonPath - Path to ~/.claude.json
144
+ * @param {object} logger
145
+ * @param {function} askFn
146
+ * @returns {Promise<void>}
147
+ */
148
+ async function checkAndPromptOnboarding(claudeJsonPath, logger, askFn) {
149
+ let claudeJson = {}
150
+ let fileExisted = false
151
+
152
+ try {
153
+ const raw = await fse.readFile(claudeJsonPath, 'utf8')
154
+ claudeJson = JSON.parse(raw)
155
+ fileExisted = true
156
+ } catch (err) {
157
+ if (err.code !== 'ENOENT') {
158
+ logger.warn(`Failed to read ${claudeJsonPath}: ${err.message}`)
159
+ return
160
+ }
161
+ // File doesn't exist - will create new
162
+ }
163
+
164
+ // Check if hasCompletedOnboarding already exists
165
+ if ('hasCompletedOnboarding' in claudeJson) {
166
+ return
167
+ }
168
+
169
+ // Prompt user
170
+ const input = await askFn(' Add hasCompletedOnboarding: true to ~/.claude.json? (Y/n): ')
171
+ const answer = (input || '').trim().toLowerCase()
172
+
173
+ // Default to yes if empty input
174
+ if (answer === '' || answer === 'y' || answer === 'yes') {
175
+ claudeJson.hasCompletedOnboarding = true
176
+ await fse.writeFile(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + '\n', 'utf8')
177
+ logger.info(` Added hasCompletedOnboarding: true to ~/.claude.json`)
178
+ } else {
179
+ logger.info(` Skipped adding hasCompletedOnboarding`)
180
+ }
181
+ }
@@ -1,97 +1,97 @@
1
- /**
2
- * Step: Enable Claude Code plugins in `.claude/settings.json`.
3
- *
4
- * Writes `enabledPlugins`, `extraKnownMarketplaces`, and environment
5
- * defaults via read-merge-write to preserve existing user settings.
6
- */
7
-
8
- import path from 'node:path'
9
- import fse from 'fs-extra'
10
-
11
- const ENV_DEFAULTS = {
12
- DISABLE_BUG_COMMAND: '1',
13
- DISABLE_ERROR_REPORTING: '1',
14
- DISABLE_TELEMETRY: '1',
15
- MCP_TIMEOUT: '60000',
16
- MAX_THINKING_TOKENS: '31999',
17
- CLAUDE_CODE_MAX_OUTPUT_TOKENS: '64000',
18
- }
19
-
20
- /**
21
- * @param {object} opts
22
- * @param {string} opts.targetDir - Project root
23
- * @param {Array<{id: string, def: object}>} opts.plugins - Resolved plugins
24
- * @param {boolean} opts.skip - Skip this step
25
- * @param {object} logger
26
- * @returns {Promise<import('../core/orchestrator.js').StepResult>}
27
- */
28
- export async function enablePlugins(opts, logger) {
29
- if (opts.skip) {
30
- return { stepId: 'enable-plugins', name: 'Enable Plugins', success: true, skipped: true, details: {} }
31
- }
32
-
33
- const settingsPath = path.join(opts.targetDir, '.claude', 'settings.json')
34
- let existing = {}
35
-
36
- try {
37
- const raw = await fse.readFile(settingsPath, 'utf8')
38
- existing = JSON.parse(raw)
39
- } catch (err) {
40
- if (err.code === 'ENOENT') {
41
- // No existing settings — start fresh
42
- } else {
43
- logger.warn(`settings.json parse error, backing up and recreating: ${err.message}`)
44
- const backupPath = `${settingsPath}.bak.${Date.now()}`
45
- await fse.copy(settingsPath, backupPath)
46
- }
47
- existing = {}
48
- }
49
-
50
- const newPlugins = {}
51
- const newMarketplaces = {}
52
-
53
- for (const { id, def } of opts.plugins) {
54
- const key = `${id}@${def.marketplace}`
55
- newPlugins[key] = true
56
-
57
- if (def.marketplaceSource && !newMarketplaces[def.marketplace]) {
58
- newMarketplaces[def.marketplace] = { source: { ...def.marketplaceSource } }
59
- }
60
- }
61
-
62
- const merged = {
63
- ...existing,
64
- env: {
65
- ...(existing.env || {}),
66
- ...ENV_DEFAULTS,
67
- },
68
- enabledPlugins: {
69
- ...(existing.enabledPlugins || {}),
70
- ...newPlugins,
71
- },
72
- }
73
-
74
- if (Object.keys(newMarketplaces).length > 0) {
75
- merged.extraKnownMarketplaces = {
76
- ...(existing.extraKnownMarketplaces || {}),
77
- ...newMarketplaces,
78
- }
79
- }
80
-
81
- await fse.ensureDir(path.dirname(settingsPath))
82
- await fse.writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8')
83
-
84
- const enabledKeys = Object.keys(newPlugins)
85
- logger.debug(`Enabled ${enabledKeys.length} plugins in settings.json`)
86
-
87
- return {
88
- stepId: 'enable-plugins',
89
- name: 'Enable Plugins',
90
- success: true,
91
- skipped: false,
92
- details: {
93
- enabledKeys,
94
- marketplaces: Object.keys(newMarketplaces),
95
- },
96
- }
97
- }
1
+ /**
2
+ * Step: Enable Claude Code plugins in `.claude/settings.json`.
3
+ *
4
+ * Writes `enabledPlugins`, `extraKnownMarketplaces`, and environment
5
+ * defaults via read-merge-write to preserve existing user settings.
6
+ */
7
+
8
+ import path from 'node:path'
9
+ import fse from 'fs-extra'
10
+
11
+ const ENV_DEFAULTS = {
12
+ DISABLE_BUG_COMMAND: '1',
13
+ DISABLE_ERROR_REPORTING: '1',
14
+ DISABLE_TELEMETRY: '1',
15
+ MCP_TIMEOUT: '60000',
16
+ MAX_THINKING_TOKENS: '31999',
17
+ CLAUDE_CODE_MAX_OUTPUT_TOKENS: '64000',
18
+ }
19
+
20
+ /**
21
+ * @param {object} opts
22
+ * @param {string} opts.targetDir - Project root
23
+ * @param {Array<{id: string, def: object}>} opts.plugins - Resolved plugins
24
+ * @param {boolean} opts.skip - Skip this step
25
+ * @param {object} logger
26
+ * @returns {Promise<import('../core/orchestrator.js').StepResult>}
27
+ */
28
+ export async function enablePlugins(opts, logger) {
29
+ if (opts.skip) {
30
+ return { stepId: 'enable-plugins', name: 'Enable Plugins', success: true, skipped: true, details: {} }
31
+ }
32
+
33
+ const settingsPath = path.join(opts.targetDir, '.claude', 'settings.json')
34
+ let existing = {}
35
+
36
+ try {
37
+ const raw = await fse.readFile(settingsPath, 'utf8')
38
+ existing = JSON.parse(raw)
39
+ } catch (err) {
40
+ if (err.code === 'ENOENT') {
41
+ // No existing settings — start fresh
42
+ } else {
43
+ logger.warn(`settings.json parse error, backing up and recreating: ${err.message}`)
44
+ const backupPath = `${settingsPath}.bak.${Date.now()}`
45
+ await fse.copy(settingsPath, backupPath)
46
+ }
47
+ existing = {}
48
+ }
49
+
50
+ const newPlugins = {}
51
+ const newMarketplaces = {}
52
+
53
+ for (const { id, def } of opts.plugins) {
54
+ const key = `${id}@${def.marketplace}`
55
+ newPlugins[key] = true
56
+
57
+ if (def.marketplaceSource && !newMarketplaces[def.marketplace]) {
58
+ newMarketplaces[def.marketplace] = { source: { ...def.marketplaceSource } }
59
+ }
60
+ }
61
+
62
+ const merged = {
63
+ ...existing,
64
+ env: {
65
+ ...(existing.env || {}),
66
+ ...ENV_DEFAULTS,
67
+ },
68
+ enabledPlugins: {
69
+ ...(existing.enabledPlugins || {}),
70
+ ...newPlugins,
71
+ },
72
+ }
73
+
74
+ if (Object.keys(newMarketplaces).length > 0) {
75
+ merged.extraKnownMarketplaces = {
76
+ ...(existing.extraKnownMarketplaces || {}),
77
+ ...newMarketplaces,
78
+ }
79
+ }
80
+
81
+ await fse.ensureDir(path.dirname(settingsPath))
82
+ await fse.writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8')
83
+
84
+ const enabledKeys = Object.keys(newPlugins)
85
+ logger.debug(`Enabled ${enabledKeys.length} plugins in settings.json`)
86
+
87
+ return {
88
+ stepId: 'enable-plugins',
89
+ name: 'Enable Plugins',
90
+ success: true,
91
+ skipped: false,
92
+ details: {
93
+ enabledKeys,
94
+ marketplaces: Object.keys(newMarketplaces),
95
+ },
96
+ }
97
+ }