@soleri/cli 9.0.1 → 9.2.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.
Files changed (49) hide show
  1. package/dist/commands/agent.js +116 -3
  2. package/dist/commands/agent.js.map +1 -1
  3. package/dist/commands/create.js +10 -3
  4. package/dist/commands/create.js.map +1 -1
  5. package/dist/commands/hooks.js +43 -49
  6. package/dist/commands/hooks.js.map +1 -1
  7. package/dist/commands/install.d.ts +1 -0
  8. package/dist/commands/install.js +61 -12
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/pack.js +0 -1
  11. package/dist/commands/pack.js.map +1 -1
  12. package/dist/commands/staging.d.ts +2 -0
  13. package/dist/commands/staging.js +175 -0
  14. package/dist/commands/staging.js.map +1 -0
  15. package/dist/hook-packs/full/manifest.json +2 -2
  16. package/dist/hook-packs/installer.d.ts +4 -11
  17. package/dist/hook-packs/installer.js +192 -23
  18. package/dist/hook-packs/installer.js.map +1 -1
  19. package/dist/hook-packs/installer.ts +173 -60
  20. package/dist/hook-packs/registry.d.ts +16 -13
  21. package/dist/hook-packs/registry.js +13 -28
  22. package/dist/hook-packs/registry.js.map +1 -1
  23. package/dist/hook-packs/registry.ts +33 -46
  24. package/dist/hook-packs/yolo-safety/manifest.json +23 -0
  25. package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
  26. package/dist/hooks/templates.js +1 -1
  27. package/dist/hooks/templates.js.map +1 -1
  28. package/dist/main.js +2 -0
  29. package/dist/main.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/__tests__/create.test.ts +6 -2
  32. package/src/__tests__/hook-packs.test.ts +66 -44
  33. package/src/__tests__/wizard-e2e.mjs +17 -11
  34. package/src/commands/agent.ts +146 -3
  35. package/src/commands/create.ts +8 -2
  36. package/src/commands/hooks.ts +88 -187
  37. package/src/commands/install.ts +62 -22
  38. package/src/commands/pack.ts +0 -1
  39. package/src/commands/staging.ts +208 -0
  40. package/src/hook-packs/full/manifest.json +2 -2
  41. package/src/hook-packs/installer.ts +173 -60
  42. package/src/hook-packs/registry.ts +33 -46
  43. package/src/hook-packs/yolo-safety/manifest.json +23 -0
  44. package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
  45. package/src/hooks/templates.ts +1 -1
  46. package/src/main.ts +2 -0
  47. package/dist/commands/cognee.d.ts +0 -10
  48. package/dist/commands/cognee.js +0 -364
  49. package/dist/commands/cognee.js.map +0 -1
@@ -1,27 +1,23 @@
1
1
  /**
2
- * Hook pack installer — copies hookify files to ~/.claude/ (global) or project .claude/ (local).
2
+ * Hook pack installer — copies hookify files and scripts to ~/.claude/ (global) or project .claude/ (local).
3
+ * Also manages lifecycle hooks in ~/.claude/settings.json.
3
4
  */
4
- import { existsSync, copyFileSync, unlinkSync, mkdirSync } from 'node:fs';
5
+ import { existsSync, copyFileSync, unlinkSync, mkdirSync, readFileSync, writeFileSync, chmodSync, } from 'node:fs';
5
6
  import { join } from 'node:path';
6
7
  import { homedir } from 'node:os';
7
8
  import { getPack } from './registry.js';
8
- /** Resolve the target .claude/ directory. */
9
+ const PACK_MARKER = '_soleriPack';
9
10
  function resolveClaudeDir(projectDir) {
10
11
  if (projectDir)
11
12
  return join(projectDir, '.claude');
12
13
  return join(homedir(), '.claude');
13
14
  }
