@claude-code-hooks/cli 0.1.8 → 0.1.10
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/package.json +2 -2
- package/src/cli.js +225 -115
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-code-hooks/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Wizard CLI to set up and manage @claude-code-hooks packages for Claude Code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/beefiker/claude-code-hooks/tree/main/packages/cli",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@claude-code-hooks/core": "0.1.3",
|
|
31
31
|
"@claude-code-hooks/security": "0.1.7",
|
|
32
32
|
"@claude-code-hooks/secrets": "0.1.8",
|
|
33
|
-
"@claude-code-hooks/sound": "0.2.
|
|
33
|
+
"@claude-code-hooks/sound": "0.2.11",
|
|
34
34
|
"@claude-code-hooks/notification": "0.1.5"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import readline from 'node:readline';
|
|
4
5
|
import fs from 'node:fs/promises';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
|
|
@@ -16,6 +17,11 @@ import {
|
|
|
16
17
|
spinner
|
|
17
18
|
} from '@clack/prompts';
|
|
18
19
|
|
|
20
|
+
// Enable keypress events so we can intercept Backspace
|
|
21
|
+
if (process.stdin.isTTY) {
|
|
22
|
+
readline.emitKeypressEvents(process.stdin);
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
import {
|
|
20
26
|
ansi as pc,
|
|
21
27
|
configPathForScope,
|
|
@@ -25,7 +31,9 @@ import {
|
|
|
25
31
|
configFilePath,
|
|
26
32
|
readProjectConfig,
|
|
27
33
|
writeProjectConfig,
|
|
28
|
-
removeLegacyClaudeSoundHooks
|
|
34
|
+
removeLegacyClaudeSoundHooks,
|
|
35
|
+
t,
|
|
36
|
+
detectLanguage
|
|
29
37
|
} from '@claude-code-hooks/core';
|
|
30
38
|
|
|
31
39
|
import { buildSettingsSnippet } from './snippet.js';
|
|
@@ -37,14 +45,37 @@ import { planInteractiveSetup as planSecretsSetup } from '@claude-code-hooks/sec
|
|
|
37
45
|
import { planInteractiveSetup as planSoundSetup } from '@claude-code-hooks/sound/src/plan.js';
|
|
38
46
|
import { planInteractiveSetup as planNotificationSetup } from '@claude-code-hooks/notification/src/plan.js';
|
|
39
47
|
|
|
40
|
-
function dieCancelled(msg
|
|
41
|
-
cancel(msg);
|
|
48
|
+
function dieCancelled(msg) {
|
|
49
|
+
cancel(msg ?? t('cli.cancelled'));
|
|
42
50
|
process.exit(0);
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Run a prompt with Backspace = go back. When Backspace is pressed, aborts and returns
|
|
55
|
+
* { wentBack: true }. ESC still exits. Caller must handle wentBack by continuing the loop.
|
|
56
|
+
* @param {AbortController} controller
|
|
57
|
+
* @param {() => Promise<T>} runPrompt - async function that runs the prompt (receives no args)
|
|
58
|
+
* @returns {Promise<{ result: T; wentBack: boolean }>}
|
|
59
|
+
*/
|
|
60
|
+
async function withBackspaceBack(controller, runPrompt) {
|
|
61
|
+
let wentBack = false;
|
|
62
|
+
const handler = (_str, key) => {
|
|
63
|
+
if (key?.name === 'backspace') {
|
|
64
|
+
wentBack = true;
|
|
65
|
+
controller.abort();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
process.stdin.on('keypress', handler);
|
|
69
|
+
try {
|
|
70
|
+
const result = await runPrompt();
|
|
71
|
+
return { result, wentBack };
|
|
72
|
+
} finally {
|
|
73
|
+
process.stdin.off('keypress', handler);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
45
77
|
function usage(exitCode = 0) {
|
|
46
|
-
process.stdout.write(
|
|
47
|
-
claude-code-hooks\n\nUsage:\n claude-code-hooks\n npx @claude-code-hooks/cli@latest\n\nWhat it does:\n - Update Claude Code settings (global), or generate a project-only config + pasteable snippet.\n`);
|
|
78
|
+
process.stdout.write(t('cli.usage') + '\n');
|
|
48
79
|
process.exit(exitCode);
|
|
49
80
|
}
|
|
50
81
|
|
|
@@ -67,7 +98,7 @@ async function ensureProjectOnlyConfig(projectDir, selected, perPackageConfig) {
|
|
|
67
98
|
|
|
68
99
|
async function maybeWriteSnippet(projectDir, snippetObj) {
|
|
69
100
|
const ok = await confirm({
|
|
70
|
-
message:
|
|
101
|
+
message: t('cli.writeSnippet'),
|
|
71
102
|
initialValue: false
|
|
72
103
|
});
|
|
73
104
|
if (isCancel(ok)) return;
|
|
@@ -75,130 +106,209 @@ async function maybeWriteSnippet(projectDir, snippetObj) {
|
|
|
75
106
|
|
|
76
107
|
const filePath = path.join(projectDir, 'claude-code-hooks.snippet.json');
|
|
77
108
|
await fs.writeFile(filePath, JSON.stringify(snippetObj, null, 2) + '\n');
|
|
78
|
-
note(filePath, '
|
|
109
|
+
note(filePath, t('cli.wroteSnippet'));
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
async function main() {
|
|
82
113
|
const args = process.argv.slice(2);
|
|
83
114
|
if (args.includes('-h') || args.includes('--help')) usage(0);
|
|
84
115
|
|
|
116
|
+
// i18n: --lang ko or env CLAUDE_CODE_HOOKS_LANG
|
|
117
|
+
const langIdx = args.indexOf('--lang');
|
|
118
|
+
if (langIdx !== -1 && args[langIdx + 1]) {
|
|
119
|
+
process.env.CLAUDE_CODE_HOOKS_LANG = args[langIdx + 1].toLowerCase().slice(0, 2);
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
const projectDir = process.cwd();
|
|
86
123
|
|
|
87
124
|
intro('claude-code-hooks');
|
|
125
|
+
note(t('cli.navHint'), t('cli.navTitle'));
|
|
88
126
|
|
|
89
|
-
// ── Step 1–3: action, target, packages (
|
|
127
|
+
// ── Step 1–3: action, target, packages (Backspace = go to previous step) ──
|
|
90
128
|
let action;
|
|
91
129
|
let target;
|
|
92
130
|
let selected;
|
|
131
|
+
let step = 1;
|
|
132
|
+
/** @type {{ security: unknown; secrets: unknown; sound: unknown; notification: unknown }} */
|
|
133
|
+
let perPackage = { security: null, secrets: null, sound: null, notification: null };
|
|
93
134
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{ value: 'exit', label: 'Exit' }
|
|
101
|
-
]
|
|
102
|
-
});
|
|
103
|
-
if (isCancel(action) || action === 'exit') dieCancelled('Bye');
|
|
104
|
-
|
|
105
|
-
target = await select({
|
|
106
|
-
message: `${pc.dim('Step 2/5')} Choose a target`,
|
|
107
|
-
options: [
|
|
108
|
-
{ value: 'global', label: `Global (default): ${pc.dim('~/.claude/settings.json')}` },
|
|
109
|
-
{ value: 'projectOnly', label: `Project-only: write ${pc.bold(CONFIG_FILENAME)} + print a snippet` },
|
|
110
|
-
{ value: '__back__', label: 'Back' }
|
|
111
|
-
]
|
|
112
|
-
});
|
|
113
|
-
if (isCancel(target)) dieCancelled();
|
|
114
|
-
if (target === '__back__') continue;
|
|
115
|
-
|
|
116
|
-
selected = await multiselect({
|
|
117
|
-
message: `${pc.dim('Step 3/5')} Select packages`,
|
|
118
|
-
options: [
|
|
119
|
-
{ value: 'security', label: '@claude-code-hooks/security', hint: 'Warn/block risky commands' },
|
|
120
|
-
{ value: 'secrets', label: '@claude-code-hooks/secrets', hint: 'Detect secret-like tokens' },
|
|
121
|
-
{ value: 'sound', label: '@claude-code-hooks/sound', hint: 'Play sounds for key events' },
|
|
122
|
-
{ value: 'notification', label: '@claude-code-hooks/notification', hint: 'OS notifications for key events' }
|
|
123
|
-
],
|
|
124
|
-
required: true
|
|
125
|
-
});
|
|
126
|
-
if (isCancel(selected)) dieCancelled();
|
|
135
|
+
const packageOptions = [
|
|
136
|
+
{ value: 'security', label: t('cli.pkgSecurity'), hint: t('cli.pkgSecurityHint') },
|
|
137
|
+
{ value: 'secrets', label: t('cli.pkgSecrets'), hint: t('cli.pkgSecretsHint') },
|
|
138
|
+
{ value: 'sound', label: t('cli.pkgSound'), hint: t('cli.pkgSoundHint') },
|
|
139
|
+
{ value: 'notification', label: t('cli.pkgNotification'), hint: t('cli.pkgNotificationHint') }
|
|
140
|
+
];
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
142
|
+
while (true) {
|
|
143
|
+
if (step === 1) {
|
|
144
|
+
action = await select({
|
|
145
|
+
message: `${pc.dim(t('cli.stepFormat', { n: 1 }))} ${t('cli.step1ChooseAction')}`,
|
|
146
|
+
options: [
|
|
147
|
+
{ value: 'setup', label: t('cli.actionSetup') },
|
|
148
|
+
{ value: 'uninstall', label: t('cli.actionUninstall') },
|
|
149
|
+
{ value: 'exit', label: t('cli.actionExit') }
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
if (isCancel(action) || action === 'exit') dieCancelled(t('cli.bye'));
|
|
153
|
+
step = 2;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (step === 2) {
|
|
157
|
+
const targetCtrl = new AbortController();
|
|
158
|
+
const { result: targetResult, wentBack: targetBack } = await withBackspaceBack(targetCtrl, () =>
|
|
159
|
+
select({
|
|
160
|
+
message: `${pc.dim(t('cli.stepFormat', { n: 2 }))} ${t('cli.step2ChooseTarget')}`,
|
|
161
|
+
options: [
|
|
162
|
+
{ value: 'global', label: t('cli.targetGlobal') },
|
|
163
|
+
{ value: 'projectOnly', label: t('cli.targetProjectOnly').replace(CONFIG_FILENAME, pc.bold(CONFIG_FILENAME)) }
|
|
164
|
+
],
|
|
165
|
+
signal: targetCtrl.signal
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
if (targetBack) {
|
|
169
|
+
step = 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (isCancel(targetResult)) dieCancelled();
|
|
173
|
+
target = targetResult;
|
|
174
|
+
step = 3;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (step === 3) {
|
|
178
|
+
const pkgsCtrl = new AbortController();
|
|
179
|
+
const { result: pkgsResult, wentBack: pkgsBack } = await withBackspaceBack(pkgsCtrl, () =>
|
|
180
|
+
multiselect({
|
|
181
|
+
message: `${pc.dim(t('cli.stepFormat', { n: 3 }))} ${t('cli.step3SelectPackages')}`,
|
|
182
|
+
options: packageOptions,
|
|
183
|
+
required: true,
|
|
184
|
+
signal: pkgsCtrl.signal
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
if (pkgsBack) {
|
|
188
|
+
step = 2;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (isCancel(pkgsResult)) dieCancelled();
|
|
192
|
+
selected = pkgsResult;
|
|
193
|
+
step = 4;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (step === 4) {
|
|
197
|
+
const proceedCtrl = new AbortController();
|
|
198
|
+
const { result: proceedResult, wentBack: proceedBack } = await withBackspaceBack(proceedCtrl, () =>
|
|
199
|
+
confirm({ message: t('cli.configureNow'), initialValue: true, signal: proceedCtrl.signal })
|
|
200
|
+
);
|
|
201
|
+
if (proceedBack) {
|
|
202
|
+
step = 3;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (isCancel(proceedResult)) dieCancelled();
|
|
206
|
+
if (!proceedResult) {
|
|
207
|
+
step = 2;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
step = 5;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (step !== 5) continue;
|
|
214
|
+
|
|
215
|
+
// Build per-package plan/config
|
|
216
|
+
perPackage = { security: null, secrets: null, sound: null, notification: null };
|
|
217
|
+
|
|
218
|
+
const packageDescs = {
|
|
219
|
+
security: t('cli.pkgSecurityHint'),
|
|
220
|
+
secrets: t('cli.pkgSecretsHint'),
|
|
221
|
+
sound: t('cli.pkgSoundHint'),
|
|
222
|
+
notification: t('cli.pkgNotificationHint')
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const formatPackageList = (/** @type {string | null} */ highlightKey) =>
|
|
226
|
+
selected
|
|
227
|
+
.map(
|
|
228
|
+
(k) => {
|
|
229
|
+
const desc = pc.dim(packageDescs[k] || '');
|
|
230
|
+
const label = highlightKey === k ? pc.cyan(pc.bold(k)) : pc.bold(k);
|
|
231
|
+
return `${label}: ${desc}`;
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
.join('\n');
|
|
235
|
+
|
|
236
|
+
// ── Step 4/5: configure (highlight current package) ──
|
|
237
|
+
if (selected.includes('security')) {
|
|
238
|
+
note(formatPackageList('security'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
|
|
239
|
+
perPackage.security = await planSecuritySetup({ action, projectDir, ui: 'umbrella' });
|
|
240
|
+
}
|
|
241
|
+
if (selected.includes('secrets')) {
|
|
242
|
+
note(formatPackageList('secrets'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
|
|
243
|
+
perPackage.secrets = await planSecretsSetup({ action, projectDir, ui: 'umbrella' });
|
|
244
|
+
}
|
|
245
|
+
if (selected.includes('sound')) {
|
|
246
|
+
note(formatPackageList('sound'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
|
|
247
|
+
perPackage.sound = await planSoundSetup({ action, projectDir, ui: 'umbrella' });
|
|
248
|
+
}
|
|
249
|
+
if (selected.includes('notification')) {
|
|
250
|
+
note(formatPackageList('notification'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
|
|
251
|
+
perPackage.notification = await planNotificationSetup({ action, projectDir, ui: 'umbrella' });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Step 5/5: review ──
|
|
255
|
+
const files = [];
|
|
256
|
+
if (target === 'global') files.push(configPathForScope('global', projectDir));
|
|
257
|
+
if (target === 'projectOnly') {
|
|
258
|
+
files.push(path.join(projectDir, CONFIG_FILENAME));
|
|
259
|
+
files.push(path.join(projectDir, 'claude-code-hooks.snippet.json (optional)'));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function summarizePlan(key, plan) {
|
|
263
|
+
if (!plan) return `${key}: ${t('cli.summarySkipped')}`;
|
|
264
|
+
if (action === 'uninstall') return `${key}: ${t('cli.summaryRemoveHooks')}`;
|
|
265
|
+
|
|
266
|
+
const events = plan.snippetHooks ? Object.keys(plan.snippetHooks) : [];
|
|
267
|
+
const list = events.slice(0, 5);
|
|
268
|
+
const tail = events.length > 5 ? ` +${events.length - 5} more` : '';
|
|
269
|
+
return `${key}: ${t('cli.summaryEvents', { count: events.length })}${events.length ? ` (${list.join(', ')}${tail})` : ''}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
note(
|
|
273
|
+
[
|
|
274
|
+
`${pc.dim(t('cli.stepFormat', { n: 5 }))} ${t('cli.step5Review')}`,
|
|
275
|
+
'',
|
|
276
|
+
`${t('cli.reviewAction')}: ${pc.bold(action)}`,
|
|
277
|
+
`${t('cli.reviewTarget')}: ${pc.bold(target === 'global' ? t('cli.reviewTargetGlobal') : t('cli.reviewTargetProjectOnly'))}`,
|
|
278
|
+
'',
|
|
279
|
+
`${pc.bold(t('cli.reviewPackages'))}`,
|
|
280
|
+
...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
|
|
281
|
+
'',
|
|
282
|
+
`${pc.bold(t('cli.reviewFiles'))}`,
|
|
283
|
+
...files.map((f) => ` - ${f}`)
|
|
284
|
+
].join('\n'),
|
|
285
|
+
t('cli.step5Review')
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const applyCtrl = new AbortController();
|
|
289
|
+
const { result: applyResult, wentBack: applyBack } = await withBackspaceBack(applyCtrl, () =>
|
|
290
|
+
select({
|
|
291
|
+
message: t('cli.applyChanges'),
|
|
292
|
+
options: [
|
|
293
|
+
{ value: 'yes', label: t('cli.applyYes') },
|
|
294
|
+
{ value: 'cancel', label: t('cli.applyCancel') }
|
|
295
|
+
],
|
|
296
|
+
signal: applyCtrl.signal
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
if (applyBack) {
|
|
300
|
+
step = 3;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (isCancel(applyResult) || applyResult === 'cancel') dieCancelled(t('cli.noChanges'));
|
|
131
304
|
|
|
132
305
|
break;
|
|
133
306
|
}
|
|
134
307
|
|
|
135
|
-
// Build per-package plan/config
|
|
136
|
-
const perPackage = { security: null, secrets: null, sound: null, notification: null };
|
|
137
|
-
|
|
138
|
-
// ── Step 4/5: configure ──
|
|
139
|
-
note(
|
|
140
|
-
selected
|
|
141
|
-
.map(
|
|
142
|
-
(k) =>
|
|
143
|
-
`${pc.bold(k)}: ${pc.dim(
|
|
144
|
-
{
|
|
145
|
-
security: 'Warn/block risky commands',
|
|
146
|
-
secrets: 'Detect secret-like tokens',
|
|
147
|
-
sound: 'Play sounds for key events',
|
|
148
|
-
notification: 'OS notifications for key events'
|
|
149
|
-
}[k] || ''
|
|
150
|
-
)}`
|
|
151
|
-
)
|
|
152
|
-
.join('\n'),
|
|
153
|
-
`${pc.dim('Step 4/5')} Configure packages`
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
if (selected.includes('security')) perPackage.security = await planSecuritySetup({ action, projectDir, ui: 'umbrella' });
|
|
157
|
-
if (selected.includes('secrets')) perPackage.secrets = await planSecretsSetup({ action, projectDir, ui: 'umbrella' });
|
|
158
|
-
if (selected.includes('sound')) perPackage.sound = await planSoundSetup({ action, projectDir, ui: 'umbrella' });
|
|
159
|
-
if (selected.includes('notification')) perPackage.notification = await planNotificationSetup({ action, projectDir, ui: 'umbrella' });
|
|
160
|
-
|
|
161
|
-
// ── Step 5/5: review ──
|
|
162
|
-
const files = [];
|
|
163
|
-
if (target === 'global') files.push(configPathForScope('global', projectDir));
|
|
164
|
-
if (target === 'projectOnly') {
|
|
165
|
-
files.push(path.join(projectDir, CONFIG_FILENAME));
|
|
166
|
-
files.push(path.join(projectDir, 'claude-code-hooks.snippet.json (optional)'));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function summarizePlan(key, plan) {
|
|
170
|
-
if (!plan) return `${key}: (skipped)`;
|
|
171
|
-
if (action === 'uninstall') return `${key}: remove managed hooks`;
|
|
172
|
-
|
|
173
|
-
const events = plan.snippetHooks ? Object.keys(plan.snippetHooks) : [];
|
|
174
|
-
const list = events.slice(0, 5);
|
|
175
|
-
const tail = events.length > 5 ? ` +${events.length - 5} more` : '';
|
|
176
|
-
return `${key}: ${events.length} event(s)${events.length ? ` (${list.join(', ')}${tail})` : ''}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
note(
|
|
180
|
-
[
|
|
181
|
-
`${pc.dim('Step 5/5')} Review`,
|
|
182
|
-
'',
|
|
183
|
-
`Action: ${pc.bold(action)}`,
|
|
184
|
-
`Target: ${pc.bold(target === 'global' ? 'global settings' : 'project-only')}`,
|
|
185
|
-
'',
|
|
186
|
-
`${pc.bold('Packages')}`,
|
|
187
|
-
...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
|
|
188
|
-
'',
|
|
189
|
-
`${pc.bold('Files')}`,
|
|
190
|
-
...files.map((f) => ` - ${f}`)
|
|
191
|
-
].join('\n'),
|
|
192
|
-
'Review'
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const ok = await confirm({ message: 'Apply changes?', initialValue: true });
|
|
196
|
-
if (isCancel(ok) || !ok) dieCancelled('No changes made');
|
|
197
|
-
|
|
198
308
|
if (target === 'projectOnly') {
|
|
199
309
|
// Write project config
|
|
200
310
|
const s = spinner();
|
|
201
|
-
s.start('
|
|
311
|
+
s.start(t('cli.writingProjectConfig'));
|
|
202
312
|
|
|
203
313
|
// perPackage.*.projectConfigSection is shaped for claude-code-hooks.config.json sections.
|
|
204
314
|
const projectCfg = await ensureProjectOnlyConfig(projectDir, selected, {
|
|
@@ -220,12 +330,12 @@ async function main() {
|
|
|
220
330
|
}
|
|
221
331
|
});
|
|
222
332
|
|
|
223
|
-
s.stop('
|
|
333
|
+
s.stop(t('cli.done'));
|
|
224
334
|
|
|
225
|
-
note(JSON.stringify(snippetObj, null, 2), '
|
|
335
|
+
note(JSON.stringify(snippetObj, null, 2), t('cli.pasteSnippet'));
|
|
226
336
|
await maybeWriteSnippet(projectDir, snippetObj);
|
|
227
337
|
|
|
228
|
-
outro(
|
|
338
|
+
outro(`${t('cli.projectConfigWritten')}: ${pc.bold(configFilePath(projectDir))}`);
|
|
229
339
|
return;
|
|
230
340
|
}
|
|
231
341
|
|
|
@@ -233,14 +343,14 @@ async function main() {
|
|
|
233
343
|
const settingsPath = configPathForScope('global', projectDir);
|
|
234
344
|
const res = await readJsonIfExists(settingsPath);
|
|
235
345
|
if (!res.ok) {
|
|
236
|
-
cancel(
|
|
346
|
+
cancel(t('cli.couldNotReadJson', { path: settingsPath }));
|
|
237
347
|
process.exit(1);
|
|
238
348
|
}
|
|
239
349
|
|
|
240
350
|
let settings = res.value;
|
|
241
351
|
|
|
242
352
|
const s = spinner();
|
|
243
|
-
s.start('
|
|
353
|
+
s.start(t('cli.applyingChanges'));
|
|
244
354
|
|
|
245
355
|
for (const key of selected) {
|
|
246
356
|
const plan = perPackage[key];
|
|
@@ -263,8 +373,8 @@ async function main() {
|
|
|
263
373
|
});
|
|
264
374
|
}
|
|
265
375
|
|
|
266
|
-
s.stop('
|
|
267
|
-
outro(
|
|
376
|
+
s.stop(t('cli.done'));
|
|
377
|
+
outro(`${t('cli.saved')}: ${pc.bold(settingsPath)}`);
|
|
268
378
|
}
|
|
269
379
|
|
|
270
380
|
main().catch((err) => {
|