@claude-code-hooks/cli 0.1.14 → 0.1.16
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 +110 -64
- 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.16",
|
|
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('claude-code-hooks'))}${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,7 +205,7 @@ async function main() {
|
|
|
132
205
|
|
|
133
206
|
const projectDir = process.cwd();
|
|
134
207
|
|
|
135
|
-
|
|
208
|
+
showWelcome();
|
|
136
209
|
note(t('cli.navHint'), t('cli.navTitle'));
|
|
137
210
|
|
|
138
211
|
// ── Step 1–3: action, target, packages (Backspace = go to previous step) ──
|
|
@@ -171,7 +244,8 @@ async function main() {
|
|
|
171
244
|
message: `${pc.dim(t('cli.stepFormat', { n: 2 }))} ${t('cli.step2ChooseTarget')}`,
|
|
172
245
|
options: [
|
|
173
246
|
{ value: 'global', label: t('cli.targetGlobal') },
|
|
174
|
-
{ value: '
|
|
247
|
+
{ value: 'project', label: t('common.scopeProject') },
|
|
248
|
+
{ value: 'projectLocal', label: t('common.scopeProjectLocal') }
|
|
175
249
|
],
|
|
176
250
|
signal: targetCtrl.signal
|
|
177
251
|
})
|
|
@@ -208,7 +282,7 @@ async function main() {
|
|
|
208
282
|
if (step === 4) {
|
|
209
283
|
const proceedCtrl = new AbortController();
|
|
210
284
|
const { result: proceedResult, wentBack: proceedBack } = await withBackspaceBack(proceedCtrl, () =>
|
|
211
|
-
confirm({ message: t('cli.configureNow'), initialValue: true, signal: proceedCtrl.signal })
|
|
285
|
+
confirm({ message: t('cli.configureNow'), initialValue: true, active: t('common.yes'), inactive: t('common.no'), signal: proceedCtrl.signal })
|
|
212
286
|
);
|
|
213
287
|
if (proceedBack) {
|
|
214
288
|
step = 3;
|
|
@@ -239,7 +313,7 @@ async function main() {
|
|
|
239
313
|
.map(
|
|
240
314
|
(k) => {
|
|
241
315
|
const desc = pc.dim(packageDescs[k] || '');
|
|
242
|
-
const label = highlightKey === k ? pc.
|
|
316
|
+
const label = highlightKey === k ? pc.blue(pc.bold(k)) : pc.bold(k);
|
|
243
317
|
return `${label}: ${desc}`;
|
|
244
318
|
}
|
|
245
319
|
)
|
|
@@ -266,10 +340,8 @@ async function main() {
|
|
|
266
340
|
// ── Step 5/5: review ──
|
|
267
341
|
const files = [];
|
|
268
342
|
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
|
-
}
|
|
343
|
+
if (target === 'project') files.push(configPathForScope('project', projectDir));
|
|
344
|
+
if (target === 'projectLocal') files.push(configPathForScope('projectLocal', projectDir));
|
|
273
345
|
|
|
274
346
|
function summarizePlan(key, plan) {
|
|
275
347
|
if (!plan) return `${key}: ${t('cli.summarySkipped')}`;
|
|
@@ -286,7 +358,11 @@ async function main() {
|
|
|
286
358
|
`${pc.dim(t('cli.stepFormat', { n: 5 }))} ${t('cli.step5Review')}`,
|
|
287
359
|
'',
|
|
288
360
|
`${t('cli.reviewAction')}: ${pc.bold(action)}`,
|
|
289
|
-
`${t('cli.reviewTarget')}: ${pc.bold(
|
|
361
|
+
`${t('cli.reviewTarget')}: ${pc.bold(
|
|
362
|
+
target === 'global' ? t('cli.reviewTargetGlobal') :
|
|
363
|
+
target === 'project' ? t('cli.reviewTargetProject') :
|
|
364
|
+
t('cli.reviewTargetProjectLocal')
|
|
365
|
+
)}`,
|
|
290
366
|
'',
|
|
291
367
|
`${pc.bold(t('cli.reviewPackages'))}`,
|
|
292
368
|
...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
|
|
@@ -317,42 +393,8 @@ async function main() {
|
|
|
317
393
|
break;
|
|
318
394
|
}
|
|
319
395
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const s = spinner();
|
|
323
|
-
s.start(t('cli.writingProjectConfig'));
|
|
324
|
-
|
|
325
|
-
// perPackage.*.projectConfigSection is shaped for claude-code-hooks.config.json sections.
|
|
326
|
-
const projectCfg = await ensureProjectOnlyConfig(projectDir, selected, {
|
|
327
|
-
security: perPackage.security?.projectConfigSection,
|
|
328
|
-
secrets: perPackage.secrets?.projectConfigSection,
|
|
329
|
-
sound: perPackage.sound?.projectConfigSection,
|
|
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
|
-
}
|
|
353
|
-
|
|
354
|
-
// Global apply: read settings.json, apply transforms, write once.
|
|
355
|
-
const settingsPath = configPathForScope('global', projectDir);
|
|
396
|
+
// Global / project / projectLocal apply: read settings.json, apply transforms, write once.
|
|
397
|
+
const settingsPath = configPathForScope(target, projectDir);
|
|
356
398
|
const res = await readJsonIfExists(settingsPath);
|
|
357
399
|
if (!res.ok) {
|
|
358
400
|
cancel(t('cli.couldNotReadJson', { path: settingsPath }));
|
|
@@ -375,6 +417,10 @@ async function main() {
|
|
|
375
417
|
|
|
376
418
|
await writeJson(settingsPath, settings);
|
|
377
419
|
|
|
420
|
+
if (target === 'projectLocal') {
|
|
421
|
+
await ensureGitignoreLocalEntry(projectDir);
|
|
422
|
+
}
|
|
423
|
+
|
|
378
424
|
// Update project config only on setup.
|
|
379
425
|
if (action === 'setup') {
|
|
380
426
|
await ensureProjectOnlyConfig(projectDir, selected, {
|
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
|
-
}
|