@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.
- package/LICENSE +21 -21
- package/bin/cckit.js +3 -3
- package/package.json +53 -53
- package/registry.json +145 -145
- package/src/cli.js +79 -79
- package/src/commands/init.js +174 -174
- package/src/commands/status.js +125 -125
- package/src/commands/update.js +192 -192
- package/src/core/config.js +82 -75
- package/src/core/orchestrator.js +79 -79
- package/src/core/registry.js +60 -60
- package/src/steps/add-plugin.js +148 -116
- package/src/steps/configure-user.js +181 -181
- package/src/steps/enable-plugins.js +97 -97
- package/src/steps/install-bmad.js +85 -85
- package/src/steps/install-mcp.js +70 -70
- package/src/steps/install-rules.js +69 -69
- package/src/steps/install-skills.js +56 -56
- package/src/utils/compare-versions.js +106 -106
- package/src/utils/fs.js +33 -33
- package/src/utils/logger.js +16 -16
- package/src/utils/manifest.js +101 -101
- package/src/utils/prompt.js +41 -41
- package/templates/mcp/claude-code/.mcp.json +40 -40
- package/templates/rules/README.md +103 -103
- package/templates/rules/common/agents.md +49 -49
- package/templates/rules/common/coding-style.md +48 -48
- package/templates/rules/common/development-workflow.md +37 -37
- package/templates/rules/common/git-workflow.md +24 -24
- package/templates/rules/common/hooks.md +30 -30
- package/templates/rules/common/patterns.md +31 -31
- package/templates/rules/common/performance.md +55 -55
- package/templates/rules/common/security.md +29 -29
- package/templates/rules/common/testing.md +29 -29
- package/templates/rules/golang/coding-style.md +32 -32
- package/templates/rules/golang/hooks.md +17 -17
- package/templates/rules/golang/patterns.md +45 -45
- package/templates/rules/golang/security.md +34 -34
- package/templates/rules/golang/testing.md +31 -31
- package/templates/rules/python/coding-style.md +42 -42
- package/templates/rules/python/hooks.md +19 -19
- package/templates/rules/python/patterns.md +39 -39
- package/templates/rules/python/security.md +30 -30
- package/templates/rules/python/testing.md +38 -38
- package/templates/rules/swift/coding-style.md +47 -47
- package/templates/rules/swift/hooks.md +20 -20
- package/templates/rules/swift/patterns.md +66 -66
- package/templates/rules/swift/security.md +33 -33
- package/templates/rules/swift/testing.md +45 -45
- package/templates/rules/typescript/coding-style.md +65 -65
- package/templates/rules/typescript/hooks.md +22 -22
- package/templates/rules/typescript/patterns.md +52 -52
- package/templates/rules/typescript/security.md +28 -28
- 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
|
+
}
|