@claude-code-hooks/cli 0.1.15 → 0.1.17
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 +1 -1
- package/src/cli.js +159 -85
- package/src/snippet.js +0 -29
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.17",
|
|
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
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import readline from 'node:readline';
|
|
5
|
-
import fs from 'node:fs/promises';
|
|
6
5
|
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
12
|
|
|
8
13
|
import {
|
|
9
|
-
intro,
|
|
10
14
|
outro,
|
|
11
15
|
select,
|
|
12
16
|
multiselect,
|
|
@@ -28,8 +32,6 @@ import {
|
|
|
28
32
|
configPathForScope,
|
|
29
33
|
readJsonIfExists,
|
|
30
34
|
writeJson,
|
|
31
|
-
CONFIG_FILENAME,
|
|
32
|
-
configFilePath,
|
|
33
35
|
readProjectConfig,
|
|
34
36
|
writeProjectConfig,
|
|
35
37
|
removeLegacyClaudeSoundHooks,
|
|
@@ -47,8 +49,6 @@ if (detectLanguage() !== 'en') {
|
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
import { buildSettingsSnippet } from './snippet.js';
|
|
51
|
-
|
|
52
52
|
// In-workspace imports (when running from monorepo) and normal Node resolution
|
|
53
53
|
// (when installed from npm) both resolve these packages.
|
|
54
54
|
import { planInteractiveSetup as planSecuritySetup } from '@claude-code-hooks/security/src/plan.js';
|
|
@@ -61,6 +61,51 @@ function dieCancelled(msg) {
|
|
|
61
61
|
process.exit(0);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function showWelcome() {
|
|
65
|
+
let version = '';
|
|
66
|
+
try {
|
|
67
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
68
|
+
version = pkg.version ? ` v${pkg.version}` : '';
|
|
69
|
+
} catch {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const width = 48;
|
|
74
|
+
const pad = '\n';
|
|
75
|
+
const particles = ['·', '•', '✦', '✧', '◦', '▪', 'º', '∗'];
|
|
76
|
+
const colors = [pc.blue, pc.cyan, pc.yellow, pc.magenta, pc.green];
|
|
77
|
+
|
|
78
|
+
const particleLine = (seed) => {
|
|
79
|
+
const len = width - 2;
|
|
80
|
+
let s = ' ';
|
|
81
|
+
for (let i = 0; i < len; i++) {
|
|
82
|
+
const show =
|
|
83
|
+
((i * 17 + seed) % 5 === 0) ||
|
|
84
|
+
((i * 13 + seed + 3) % 6 === 0) ||
|
|
85
|
+
((i * 11 + seed + 1) % 4 === 0) ||
|
|
86
|
+
((i * 19 + seed + 5) % 7 === 0);
|
|
87
|
+
s += show ? colors[(i + seed) % colors.length](particles[(i + seed) % particles.length]) : ' ';
|
|
88
|
+
}
|
|
89
|
+
return s + '\n';
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const icon = pc.blue('◆');
|
|
93
|
+
const line = ` ${pc.blue('═')}${pc.cyan('═'.repeat(width - 4))}${pc.blue('═')}`;
|
|
94
|
+
const lineBottom = ` ${pc.yellow('═')}${pc.magenta('═'.repeat(width - 4))}${pc.yellow('═')}`;
|
|
95
|
+
|
|
96
|
+
process.stdout.write(pad);
|
|
97
|
+
process.stdout.write(particleLine(0));
|
|
98
|
+
process.stdout.write(line + pad);
|
|
99
|
+
process.stdout.write(pad);
|
|
100
|
+
process.stdout.write(` ${icon} ${pc.blue(pc.bold(t('cli.welcomeTitle')))}${version ? pc.gray(version) : ''}\n`);
|
|
101
|
+
process.stdout.write(pad);
|
|
102
|
+
process.stdout.write(` ${pc.brightCyan('Customize Claude Code with zero dependencies')}\n`);
|
|
103
|
+
process.stdout.write(pad);
|
|
104
|
+
process.stdout.write(lineBottom + pad);
|
|
105
|
+
process.stdout.write(particleLine(5));
|
|
106
|
+
process.stdout.write(pad);
|
|
107
|
+
}
|
|
108
|
+
|
|
64
109
|
/**
|
|
65
110
|
* Run a prompt with Backspace = go back. When Backspace is pressed, aborts and returns
|
|
66
111
|
* { wentBack: true }. ESC still exits. Caller must handle wentBack by continuing the loop.
|
|
@@ -90,6 +135,47 @@ function usage(exitCode = 0) {
|
|
|
90
135
|
process.exit(exitCode);
|
|
91
136
|
}
|
|
92
137
|
|
|
138
|
+
const GITIGNORE_LOCAL_ENTRY = '.claude/settings.local.json';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns true if the trimmed gitignore line would cause settings.local.json to be ignored.
|
|
142
|
+
*/
|
|
143
|
+
function wouldIgnoreLocalSettings(line) {
|
|
144
|
+
const t = line.replace(/#.*$/, '').trim();
|
|
145
|
+
if (!t) return false;
|
|
146
|
+
if (t === GITIGNORE_LOCAL_ENTRY || t === '**/' + GITIGNORE_LOCAL_ENTRY) return true;
|
|
147
|
+
if (t === '.claude/' || t === '.claude' || t === '**/.claude/' || t === '**/.claude') return true;
|
|
148
|
+
if (t === '.claude/*' || t === '.claude/**' || t.includes('**/.claude')) return true;
|
|
149
|
+
if (t.includes('settings.local.json')) return true;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Ensures .gitignore in projectDir contains an entry to ignore settings.local.json.
|
|
155
|
+
* Called when user selects project (local) so their per-developer settings stay untracked.
|
|
156
|
+
* Never throws: catches all errors to avoid crashing the CLI.
|
|
157
|
+
*/
|
|
158
|
+
async function ensureGitignoreLocalEntry(projectDir) {
|
|
159
|
+
try {
|
|
160
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
161
|
+
let content = '';
|
|
162
|
+
try {
|
|
163
|
+
content = await fs.readFile(gitignorePath, 'utf-8');
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err?.code !== 'ENOENT') return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const lines = content.split(/\r?\n/);
|
|
169
|
+
if (lines.some(wouldIgnoreLocalSettings)) return;
|
|
170
|
+
|
|
171
|
+
const needsNewline = content.length > 0 && !content.endsWith('\n');
|
|
172
|
+
const block = '\n# claude-code-hooks: per-developer local settings\n' + GITIGNORE_LOCAL_ENTRY + '\n';
|
|
173
|
+
await fs.appendFile(gitignorePath, (needsNewline ? '\n' : '') + block);
|
|
174
|
+
} catch {
|
|
175
|
+
// EACCES, EPERM, ENOSPC, etc. — don't crash CLI; settings were already written
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
93
179
|
async function ensureProjectOnlyConfig(projectDir, selected, perPackageConfig) {
|
|
94
180
|
const cfgRes = await readProjectConfig(projectDir);
|
|
95
181
|
const rawCfg = cfgRes.ok ? { ...(cfgRes.value || {}) } : {};
|
|
@@ -107,19 +193,6 @@ async function ensureProjectOnlyConfig(projectDir, selected, perPackageConfig) {
|
|
|
107
193
|
return out;
|
|
108
194
|
}
|
|
109
195
|
|
|
110
|
-
async function maybeWriteSnippet(projectDir, snippetObj) {
|
|
111
|
-
const ok = await confirm({
|
|
112
|
-
message: t('cli.writeSnippet'),
|
|
113
|
-
initialValue: false
|
|
114
|
-
});
|
|
115
|
-
if (isCancel(ok)) return;
|
|
116
|
-
if (!ok) return;
|
|
117
|
-
|
|
118
|
-
const filePath = path.join(projectDir, 'claude-code-hooks.snippet.json');
|
|
119
|
-
await fs.writeFile(filePath, JSON.stringify(snippetObj, null, 2) + '\n');
|
|
120
|
-
note(filePath, t('cli.wroteSnippet'));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
196
|
async function main() {
|
|
124
197
|
const args = process.argv.slice(2);
|
|
125
198
|
if (args.includes('-h') || args.includes('--help')) usage(0);
|
|
@@ -132,10 +205,13 @@ async function main() {
|
|
|
132
205
|
|
|
133
206
|
const projectDir = process.cwd();
|
|
134
207
|
|
|
135
|
-
|
|
136
|
-
|
|
208
|
+
showWelcome();
|
|
209
|
+
|
|
210
|
+
/** Build step header: "Step N/5 — <title> (ESC exit · Backspace back)" */
|
|
211
|
+
const stepHeader = (n, title) =>
|
|
212
|
+
pc.dim(t('cli.stepHeader', { n, title, suffix: t('cli.stepHeaderSuffix') }));
|
|
137
213
|
|
|
138
|
-
// ── Step 1–
|
|
214
|
+
// ── Step 1–5: action, target, packages, configure, review (Backspace = go to previous step) ──
|
|
139
215
|
let action;
|
|
140
216
|
let target;
|
|
141
217
|
let selected;
|
|
@@ -153,7 +229,7 @@ async function main() {
|
|
|
153
229
|
while (true) {
|
|
154
230
|
if (step === 1) {
|
|
155
231
|
action = await select({
|
|
156
|
-
message:
|
|
232
|
+
message: stepHeader(1, t('cli.step1ChooseAction')),
|
|
157
233
|
options: [
|
|
158
234
|
{ value: 'setup', label: t('cli.actionSetup') },
|
|
159
235
|
{ value: 'uninstall', label: t('cli.actionUninstall') },
|
|
@@ -168,10 +244,11 @@ async function main() {
|
|
|
168
244
|
const targetCtrl = new AbortController();
|
|
169
245
|
const { result: targetResult, wentBack: targetBack } = await withBackspaceBack(targetCtrl, () =>
|
|
170
246
|
select({
|
|
171
|
-
message:
|
|
247
|
+
message: stepHeader(2, t('cli.step2ChooseTarget')),
|
|
172
248
|
options: [
|
|
173
249
|
{ value: 'global', label: t('cli.targetGlobal') },
|
|
174
|
-
{ value: '
|
|
250
|
+
{ value: 'project', label: t('common.scopeProject') },
|
|
251
|
+
{ value: 'projectLocal', label: t('common.scopeProjectLocal') }
|
|
175
252
|
],
|
|
176
253
|
signal: targetCtrl.signal
|
|
177
254
|
})
|
|
@@ -189,7 +266,7 @@ async function main() {
|
|
|
189
266
|
const pkgsCtrl = new AbortController();
|
|
190
267
|
const { result: pkgsResult, wentBack: pkgsBack } = await withBackspaceBack(pkgsCtrl, () =>
|
|
191
268
|
multiselect({
|
|
192
|
-
message:
|
|
269
|
+
message: stepHeader(3, t('cli.step3SelectPackages')),
|
|
193
270
|
options: packageOptions,
|
|
194
271
|
required: true,
|
|
195
272
|
validate: (v) => (!v || v.length === 0) ? t('common.selectAtLeastOne') : true,
|
|
@@ -208,7 +285,7 @@ async function main() {
|
|
|
208
285
|
if (step === 4) {
|
|
209
286
|
const proceedCtrl = new AbortController();
|
|
210
287
|
const { result: proceedResult, wentBack: proceedBack } = await withBackspaceBack(proceedCtrl, () =>
|
|
211
|
-
confirm({ message: t('cli.configureNow'), initialValue: true, signal: proceedCtrl.signal })
|
|
288
|
+
confirm({ message: stepHeader(4, t('cli.configureNow')), initialValue: true, active: t('common.yes'), inactive: t('common.no'), signal: proceedCtrl.signal })
|
|
212
289
|
);
|
|
213
290
|
if (proceedBack) {
|
|
214
291
|
step = 3;
|
|
@@ -239,7 +316,7 @@ async function main() {
|
|
|
239
316
|
.map(
|
|
240
317
|
(k) => {
|
|
241
318
|
const desc = pc.dim(packageDescs[k] || '');
|
|
242
|
-
const label = highlightKey === k ? pc.
|
|
319
|
+
const label = highlightKey === k ? pc.blue(pc.bold(k)) : pc.bold(k);
|
|
243
320
|
return `${label}: ${desc}`;
|
|
244
321
|
}
|
|
245
322
|
)
|
|
@@ -247,29 +324,27 @@ async function main() {
|
|
|
247
324
|
|
|
248
325
|
// ── Step 4/5: configure (highlight current package) ──
|
|
249
326
|
if (selected.includes('security')) {
|
|
250
|
-
note(formatPackageList('security'),
|
|
327
|
+
note(formatPackageList('security'), stepHeader(4, t('cli.step4Configure')));
|
|
251
328
|
perPackage.security = await planSecuritySetup({ action, projectDir, ui: 'umbrella' });
|
|
252
329
|
}
|
|
253
330
|
if (selected.includes('secrets')) {
|
|
254
|
-
note(formatPackageList('secrets'),
|
|
331
|
+
note(formatPackageList('secrets'), stepHeader(4, t('cli.step4Configure')));
|
|
255
332
|
perPackage.secrets = await planSecretsSetup({ action, projectDir, ui: 'umbrella' });
|
|
256
333
|
}
|
|
257
334
|
if (selected.includes('sound')) {
|
|
258
|
-
note(formatPackageList('sound'),
|
|
335
|
+
note(formatPackageList('sound'), stepHeader(4, t('cli.step4Configure')));
|
|
259
336
|
perPackage.sound = await planSoundSetup({ action, projectDir, ui: 'umbrella' });
|
|
260
337
|
}
|
|
261
338
|
if (selected.includes('notification')) {
|
|
262
|
-
note(formatPackageList('notification'),
|
|
339
|
+
note(formatPackageList('notification'), stepHeader(4, t('cli.step4Configure')));
|
|
263
340
|
perPackage.notification = await planNotificationSetup({ action, projectDir, ui: 'umbrella' });
|
|
264
341
|
}
|
|
265
342
|
|
|
266
343
|
// ── Step 5/5: review ──
|
|
267
344
|
const files = [];
|
|
268
345
|
if (target === 'global') files.push(configPathForScope('global', projectDir));
|
|
269
|
-
if (target === '
|
|
270
|
-
|
|
271
|
-
files.push(path.join(projectDir, 'claude-code-hooks.snippet.json (optional)'));
|
|
272
|
-
}
|
|
346
|
+
if (target === 'project') files.push(configPathForScope('project', projectDir));
|
|
347
|
+
if (target === 'projectLocal') files.push(configPathForScope('projectLocal', projectDir));
|
|
273
348
|
|
|
274
349
|
function summarizePlan(key, plan) {
|
|
275
350
|
if (!plan) return `${key}: ${t('cli.summarySkipped')}`;
|
|
@@ -283,10 +358,15 @@ async function main() {
|
|
|
283
358
|
|
|
284
359
|
note(
|
|
285
360
|
[
|
|
286
|
-
|
|
361
|
+
stepHeader(5, t('cli.step5Review')),
|
|
287
362
|
'',
|
|
288
|
-
`${t('cli.
|
|
289
|
-
|
|
363
|
+
`${pc.bold(t('cli.reviewSectionActionTarget'))}`,
|
|
364
|
+
` ${t('cli.reviewAction')}: ${pc.bold(action)}`,
|
|
365
|
+
` ${t('cli.reviewTarget')}: ${pc.bold(
|
|
366
|
+
target === 'global' ? t('cli.reviewTargetGlobal') :
|
|
367
|
+
target === 'project' ? t('cli.reviewTargetProject') :
|
|
368
|
+
t('cli.reviewTargetProjectLocal')
|
|
369
|
+
)}`,
|
|
290
370
|
'',
|
|
291
371
|
`${pc.bold(t('cli.reviewPackages'))}`,
|
|
292
372
|
...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
|
|
@@ -297,10 +377,10 @@ async function main() {
|
|
|
297
377
|
t('cli.step5Review')
|
|
298
378
|
);
|
|
299
379
|
|
|
300
|
-
|
|
380
|
+
const applyCtrl = new AbortController();
|
|
301
381
|
const { result: applyResult, wentBack: applyBack } = await withBackspaceBack(applyCtrl, () =>
|
|
302
382
|
select({
|
|
303
|
-
message: t('cli.applyChanges'),
|
|
383
|
+
message: stepHeader(5, t('cli.applyChanges')),
|
|
304
384
|
options: [
|
|
305
385
|
{ value: 'yes', label: t('cli.applyYes') },
|
|
306
386
|
{ value: 'cancel', label: t('cli.applyCancel') }
|
|
@@ -317,66 +397,42 @@ async function main() {
|
|
|
317
397
|
break;
|
|
318
398
|
}
|
|
319
399
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
notification: perPackage.notification?.projectConfigSection
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// Print snippet for user to paste into global settings.
|
|
334
|
-
const snippetObj = buildSettingsSnippet({
|
|
335
|
-
projectDir,
|
|
336
|
-
selected,
|
|
337
|
-
packagePlans: {
|
|
338
|
-
security: perPackage.security,
|
|
339
|
-
secrets: perPackage.secrets,
|
|
340
|
-
sound: perPackage.sound,
|
|
341
|
-
notification: perPackage.notification
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
s.stop(t('cli.done'));
|
|
346
|
-
|
|
347
|
-
note(JSON.stringify(snippetObj, null, 2), t('cli.pasteSnippet'));
|
|
348
|
-
await maybeWriteSnippet(projectDir, snippetObj);
|
|
349
|
-
|
|
350
|
-
outro(`${t('cli.projectConfigWritten')}: ${pc.bold(configFilePath(projectDir))}`);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
400
|
+
// Global / project / projectLocal apply: read settings.json, apply transforms, write once.
|
|
401
|
+
const settingsPath = configPathForScope(target, projectDir);
|
|
402
|
+
const pkgLabels = {
|
|
403
|
+
security: t('cli.pkgSecurity'),
|
|
404
|
+
secrets: t('cli.pkgSecrets'),
|
|
405
|
+
sound: t('cli.pkgSound'),
|
|
406
|
+
notification: t('cli.pkgNotification')
|
|
407
|
+
};
|
|
408
|
+
const s = spinner();
|
|
409
|
+
s.start(t('cli.applyingChanges'));
|
|
353
410
|
|
|
354
|
-
// Global apply: read settings.json, apply transforms, write once.
|
|
355
|
-
const settingsPath = configPathForScope('global', projectDir);
|
|
356
411
|
const res = await readJsonIfExists(settingsPath);
|
|
357
412
|
if (!res.ok) {
|
|
413
|
+
s.stop(t('cli.done'));
|
|
358
414
|
cancel(t('cli.couldNotReadJson', { path: settingsPath }));
|
|
359
415
|
process.exit(1);
|
|
360
416
|
}
|
|
361
|
-
|
|
362
417
|
let settings = res.value;
|
|
363
418
|
|
|
364
|
-
const s = spinner();
|
|
365
|
-
s.start(t('cli.applyingChanges'));
|
|
366
|
-
|
|
367
419
|
for (const key of selected) {
|
|
368
420
|
const plan = perPackage[key];
|
|
369
421
|
if (!plan) continue;
|
|
422
|
+
const pkgName = pkgLabels[key] ?? key;
|
|
423
|
+
s.message(t('cli.applyStepApplyPackage', { packageName: pkgName }));
|
|
370
424
|
settings = await plan.applyToSettings(settings);
|
|
371
425
|
}
|
|
372
426
|
|
|
373
|
-
|
|
427
|
+
s.message(t('cli.applyStepWriteSettings'));
|
|
374
428
|
settings = removeLegacyClaudeSoundHooks(settings);
|
|
375
|
-
|
|
376
429
|
await writeJson(settingsPath, settings);
|
|
430
|
+
if (target === 'projectLocal') {
|
|
431
|
+
await ensureGitignoreLocalEntry(projectDir);
|
|
432
|
+
}
|
|
377
433
|
|
|
378
|
-
// Update project config only on setup.
|
|
379
434
|
if (action === 'setup') {
|
|
435
|
+
s.message(t('cli.applyStepWriteProjectConfig'));
|
|
380
436
|
await ensureProjectOnlyConfig(projectDir, selected, {
|
|
381
437
|
security: perPackage.security?.projectConfigSection,
|
|
382
438
|
secrets: perPackage.secrets?.projectConfigSection,
|
|
@@ -384,9 +440,27 @@ async function main() {
|
|
|
384
440
|
notification: perPackage.notification?.projectConfigSection
|
|
385
441
|
});
|
|
386
442
|
}
|
|
443
|
+
s.stop('');
|
|
444
|
+
|
|
445
|
+
// Build contextual completion note
|
|
446
|
+
const traits = [];
|
|
447
|
+
if (selected.includes('security') || selected.includes('secrets')) traits.push(t('cli.doneTraitSafer'));
|
|
448
|
+
if (selected.includes('sound')) traits.push(t('cli.doneTraitLouder'));
|
|
449
|
+
if (selected.includes('notification')) traits.push(t('cli.doneTraitAttentive'));
|
|
450
|
+
|
|
451
|
+
const lines = [];
|
|
452
|
+
lines.push(`${t('cli.saved')}: ${pc.cyan(settingsPath)}`);
|
|
453
|
+
lines.push(`${t('cli.reviewPackages')}: ${selected.map((k) => pc.green(pkgLabels[k] ?? k)).join(', ')}`);
|
|
454
|
+
|
|
455
|
+
if (traits.length > 0) {
|
|
456
|
+
const joined = traits.length === 1
|
|
457
|
+
? traits[0]
|
|
458
|
+
: traits.slice(0, -1).join(', ') + ' & ' + traits[traits.length - 1];
|
|
459
|
+
lines.push('');
|
|
460
|
+
lines.push(pc.bold(t('cli.doneWithTraits', { traits: joined })));
|
|
461
|
+
}
|
|
387
462
|
|
|
388
|
-
|
|
389
|
-
outro(`${t('cli.saved')}: ${pc.bold(settingsPath)}`);
|
|
463
|
+
note(lines.join('\n'), t('cli.doneNoteTitle'));
|
|
390
464
|
}
|
|
391
465
|
|
|
392
466
|
main().catch((err) => {
|
package/src/snippet.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
// This file generates a snippet the user can paste into ~/.claude/settings.json.
|
|
4
|
-
// We keep it simple: a "hooks" object with managed hook handlers.
|
|
5
|
-
|
|
6
|
-
export function buildSettingsSnippet({ projectDir, selected, packagePlans }) {
|
|
7
|
-
const hooks = {};
|
|
8
|
-
|
|
9
|
-
for (const key of selected) {
|
|
10
|
-
const plan = packagePlans[key];
|
|
11
|
-
if (!plan) continue;
|
|
12
|
-
|
|
13
|
-
// plan.snippetHooks is: { [eventName]: [ { matcher, hooks:[{type,command,async,timeout}]} ] }
|
|
14
|
-
for (const [eventName, groups] of Object.entries(plan.snippetHooks || {})) {
|
|
15
|
-
if (!Array.isArray(groups) || groups.length === 0) continue;
|
|
16
|
-
const existing = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
|
|
17
|
-
hooks[eventName] = [...existing, ...groups];
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Include a comment-like pointer to project config path (JSON doesn't support comments, so we use a metadata key).
|
|
22
|
-
const cfgPath = path.join(projectDir, 'claude-code-hooks.config.json');
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
"__generated_by": "@claude-code-hooks/cli",
|
|
26
|
-
"__project_config": cfgPath,
|
|
27
|
-
hooks
|
|
28
|
-
};
|
|
29
|
-
}
|