mcpick 0.0.15 → 0.0.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/CHANGELOG.md +16 -0
- package/dist/cli/commands/add-json.js +60 -0
- package/dist/cli/commands/get.js +45 -0
- package/dist/cli/commands/hooks.js +314 -0
- package/dist/cli/commands/marketplace.js +63 -14
- package/dist/cli/commands/plugins.js +45 -2
- package/dist/cli/commands/reset-project-choices.js +32 -0
- package/dist/cli/index.js +4 -0
- package/dist/commands/manage-hooks.js +99 -0
- package/dist/commands/manage-marketplace.js +293 -0
- package/dist/core/hook-state.js +220 -0
- package/dist/core/settings.js +191 -0
- package/dist/index.js +22 -0
- package/dist/utils/claude-cli.js +163 -2
- package/dist/utils/paths.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { isCancel, log, multiselect, note } from '@clack/prompts';
|
|
2
|
+
import { disable_plugin_hook, enable_plugin_hook, read_disabled_hooks, } from '../core/hook-state.js';
|
|
3
|
+
import { get_all_hooks } from '../core/settings.js';
|
|
4
|
+
function format_hook(entry) {
|
|
5
|
+
const detail = entry.handler.command ||
|
|
6
|
+
entry.handler.url ||
|
|
7
|
+
entry.handler.prompt ||
|
|
8
|
+
'(unknown)';
|
|
9
|
+
const truncated = detail.length > 50 ? detail.substring(0, 47) + '...' : detail;
|
|
10
|
+
return `${entry.event} → ${entry.handler.type}: ${truncated}`;
|
|
11
|
+
}
|
|
12
|
+
function format_source(entry) {
|
|
13
|
+
if (entry.source === 'plugin' && entry.plugin_key) {
|
|
14
|
+
return entry.plugin_key;
|
|
15
|
+
}
|
|
16
|
+
return entry.scope;
|
|
17
|
+
}
|
|
18
|
+
export async function manage_hooks() {
|
|
19
|
+
// Get all active hooks + disabled hooks
|
|
20
|
+
const active_hooks = await get_all_hooks();
|
|
21
|
+
const disabled = await read_disabled_hooks();
|
|
22
|
+
const items = [];
|
|
23
|
+
// Active hooks
|
|
24
|
+
for (let i = 0; i < active_hooks.length; i++) {
|
|
25
|
+
const h = active_hooks[i];
|
|
26
|
+
items.push({
|
|
27
|
+
id: `active:${i}`,
|
|
28
|
+
active_entry: h,
|
|
29
|
+
label: format_hook(h),
|
|
30
|
+
hint: format_source(h),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// Disabled hooks
|
|
34
|
+
for (let i = 0; i < disabled.length; i++) {
|
|
35
|
+
const d = disabled[i];
|
|
36
|
+
const detail = d.original_handler.command ||
|
|
37
|
+
d.original_handler.url ||
|
|
38
|
+
d.original_handler.prompt ||
|
|
39
|
+
'(unknown)';
|
|
40
|
+
const truncated = detail.length > 50 ? detail.substring(0, 47) + '...' : detail;
|
|
41
|
+
items.push({
|
|
42
|
+
id: `disabled:${i}`,
|
|
43
|
+
disabled_index: i,
|
|
44
|
+
label: `${d.event} → ${d.original_handler.type}: ${truncated}`,
|
|
45
|
+
hint: `${d.plugin_key} (disabled)`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (items.length === 0) {
|
|
49
|
+
note('No hooks found (settings or plugins).');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Currently enabled = active hooks
|
|
53
|
+
const currently_enabled = items
|
|
54
|
+
.filter((item) => item.active_entry)
|
|
55
|
+
.map((item) => item.id);
|
|
56
|
+
const selected = await multiselect({
|
|
57
|
+
message: 'Toggle hooks on/off:',
|
|
58
|
+
options: items.map((item) => ({
|
|
59
|
+
value: item.id,
|
|
60
|
+
label: item.label,
|
|
61
|
+
hint: item.hint,
|
|
62
|
+
})),
|
|
63
|
+
initialValues: currently_enabled,
|
|
64
|
+
required: false,
|
|
65
|
+
});
|
|
66
|
+
if (isCancel(selected))
|
|
67
|
+
return;
|
|
68
|
+
const selected_set = new Set(selected);
|
|
69
|
+
let changes = 0;
|
|
70
|
+
// Disable hooks that were deselected (active → disabled)
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
if (!item.active_entry)
|
|
73
|
+
continue;
|
|
74
|
+
if (selected_set.has(item.id))
|
|
75
|
+
continue;
|
|
76
|
+
// Only plugin hooks can be disabled — settings hooks get removed
|
|
77
|
+
if (item.active_entry.source === 'plugin') {
|
|
78
|
+
await disable_plugin_hook(item.active_entry);
|
|
79
|
+
changes++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Enable hooks that were selected (disabled → active)
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
if (item.disabled_index === undefined)
|
|
85
|
+
continue;
|
|
86
|
+
if (!selected_set.has(item.id))
|
|
87
|
+
continue;
|
|
88
|
+
await enable_plugin_hook(disabled[item.disabled_index]);
|
|
89
|
+
changes++;
|
|
90
|
+
}
|
|
91
|
+
if (changes > 0) {
|
|
92
|
+
log.success(`${changes} hook(s) updated.`);
|
|
93
|
+
log.info('Restart Claude Code for changes to take effect.');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
log.info('No changes.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=manage-hooks.js.map
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { confirm, isCancel, log, multiselect, note, select, text, } from '@clack/prompts';
|
|
2
|
+
import { check_restored_hooks, redisable_restored_hooks, } from '../core/hook-state.js';
|
|
3
|
+
import { read_known_marketplaces, read_marketplace_manifest, } from '../core/plugin-cache.js';
|
|
4
|
+
import { get_all_plugins, read_claude_settings, } from '../core/settings.js';
|
|
5
|
+
import { install_plugin_via_cli, marketplace_add_via_cli, marketplace_remove_via_cli, marketplace_update_via_cli, uninstall_plugin_via_cli, } from '../utils/claude-cli.js';
|
|
6
|
+
/**
|
|
7
|
+
* Browse all available plugins across all marketplaces.
|
|
8
|
+
* Shows a multiselect with installed plugins pre-selected — toggle to install/uninstall.
|
|
9
|
+
*/
|
|
10
|
+
async function handle_browse() {
|
|
11
|
+
const known = await read_known_marketplaces();
|
|
12
|
+
const marketplace_names = Object.keys(known);
|
|
13
|
+
if (marketplace_names.length === 0) {
|
|
14
|
+
note('No marketplaces configured.\nAdd one first to browse plugins.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Build list of all available plugins across marketplaces
|
|
18
|
+
const all_available = [];
|
|
19
|
+
for (const mkt_name of marketplace_names) {
|
|
20
|
+
const manifest = await read_marketplace_manifest(mkt_name);
|
|
21
|
+
if (!manifest?.plugins?.length)
|
|
22
|
+
continue;
|
|
23
|
+
for (const p of manifest.plugins) {
|
|
24
|
+
all_available.push({
|
|
25
|
+
key: `${p.name}@${mkt_name}`,
|
|
26
|
+
name: p.name,
|
|
27
|
+
marketplace: mkt_name,
|
|
28
|
+
description: p.description,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (all_available.length === 0) {
|
|
33
|
+
note('No plugins found in any marketplace.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Get currently installed plugins
|
|
37
|
+
const settings = await read_claude_settings();
|
|
38
|
+
const installed = get_all_plugins(settings);
|
|
39
|
+
const installed_keys = new Set(installed.map((p) => `${p.name}@${p.marketplace}`));
|
|
40
|
+
const selected = await multiselect({
|
|
41
|
+
message: `Available plugins (${all_available.length}) — toggle to install/uninstall:`,
|
|
42
|
+
options: all_available.map((p) => ({
|
|
43
|
+
value: p.key,
|
|
44
|
+
label: p.name,
|
|
45
|
+
hint: `${p.marketplace}${p.description ? ` · ${p.description}` : ''}`,
|
|
46
|
+
})),
|
|
47
|
+
initialValues: all_available
|
|
48
|
+
.filter((p) => installed_keys.has(p.key))
|
|
49
|
+
.map((p) => p.key),
|
|
50
|
+
required: false,
|
|
51
|
+
});
|
|
52
|
+
if (isCancel(selected))
|
|
53
|
+
return;
|
|
54
|
+
const selected_set = new Set(selected);
|
|
55
|
+
// Determine what to install and uninstall
|
|
56
|
+
const to_install = all_available.filter((p) => selected_set.has(p.key) && !installed_keys.has(p.key));
|
|
57
|
+
const to_uninstall = all_available.filter((p) => !selected_set.has(p.key) && installed_keys.has(p.key));
|
|
58
|
+
if (to_install.length === 0 && to_uninstall.length === 0) {
|
|
59
|
+
log.info('No changes.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Install new plugins
|
|
63
|
+
for (const p of to_install) {
|
|
64
|
+
log.info(`Installing ${p.key}...`);
|
|
65
|
+
const result = await install_plugin_via_cli(p.key);
|
|
66
|
+
if (result.success) {
|
|
67
|
+
log.success(`Installed: ${p.key}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
log.error(`Failed: ${p.key} - ${result.error}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Uninstall deselected plugins
|
|
74
|
+
for (const p of to_uninstall) {
|
|
75
|
+
log.info(`Uninstalling ${p.key}...`);
|
|
76
|
+
const result = await uninstall_plugin_via_cli(p.key);
|
|
77
|
+
if (result.success) {
|
|
78
|
+
log.success(`Uninstalled: ${p.key}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
log.error(`Failed: ${p.key} - ${result.error}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (to_install.length > 0)
|
|
86
|
+
parts.push(`${to_install.length} installed`);
|
|
87
|
+
if (to_uninstall.length > 0)
|
|
88
|
+
parts.push(`${to_uninstall.length} uninstalled`);
|
|
89
|
+
note(parts.join(', '), 'Plugins updated');
|
|
90
|
+
}
|
|
91
|
+
async function handle_add() {
|
|
92
|
+
const source = await text({
|
|
93
|
+
message: 'Marketplace source:',
|
|
94
|
+
placeholder: 'e.g. owner/repo or https://github.com/owner/repo',
|
|
95
|
+
validate: (value) => {
|
|
96
|
+
if (!value || value.trim().length === 0) {
|
|
97
|
+
return 'Marketplace source is required';
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (isCancel(source))
|
|
102
|
+
return;
|
|
103
|
+
log.info(`Adding marketplace: ${source}`);
|
|
104
|
+
const result = await marketplace_add_via_cli(source);
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
log.error(result.error || 'Unknown error');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
log.success(`Marketplace added: ${source}`);
|
|
110
|
+
// Try to find and offer available plugins
|
|
111
|
+
const marketplace_name = derive_marketplace_name(source);
|
|
112
|
+
const manifest = marketplace_name
|
|
113
|
+
? await read_marketplace_manifest(marketplace_name)
|
|
114
|
+
: null;
|
|
115
|
+
if (!manifest?.plugins?.length) {
|
|
116
|
+
log.info('Install plugins with: mcpick plugins install <name>@<marketplace>');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const should_install = await confirm({
|
|
120
|
+
message: `${manifest.plugins.length} plugins available. Install now?`,
|
|
121
|
+
});
|
|
122
|
+
if (isCancel(should_install) || !should_install)
|
|
123
|
+
return;
|
|
124
|
+
const to_install = await multiselect({
|
|
125
|
+
message: 'Select plugins to install:',
|
|
126
|
+
options: manifest.plugins.map((p) => ({
|
|
127
|
+
value: `${p.name}@${marketplace_name}`,
|
|
128
|
+
label: p.name,
|
|
129
|
+
hint: p.description,
|
|
130
|
+
})),
|
|
131
|
+
required: false,
|
|
132
|
+
});
|
|
133
|
+
if (isCancel(to_install) || to_install.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
for (const key of to_install) {
|
|
136
|
+
log.info(`Installing ${key}...`);
|
|
137
|
+
const install_result = await install_plugin_via_cli(key);
|
|
138
|
+
if (install_result.success) {
|
|
139
|
+
log.success(`Installed: ${key}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
log.error(`Failed: ${key} - ${install_result.error}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function derive_marketplace_name(source) {
|
|
147
|
+
const match = source.match(/([^/]+?)(?:\.git)?$/);
|
|
148
|
+
return match ? match[1].toLowerCase() : null;
|
|
149
|
+
}
|
|
150
|
+
async function handle_remove() {
|
|
151
|
+
const known = await read_known_marketplaces();
|
|
152
|
+
const names = Object.keys(known);
|
|
153
|
+
if (names.length === 0) {
|
|
154
|
+
note('No marketplaces configured.');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const name = await select({
|
|
158
|
+
message: 'Select marketplace to remove:',
|
|
159
|
+
options: names.map((n) => ({
|
|
160
|
+
value: n,
|
|
161
|
+
label: n,
|
|
162
|
+
hint: known[n].source.repo || known[n].source.url,
|
|
163
|
+
})),
|
|
164
|
+
});
|
|
165
|
+
if (isCancel(name))
|
|
166
|
+
return;
|
|
167
|
+
const should_remove = await confirm({
|
|
168
|
+
message: `Remove marketplace '${name}'? Its plugins will also be removed.`,
|
|
169
|
+
});
|
|
170
|
+
if (isCancel(should_remove) || !should_remove)
|
|
171
|
+
return;
|
|
172
|
+
const remove_result = await marketplace_remove_via_cli(name);
|
|
173
|
+
if (remove_result.success) {
|
|
174
|
+
log.success(`Marketplace removed: ${name}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
log.error(remove_result.error || 'Unknown error');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function handle_update() {
|
|
181
|
+
const known = await read_known_marketplaces();
|
|
182
|
+
const names = Object.keys(known);
|
|
183
|
+
if (names.length === 0) {
|
|
184
|
+
note('No marketplaces configured.');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const choice = await select({
|
|
188
|
+
message: 'What to update:',
|
|
189
|
+
options: [
|
|
190
|
+
{ value: '__all__', label: 'All marketplaces' },
|
|
191
|
+
...names.map((n) => ({
|
|
192
|
+
value: n,
|
|
193
|
+
label: n,
|
|
194
|
+
hint: known[n].source.repo || known[n].source.url,
|
|
195
|
+
})),
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
if (isCancel(choice))
|
|
199
|
+
return;
|
|
200
|
+
if (choice === '__all__') {
|
|
201
|
+
log.info('Updating all marketplaces...');
|
|
202
|
+
const result = await marketplace_update_via_cli();
|
|
203
|
+
if (result.success) {
|
|
204
|
+
log.success('All marketplaces updated.');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
log.error(result.error || 'Unknown error');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
log.info(`Updating ${choice}...`);
|
|
212
|
+
const result = await marketplace_update_via_cli(choice);
|
|
213
|
+
if (result.success) {
|
|
214
|
+
log.success(`Marketplace updated: ${choice}`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
log.error(result.error || 'Unknown error');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Check if update restored any disabled hooks
|
|
221
|
+
const restored = await check_restored_hooks();
|
|
222
|
+
if (restored.length > 0) {
|
|
223
|
+
log.warn(`${restored.length} disabled hook(s) were restored by the update.`);
|
|
224
|
+
const should_redisable = await confirm({
|
|
225
|
+
message: 'Re-disable these hooks?',
|
|
226
|
+
});
|
|
227
|
+
if (!isCancel(should_redisable) && should_redisable) {
|
|
228
|
+
const redisable_result = await redisable_restored_hooks(restored);
|
|
229
|
+
if (redisable_result.success > 0) {
|
|
230
|
+
log.success(`Re-disabled ${redisable_result.success} hook(s).`);
|
|
231
|
+
}
|
|
232
|
+
if (redisable_result.failed > 0) {
|
|
233
|
+
log.error(`Failed to re-disable ${redisable_result.failed} hook(s).`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export async function manage_marketplace() {
|
|
239
|
+
while (true) {
|
|
240
|
+
const action = await select({
|
|
241
|
+
message: 'Marketplace & plugins:',
|
|
242
|
+
options: [
|
|
243
|
+
{
|
|
244
|
+
value: 'browse',
|
|
245
|
+
label: 'Browse & install plugins',
|
|
246
|
+
hint: 'Toggle plugins on/off across all marketplaces',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
value: 'add',
|
|
250
|
+
label: 'Add marketplace',
|
|
251
|
+
hint: 'Add a plugin catalog, then install plugins from it',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
value: 'remove',
|
|
255
|
+
label: 'Remove marketplace',
|
|
256
|
+
hint: 'Remove a marketplace and its plugins',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
value: 'update',
|
|
260
|
+
label: 'Update marketplace(s)',
|
|
261
|
+
hint: 'Pull latest from source',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
value: 'back',
|
|
265
|
+
label: 'Back',
|
|
266
|
+
hint: 'Return to main menu',
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
if (isCancel(action) || action === 'back')
|
|
271
|
+
return;
|
|
272
|
+
try {
|
|
273
|
+
switch (action) {
|
|
274
|
+
case 'browse':
|
|
275
|
+
await handle_browse();
|
|
276
|
+
break;
|
|
277
|
+
case 'add':
|
|
278
|
+
await handle_add();
|
|
279
|
+
break;
|
|
280
|
+
case 'remove':
|
|
281
|
+
await handle_remove();
|
|
282
|
+
break;
|
|
283
|
+
case 'update':
|
|
284
|
+
await handle_update();
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
log.error(err instanceof Error ? err.message : 'Unknown error');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=manage-marketplace.js.map
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ensure_directory_exists, get_disabled_hooks_path, get_marketplaces_dir, get_mcpick_dir, } from '../utils/paths.js';
|
|
4
|
+
export async function read_disabled_hooks() {
|
|
5
|
+
try {
|
|
6
|
+
const content = await readFile(get_disabled_hooks_path(), 'utf-8');
|
|
7
|
+
return JSON.parse(content);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function write_disabled_hooks(entries) {
|
|
14
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
15
|
+
await writeFile(get_disabled_hooks_path(), JSON.stringify(entries, null, '\t'), 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Remove a specific hook handler from a hooks.json file by matching the handler.
|
|
19
|
+
* Returns true if the hook was found and removed.
|
|
20
|
+
*/
|
|
21
|
+
async function remove_hook_from_file(hooks_path, event, handler) {
|
|
22
|
+
let content;
|
|
23
|
+
try {
|
|
24
|
+
content = await readFile(hooks_path, 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const hooks_data = JSON.parse(content);
|
|
30
|
+
const hooks_obj = (hooks_data.hooks || hooks_data);
|
|
31
|
+
const matchers = hooks_obj[event];
|
|
32
|
+
if (!matchers)
|
|
33
|
+
return false;
|
|
34
|
+
let removed = false;
|
|
35
|
+
for (const m of matchers) {
|
|
36
|
+
const idx = m.hooks?.findIndex((h) => h.type === handler.type &&
|
|
37
|
+
h.command === handler.command &&
|
|
38
|
+
h.url === handler.url &&
|
|
39
|
+
h.prompt === handler.prompt);
|
|
40
|
+
if (idx !== undefined && idx >= 0) {
|
|
41
|
+
m.hooks.splice(idx, 1);
|
|
42
|
+
removed = true;
|
|
43
|
+
if (m.hooks.length === 0) {
|
|
44
|
+
matchers.splice(matchers.indexOf(m), 1);
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!removed)
|
|
50
|
+
return false;
|
|
51
|
+
if (matchers.length === 0) {
|
|
52
|
+
delete hooks_obj[event];
|
|
53
|
+
}
|
|
54
|
+
await writeFile(hooks_path, JSON.stringify(hooks_data, null, '\t'), 'utf-8');
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get all hooks.json paths for a plugin (cache + marketplace source).
|
|
59
|
+
*/
|
|
60
|
+
function get_all_hooks_paths(plugin_key, primary_path) {
|
|
61
|
+
const paths = [primary_path];
|
|
62
|
+
const at_index = plugin_key.lastIndexOf('@');
|
|
63
|
+
if (at_index > 0) {
|
|
64
|
+
const plugin_name = plugin_key.substring(0, at_index);
|
|
65
|
+
const marketplace_name = plugin_key.substring(at_index + 1);
|
|
66
|
+
paths.push(join(get_marketplaces_dir(), marketplace_name, 'plugins', plugin_name, 'hooks', 'hooks.json'));
|
|
67
|
+
}
|
|
68
|
+
return [...new Set(paths)]; // deduplicate
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Disable a specific hook from a plugin.
|
|
72
|
+
* Removes from both cache and marketplace source hooks.json files.
|
|
73
|
+
*/
|
|
74
|
+
export async function disable_plugin_hook(entry) {
|
|
75
|
+
if (!entry.hooks_json_path || !entry.plugin_key) {
|
|
76
|
+
throw new Error('Not a plugin hook');
|
|
77
|
+
}
|
|
78
|
+
// Save to disabled state
|
|
79
|
+
const disabled = await read_disabled_hooks();
|
|
80
|
+
disabled.push({
|
|
81
|
+
plugin_key: entry.plugin_key,
|
|
82
|
+
hooks_json_path: entry.hooks_json_path,
|
|
83
|
+
event: entry.event,
|
|
84
|
+
matcher: entry.matcher,
|
|
85
|
+
matcher_index: entry.matcher_index,
|
|
86
|
+
hook_index: entry.hook_index,
|
|
87
|
+
original_handler: entry.handler,
|
|
88
|
+
disabled_at: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
await write_disabled_hooks(disabled);
|
|
91
|
+
// Remove from all hooks.json files (cache + marketplace source)
|
|
92
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
93
|
+
for (const hooks_path of all_paths) {
|
|
94
|
+
await remove_hook_from_file(hooks_path, entry.event, entry.handler);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Add a hook handler back into a hooks.json file.
|
|
99
|
+
*/
|
|
100
|
+
async function add_hook_to_file(hooks_path, event, matcher_pattern, handler) {
|
|
101
|
+
let hooks_data;
|
|
102
|
+
try {
|
|
103
|
+
const content = await readFile(hooks_path, 'utf-8');
|
|
104
|
+
hooks_data = JSON.parse(content);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
hooks_data = { hooks: {} };
|
|
108
|
+
}
|
|
109
|
+
const hooks_obj = (hooks_data.hooks ||
|
|
110
|
+
(hooks_data.hooks = {}));
|
|
111
|
+
if (!hooks_obj[event])
|
|
112
|
+
hooks_obj[event] = [];
|
|
113
|
+
const matchers = hooks_obj[event];
|
|
114
|
+
let matcher = matchers.find((m) => (m.matcher || undefined) === matcher_pattern);
|
|
115
|
+
if (!matcher) {
|
|
116
|
+
matcher = { hooks: [] };
|
|
117
|
+
if (matcher_pattern)
|
|
118
|
+
matcher.matcher = matcher_pattern;
|
|
119
|
+
matchers.push(matcher);
|
|
120
|
+
}
|
|
121
|
+
// Only add if not already present (avoids duplicates)
|
|
122
|
+
const already_exists = matcher.hooks.some((h) => h.type === handler.type &&
|
|
123
|
+
h.command === handler.command &&
|
|
124
|
+
h.url === handler.url &&
|
|
125
|
+
h.prompt === handler.prompt);
|
|
126
|
+
if (already_exists)
|
|
127
|
+
return;
|
|
128
|
+
matcher.hooks.push(handler);
|
|
129
|
+
await writeFile(hooks_path, JSON.stringify(hooks_data, null, '\t'), 'utf-8');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Re-enable a previously disabled plugin hook.
|
|
133
|
+
* Restores to both cache and marketplace source hooks.json files.
|
|
134
|
+
*/
|
|
135
|
+
export async function enable_plugin_hook(disabled_entry) {
|
|
136
|
+
const all_paths = get_all_hooks_paths(disabled_entry.plugin_key, disabled_entry.hooks_json_path);
|
|
137
|
+
for (const hooks_path of all_paths) {
|
|
138
|
+
await add_hook_to_file(hooks_path, disabled_entry.event, disabled_entry.matcher, disabled_entry.original_handler);
|
|
139
|
+
}
|
|
140
|
+
// Remove from disabled state
|
|
141
|
+
const disabled = await read_disabled_hooks();
|
|
142
|
+
const updated = disabled.filter((d) => !(d.plugin_key === disabled_entry.plugin_key &&
|
|
143
|
+
d.event === disabled_entry.event &&
|
|
144
|
+
d.disabled_at === disabled_entry.disabled_at));
|
|
145
|
+
await write_disabled_hooks(updated);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check if any previously disabled hooks have been restored (e.g. by marketplace update).
|
|
149
|
+
* Returns entries that were re-added and need to be re-disabled.
|
|
150
|
+
*/
|
|
151
|
+
export async function check_restored_hooks() {
|
|
152
|
+
const disabled = await read_disabled_hooks();
|
|
153
|
+
if (disabled.length === 0)
|
|
154
|
+
return [];
|
|
155
|
+
const restored = [];
|
|
156
|
+
for (const entry of disabled) {
|
|
157
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
158
|
+
let found = false;
|
|
159
|
+
for (const hooks_path of all_paths) {
|
|
160
|
+
let hooks_data;
|
|
161
|
+
try {
|
|
162
|
+
const content = await readFile(hooks_path, 'utf-8');
|
|
163
|
+
hooks_data = JSON.parse(content);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const hooks_obj = (hooks_data.hooks || hooks_data);
|
|
169
|
+
const matchers = hooks_obj[entry.event];
|
|
170
|
+
if (!matchers)
|
|
171
|
+
continue;
|
|
172
|
+
for (const m of matchers) {
|
|
173
|
+
if ((m.matcher || undefined) !== entry.matcher)
|
|
174
|
+
continue;
|
|
175
|
+
const has_match = m.hooks?.some((h) => h.type === entry.original_handler.type &&
|
|
176
|
+
(h.command === entry.original_handler.command ||
|
|
177
|
+
h.url === entry.original_handler.url ||
|
|
178
|
+
h.prompt === entry.original_handler.prompt));
|
|
179
|
+
if (has_match) {
|
|
180
|
+
found = true;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (found)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (found)
|
|
188
|
+
restored.push(entry);
|
|
189
|
+
}
|
|
190
|
+
return restored;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Re-disable hooks that were restored by a marketplace update.
|
|
194
|
+
*/
|
|
195
|
+
export async function redisable_restored_hooks(restored) {
|
|
196
|
+
let success = 0;
|
|
197
|
+
let failed = 0;
|
|
198
|
+
for (const entry of restored) {
|
|
199
|
+
try {
|
|
200
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
201
|
+
let any_removed = false;
|
|
202
|
+
for (const hooks_path of all_paths) {
|
|
203
|
+
const removed = await remove_hook_from_file(hooks_path, entry.event, entry.original_handler);
|
|
204
|
+
if (removed)
|
|
205
|
+
any_removed = true;
|
|
206
|
+
}
|
|
207
|
+
if (any_removed) {
|
|
208
|
+
success++;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
failed++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
failed++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { success, failed };
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=hook-state.js.map
|