14
- /**
15
- * Resolve all hookify file paths for a pack, handling composed packs.
16
- * Returns a map of hook name → source file path.
17
- */
18
15
  function resolveHookFiles(packName) {
19
16
  const pack = getPack(packName);
20
17
  if (!pack)
21
18
  return new Map();
22
19
  const files = new Map();
23
20
  if (pack.manifest.composedFrom) {
24
- // Composed pack: gather files from constituent packs
25
21
  for (const subPackName of pack.manifest.composedFrom) {
26
22
  const subFiles = resolveHookFiles(subPackName);
27
23
  for (const [hook, path] of subFiles) {
@@ -30,7 +26,6 @@ function resolveHookFiles(packName) {
30
26
  }
31
27
  }
32
28
  else {
33
- // Direct pack: look for hookify files in the pack directory
34
29
  for (const hook of pack.manifest.hooks) {
35
30
  const filePath = join(pack.dir, `hookify.${hook}.local.md`);
36
31
  if (existsSync(filePath)) {
@@ -40,10 +35,110 @@ function resolveHookFiles(packName) {
40
35
  }
41
36
  return files;
42
37
  }
43
- /**
44
- * Install a hook pack to ~/.claude/ (default) or project .claude/ (--project).
45
- * Skips files that already exist (idempotent).
46
- */
38
+ function resolveScripts(packName) {
39
+ const pack = getPack(packName);
40
+ if (!pack)
41
+ return new Map();
42
+ const scripts = new Map();
43
+ if (pack.manifest.composedFrom) {
44
+ for (const subPackName of pack.manifest.composedFrom) {
45
+ const subScripts = resolveScripts(subPackName);
46
+ for (const [name, info] of subScripts) {
47
+ scripts.set(name, info);
48
+ }
49
+ }
50
+ }
51
+ else if (pack.manifest.scripts) {
52
+ for (const script of pack.manifest.scripts) {
53
+ const sourcePath = join(pack.dir, 'scripts', script.file);
54
+ if (existsSync(sourcePath)) {
55
+ scripts.set(script.name, { sourcePath, targetDir: script.targetDir, file: script.file });
56
+ }
57
+ }
58
+ }
59
+ return scripts;
60
+ }
61
+ function resolveLifecycleHooks(packName) {
62
+ const pack = getPack(packName);
63
+ if (!pack)
64
+ return [];
65
+ const hooks = [];
66
+ if (pack.manifest.composedFrom) {
67
+ for (const subPackName of pack.manifest.composedFrom) {
68
+ hooks.push(...resolveLifecycleHooks(subPackName));
69
+ }
70
+ }
71
+ else if (pack.manifest.lifecycleHooks) {
72
+ for (const hook of pack.manifest.lifecycleHooks) {
73
+ hooks.push({ packName: pack.manifest.name, hook });
74
+ }
75
+ }
76
+ return hooks;
77
+ }
78
+ function readClaudeSettings(claudeDir) {
79
+ const settingsPath = join(claudeDir, 'settings.json');
80
+ if (!existsSync(settingsPath))
81
+ return {};
82
+ try {
83
+ return JSON.parse(readFileSync(settingsPath, 'utf-8'));
84
+ }
85
+ catch {
86
+ return {};
87
+ }
88
+ }
89
+ function writeClaudeSettings(claudeDir, settings) {
90
+ const settingsPath = join(claudeDir, 'settings.json');
91
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
92
+ }
93
+ function addLifecycleHooks(claudeDir, lifecycleHooks) {
94
+ if (lifecycleHooks.length === 0)
95
+ return [];
96
+ const settings = readClaudeSettings(claudeDir);
97
+ const hooks = (settings['hooks'] ?? {});
98
+ const added = [];
99
+ for (const { packName: sourcePack, hook } of lifecycleHooks) {
100
+ const eventKey = hook.event;
101
+ const eventHooks = (hooks[eventKey] ?? []);
102
+ const alreadyExists = eventHooks.some((h) => h.command === hook.command && h[PACK_MARKER] === sourcePack);
103
+ if (!alreadyExists) {
104
+ const entry = { type: hook.type, command: hook.command, [PACK_MARKER]: sourcePack };
105
+ if (hook.timeout) {
106
+ entry.timeout = hook.timeout;
107
+ }
108
+ eventHooks.push(entry);
109
+ hooks[eventKey] = eventHooks;
110
+ added.push(`${eventKey}:${hook.matcher}`);
111
+ }
112
+ }
113
+ settings['hooks'] = hooks;
114
+ writeClaudeSettings(claudeDir, settings);
115
+ return added;
116
+ }
117
+ function removeLifecycleHooks(claudeDir, packName) {
118
+ const settings = readClaudeSettings(claudeDir);
119
+ const hooks = (settings['hooks'] ?? {});
120
+ const removed = [];
121
+ for (const [eventKey, eventHooks] of Object.entries(hooks)) {
122
+ if (!Array.isArray(eventHooks))
123
+ continue;
124
+ const before = eventHooks.length;
125
+ const filtered = eventHooks.filter((h) => h[PACK_MARKER] !== packName);
126
+ if (filtered.length < before) {
127
+ removed.push(eventKey);
128
+ if (filtered.length === 0) {
129
+ delete hooks[eventKey];
130
+ }
131
+ else {
132
+ hooks[eventKey] = filtered;
133
+ }
134
+ }
135
+ }
136
+ if (removed.length > 0) {
137
+ settings['hooks'] = hooks;
138
+ writeClaudeSettings(claudeDir, settings);
139
+ }
140
+ return removed;
141
+ }
47
142
  export function installPack(packName, options) {
48
143
  const pack = getPack(packName);
49
144
  if (!pack) {
@@ -64,11 +159,20 @@ export function installPack(packName, options) {
64
159
  installed.push(hook);
65
160
  }
66
161
  }
67
- return { installed, skipped };
162
+ const scriptFiles = resolveScripts(packName);
163
+ const installedScripts = [];
164
+ for (const [, { sourcePath, targetDir, file }] of scriptFiles) {
165
+ const destDir = join(claudeDir, targetDir);
166
+ mkdirSync(destDir, { recursive: true });
167
+ const destPath = join(destDir, file);
168
+ copyFileSync(sourcePath, destPath);
169
+ chmodSync(destPath, 0o755);
170
+ installedScripts.push(`${targetDir}/${file}`);
171
+ }
172
+ const lcHooks = resolveLifecycleHooks(packName);
173
+ const addedHooks = addLifecycleHooks(claudeDir, lcHooks);
174
+ return { installed, skipped, scripts: installedScripts, lifecycleHooks: addedHooks };
68
175
  }
69
- /**
70
- * Remove a hook pack's files from target directory.
71
- */
72
176
  export function removePack(packName, options) {
73
177
  const pack = getPack(packName);
74
178
  if (!pack) {
@@ -83,26 +187,91 @@ export function removePack(packName, options) {
83
187
  removed.push(hook);
84
188
  }
85
189
  }
86
- return { removed };
190
+ const removedScripts = [];
191
+ if (pack.manifest.scripts) {
192
+ for (const script of pack.manifest.scripts) {
193
+ const filePath = join(claudeDir, script.targetDir, script.file);
194
+ if (existsSync(filePath)) {
195
+ unlinkSync(filePath);
196
+ removedScripts.push(`${script.targetDir}/${script.file}`);
197
+ }
198
+ }
199
+ }
200
+ if (pack.manifest.composedFrom) {
201
+ for (const subPackName of pack.manifest.composedFrom) {
202
+ const subPack = getPack(subPackName);
203
+ if (subPack?.manifest.scripts) {
204
+ for (const script of subPack.manifest.scripts) {
205
+ const filePath = join(claudeDir, script.targetDir, script.file);
206
+ if (existsSync(filePath)) {
207
+ unlinkSync(filePath);
208
+ removedScripts.push(`${script.targetDir}/${script.file}`);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ const removedHooks = removeLifecycleHooks(claudeDir, packName);
215
+ if (pack.manifest.composedFrom) {
216
+ for (const subPackName of pack.manifest.composedFrom) {
217
+ removedHooks.push(...removeLifecycleHooks(claudeDir, subPackName));
218
+ }
219
+ }
220
+ return { removed, scripts: removedScripts, lifecycleHooks: removedHooks };
87
221
  }
88
- /**
89
- * Check if a pack is installed.
90
- * Returns true (all hooks present), false (none present), or 'partial'.
91
- */
92
222
  export function isPackInstalled(packName, options) {
93
223
  const pack = getPack(packName);
94
224
  if (!pack)
95
225
  return false;
96
226
  const claudeDir = resolveClaudeDir(options?.projectDir);
227
+ let total = 0;
97
228
  let present = 0;
98
229
  for (const hook of pack.manifest.hooks) {
230
+ total++;
99
231
  if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) {
100
232
  present++;
101
233
  }
102
234
  }
235
+ if (pack.manifest.scripts) {
236
+ for (const script of pack.manifest.scripts) {
237
+ total++;
238
+ if (existsSync(join(claudeDir, script.targetDir, script.file))) {
239
+ present++;
240
+ }
241
+ }
242
+ }
243
+ if (pack.manifest.composedFrom) {
244
+ for (const subPackName of pack.manifest.composedFrom) {
245
+ const subPack = getPack(subPackName);
246
+ if (subPack?.manifest.scripts) {
247
+ for (const script of subPack.manifest.scripts) {
248
+ total++;
249
+ if (existsSync(join(claudeDir, script.targetDir, script.file))) {
250
+ present++;
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ if (total === 0) {
257
+ const lcHooks = resolveLifecycleHooks(packName);
258
+ if (lcHooks.length > 0) {
259
+ const settings = readClaudeSettings(claudeDir);
260
+ const hooksObj = (settings['hooks'] ?? {});
261
+ for (const { packName: sourcePack, hook } of lcHooks) {
262
+ total++;
263
+ const eventHooks = hooksObj[hook.event];
264
+ if (Array.isArray(eventHooks) && eventHooks.some((h) => h.command === hook.command && h[PACK_MARKER] === sourcePack)) {
265
+ present++;
266
+ }
267
+ }
268
+ }
269
+ }
270
+ if (total === 0)
271
+ return false;
103
272
  if (present === 0)
104
273
  return false;
105
- if (present === pack.manifest.hooks.length)
274
+ if (present === total)
106
275
  return true;
107
276
  return 'partial';
108
277
  }
@@ -1 +1 @@
1
- {"version":3,"file":"installer.js","sourceRoot":"","sources":["../../src/hook-packs/installer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"installer.js","sourceRoot":"","sources":["../../src/hook-packs/installer.ts"],"names":[],"mappings":"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"}
@@ -1,128 +1,241 @@
1
1
  /**
2
- * Hook pack installer — copies hookify files to ~/.claude/ (global) or project .claude/ (local).
2
+ * Hook pack installer — copies hookify files and scripts to ~/.claude/ (global) or project .claude/ (local).
3
+ * Also manages lifecycle hooks in ~/.claude/settings.json.
3
4
  */
4
- import { existsSync, copyFileSync, unlinkSync, mkdirSync } from 'node:fs';
5
+ import {
6
+ existsSync,
7
+ copyFileSync,
8
+ unlinkSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ writeFileSync,
12
+ chmodSync,
13
+ } from 'node:fs';
5
14
  import { join } from 'node:path';
6
15
  import { homedir } from 'node:os';
7
16
  import { getPack } from './registry.js';
17
+ import type { HookPackLifecycleHook } from './registry.js';
18
+
19
+ const PACK_MARKER = '_soleriPack';
8
20
 
9
- /** Resolve the target .claude/ directory. */
10
21
  function resolveClaudeDir(projectDir?: string): string {
11
22
  if (projectDir) return join(projectDir, '.claude');
12
23
  return join(homedir(), '.claude');
13
24
  }
14
25
 
15
- /**
16
- * Resolve all hookify file paths for a pack, handling composed packs.
17
- * Returns a map of hook name → source file path.
18
- */
19
26
  function resolveHookFiles(packName: string): Map<string, string> {
20
27
  const pack = getPack(packName);
21
28
  if (!pack) return new Map();
22
-
23
29
  const files = new Map<string, string>();
24
-
25
30
  if (pack.manifest.composedFrom) {
26
- // Composed pack: gather files from constituent packs
27
31
  for (const subPackName of pack.manifest.composedFrom) {
28
32
  const subFiles = resolveHookFiles(subPackName);
29
- for (const [hook, path] of subFiles) {
30
- files.set(hook, path);
31
- }
33
+ for (const [hook, path] of subFiles) { files.set(hook, path); }
32
34
  }
33
35
  } else {
34
- // Direct pack: look for hookify files in the pack directory
35
36
  for (const hook of pack.manifest.hooks) {
36
37
  const filePath = join(pack.dir, `hookify.${hook}.local.md`);
37
- if (existsSync(filePath)) {
38
- files.set(hook, filePath);
38
+ if (existsSync(filePath)) { files.set(hook, filePath); }
39
+ }
40
+ }
41
+ return files;
42
+ }
43
+
44
+ function resolveScripts(packName: string): Map<string, { sourcePath: string; targetDir: string; file: string }> {
45
+ const pack = getPack(packName);
46
+ if (!pack) return new Map();
47
+ const scripts = new Map<string, { sourcePath: string; targetDir: string; file: string }>();
48
+ if (pack.manifest.composedFrom) {
49
+ for (const subPackName of pack.manifest.composedFrom) {
50
+ const subScripts = resolveScripts(subPackName);
51
+ for (const [name, info] of subScripts) { scripts.set(name, info); }
52
+ }
53
+ } else if (pack.manifest.scripts) {
54
+ for (const script of pack.manifest.scripts) {
55
+ const sourcePath = join(pack.dir, 'scripts', script.file);
56
+ if (existsSync(sourcePath)) {
57
+ scripts.set(script.name, { sourcePath, targetDir: script.targetDir, file: script.file });
39
58
  }
40
59
  }
41
60
  }
61
+ return scripts;
62
+ }
42
63
 
43
- return files;
64
+ function resolveLifecycleHooks(packName: string): { packName: string; hook: HookPackLifecycleHook }[] {
65
+ const pack = getPack(packName);
66
+ if (!pack) return [];
67
+ const hooks: { packName: string; hook: HookPackLifecycleHook }[] = [];
68
+ if (pack.manifest.composedFrom) {
69
+ for (const subPackName of pack.manifest.composedFrom) { hooks.push(...resolveLifecycleHooks(subPackName)); }
70
+ } else if (pack.manifest.lifecycleHooks) {
71
+ for (const hook of pack.manifest.lifecycleHooks) { hooks.push({ packName: pack.manifest.name, hook }); }
72
+ }
73
+ return hooks;
74
+ }
75
+
76
+ interface SettingsHookEntry { type: 'command'; command: string; timeout?: number; [key: string]: unknown; }
77
+
78
+ function readClaudeSettings(claudeDir: string): Record<string, unknown> {
79
+ const settingsPath = join(claudeDir, 'settings.json');
80
+ if (!existsSync(settingsPath)) return {};
81
+ try { return JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { return {}; }
82
+ }
83
+
84
+ function writeClaudeSettings(claudeDir: string, settings: Record<string, unknown>): void {
85
+ const settingsPath = join(claudeDir, 'settings.json');
86
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
87
+ }
88
+
89
+ function addLifecycleHooks(claudeDir: string, lifecycleHooks: { packName: string; hook: HookPackLifecycleHook }[]): string[] {
90
+ if (lifecycleHooks.length === 0) return [];
91
+ const settings = readClaudeSettings(claudeDir);
92
+ const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;
93
+ const added: string[] = [];
94
+ for (const { packName: sourcePack, hook } of lifecycleHooks) {
95
+ const eventKey = hook.event;
96
+ const eventHooks = (hooks[eventKey] ?? []) as SettingsHookEntry[];
97
+ const alreadyExists = eventHooks.some((h) => h.command === hook.command && h[PACK_MARKER] === sourcePack);
98
+ if (!alreadyExists) {
99
+ const entry: SettingsHookEntry = { type: hook.type, command: hook.command, [PACK_MARKER]: sourcePack };
100
+ if (hook.timeout) { entry.timeout = hook.timeout; }
101
+ eventHooks.push(entry);
102
+ hooks[eventKey] = eventHooks;
103
+ added.push(`${eventKey}:${hook.matcher}`);
104
+ }
105
+ }
106
+ settings['hooks'] = hooks;
107
+ writeClaudeSettings(claudeDir, settings);
108
+ return added;
109
+ }
110
+
111
+ function removeLifecycleHooks(claudeDir: string, packName: string): string[] {
112
+ const settings = readClaudeSettings(claudeDir);
113
+ const hooks = (settings['hooks'] ?? {}) as Record<string, SettingsHookEntry[]>;
114
+ const removed: string[] = [];
115
+ for (const [eventKey, eventHooks] of Object.entries(hooks)) {
116
+ if (!Array.isArray(eventHooks)) continue;
117
+ const before = eventHooks.length;
118
+ const filtered = eventHooks.filter((h) => h[PACK_MARKER] !== packName);
119
+ if (filtered.length < before) {
120
+ removed.push(eventKey);
121
+ if (filtered.length === 0) { delete hooks[eventKey]; } else { hooks[eventKey] = filtered; }
122
+ }
123
+ }
124
+ if (removed.length > 0) { settings['hooks'] = hooks; writeClaudeSettings(claudeDir, settings); }
125
+ return removed;
44
126
  }
45
127
 
46
- /**
47
- * Install a hook pack to ~/.claude/ (default) or project .claude/ (--project).
48
- * Skips files that already exist (idempotent).
49
- */
50
128
  export function installPack(
51
129
  packName: string,
52
130
  options?: { projectDir?: string },
53
- ): { installed: string[]; skipped: string[] } {
131
+ ): { installed: string[]; skipped: string[]; scripts: string[]; lifecycleHooks: string[] } {
54
132
  const pack = getPack(packName);
55
- if (!pack) {
56
- throw new Error(`Unknown hook pack: "${packName}"`);
57
- }
58
-
133
+ if (!pack) { throw new Error(`Unknown hook pack: "${packName}"`); }
59
134
  const claudeDir = resolveClaudeDir(options?.projectDir);
60
135
  mkdirSync(claudeDir, { recursive: true });
61
-
62
136
  const hookFiles = resolveHookFiles(packName);
63
137
  const installed: string[] = [];
64
138
  const skipped: string[] = [];
65
-
66
139
  for (const [hook, sourcePath] of hookFiles) {
67
140
  const destPath = join(claudeDir, `hookify.${hook}.local.md`);
68
- if (existsSync(destPath)) {
69
- skipped.push(hook);
70
- } else {
71
- copyFileSync(sourcePath, destPath);
72
- installed.push(hook);
73
- }
141
+ if (existsSync(destPath)) { skipped.push(hook); } else { copyFileSync(sourcePath, destPath); installed.push(hook); }
74
142
  }
75
-
76
- return { installed, skipped };
143
+ const scriptFiles = resolveScripts(packName);
144
+ const installedScripts: string[] = [];
145
+ for (const [, { sourcePath, targetDir, file }] of scriptFiles) {
146
+ const destDir = join(claudeDir, targetDir);
147
+ mkdirSync(destDir, { recursive: true });
148
+ const destPath = join(destDir, file);
149
+ copyFileSync(sourcePath, destPath);
150
+ chmodSync(destPath, 0o755);
151
+ installedScripts.push(`${targetDir}/${file}`);
152
+ }
153
+ const lcHooks = resolveLifecycleHooks(packName);
154
+ const addedHooks = addLifecycleHooks(claudeDir, lcHooks);
155
+ return { installed, skipped, scripts: installedScripts, lifecycleHooks: addedHooks };
77
156
  }
78
157
 
79
- /**
80
- * Remove a hook pack's files from target directory.
81
- */
82
158
  export function removePack(
83
159
  packName: string,
84
160
  options?: { projectDir?: string },
85
- ): { removed: string[] } {
161
+ ): { removed: string[]; scripts: string[]; lifecycleHooks: string[] } {
86
162
  const pack = getPack(packName);
87
- if (!pack) {
88
- throw new Error(`Unknown hook pack: "${packName}"`);
89
- }
90
-
163
+ if (!pack) { throw new Error(`Unknown hook pack: "${packName}"`); }
91
164
  const claudeDir = resolveClaudeDir(options?.projectDir);
92
165
  const removed: string[] = [];
93
-
94
166
  for (const hook of pack.manifest.hooks) {
95
167
  const filePath = join(claudeDir, `hookify.${hook}.local.md`);
96
- if (existsSync(filePath)) {
97
- unlinkSync(filePath);
98
- removed.push(hook);
168
+ if (existsSync(filePath)) { unlinkSync(filePath); removed.push(hook); }
169
+ }
170
+ const removedScripts: string[] = [];
171
+ if (pack.manifest.scripts) {
172
+ for (const script of pack.manifest.scripts) {
173
+ const filePath = join(claudeDir, script.targetDir, script.file);
174
+ if (existsSync(filePath)) { unlinkSync(filePath); removedScripts.push(`${script.targetDir}/${script.file}`); }
99
175
  }
100
176
  }
101
-
102
- return { removed };
177
+ if (pack.manifest.composedFrom) {
178
+ for (const subPackName of pack.manifest.composedFrom) {
179
+ const subPack = getPack(subPackName);
180
+ if (subPack?.manifest.scripts) {
181
+ for (const script of subPack.manifest.scripts) {
182
+ const filePath = join(claudeDir, script.targetDir, script.file);
183
+ if (existsSync(filePath)) { unlinkSync(filePath); removedScripts.push(`${script.targetDir}/${script.file}`); }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ const removedHooks = removeLifecycleHooks(claudeDir, packName);
189
+ if (pack.manifest.composedFrom) {
190
+ for (const subPackName of pack.manifest.composedFrom) { removedHooks.push(...removeLifecycleHooks(claudeDir, subPackName)); }
191
+ }
192
+ return { removed, scripts: removedScripts, lifecycleHooks: removedHooks };
103
193
  }
104
194
 
105
- /**
106
- * Check if a pack is installed.
107
- * Returns true (all hooks present), false (none present), or 'partial'.
108
- */
109
195
  export function isPackInstalled(
110
196
  packName: string,
111
197
  options?: { projectDir?: string },
112
198
  ): boolean | 'partial' {
113
199
  const pack = getPack(packName);
114
200
  if (!pack) return false;
115
-
116
201
  const claudeDir = resolveClaudeDir(options?.projectDir);
202
+ let total = 0;
117
203
  let present = 0;
118
-
119
204
  for (const hook of pack.manifest.hooks) {
120
- if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) {
121
- present++;
205
+ total++;
206
+ if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) { present++; }
207
+ }
208
+ if (pack.manifest.scripts) {
209
+ for (const script of pack.manifest.scripts) {
210
+ total++;
211
+ if (existsSync(join(claudeDir, script.targetDir, script.file))) { present++; }
122
212
  }
123
213
  }
124
-
214
+ if (pack.manifest.composedFrom) {
215
+ for (const subPackName of pack.manifest.composedFrom) {
216
+ const subPack = getPack(subPackName);
217
+ if (subPack?.manifest.scripts) {
218
+ for (const script of subPack.manifest.scripts) {
219
+ total++;
220
+ if (existsSync(join(claudeDir, script.targetDir, script.file))) { present++; }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ if (total === 0) {
226
+ const lcHooks = resolveLifecycleHooks(packName);
227
+ if (lcHooks.length > 0) {
228
+ const settings = readClaudeSettings(claudeDir);
229
+ const hooksObj = (settings['hooks'] ?? {}) as Record<string, SettingsHookEntry[]>;
230
+ for (const { packName: sourcePack, hook } of lcHooks) {
231
+ total++;
232
+ const eventHooks = hooksObj[hook.event];
233
+ if (Array.isArray(eventHooks) && eventHooks.some((h) => h.command === hook.command && h[PACK_MARKER] === sourcePack)) { present++; }
234
+ }
235
+ }
236
+ }
237
+ if (total === 0) return false;
125
238
  if (present === 0) return false;
126
- if (present === pack.manifest.hooks.length) return true;
239
+ if (present === total) return true;
127
240
  return 'partial';
128
241
  }
@@ -1,26 +1,29 @@
1
- interface HookPackManifest {
1
+ export interface HookPackScript {
2
+ name: string;
3
+ file: string;
4
+ targetDir: string;
5
+ }
6
+ export interface HookPackLifecycleHook {
7
+ event: string;
8
+ matcher: string;
9
+ type: 'command';
10
+ command: string;
11
+ timeout?: number;
12
+ statusMessage?: string;
13
+ }
14
+ export interface HookPackManifest {
2
15
  name: string;
3
16
  description: string;
4
17
  hooks: string[];
5
18
  composedFrom?: string[];
6
19
  version?: string;
7
- /** Whether this pack is built-in or user-defined */
20
+ scripts?: HookPackScript[];
21
+ lifecycleHooks?: HookPackLifecycleHook[];
8
22
  source?: 'built-in' | 'local';
9
23
  }
10
- /**
11
- * List all available hook packs (built-in + local custom).
12
- * Local packs in .soleri/hook-packs/ override built-in packs with the same name.
13
- */
14
24
  export declare function listPacks(): HookPackManifest[];
15
- /**
16
- * Get a specific pack by name. Local packs take precedence.
17
- */
18
25
  export declare function getPack(name: string): {
19
26
  manifest: HookPackManifest;
20
27
  dir: string;
21
28
  } | null;
22
- /**
23
- * Get names of packs that are fully installed in ~/.claude/.
24
- */
25
29
  export declare function getInstalledPacks(): string[];
26
- export {};