@soleri/cli 9.2.0 → 9.3.0
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/dist/commands/hooks.js +36 -7
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.js.map +1 -1
- package/dist/hook-packs/installer.js +7 -2
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +98 -26
- package/dist/hook-packs/registry.js +12 -4
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +21 -7
- package/package.json +1 -1
- package/src/__tests__/hook-packs.test.ts +23 -3
- package/src/__tests__/wizard-e2e.mjs +148 -58
- package/src/commands/hooks.ts +186 -82
- package/src/commands/install.ts +7 -4
- package/src/hook-packs/installer.ts +98 -26
- package/src/hook-packs/registry.ts +21 -7
|
@@ -27,7 +27,14 @@ describe('hook-packs', () => {
|
|
|
27
27
|
const packs = listPacks();
|
|
28
28
|
expect(packs.length).toBe(6);
|
|
29
29
|
const names = packs.map((p) => p.name).sort();
|
|
30
|
-
expect(names).toEqual([
|
|
30
|
+
expect(names).toEqual([
|
|
31
|
+
'a11y',
|
|
32
|
+
'clean-commits',
|
|
33
|
+
'css-discipline',
|
|
34
|
+
'full',
|
|
35
|
+
'typescript-safety',
|
|
36
|
+
'yolo-safety',
|
|
37
|
+
]);
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
it('should get a specific pack by name', () => {
|
|
@@ -46,7 +53,11 @@ describe('hook-packs', () => {
|
|
|
46
53
|
const pack = getPack('full');
|
|
47
54
|
expect(pack).not.toBeNull();
|
|
48
55
|
expect(pack!.manifest.composedFrom).toEqual([
|
|
49
|
-
'typescript-safety',
|
|
56
|
+
'typescript-safety',
|
|
57
|
+
'a11y',
|
|
58
|
+
'css-discipline',
|
|
59
|
+
'clean-commits',
|
|
60
|
+
'yolo-safety',
|
|
50
61
|
]);
|
|
51
62
|
expect(pack!.manifest.hooks).toHaveLength(8);
|
|
52
63
|
});
|
|
@@ -93,7 +104,16 @@ describe('hook-packs', () => {
|
|
|
93
104
|
expect(result.installed).toHaveLength(8);
|
|
94
105
|
expect(result.skipped).toEqual([]);
|
|
95
106
|
const claudeDir = join(tempHome, '.claude');
|
|
96
|
-
for (const hook of [
|
|
107
|
+
for (const hook of [
|
|
108
|
+
'no-any-types',
|
|
109
|
+
'no-console-log',
|
|
110
|
+
'no-important',
|
|
111
|
+
'no-inline-styles',
|
|
112
|
+
'semantic-html',
|
|
113
|
+
'focus-ring-required',
|
|
114
|
+
'ux-touch-targets',
|
|
115
|
+
'no-ai-attribution',
|
|
116
|
+
]) {
|
|
97
117
|
expect(existsSync(join(claudeDir, `hookify.${hook}.local.md`))).toBe(true);
|
|
98
118
|
}
|
|
99
119
|
expect(result.scripts).toHaveLength(1);
|
|
@@ -70,8 +70,12 @@ function runWizard(name, actions, opts = {}) {
|
|
|
70
70
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
proc.stdout.on('data', (d) => {
|
|
74
|
-
|
|
73
|
+
proc.stdout.on('data', (d) => {
|
|
74
|
+
buffer += d.toString();
|
|
75
|
+
});
|
|
76
|
+
proc.stderr.on('data', (d) => {
|
|
77
|
+
buffer += d.toString();
|
|
78
|
+
});
|
|
75
79
|
|
|
76
80
|
async function drive() {
|
|
77
81
|
while (actionIndex < actions.length && !state.completed) {
|
|
@@ -120,7 +124,9 @@ function runWizard(name, actions, opts = {}) {
|
|
|
120
124
|
clearInterval(poller);
|
|
121
125
|
proc.kill('SIGTERM');
|
|
122
126
|
setTimeout(() => {
|
|
123
|
-
try {
|
|
127
|
+
try {
|
|
128
|
+
proc.kill('SIGKILL');
|
|
129
|
+
} catch {}
|
|
124
130
|
resolve({
|
|
125
131
|
exitCode: -1,
|
|
126
132
|
output: stripAnsi(buffer) + '\n[TIMEOUT]',
|
|
@@ -157,13 +163,62 @@ function archetypeActions(outDir, { downCount = 0 } = {}) {
|
|
|
157
163
|
// Note: agentId is slugify(label), not the archetype value.
|
|
158
164
|
// e.g., "Full-Stack Assistant" → "full-stack-assistant"
|
|
159
165
|
const ARCHETYPES = [
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
{
|
|
167
|
+
value: 'code-reviewer',
|
|
168
|
+
agentId: 'code-reviewer',
|
|
169
|
+
label: 'Code Reviewer',
|
|
170
|
+
tone: 'mentor',
|
|
171
|
+
totalSkills: 10,
|
|
172
|
+
downCount: 0,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
value: 'security-auditor',
|
|
176
|
+
agentId: 'security-auditor',
|
|
177
|
+
label: 'Security Auditor',
|
|
178
|
+
tone: 'precise',
|
|
179
|
+
totalSkills: 10,
|
|
180
|
+
downCount: 1,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
value: 'api-architect',
|
|
184
|
+
agentId: 'api-architect',
|
|
185
|
+
label: 'API Architect',
|
|
186
|
+
tone: 'pragmatic',
|
|
187
|
+
totalSkills: 10,
|
|
188
|
+
downCount: 2,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
value: 'test-engineer',
|
|
192
|
+
agentId: 'test-engineer',
|
|
193
|
+
label: 'Test Engineer',
|
|
194
|
+
tone: 'mentor',
|
|
195
|
+
totalSkills: 10,
|
|
196
|
+
downCount: 3,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
value: 'devops-pilot',
|
|
200
|
+
agentId: 'devops-pilot',
|
|
201
|
+
label: 'DevOps Pilot',
|
|
202
|
+
tone: 'pragmatic',
|
|
203
|
+
totalSkills: 10,
|
|
204
|
+
downCount: 4,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
value: 'database-architect',
|
|
208
|
+
agentId: 'database-architect',
|
|
209
|
+
label: 'Database Architect',
|
|
210
|
+
tone: 'precise',
|
|
211
|
+
totalSkills: 10,
|
|
212
|
+
downCount: 5,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
value: 'full-stack',
|
|
216
|
+
agentId: 'full-stack-assistant',
|
|
217
|
+
label: 'Full-Stack Assistant',
|
|
218
|
+
tone: 'mentor',
|
|
219
|
+
totalSkills: 11,
|
|
220
|
+
downCount: 6,
|
|
221
|
+
},
|
|
167
222
|
];
|
|
168
223
|
|
|
169
224
|
// ══════════════════════════════════════════════════════════
|
|
@@ -172,43 +227,55 @@ const ARCHETYPES = [
|
|
|
172
227
|
|
|
173
228
|
async function testCancelArchetype() {
|
|
174
229
|
console.log('\n [1/14] Cancel at archetype (Ctrl+C)');
|
|
175
|
-
const r = await runWizard('cancel-arch', [
|
|
176
|
-
|
|
177
|
-
|
|
230
|
+
const r = await runWizard('cancel-arch', [{ waitFor: 'kind of agent', send: CTRL_C }], {
|
|
231
|
+
timeout: 15000,
|
|
232
|
+
});
|
|
178
233
|
assert(r.actionsCompleted >= 1, 'prompt reached', 'cancel-archetype');
|
|
179
234
|
}
|
|
180
235
|
|
|
181
236
|
async function testCancelName() {
|
|
182
237
|
console.log('\n [2/14] Cancel at display name');
|
|
183
|
-
const r = await runWizard(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
238
|
+
const r = await runWizard(
|
|
239
|
+
'cancel-name',
|
|
240
|
+
[
|
|
241
|
+
{ waitFor: 'kind of agent', send: SPACE + ENTER },
|
|
242
|
+
{ waitFor: 'Display name', send: CTRL_C },
|
|
243
|
+
],
|
|
244
|
+
{ timeout: 15000 },
|
|
245
|
+
);
|
|
187
246
|
assert(r.actionsCompleted >= 2, 'reached name prompt', 'cancel-name');
|
|
188
247
|
}
|
|
189
248
|
|
|
190
249
|
async function testCancelRole() {
|
|
191
250
|
console.log('\n [3/14] Cancel at role');
|
|
192
|
-
const r = await runWizard(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
251
|
+
const r = await runWizard(
|
|
252
|
+
'cancel-role',
|
|
253
|
+
[
|
|
254
|
+
{ waitFor: 'kind of agent', send: SPACE + ENTER },
|
|
255
|
+
{ waitFor: 'Display name', send: ENTER },
|
|
256
|
+
{ waitFor: 'Role', send: CTRL_C },
|
|
257
|
+
],
|
|
258
|
+
{ timeout: 15000 },
|
|
259
|
+
);
|
|
197
260
|
assert(r.actionsCompleted >= 3, 'reached role prompt', 'cancel-role');
|
|
198
261
|
}
|
|
199
262
|
|
|
200
263
|
async function testCancelSkills() {
|
|
201
264
|
console.log('\n [4/14] Cancel at skills');
|
|
202
|
-
const r = await runWizard(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
265
|
+
const r = await runWizard(
|
|
266
|
+
'cancel-skills',
|
|
267
|
+
[
|
|
268
|
+
{ waitFor: 'kind of agent', send: SPACE + ENTER },
|
|
269
|
+
{ waitFor: 'Display name', send: ENTER },
|
|
270
|
+
{ waitFor: 'Role', send: ENTER },
|
|
271
|
+
{ waitFor: 'Description', send: ENTER },
|
|
272
|
+
{ waitFor: /domain|expertise/i, send: ENTER },
|
|
273
|
+
{ waitFor: /principle|guiding/i, send: ENTER },
|
|
274
|
+
{ waitFor: /tone/i, send: ENTER },
|
|
275
|
+
{ waitFor: /skill/i, send: CTRL_C },
|
|
276
|
+
],
|
|
277
|
+
{ timeout: 15000 },
|
|
278
|
+
);
|
|
212
279
|
assert(r.actionsCompleted >= 8, 'reached skills prompt', 'cancel-skills');
|
|
213
280
|
}
|
|
214
281
|
|
|
@@ -221,20 +288,24 @@ async function testDeclineConfirm() {
|
|
|
221
288
|
const outDir = join(TEST_ROOT, 'decline');
|
|
222
289
|
mkdirSync(outDir, { recursive: true });
|
|
223
290
|
|
|
224
|
-
const r = await runWizard(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
291
|
+
const r = await runWizard(
|
|
292
|
+
'decline',
|
|
293
|
+
[
|
|
294
|
+
{ waitFor: 'kind of agent', send: SPACE + ENTER },
|
|
295
|
+
{ waitFor: 'Display name', send: ENTER },
|
|
296
|
+
{ waitFor: 'Role', send: ENTER },
|
|
297
|
+
{ waitFor: 'Description', send: ENTER },
|
|
298
|
+
{ waitFor: /domain|expertise/i, send: ENTER },
|
|
299
|
+
{ waitFor: /principle|guiding/i, send: ENTER },
|
|
300
|
+
{ waitFor: /tone/i, send: ENTER },
|
|
301
|
+
{ waitFor: /skill/i, send: ENTER },
|
|
302
|
+
{ waitFor: /greeting/i, send: ENTER },
|
|
303
|
+
{ waitFor: /output|directory/i, send: CTRL_U + outDir + ENTER, delay: 300 },
|
|
304
|
+
{ waitFor: /hook|pack/i, send: ENTER },
|
|
305
|
+
{ waitFor: /create agent/i, send: LEFT + ENTER },
|
|
306
|
+
],
|
|
307
|
+
{ timeout: 15000 },
|
|
308
|
+
);
|
|
238
309
|
|
|
239
310
|
assert(r.actionsCompleted >= 12, `all prompts reached (${r.actionsCompleted}/12)`, 'decline');
|
|
240
311
|
assert(!existsSync(join(outDir, 'code-reviewer', 'package.json')), 'no agent created', 'decline');
|
|
@@ -264,8 +335,11 @@ async function testArchetype(arch, idx) {
|
|
|
264
335
|
const personaPath = join(ad, 'src', 'identity', 'persona.ts');
|
|
265
336
|
if (existsSync(personaPath)) {
|
|
266
337
|
const persona = readFileSync(personaPath, 'utf-8');
|
|
267
|
-
assert(
|
|
268
|
-
`
|
|
338
|
+
assert(
|
|
339
|
+
persona.includes(`'${arch.label}'`) || persona.includes(`"${arch.label}"`),
|
|
340
|
+
`name = ${arch.label}`,
|
|
341
|
+
ctx,
|
|
342
|
+
);
|
|
269
343
|
assert(persona.includes(`tone: '${arch.tone}'`), `tone = ${arch.tone}`, ctx);
|
|
270
344
|
} else {
|
|
271
345
|
assert(false, 'persona.ts exists', ctx);
|
|
@@ -275,9 +349,21 @@ async function testArchetype(arch, idx) {
|
|
|
275
349
|
const skillsDir = join(ad, 'skills');
|
|
276
350
|
if (existsSync(skillsDir)) {
|
|
277
351
|
const skills = readdirSync(skillsDir);
|
|
278
|
-
assert(
|
|
352
|
+
assert(
|
|
353
|
+
skills.length === arch.totalSkills,
|
|
354
|
+
`${arch.totalSkills} skills (got ${skills.length})`,
|
|
355
|
+
ctx,
|
|
356
|
+
);
|
|
279
357
|
// Core skills always present
|
|
280
|
-
for (const core of [
|
|
358
|
+
for (const core of [
|
|
359
|
+
'brainstorming',
|
|
360
|
+
'systematic-debugging',
|
|
361
|
+
'verification-before-completion',
|
|
362
|
+
'health-check',
|
|
363
|
+
'context-resume',
|
|
364
|
+
'writing-plans',
|
|
365
|
+
'executing-plans',
|
|
366
|
+
]) {
|
|
281
367
|
assert(skills.includes(core), `core skill: ${core}`, ctx);
|
|
282
368
|
}
|
|
283
369
|
} else {
|
|
@@ -314,8 +400,9 @@ async function testCustomArchetype() {
|
|
|
314
400
|
const customName = 'GraphQL Guardian';
|
|
315
401
|
const customId = 'graphql-guardian';
|
|
316
402
|
const customRole = 'Validates GraphQL schemas against federation rules';
|
|
317
|
-
const customDesc =
|
|
318
|
-
|
|
403
|
+
const customDesc =
|
|
404
|
+
'This agent checks GraphQL schemas for breaking changes, naming conventions, and federation compatibility across subgraphs.';
|
|
405
|
+
const customGreeting = 'Hey! Drop your GraphQL schema and I will check it for issues.';
|
|
319
406
|
|
|
320
407
|
const r = await runWizard('custom', [
|
|
321
408
|
// Step 1: Select "✦ Create Custom" (9 downs — 9 archetypes before _custom)
|
|
@@ -426,7 +513,11 @@ async function testHookPacks() {
|
|
|
426
513
|
// Validate hooks were installed
|
|
427
514
|
const output = r.output;
|
|
428
515
|
assert(output.includes('a11y') && output.includes('installed'), 'a11y pack installed', ctx);
|
|
429
|
-
assert(
|
|
516
|
+
assert(
|
|
517
|
+
output.includes('typescript-safety') && output.includes('installed'),
|
|
518
|
+
'typescript-safety pack installed',
|
|
519
|
+
ctx,
|
|
520
|
+
);
|
|
430
521
|
|
|
431
522
|
// Check .claude directory has hooks
|
|
432
523
|
const claudeDir = join(ad, '.claude');
|
|
@@ -435,7 +526,9 @@ async function testHookPacks() {
|
|
|
435
526
|
assert(files.length > 0, `.claude/ has hook files (${files.length})`, ctx);
|
|
436
527
|
}
|
|
437
528
|
|
|
438
|
-
console.log(
|
|
529
|
+
console.log(
|
|
530
|
+
` exit=${r.exitCode}, agent=${existsSync(ad)}, hooks=${r.output.includes('installed')}`,
|
|
531
|
+
);
|
|
439
532
|
}
|
|
440
533
|
|
|
441
534
|
// ══════════════════════════════════════════════════════════
|
|
@@ -474,10 +567,7 @@ rmSync(TEST_ROOT, { recursive: true, force: true });
|
|
|
474
567
|
|
|
475
568
|
// Clean up any MCP registrations
|
|
476
569
|
try {
|
|
477
|
-
const claudeJson = join(
|
|
478
|
-
process.env.HOME || process.env.USERPROFILE || '',
|
|
479
|
-
'.claude.json',
|
|
480
|
-
);
|
|
570
|
+
const claudeJson = join(process.env.HOME || process.env.USERPROFILE || '', '.claude.json');
|
|
481
571
|
if (existsSync(claudeJson)) {
|
|
482
572
|
const c = JSON.parse(readFileSync(claudeJson, 'utf-8'));
|
|
483
573
|
let changed = false;
|
package/src/commands/hooks.ts
CHANGED
|
@@ -9,94 +9,198 @@ import * as log from '../utils/logger.js';
|
|
|
9
9
|
export function registerHooks(program: Command): void {
|
|
10
10
|
const hooks = program.command('hooks').description('Manage editor hooks and hook packs');
|
|
11
11
|
|
|
12
|
-
hooks
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
hooks
|
|
13
|
+
.command('add')
|
|
14
|
+
.argument('<editor>', `Editor: ${SUPPORTED_EDITORS.join(', ')}`)
|
|
15
|
+
.description('Generate editor hooks/config files')
|
|
16
|
+
.action((editor: string) => {
|
|
17
|
+
if (!isValidEditor(editor)) {
|
|
18
|
+
log.fail(`Unknown editor "${editor}". Supported: ${SUPPORTED_EDITORS.join(', ')}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const ctx = detectAgent();
|
|
22
|
+
if (!ctx) {
|
|
23
|
+
log.fail('No agent project detected in current directory.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const files = installHooks(editor, ctx.agentPath);
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
log.pass(`Created ${f}`);
|
|
29
|
+
}
|
|
30
|
+
log.info(`${editor} hooks installed for ${ctx.agentId}`);
|
|
31
|
+
});
|
|
20
32
|
|
|
21
|
-
hooks
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
hooks
|
|
34
|
+
.command('remove')
|
|
35
|
+
.argument('<editor>', `Editor: ${SUPPORTED_EDITORS.join(', ')}`)
|
|
36
|
+
.description('Remove editor hooks/config files')
|
|
37
|
+
.action((editor: string) => {
|
|
38
|
+
if (!isValidEditor(editor)) {
|
|
39
|
+
log.fail(`Unknown editor "${editor}". Supported: ${SUPPORTED_EDITORS.join(', ')}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const ctx = detectAgent();
|
|
43
|
+
if (!ctx) {
|
|
44
|
+
log.fail('No agent project detected in current directory.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const removed = removeHooks(editor, ctx.agentPath);
|
|
48
|
+
if (removed.length === 0) {
|
|
49
|
+
log.info(`No ${editor} hooks found to remove.`);
|
|
50
|
+
} else {
|
|
51
|
+
for (const f of removed) {
|
|
52
|
+
log.warn(`Removed ${f}`);
|
|
53
|
+
}
|
|
54
|
+
log.info(`${editor} hooks removed from ${ctx.agentId}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
31
57
|
|
|
32
|
-
hooks
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
hooks
|
|
59
|
+
.command('list')
|
|
60
|
+
.description('Show which editor hooks are installed')
|
|
61
|
+
.action(() => {
|
|
62
|
+
const ctx = detectAgent();
|
|
63
|
+
if (!ctx) {
|
|
64
|
+
log.fail('No agent project detected in current directory.');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const installed = detectInstalledHooks(ctx.agentPath);
|
|
68
|
+
log.heading(`Editor hooks for ${ctx.agentId}`);
|
|
69
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
70
|
+
if (installed.includes(editor)) {
|
|
71
|
+
log.pass(editor, 'installed');
|
|
72
|
+
} else {
|
|
73
|
+
log.dim(` ${editor} — not installed`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
41
77
|
|
|
42
|
-
hooks
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
hooks
|
|
79
|
+
.command('add-pack')
|
|
80
|
+
.argument('<pack>', 'Hook pack name')
|
|
81
|
+
.option('--project', 'Install to project .claude/ instead of global ~/.claude/')
|
|
82
|
+
.description('Install a hook pack globally (~/.claude/) or per-project (--project)')
|
|
83
|
+
.action((packName: string, opts: { project?: boolean }) => {
|
|
84
|
+
const pack = getPack(packName);
|
|
85
|
+
if (!pack) {
|
|
86
|
+
const available = listPacks().map((p) => p.name);
|
|
87
|
+
log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const projectDir = opts.project ? process.cwd() : undefined;
|
|
91
|
+
const target = opts.project ? '.claude/' : '~/.claude/';
|
|
92
|
+
const { installed, skipped, scripts, lifecycleHooks } = installPack(packName, { projectDir });
|
|
93
|
+
for (const hook of installed) {
|
|
94
|
+
log.pass(`Installed hookify.${hook}.local.md → ${target}`);
|
|
95
|
+
}
|
|
96
|
+
for (const hook of skipped) {
|
|
97
|
+
log.dim(` hookify.${hook}.local.md — already exists, skipped`);
|
|
98
|
+
}
|
|
99
|
+
for (const script of scripts) {
|
|
100
|
+
log.pass(`Installed ${script} → ${target}`);
|
|
101
|
+
}
|
|
102
|
+
for (const lc of lifecycleHooks) {
|
|
103
|
+
log.pass(`Registered lifecycle hook: ${lc}`);
|
|
104
|
+
}
|
|
105
|
+
const totalInstalled = installed.length + scripts.length + lifecycleHooks.length;
|
|
106
|
+
if (totalInstalled > 0) {
|
|
107
|
+
log.info(`Pack "${packName}" installed (${totalInstalled} items) → ${target}`);
|
|
108
|
+
} else {
|
|
109
|
+
log.info(`Pack "${packName}" — all hooks already installed`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
55
112
|
|
|
56
|
-
hooks
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
113
|
+
hooks
|
|
114
|
+
.command('remove-pack')
|
|
115
|
+
.argument('<pack>', 'Hook pack name')
|
|
116
|
+
.option('--project', 'Remove from project .claude/ instead of global ~/.claude/')
|
|
117
|
+
.description('Remove a hook pack')
|
|
118
|
+
.action((packName: string, opts: { project?: boolean }) => {
|
|
119
|
+
const pack = getPack(packName);
|
|
120
|
+
if (!pack) {
|
|
121
|
+
const available = listPacks().map((p) => p.name);
|
|
122
|
+
log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const projectDir = opts.project ? process.cwd() : undefined;
|
|
126
|
+
const { removed, scripts, lifecycleHooks } = removePack(packName, { projectDir });
|
|
127
|
+
const totalRemoved = removed.length + scripts.length + lifecycleHooks.length;
|
|
128
|
+
if (totalRemoved === 0) {
|
|
129
|
+
log.info(`No hooks from pack "${packName}" found to remove.`);
|
|
130
|
+
} else {
|
|
131
|
+
for (const hook of removed) {
|
|
132
|
+
log.warn(`Removed hookify.${hook}.local.md`);
|
|
133
|
+
}
|
|
134
|
+
for (const script of scripts) {
|
|
135
|
+
log.warn(`Removed ${script}`);
|
|
136
|
+
}
|
|
137
|
+
for (const lc of lifecycleHooks) {
|
|
138
|
+
log.warn(`Removed lifecycle hook: ${lc}`);
|
|
139
|
+
}
|
|
140
|
+
log.info(`Pack "${packName}" removed (${totalRemoved} items)`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
69
143
|
|
|
70
|
-
hooks
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
144
|
+
hooks
|
|
145
|
+
.command('list-packs')
|
|
146
|
+
.description('Show available hook packs and their status')
|
|
147
|
+
.action(() => {
|
|
148
|
+
const packs = listPacks();
|
|
149
|
+
log.heading('Hook Packs');
|
|
150
|
+
for (const pack of packs) {
|
|
151
|
+
const status = isPackInstalled(pack.name);
|
|
152
|
+
const versionLabel = pack.version ? ` v${pack.version}` : '';
|
|
153
|
+
const sourceLabel = pack.source === 'local' ? ' [local]' : '';
|
|
154
|
+
const hookCount = pack.hooks.length;
|
|
155
|
+
const scriptCount = pack.scripts?.length ?? 0;
|
|
156
|
+
const itemCount = hookCount + scriptCount;
|
|
157
|
+
const itemLabel = itemCount === 1 ? '1 item' : `${itemCount} items`;
|
|
158
|
+
if (status === true) {
|
|
159
|
+
log.pass(
|
|
160
|
+
`${pack.name}${versionLabel}${sourceLabel}`,
|
|
161
|
+
`${pack.description} (${itemLabel})`,
|
|
162
|
+
);
|
|
163
|
+
} else if (status === 'partial') {
|
|
164
|
+
log.warn(
|
|
165
|
+
`${pack.name}${versionLabel}${sourceLabel}`,
|
|
166
|
+
`${pack.description} (${itemLabel}) — partial`,
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
log.dim(
|
|
170
|
+
` ${pack.name}${versionLabel}${sourceLabel} — ${pack.description} (${itemLabel})`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
86
175
|
|
|
87
|
-
hooks
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
176
|
+
hooks
|
|
177
|
+
.command('upgrade-pack')
|
|
178
|
+
.argument('<pack>', 'Hook pack name')
|
|
179
|
+
.option('--project', 'Upgrade in project .claude/ instead of global ~/.claude/')
|
|
180
|
+
.description('Upgrade a hook pack to the latest version (overwrites existing files)')
|
|
181
|
+
.action((packName: string, opts: { project?: boolean }) => {
|
|
182
|
+
const pack = getPack(packName);
|
|
183
|
+
if (!pack) {
|
|
184
|
+
const available = listPacks().map((p) => p.name);
|
|
185
|
+
log.fail(`Unknown pack "${packName}". Available: ${available.join(', ')}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
const projectDir = opts.project ? process.cwd() : undefined;
|
|
189
|
+
const packVersion = pack.manifest.version ?? 'unknown';
|
|
190
|
+
removePack(packName, { projectDir });
|
|
191
|
+
const { installed, scripts, lifecycleHooks } = installPack(packName, { projectDir });
|
|
192
|
+
for (const hook of installed) {
|
|
193
|
+
log.pass(`hookify.${hook}.local.md → v${packVersion}`);
|
|
194
|
+
}
|
|
195
|
+
for (const script of scripts) {
|
|
196
|
+
log.pass(`${script} → v${packVersion}`);
|
|
197
|
+
}
|
|
198
|
+
for (const lc of lifecycleHooks) {
|
|
199
|
+
log.pass(`lifecycle hook ${lc} → v${packVersion}`);
|
|
200
|
+
}
|
|
201
|
+
const total = installed.length + scripts.length + lifecycleHooks.length;
|
|
202
|
+
log.info(`Pack "${packName}" upgraded to v${packVersion} (${total} items)`);
|
|
203
|
+
});
|
|
100
204
|
}
|
|
101
205
|
|
|
102
206
|
function isValidEditor(editor: string): editor is EditorId {
|