@claude-code-hooks/cli 0.1.9 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +58 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-code-hooks/cli",
3
- "version": "0.1.9",
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",
package/src/cli.js CHANGED
@@ -31,7 +31,9 @@ import {
31
31
  configFilePath,
32
32
  readProjectConfig,
33
33
  writeProjectConfig,
34
- removeLegacyClaudeSoundHooks
34
+ removeLegacyClaudeSoundHooks,
35
+ t,
36
+ detectLanguage
35
37
  } from '@claude-code-hooks/core';
36
38
 
37
39
  import { buildSettingsSnippet } from './snippet.js';
@@ -43,8 +45,8 @@ import { planInteractiveSetup as planSecretsSetup } from '@claude-code-hooks/sec
43
45
  import { planInteractiveSetup as planSoundSetup } from '@claude-code-hooks/sound/src/plan.js';
44
46
  import { planInteractiveSetup as planNotificationSetup } from '@claude-code-hooks/notification/src/plan.js';
45
47
 
46
- function dieCancelled(msg = 'Cancelled') {
47
- cancel(msg);
48
+ function dieCancelled(msg) {
49
+ cancel(msg ?? t('cli.cancelled'));
48
50
  process.exit(0);
49
51
  }
50
52
 
@@ -73,8 +75,7 @@ async function withBackspaceBack(controller, runPrompt) {
73
75
  }
74
76
 
75
77
  function usage(exitCode = 0) {
76
- process.stdout.write(`\
77
- 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');
78
79
  process.exit(exitCode);
79
80
  }
80
81
 
@@ -97,7 +98,7 @@ async function ensureProjectOnlyConfig(projectDir, selected, perPackageConfig) {
97
98
 
98
99
  async function maybeWriteSnippet(projectDir, snippetObj) {
99
100
  const ok = await confirm({
100
- message: `Write snippet file (${pc.bold('claude-code-hooks.snippet.json')}) to this project?`,
101
+ message: t('cli.writeSnippet'),
101
102
  initialValue: false
102
103
  });
103
104
  if (isCancel(ok)) return;
@@ -105,17 +106,23 @@ async function maybeWriteSnippet(projectDir, snippetObj) {
105
106
 
106
107
  const filePath = path.join(projectDir, 'claude-code-hooks.snippet.json');
107
108
  await fs.writeFile(filePath, JSON.stringify(snippetObj, null, 2) + '\n');
108
- note(filePath, 'Wrote snippet');
109
+ note(filePath, t('cli.wroteSnippet'));
109
110
  }
110
111
 
111
112
  async function main() {
112
113
  const args = process.argv.slice(2);
113
114
  if (args.includes('-h') || args.includes('--help')) usage(0);
114
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
+
115
122
  const projectDir = process.cwd();
116
123
 
117
124
  intro('claude-code-hooks');
118
- note(`${pc.dim('ESC')} to exit • ${pc.dim('Backspace')} to go back`, 'Navigation');
125
+ note(t('cli.navHint'), t('cli.navTitle'));
119
126
 
120
127
  // ── Step 1–3: action, target, packages (Backspace = go to previous step) ──
121
128
  let action;
@@ -126,23 +133,23 @@ async function main() {
126
133
  let perPackage = { security: null, secrets: null, sound: null, notification: null };
127
134
 
128
135
  const packageOptions = [
129
- { value: 'security', label: '@claude-code-hooks/security', hint: 'Warn/block risky commands' },
130
- { value: 'secrets', label: '@claude-code-hooks/secrets', hint: 'Detect secret-like tokens' },
131
- { value: 'sound', label: '@claude-code-hooks/sound', hint: 'Play sounds for key events' },
132
- { value: 'notification', label: '@claude-code-hooks/notification', hint: 'OS notifications for key events' }
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') }
133
140
  ];
134
141
 
135
142
  while (true) {
136
143
  if (step === 1) {
137
144
  action = await select({
138
- message: `${pc.dim('Step 1/5')} Choose an action`,
145
+ message: `${pc.dim(t('cli.stepFormat', { n: 1 }))} ${t('cli.step1ChooseAction')}`,
139
146
  options: [
140
- { value: 'setup', label: 'Install / update packages' },
141
- { value: 'uninstall', label: 'Uninstall (remove managed hooks)' },
142
- { value: 'exit', label: 'Exit' }
147
+ { value: 'setup', label: t('cli.actionSetup') },
148
+ { value: 'uninstall', label: t('cli.actionUninstall') },
149
+ { value: 'exit', label: t('cli.actionExit') }
143
150
  ]
144
151
  });
145
- if (isCancel(action) || action === 'exit') dieCancelled('Bye');
152
+ if (isCancel(action) || action === 'exit') dieCancelled(t('cli.bye'));
146
153
  step = 2;
147
154
  }
148
155
 
@@ -150,10 +157,10 @@ async function main() {
150
157
  const targetCtrl = new AbortController();
151
158
  const { result: targetResult, wentBack: targetBack } = await withBackspaceBack(targetCtrl, () =>
152
159
  select({
153
- message: `${pc.dim('Step 2/5')} Choose a target`,
160
+ message: `${pc.dim(t('cli.stepFormat', { n: 2 }))} ${t('cli.step2ChooseTarget')}`,
154
161
  options: [
155
- { value: 'global', label: `Global (default): ${pc.dim('~/.claude/settings.json')}` },
156
- { value: 'projectOnly', label: `Project-only: write ${pc.bold(CONFIG_FILENAME)} + print a snippet` }
162
+ { value: 'global', label: t('cli.targetGlobal') },
163
+ { value: 'projectOnly', label: t('cli.targetProjectOnly').replace(CONFIG_FILENAME, pc.bold(CONFIG_FILENAME)) }
157
164
  ],
158
165
  signal: targetCtrl.signal
159
166
  })
@@ -171,7 +178,7 @@ async function main() {
171
178
  const pkgsCtrl = new AbortController();
172
179
  const { result: pkgsResult, wentBack: pkgsBack } = await withBackspaceBack(pkgsCtrl, () =>
173
180
  multiselect({
174
- message: `${pc.dim('Step 3/5')} Select packages`,
181
+ message: `${pc.dim(t('cli.stepFormat', { n: 3 }))} ${t('cli.step3SelectPackages')}`,
175
182
  options: packageOptions,
176
183
  required: true,
177
184
  signal: pkgsCtrl.signal
@@ -189,7 +196,7 @@ async function main() {
189
196
  if (step === 4) {
190
197
  const proceedCtrl = new AbortController();
191
198
  const { result: proceedResult, wentBack: proceedBack } = await withBackspaceBack(proceedCtrl, () =>
192
- confirm({ message: 'Configure these packages now?', initialValue: true, signal: proceedCtrl.signal })
199
+ confirm({ message: t('cli.configureNow'), initialValue: true, signal: proceedCtrl.signal })
193
200
  );
194
201
  if (proceedBack) {
195
202
  step = 3;
@@ -209,10 +216,10 @@ async function main() {
209
216
  perPackage = { security: null, secrets: null, sound: null, notification: null };
210
217
 
211
218
  const packageDescs = {
212
- security: 'Warn/block risky commands',
213
- secrets: 'Detect secret-like tokens',
214
- sound: 'Play sounds for key events',
215
- notification: 'OS notifications for key events'
219
+ security: t('cli.pkgSecurityHint'),
220
+ secrets: t('cli.pkgSecretsHint'),
221
+ sound: t('cli.pkgSoundHint'),
222
+ notification: t('cli.pkgNotificationHint')
216
223
  };
217
224
 
218
225
  const formatPackageList = (/** @type {string | null} */ highlightKey) =>
@@ -228,19 +235,19 @@ async function main() {
228
235
 
229
236
  // ── Step 4/5: configure (highlight current package) ──
230
237
  if (selected.includes('security')) {
231
- note(formatPackageList('security'), `${pc.dim('Step 4/5')} Configure packages`);
238
+ note(formatPackageList('security'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
232
239
  perPackage.security = await planSecuritySetup({ action, projectDir, ui: 'umbrella' });
233
240
  }
234
241
  if (selected.includes('secrets')) {
235
- note(formatPackageList('secrets'), `${pc.dim('Step 4/5')} Configure packages`);
242
+ note(formatPackageList('secrets'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
236
243
  perPackage.secrets = await planSecretsSetup({ action, projectDir, ui: 'umbrella' });
237
244
  }
238
245
  if (selected.includes('sound')) {
239
- note(formatPackageList('sound'), `${pc.dim('Step 4/5')} Configure packages`);
246
+ note(formatPackageList('sound'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
240
247
  perPackage.sound = await planSoundSetup({ action, projectDir, ui: 'umbrella' });
241
248
  }
242
249
  if (selected.includes('notification')) {
243
- note(formatPackageList('notification'), `${pc.dim('Step 4/5')} Configure packages`);
250
+ note(formatPackageList('notification'), `${pc.dim(t('cli.stepFormat', { n: 4 }))} ${t('cli.step4Configure')}`);
244
251
  perPackage.notification = await planNotificationSetup({ action, projectDir, ui: 'umbrella' });
245
252
  }
246
253
 
@@ -253,38 +260,38 @@ async function main() {
253
260
  }
254
261
 
255
262
  function summarizePlan(key, plan) {
256
- if (!plan) return `${key}: (skipped)`;
257
- if (action === 'uninstall') return `${key}: remove managed hooks`;
263
+ if (!plan) return `${key}: ${t('cli.summarySkipped')}`;
264
+ if (action === 'uninstall') return `${key}: ${t('cli.summaryRemoveHooks')}`;
258
265
 
259
266
  const events = plan.snippetHooks ? Object.keys(plan.snippetHooks) : [];
260
267
  const list = events.slice(0, 5);
261
268
  const tail = events.length > 5 ? ` +${events.length - 5} more` : '';
262
- return `${key}: ${events.length} event(s)${events.length ? ` (${list.join(', ')}${tail})` : ''}`;
269
+ return `${key}: ${t('cli.summaryEvents', { count: events.length })}${events.length ? ` (${list.join(', ')}${tail})` : ''}`;
263
270
  }
264
271
 
265
272
  note(
266
273
  [
267
- `${pc.dim('Step 5/5')} Review`,
274
+ `${pc.dim(t('cli.stepFormat', { n: 5 }))} ${t('cli.step5Review')}`,
268
275
  '',
269
- `Action: ${pc.bold(action)}`,
270
- `Target: ${pc.bold(target === 'global' ? 'global settings' : 'project-only')}`,
276
+ `${t('cli.reviewAction')}: ${pc.bold(action)}`,
277
+ `${t('cli.reviewTarget')}: ${pc.bold(target === 'global' ? t('cli.reviewTargetGlobal') : t('cli.reviewTargetProjectOnly'))}`,
271
278
  '',
272
- `${pc.bold('Packages')}`,
279
+ `${pc.bold(t('cli.reviewPackages'))}`,
273
280
  ...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
274
281
  '',
275
- `${pc.bold('Files')}`,
282
+ `${pc.bold(t('cli.reviewFiles'))}`,
276
283
  ...files.map((f) => ` - ${f}`)
277
284
  ].join('\n'),
278
- 'Review'
285
+ t('cli.step5Review')
279
286
  );
280
287
 
281
288
  const applyCtrl = new AbortController();
282
289
  const { result: applyResult, wentBack: applyBack } = await withBackspaceBack(applyCtrl, () =>
283
290
  select({
284
- message: 'Apply changes?',
291
+ message: t('cli.applyChanges'),
285
292
  options: [
286
- { value: 'yes', label: 'Yes, apply' },
287
- { value: 'cancel', label: 'Cancel (exit)' }
293
+ { value: 'yes', label: t('cli.applyYes') },
294
+ { value: 'cancel', label: t('cli.applyCancel') }
288
295
  ],
289
296
  signal: applyCtrl.signal
290
297
  })
@@ -293,7 +300,7 @@ async function main() {
293
300
  step = 3;
294
301
  continue;
295
302
  }
296
- if (isCancel(applyResult) || applyResult === 'cancel') dieCancelled('No changes made');
303
+ if (isCancel(applyResult) || applyResult === 'cancel') dieCancelled(t('cli.noChanges'));
297
304
 
298
305
  break;
299
306
  }
@@ -301,7 +308,7 @@ async function main() {
301
308
  if (target === 'projectOnly') {
302
309
  // Write project config
303
310
  const s = spinner();
304
- s.start('Writing project config...');
311
+ s.start(t('cli.writingProjectConfig'));
305
312
 
306
313
  // perPackage.*.projectConfigSection is shaped for claude-code-hooks.config.json sections.
307
314
  const projectCfg = await ensureProjectOnlyConfig(projectDir, selected, {
@@ -323,12 +330,12 @@ async function main() {
323
330
  }
324
331
  });
325
332
 
326
- s.stop('Done');
333
+ s.stop(t('cli.done'));
327
334
 
328
- note(JSON.stringify(snippetObj, null, 2), 'Paste into ~/.claude/settings.json (global)');
335
+ note(JSON.stringify(snippetObj, null, 2), t('cli.pasteSnippet'));
329
336
  await maybeWriteSnippet(projectDir, snippetObj);
330
337
 
331
- outro(`Project config written: ${pc.bold(configFilePath(projectDir))}`);
338
+ outro(`${t('cli.projectConfigWritten')}: ${pc.bold(configFilePath(projectDir))}`);
332
339
  return;
333
340
  }
334
341
 
@@ -336,14 +343,14 @@ async function main() {
336
343
  const settingsPath = configPathForScope('global', projectDir);
337
344
  const res = await readJsonIfExists(settingsPath);
338
345
  if (!res.ok) {
339
- cancel(`Could not read/parse JSON at ${settingsPath}`);
346
+ cancel(t('cli.couldNotReadJson', { path: settingsPath }));
340
347
  process.exit(1);
341
348
  }
342
349
 
343
350
  let settings = res.value;
344
351
 
345
352
  const s = spinner();
346
- s.start('Applying changes to global settings...');
353
+ s.start(t('cli.applyingChanges'));
347
354
 
348
355
  for (const key of selected) {
349
356
  const plan = perPackage[key];
@@ -366,8 +373,8 @@ async function main() {
366
373
  });
367
374
  }
368
375
 
369
- s.stop('Done');
370
- outro(`Saved: ${pc.bold(settingsPath)}`);
376
+ s.stop(t('cli.done'));
377
+ outro(`${t('cli.saved')}: ${pc.bold(settingsPath)}`);
371
378
  }
372
379
 
373
380
  main().catch((err) => {