@soleri/cli 9.2.0 → 9.3.1
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/.github/workflows/ci.yml +116 -0
- 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
- package/vitest.config.ts +2 -0
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 {
|
package/src/commands/install.ts
CHANGED
|
@@ -145,9 +145,10 @@ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean)
|
|
|
145
145
|
const engine = resolveEngineBin();
|
|
146
146
|
servers[agentId] = {
|
|
147
147
|
type: 'local',
|
|
148
|
-
command:
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
command:
|
|
149
|
+
engine.command === 'node'
|
|
150
|
+
? ['node', engine.bin, '--agent', join(agentDir, 'agent.yaml')]
|
|
151
|
+
: ['npx', '-y', '@soleri/engine', '--agent', join(agentDir, 'agent.yaml')],
|
|
151
152
|
};
|
|
152
153
|
} else {
|
|
153
154
|
servers[agentId] = {
|
|
@@ -234,7 +235,9 @@ export function registerInstall(program: Command): void {
|
|
|
234
235
|
if (engine.command === 'node') {
|
|
235
236
|
p.log.info(`Detected file-tree agent (v7) — using resolved engine at ${engine.bin}`);
|
|
236
237
|
} else {
|
|
237
|
-
p.log.warn(
|
|
238
|
+
p.log.warn(
|
|
239
|
+
`Could not resolve @soleri/core locally — falling back to npx (slower startup)`,
|
|
240
|
+
);
|
|
238
241
|
p.log.info(`For instant startup: npm install -g @soleri/cli`);
|
|
239
242
|
}
|
|
240
243
|
}
|
|
@@ -30,25 +30,33 @@ function resolveHookFiles(packName: string): Map<string, string> {
|
|
|
30
30
|
if (pack.manifest.composedFrom) {
|
|
31
31
|
for (const subPackName of pack.manifest.composedFrom) {
|
|
32
32
|
const subFiles = resolveHookFiles(subPackName);
|
|
33
|
-
for (const [hook, path] of subFiles) {
|
|
33
|
+
for (const [hook, path] of subFiles) {
|
|
34
|
+
files.set(hook, path);
|
|
35
|
+
}
|
|
34
36
|
}
|
|
35
37
|
} else {
|
|
36
38
|
for (const hook of pack.manifest.hooks) {
|
|
37
39
|
const filePath = join(pack.dir, `hookify.${hook}.local.md`);
|
|
38
|
-
if (existsSync(filePath)) {
|
|
40
|
+
if (existsSync(filePath)) {
|
|
41
|
+
files.set(hook, filePath);
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
return files;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
function resolveScripts(
|
|
48
|
+
function resolveScripts(
|
|
49
|
+
packName: string,
|
|
50
|
+
): Map<string, { sourcePath: string; targetDir: string; file: string }> {
|
|
45
51
|
const pack = getPack(packName);
|
|
46
52
|
if (!pack) return new Map();
|
|
47
53
|
const scripts = new Map<string, { sourcePath: string; targetDir: string; file: string }>();
|
|
48
54
|
if (pack.manifest.composedFrom) {
|
|
49
55
|
for (const subPackName of pack.manifest.composedFrom) {
|
|
50
56
|
const subScripts = resolveScripts(subPackName);
|
|
51
|
-
for (const [name, info] of subScripts) {
|
|
57
|
+
for (const [name, info] of subScripts) {
|
|
58
|
+
scripts.set(name, info);
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
} else if (pack.manifest.scripts) {
|
|
54
62
|
for (const script of pack.manifest.scripts) {
|
|
@@ -61,24 +69,39 @@ function resolveScripts(packName: string): Map<string, { sourcePath: string; tar
|
|
|
61
69
|
return scripts;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
function resolveLifecycleHooks(
|
|
72
|
+
function resolveLifecycleHooks(
|
|
73
|
+
packName: string,
|
|
74
|
+
): { packName: string; hook: HookPackLifecycleHook }[] {
|
|
65
75
|
const pack = getPack(packName);
|
|
66
76
|
if (!pack) return [];
|
|
67
77
|
const hooks: { packName: string; hook: HookPackLifecycleHook }[] = [];
|
|
68
78
|
if (pack.manifest.composedFrom) {
|
|
69
|
-
for (const subPackName of pack.manifest.composedFrom) {
|
|
79
|
+
for (const subPackName of pack.manifest.composedFrom) {
|
|
80
|
+
hooks.push(...resolveLifecycleHooks(subPackName));
|
|
81
|
+
}
|
|
70
82
|
} else if (pack.manifest.lifecycleHooks) {
|
|
71
|
-
for (const hook of pack.manifest.lifecycleHooks) {
|
|
83
|
+
for (const hook of pack.manifest.lifecycleHooks) {
|
|
84
|
+
hooks.push({ packName: pack.manifest.name, hook });
|
|
85
|
+
}
|
|
72
86
|
}
|
|
73
87
|
return hooks;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
interface SettingsHookEntry {
|
|
90
|
+
interface SettingsHookEntry {
|
|
91
|
+
type: 'command';
|
|
92
|
+
command: string;
|
|
93
|
+
timeout?: number;
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
}
|
|
77
96
|
|
|
78
97
|
function readClaudeSettings(claudeDir: string): Record<string, unknown> {
|
|
79
98
|
const settingsPath = join(claudeDir, 'settings.json');
|
|
80
99
|
if (!existsSync(settingsPath)) return {};
|
|
81
|
-
try {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
102
|
+
} catch {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
function writeClaudeSettings(claudeDir: string, settings: Record<string, unknown>): void {
|
|
@@ -86,7 +109,10 @@ function writeClaudeSettings(claudeDir: string, settings: Record<string, unknown
|
|
|
86
109
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
function addLifecycleHooks(
|
|
112
|
+
function addLifecycleHooks(
|
|
113
|
+
claudeDir: string,
|
|
114
|
+
lifecycleHooks: { packName: string; hook: HookPackLifecycleHook }[],
|
|
115
|
+
): string[] {
|
|
90
116
|
if (lifecycleHooks.length === 0) return [];
|
|
91
117
|
const settings = readClaudeSettings(claudeDir);
|
|
92
118
|
const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;
|
|
@@ -94,10 +120,18 @@ function addLifecycleHooks(claudeDir: string, lifecycleHooks: { packName: string
|
|
|
94
120
|
for (const { packName: sourcePack, hook } of lifecycleHooks) {
|
|
95
121
|
const eventKey = hook.event;
|
|
96
122
|
const eventHooks = (hooks[eventKey] ?? []) as SettingsHookEntry[];
|
|
97
|
-
const alreadyExists = eventHooks.some(
|
|
123
|
+
const alreadyExists = eventHooks.some(
|
|
124
|
+
(h) => h.command === hook.command && h[PACK_MARKER] === sourcePack,
|
|
125
|
+
);
|
|
98
126
|
if (!alreadyExists) {
|
|
99
|
-
const entry: SettingsHookEntry = {
|
|
100
|
-
|
|
127
|
+
const entry: SettingsHookEntry = {
|
|
128
|
+
type: hook.type,
|
|
129
|
+
command: hook.command,
|
|
130
|
+
[PACK_MARKER]: sourcePack,
|
|
131
|
+
};
|
|
132
|
+
if (hook.timeout) {
|
|
133
|
+
entry.timeout = hook.timeout;
|
|
134
|
+
}
|
|
101
135
|
eventHooks.push(entry);
|
|
102
136
|
hooks[eventKey] = eventHooks;
|
|
103
137
|
added.push(`${eventKey}:${hook.matcher}`);
|
|
@@ -118,10 +152,17 @@ function removeLifecycleHooks(claudeDir: string, packName: string): string[] {
|
|
|
118
152
|
const filtered = eventHooks.filter((h) => h[PACK_MARKER] !== packName);
|
|
119
153
|
if (filtered.length < before) {
|
|
120
154
|
removed.push(eventKey);
|
|
121
|
-
if (filtered.length === 0) {
|
|
155
|
+
if (filtered.length === 0) {
|
|
156
|
+
delete hooks[eventKey];
|
|
157
|
+
} else {
|
|
158
|
+
hooks[eventKey] = filtered;
|
|
159
|
+
}
|
|
122
160
|
}
|
|
123
161
|
}
|
|
124
|
-
if (removed.length > 0) {
|
|
162
|
+
if (removed.length > 0) {
|
|
163
|
+
settings['hooks'] = hooks;
|
|
164
|
+
writeClaudeSettings(claudeDir, settings);
|
|
165
|
+
}
|
|
125
166
|
return removed;
|
|
126
167
|
}
|
|
127
168
|
|
|
@@ -130,7 +171,9 @@ export function installPack(
|
|
|
130
171
|
options?: { projectDir?: string },
|
|
131
172
|
): { installed: string[]; skipped: string[]; scripts: string[]; lifecycleHooks: string[] } {
|
|
132
173
|
const pack = getPack(packName);
|
|
133
|
-
if (!pack) {
|
|
174
|
+
if (!pack) {
|
|
175
|
+
throw new Error(`Unknown hook pack: "${packName}"`);
|
|
176
|
+
}
|
|
134
177
|
const claudeDir = resolveClaudeDir(options?.projectDir);
|
|
135
178
|
mkdirSync(claudeDir, { recursive: true });
|
|
136
179
|
const hookFiles = resolveHookFiles(packName);
|
|
@@ -138,7 +181,12 @@ export function installPack(
|
|
|
138
181
|
const skipped: string[] = [];
|
|
139
182
|
for (const [hook, sourcePath] of hookFiles) {
|
|
140
183
|
const destPath = join(claudeDir, `hookify.${hook}.local.md`);
|
|
141
|
-
if (existsSync(destPath)) {
|
|
184
|
+
if (existsSync(destPath)) {
|
|
185
|
+
skipped.push(hook);
|
|
186
|
+
} else {
|
|
187
|
+
copyFileSync(sourcePath, destPath);
|
|
188
|
+
installed.push(hook);
|
|
189
|
+
}
|
|
142
190
|
}
|
|
143
191
|
const scriptFiles = resolveScripts(packName);
|
|
144
192
|
const installedScripts: string[] = [];
|
|
@@ -160,18 +208,26 @@ export function removePack(
|
|
|
160
208
|
options?: { projectDir?: string },
|
|
161
209
|
): { removed: string[]; scripts: string[]; lifecycleHooks: string[] } {
|
|
162
210
|
const pack = getPack(packName);
|
|
163
|
-
if (!pack) {
|
|
211
|
+
if (!pack) {
|
|
212
|
+
throw new Error(`Unknown hook pack: "${packName}"`);
|
|
213
|
+
}
|
|
164
214
|
const claudeDir = resolveClaudeDir(options?.projectDir);
|
|
165
215
|
const removed: string[] = [];
|
|
166
216
|
for (const hook of pack.manifest.hooks) {
|
|
167
217
|
const filePath = join(claudeDir, `hookify.${hook}.local.md`);
|
|
168
|
-
if (existsSync(filePath)) {
|
|
218
|
+
if (existsSync(filePath)) {
|
|
219
|
+
unlinkSync(filePath);
|
|
220
|
+
removed.push(hook);
|
|
221
|
+
}
|
|
169
222
|
}
|
|
170
223
|
const removedScripts: string[] = [];
|
|
171
224
|
if (pack.manifest.scripts) {
|
|
172
225
|
for (const script of pack.manifest.scripts) {
|
|
173
226
|
const filePath = join(claudeDir, script.targetDir, script.file);
|
|
174
|
-
if (existsSync(filePath)) {
|
|
227
|
+
if (existsSync(filePath)) {
|
|
228
|
+
unlinkSync(filePath);
|
|
229
|
+
removedScripts.push(`${script.targetDir}/${script.file}`);
|
|
230
|
+
}
|
|
175
231
|
}
|
|
176
232
|
}
|
|
177
233
|
if (pack.manifest.composedFrom) {
|
|
@@ -180,14 +236,19 @@ export function removePack(
|
|
|
180
236
|
if (subPack?.manifest.scripts) {
|
|
181
237
|
for (const script of subPack.manifest.scripts) {
|
|
182
238
|
const filePath = join(claudeDir, script.targetDir, script.file);
|
|
183
|
-
if (existsSync(filePath)) {
|
|
239
|
+
if (existsSync(filePath)) {
|
|
240
|
+
unlinkSync(filePath);
|
|
241
|
+
removedScripts.push(`${script.targetDir}/${script.file}`);
|
|
242
|
+
}
|
|
184
243
|
}
|
|
185
244
|
}
|
|
186
245
|
}
|
|
187
246
|
}
|
|
188
247
|
const removedHooks = removeLifecycleHooks(claudeDir, packName);
|
|
189
248
|
if (pack.manifest.composedFrom) {
|
|
190
|
-
for (const subPackName of pack.manifest.composedFrom) {
|
|
249
|
+
for (const subPackName of pack.manifest.composedFrom) {
|
|
250
|
+
removedHooks.push(...removeLifecycleHooks(claudeDir, subPackName));
|
|
251
|
+
}
|
|
191
252
|
}
|
|
192
253
|
return { removed, scripts: removedScripts, lifecycleHooks: removedHooks };
|
|
193
254
|
}
|
|
@@ -203,12 +264,16 @@ export function isPackInstalled(
|
|
|
203
264
|
let present = 0;
|
|
204
265
|
for (const hook of pack.manifest.hooks) {
|
|
205
266
|
total++;
|
|
206
|
-
if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) {
|
|
267
|
+
if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) {
|
|
268
|
+
present++;
|
|
269
|
+
}
|
|
207
270
|
}
|
|
208
271
|
if (pack.manifest.scripts) {
|
|
209
272
|
for (const script of pack.manifest.scripts) {
|
|
210
273
|
total++;
|
|
211
|
-
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
274
|
+
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
275
|
+
present++;
|
|
276
|
+
}
|
|
212
277
|
}
|
|
213
278
|
}
|
|
214
279
|
if (pack.manifest.composedFrom) {
|
|
@@ -217,7 +282,9 @@ export function isPackInstalled(
|
|
|
217
282
|
if (subPack?.manifest.scripts) {
|
|
218
283
|
for (const script of subPack.manifest.scripts) {
|
|
219
284
|
total++;
|
|
220
|
-
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
285
|
+
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
286
|
+
present++;
|
|
287
|
+
}
|
|
221
288
|
}
|
|
222
289
|
}
|
|
223
290
|
}
|
|
@@ -230,7 +297,12 @@ export function isPackInstalled(
|
|
|
230
297
|
for (const { packName: sourcePack, hook } of lcHooks) {
|
|
231
298
|
total++;
|
|
232
299
|
const eventHooks = hooksObj[hook.event];
|
|
233
|
-
if (
|
|
300
|
+
if (
|
|
301
|
+
Array.isArray(eventHooks) &&
|
|
302
|
+
eventHooks.some((h) => h.command === hook.command && h[PACK_MARKER] === sourcePack)
|
|
303
|
+
) {
|
|
304
|
+
present++;
|
|
305
|
+
}
|
|
234
306
|
}
|
|
235
307
|
}
|
|
236
308
|
}
|
|
@@ -35,8 +35,12 @@ export interface HookPackManifest {
|
|
|
35
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
36
|
const __dirname = dirname(__filename);
|
|
37
37
|
|
|
38
|
-
function getBuiltinRoot(): string {
|
|
39
|
-
|
|
38
|
+
function getBuiltinRoot(): string {
|
|
39
|
+
return __dirname;
|
|
40
|
+
}
|
|
41
|
+
function getLocalRoot(): string {
|
|
42
|
+
return join(process.cwd(), '.soleri', 'hook-packs');
|
|
43
|
+
}
|
|
40
44
|
|
|
41
45
|
function scanPacksDir(root: string, source: 'built-in' | 'local'): HookPackManifest[] {
|
|
42
46
|
if (!existsSync(root)) return [];
|
|
@@ -50,7 +54,9 @@ function scanPacksDir(root: string, source: 'built-in' | 'local'): HookPackManif
|
|
|
50
54
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as HookPackManifest;
|
|
51
55
|
manifest.source = source;
|
|
52
56
|
packs.push(manifest);
|
|
53
|
-
} catch {
|
|
57
|
+
} catch {
|
|
58
|
+
/* Skip malformed manifests */
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
return packs;
|
|
56
62
|
}
|
|
@@ -72,7 +78,9 @@ export function getPack(name: string): { manifest: HookPackManifest; dir: string
|
|
|
72
78
|
const manifest = JSON.parse(readFileSync(localManifest, 'utf-8')) as HookPackManifest;
|
|
73
79
|
manifest.source = 'local';
|
|
74
80
|
return { manifest, dir: localDir };
|
|
75
|
-
} catch {
|
|
81
|
+
} catch {
|
|
82
|
+
/* Fall through */
|
|
83
|
+
}
|
|
76
84
|
}
|
|
77
85
|
const builtinDir = join(getBuiltinRoot(), name);
|
|
78
86
|
const builtinManifest = join(builtinDir, 'manifest.json');
|
|
@@ -81,7 +89,9 @@ export function getPack(name: string): { manifest: HookPackManifest; dir: string
|
|
|
81
89
|
const manifest = JSON.parse(readFileSync(builtinManifest, 'utf-8')) as HookPackManifest;
|
|
82
90
|
manifest.source = 'built-in';
|
|
83
91
|
return { manifest, dir: builtinDir };
|
|
84
|
-
} catch {
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
export function getInstalledPacks(): string[] {
|
|
@@ -94,14 +104,18 @@ export function getInstalledPacks(): string[] {
|
|
|
94
104
|
const allScripts = pack.scripts.every((script) =>
|
|
95
105
|
existsSync(join(claudeDir, script.targetDir, script.file)),
|
|
96
106
|
);
|
|
97
|
-
if (allScripts) {
|
|
107
|
+
if (allScripts) {
|
|
108
|
+
installed.push(pack.name);
|
|
109
|
+
}
|
|
98
110
|
}
|
|
99
111
|
continue;
|
|
100
112
|
}
|
|
101
113
|
const allPresent = pack.hooks.every((hook) =>
|
|
102
114
|
existsSync(join(claudeDir, `hookify.${hook}.local.md`)),
|
|
103
115
|
);
|
|
104
|
-
if (allPresent) {
|
|
116
|
+
if (allPresent) {
|
|
117
|
+
installed.push(pack.name);
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
return installed;
|
|
107
121
|
}
|