mcpick 0.0.13 → 0.0.14
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 +6 -0
- package/dist/cli/commands/cache.js +109 -1
- package/dist/cli/commands/dev.js +162 -0
- package/dist/cli/index.js +1 -0
- package/dist/core/dev-override.js +210 -0
- package/dist/core/plugin-cache.js +159 -3
- package/dist/index.js +1 -0
- package/dist/utils/paths.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import { clean_orphaned_versions, clear_plugin_caches, get_cached_plugins_info, read_installed_plugins, refresh_all_marketplaces, } from '../../core/plugin-cache.js';
|
|
2
|
+
import { clean_orphaned_versions, clear_plugin_caches, get_cached_plugins_info, link_local_plugin, list_linked_plugins, read_installed_plugins, refresh_all_marketplaces, unlink_local_plugin, } from '../../core/plugin-cache.js';
|
|
3
3
|
import { error, output } from '../output.js';
|
|
4
4
|
const status = defineCommand({
|
|
5
5
|
meta: {
|
|
@@ -166,6 +166,111 @@ const refresh = defineCommand({
|
|
|
166
166
|
}
|
|
167
167
|
},
|
|
168
168
|
});
|
|
169
|
+
const link = defineCommand({
|
|
170
|
+
meta: {
|
|
171
|
+
name: 'link',
|
|
172
|
+
description: 'Symlink a local directory into the plugin cache for dev',
|
|
173
|
+
},
|
|
174
|
+
args: {
|
|
175
|
+
path: {
|
|
176
|
+
type: 'positional',
|
|
177
|
+
description: 'Local path to the plugin/marketplace directory',
|
|
178
|
+
required: true,
|
|
179
|
+
},
|
|
180
|
+
as: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
description: 'Plugin key (name@marketplace) for the cache entry',
|
|
183
|
+
required: true,
|
|
184
|
+
},
|
|
185
|
+
json: {
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
description: 'Output as JSON',
|
|
188
|
+
default: false,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
async run({ args }) {
|
|
192
|
+
if (!args.as) {
|
|
193
|
+
error('--as is required. Specify plugin key as name@marketplace.');
|
|
194
|
+
}
|
|
195
|
+
if (!args.as.includes('@')) {
|
|
196
|
+
error('Plugin key must be in name@marketplace format (e.g. my-plugin@my-marketplace)');
|
|
197
|
+
}
|
|
198
|
+
const result = await link_local_plugin(args.path, args.as);
|
|
199
|
+
if (args.json) {
|
|
200
|
+
output(result, true);
|
|
201
|
+
}
|
|
202
|
+
else if (result.success) {
|
|
203
|
+
console.log(`Linked: ${result.key}`);
|
|
204
|
+
console.log(` ${result.symlinkPath} → ${result.targetPath}`);
|
|
205
|
+
console.log('\nRun /reload-plugins in Claude Code or restart your session.');
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
error(result.error || 'Unknown error');
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const unlink = defineCommand({
|
|
213
|
+
meta: {
|
|
214
|
+
name: 'unlink',
|
|
215
|
+
description: 'Remove a symlink from the plugin cache',
|
|
216
|
+
},
|
|
217
|
+
args: {
|
|
218
|
+
key: {
|
|
219
|
+
type: 'positional',
|
|
220
|
+
description: 'Plugin key (name@marketplace)',
|
|
221
|
+
required: true,
|
|
222
|
+
},
|
|
223
|
+
json: {
|
|
224
|
+
type: 'boolean',
|
|
225
|
+
description: 'Output as JSON',
|
|
226
|
+
default: false,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
async run({ args }) {
|
|
230
|
+
const result = await unlink_local_plugin(args.key);
|
|
231
|
+
if (args.json) {
|
|
232
|
+
output(result, true);
|
|
233
|
+
}
|
|
234
|
+
else if (result.success) {
|
|
235
|
+
console.log(`Unlinked: ${args.key}`);
|
|
236
|
+
if (result.restored) {
|
|
237
|
+
console.log(' Original cache directory restored from backup.');
|
|
238
|
+
}
|
|
239
|
+
console.log('\nRun /reload-plugins in Claude Code or restart your session.');
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
error(result.error || 'Unknown error');
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const links = defineCommand({
|
|
247
|
+
meta: {
|
|
248
|
+
name: 'links',
|
|
249
|
+
description: 'List all symlinked plugin cache entries',
|
|
250
|
+
},
|
|
251
|
+
args: {
|
|
252
|
+
json: {
|
|
253
|
+
type: 'boolean',
|
|
254
|
+
description: 'Output as JSON',
|
|
255
|
+
default: false,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
async run({ args }) {
|
|
259
|
+
const linked = await list_linked_plugins();
|
|
260
|
+
if (args.json) {
|
|
261
|
+
output(linked, true);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (linked.length === 0) {
|
|
265
|
+
console.log('No linked plugins.');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
for (const l of linked) {
|
|
269
|
+
console.log(`${l.key}`);
|
|
270
|
+
console.log(` ${l.symlinkPath} → ${l.targetPath}`);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
|
169
274
|
export default defineCommand({
|
|
170
275
|
meta: {
|
|
171
276
|
name: 'cache',
|
|
@@ -176,6 +281,9 @@ export default defineCommand({
|
|
|
176
281
|
clear,
|
|
177
282
|
'clean-orphaned': clean_orphaned,
|
|
178
283
|
refresh,
|
|
284
|
+
link,
|
|
285
|
+
unlink,
|
|
286
|
+
links,
|
|
179
287
|
},
|
|
180
288
|
});
|
|
181
289
|
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { apply_dev_override, list_dev_overrides, restore_all_dev_overrides, restore_dev_override, } from '../../core/dev-override.js';
|
|
3
|
+
import { error, output } from '../output.js';
|
|
4
|
+
const apply = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: 'apply',
|
|
7
|
+
description: 'Override an MCP server with a local dev command',
|
|
8
|
+
},
|
|
9
|
+
args: {
|
|
10
|
+
name: {
|
|
11
|
+
type: 'positional',
|
|
12
|
+
description: 'Server name to override',
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
command: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Local command to run',
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
args: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Comma-separated arguments',
|
|
23
|
+
},
|
|
24
|
+
scope: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Scope to search: local, project, or user (default: auto-detect)',
|
|
27
|
+
},
|
|
28
|
+
json: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
description: 'Output as JSON',
|
|
31
|
+
default: false,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
async run({ args }) {
|
|
35
|
+
const scope = args.scope;
|
|
36
|
+
if (scope && !['local', 'project', 'user'].includes(scope)) {
|
|
37
|
+
error(`Invalid scope: ${scope}. Use local, project, or user.`);
|
|
38
|
+
}
|
|
39
|
+
const cmd_args = args.args
|
|
40
|
+
? args.args.split(',')
|
|
41
|
+
: [];
|
|
42
|
+
const result = await apply_dev_override(args.name, args.command, cmd_args, scope);
|
|
43
|
+
if (args.json) {
|
|
44
|
+
output(result, true);
|
|
45
|
+
}
|
|
46
|
+
else if (result.success) {
|
|
47
|
+
console.log(`Dev override applied for '${args.name}' (scope: ${result.scope})`);
|
|
48
|
+
console.log(` command: ${args.command}${cmd_args.length > 0 ? ` ${cmd_args.join(' ')}` : ''}`);
|
|
49
|
+
console.log('\nRestart Claude Code or run /reload-plugins to pick up changes.');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
error(result.error || 'Unknown error');
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const restore = defineCommand({
|
|
57
|
+
meta: {
|
|
58
|
+
name: 'restore',
|
|
59
|
+
description: 'Restore original server config from dev override',
|
|
60
|
+
},
|
|
61
|
+
args: {
|
|
62
|
+
name: {
|
|
63
|
+
type: 'positional',
|
|
64
|
+
description: 'Server name to restore (omit with --all to restore all)',
|
|
65
|
+
required: false,
|
|
66
|
+
},
|
|
67
|
+
all: {
|
|
68
|
+
type: 'boolean',
|
|
69
|
+
description: 'Restore all dev overrides',
|
|
70
|
+
default: false,
|
|
71
|
+
},
|
|
72
|
+
json: {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: 'Output as JSON',
|
|
75
|
+
default: false,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
async run({ args }) {
|
|
79
|
+
if (args.all) {
|
|
80
|
+
const result = await restore_all_dev_overrides();
|
|
81
|
+
if (args.json) {
|
|
82
|
+
output(result, true);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (result.restored.length === 0 && result.errors.length === 0) {
|
|
86
|
+
console.log('No dev overrides to restore.');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
for (const name of result.restored) {
|
|
90
|
+
console.log(`Restored: ${name}`);
|
|
91
|
+
}
|
|
92
|
+
for (const err of result.errors) {
|
|
93
|
+
console.error(`Error: ${err}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!args.name) {
|
|
100
|
+
error('Specify a server name or use --all. Run "mcpick dev list" to see active overrides.');
|
|
101
|
+
}
|
|
102
|
+
const result = await restore_dev_override(args.name);
|
|
103
|
+
if (args.json) {
|
|
104
|
+
output(result, true);
|
|
105
|
+
}
|
|
106
|
+
else if (result.success) {
|
|
107
|
+
console.log(`Restored original config for '${args.name}'`);
|
|
108
|
+
console.log('\nRestart Claude Code or run /reload-plugins to pick up changes.');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
error(result.error || 'Unknown error');
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const list = defineCommand({
|
|
116
|
+
meta: {
|
|
117
|
+
name: 'list',
|
|
118
|
+
description: 'List active dev overrides',
|
|
119
|
+
},
|
|
120
|
+
args: {
|
|
121
|
+
json: {
|
|
122
|
+
type: 'boolean',
|
|
123
|
+
description: 'Output as JSON',
|
|
124
|
+
default: false,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
async run({ args }) {
|
|
128
|
+
const overrides = await list_dev_overrides();
|
|
129
|
+
if (args.json) {
|
|
130
|
+
output(overrides, true);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (overrides.length === 0) {
|
|
134
|
+
console.log('No active dev overrides.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const o of overrides) {
|
|
138
|
+
const orig = o.original;
|
|
139
|
+
const dev = o.dev;
|
|
140
|
+
const original_cmd = orig.command
|
|
141
|
+
? `${orig.command}${orig.args ? ' ' + orig.args.join(' ') : ''}`
|
|
142
|
+
: orig.url || '?';
|
|
143
|
+
const dev_cmd = `${dev.command}${dev.args ? ' ' + dev.args.join(' ') : ''}`;
|
|
144
|
+
console.log(`${o.name} (scope: ${o.scope})`);
|
|
145
|
+
console.log(` original: ${original_cmd}`);
|
|
146
|
+
console.log(` dev: ${dev_cmd}`);
|
|
147
|
+
console.log(` since: ${o.createdAt}`);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
export default defineCommand({
|
|
152
|
+
meta: {
|
|
153
|
+
name: 'dev',
|
|
154
|
+
description: 'MCP server local development workflow',
|
|
155
|
+
},
|
|
156
|
+
subCommands: {
|
|
157
|
+
apply,
|
|
158
|
+
restore,
|
|
159
|
+
list,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
//# sourceMappingURL=dev.js.map
|
package/dist/cli/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const main = defineCommand({
|
|
|
15
15
|
profile: () => import('./commands/profile.js').then((m) => m.default),
|
|
16
16
|
plugins: () => import('./commands/plugins.js').then((m) => m.default),
|
|
17
17
|
cache: () => import('./commands/cache.js').then((m) => m.default),
|
|
18
|
+
dev: () => import('./commands/dev.js').then((m) => m.default),
|
|
18
19
|
},
|
|
19
20
|
});
|
|
20
21
|
export const run = () => runMain(main);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { atomic_json_write } from '../utils/atomic-write.js';
|
|
3
|
+
import { get_claude_config_path, get_current_project_path, get_dev_overrides_path, get_project_mcp_json_path, } from '../utils/paths.js';
|
|
4
|
+
const EMPTY_OVERRIDES = {
|
|
5
|
+
version: 1,
|
|
6
|
+
overrides: {},
|
|
7
|
+
};
|
|
8
|
+
export async function read_dev_overrides() {
|
|
9
|
+
try {
|
|
10
|
+
const content = await readFile(get_dev_overrides_path(), 'utf-8');
|
|
11
|
+
return JSON.parse(content);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { ...EMPTY_OVERRIDES, overrides: {} };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function write_dev_overrides(data) {
|
|
18
|
+
await atomic_json_write(get_dev_overrides_path(), () => data);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read full config for a given scope and return the server entry if found.
|
|
22
|
+
* Returns { config, server, scope } or null.
|
|
23
|
+
*/
|
|
24
|
+
async function find_server_in_scope(name, scope) {
|
|
25
|
+
if (scope === 'user' || scope === 'local') {
|
|
26
|
+
const config_path = get_claude_config_path();
|
|
27
|
+
try {
|
|
28
|
+
await access(config_path);
|
|
29
|
+
const content = await readFile(config_path, 'utf-8');
|
|
30
|
+
const parsed = JSON.parse(content);
|
|
31
|
+
if (scope === 'user') {
|
|
32
|
+
const server = parsed.mcpServers?.[name];
|
|
33
|
+
if (server)
|
|
34
|
+
return { server, scope: 'user' };
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// local scope: projects[cwd].mcpServers
|
|
38
|
+
const cwd = get_current_project_path();
|
|
39
|
+
const server = parsed.projects?.[cwd]?.mcpServers?.[name];
|
|
40
|
+
if (server)
|
|
41
|
+
return { server, scope: 'local' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// File doesn't exist
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (scope === 'project') {
|
|
49
|
+
const mcp_path = get_project_mcp_json_path();
|
|
50
|
+
try {
|
|
51
|
+
await access(mcp_path);
|
|
52
|
+
const content = await readFile(mcp_path, 'utf-8');
|
|
53
|
+
const parsed = JSON.parse(content);
|
|
54
|
+
const server = parsed.mcpServers?.[name];
|
|
55
|
+
if (server)
|
|
56
|
+
return { server, scope: 'project' };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// File doesn't exist
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Auto-detect which scope a server lives in.
|
|
66
|
+
* Searches local → project → user.
|
|
67
|
+
*/
|
|
68
|
+
async function detect_server_scope(name) {
|
|
69
|
+
for (const scope of ['local', 'project', 'user']) {
|
|
70
|
+
const result = await find_server_in_scope(name, scope);
|
|
71
|
+
if (result)
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Write a server config into the appropriate scope config file.
|
|
78
|
+
*/
|
|
79
|
+
async function write_server_to_scope(name, server, scope) {
|
|
80
|
+
if (scope === 'user') {
|
|
81
|
+
await atomic_json_write(get_claude_config_path(), (existing) => {
|
|
82
|
+
if (!existing.mcpServers) {
|
|
83
|
+
existing.mcpServers = {};
|
|
84
|
+
}
|
|
85
|
+
existing.mcpServers[name] =
|
|
86
|
+
server;
|
|
87
|
+
return existing;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else if (scope === 'local') {
|
|
91
|
+
const cwd = get_current_project_path();
|
|
92
|
+
await atomic_json_write(get_claude_config_path(), (existing) => {
|
|
93
|
+
if (!existing.projects) {
|
|
94
|
+
existing.projects = {};
|
|
95
|
+
}
|
|
96
|
+
const projects = existing.projects;
|
|
97
|
+
if (!projects[cwd]) {
|
|
98
|
+
projects[cwd] = {};
|
|
99
|
+
}
|
|
100
|
+
if (!projects[cwd].mcpServers) {
|
|
101
|
+
projects[cwd].mcpServers = {};
|
|
102
|
+
}
|
|
103
|
+
projects[cwd].mcpServers[name] = server;
|
|
104
|
+
return existing;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else if (scope === 'project') {
|
|
108
|
+
await atomic_json_write(get_project_mcp_json_path(), (existing) => {
|
|
109
|
+
if (!existing.mcpServers) {
|
|
110
|
+
existing.mcpServers = {};
|
|
111
|
+
}
|
|
112
|
+
existing.mcpServers[name] =
|
|
113
|
+
server;
|
|
114
|
+
return existing;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Apply a dev override: store original config, swap in local dev command.
|
|
120
|
+
*/
|
|
121
|
+
export async function apply_dev_override(name, command, args, scope) {
|
|
122
|
+
// Find the server
|
|
123
|
+
let found;
|
|
124
|
+
if (scope) {
|
|
125
|
+
found = await find_server_in_scope(name, scope);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
found = await detect_server_scope(name);
|
|
129
|
+
}
|
|
130
|
+
if (!found) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
scope: scope || 'local',
|
|
134
|
+
error: `Server '${name}' not found${scope ? ` in ${scope} scope` : ' in any scope'}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Check not already overridden
|
|
138
|
+
const overrides = await read_dev_overrides();
|
|
139
|
+
if (overrides.overrides[name]) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
scope: found.scope,
|
|
143
|
+
error: `Server '${name}' already has a dev override. Run 'mcpick dev --restore ${name}' first.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Build dev server config
|
|
147
|
+
const dev_server = {
|
|
148
|
+
command,
|
|
149
|
+
...(args.length > 0 ? { args } : {}),
|
|
150
|
+
};
|
|
151
|
+
// Store original
|
|
152
|
+
overrides.overrides[name] = {
|
|
153
|
+
original: found.server,
|
|
154
|
+
dev: dev_server,
|
|
155
|
+
scope: found.scope,
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
await write_dev_overrides(overrides);
|
|
159
|
+
// Write dev config
|
|
160
|
+
await write_server_to_scope(name, dev_server, found.scope);
|
|
161
|
+
return { success: true, scope: found.scope };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Restore original server config from dev override.
|
|
165
|
+
*/
|
|
166
|
+
export async function restore_dev_override(name) {
|
|
167
|
+
const overrides = await read_dev_overrides();
|
|
168
|
+
const entry = overrides.overrides[name];
|
|
169
|
+
if (!entry) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: `No dev override found for '${name}'`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Write original config back
|
|
176
|
+
await write_server_to_scope(name, entry.original, entry.scope);
|
|
177
|
+
// Remove override entry
|
|
178
|
+
delete overrides.overrides[name];
|
|
179
|
+
await write_dev_overrides(overrides);
|
|
180
|
+
return { success: true };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Restore all dev overrides.
|
|
184
|
+
*/
|
|
185
|
+
export async function restore_all_dev_overrides() {
|
|
186
|
+
const overrides = await read_dev_overrides();
|
|
187
|
+
const restored = [];
|
|
188
|
+
const errors = [];
|
|
189
|
+
for (const name of Object.keys(overrides.overrides)) {
|
|
190
|
+
const result = await restore_dev_override(name);
|
|
191
|
+
if (result.success) {
|
|
192
|
+
restored.push(name);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
errors.push(`${name}: ${result.error}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { restored, errors };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* List all active dev overrides.
|
|
202
|
+
*/
|
|
203
|
+
export async function list_dev_overrides() {
|
|
204
|
+
const overrides = await read_dev_overrides();
|
|
205
|
+
return Object.entries(overrides.overrides).map(([name, entry]) => ({
|
|
206
|
+
name,
|
|
207
|
+
...entry,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=dev-override.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { exec } from 'node:child_process';
|
|
2
|
-
import { readdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { lstat, readdir, readFile, readlink, rename, rm, symlink } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { atomic_json_write } from '../utils/atomic-write.js';
|
|
6
|
-
import { get_installed_plugins_path, get_known_marketplaces_path, get_marketplace_manifest_path, get_plugin_cache_dir, } from '../utils/paths.js';
|
|
6
|
+
import { ensure_directory_exists, get_installed_plugins_path, get_known_marketplaces_path, get_marketplace_manifest_path, get_plugin_cache_dir, } from '../utils/paths.js';
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
8
|
const EMPTY_INSTALLED = {
|
|
9
9
|
version: 2,
|
|
@@ -99,7 +99,7 @@ async function find_orphaned_versions(marketplace, plugin_name) {
|
|
|
99
99
|
return orphaned;
|
|
100
100
|
}
|
|
101
101
|
// --- Staleness analysis ---
|
|
102
|
-
function parse_plugin_key(key) {
|
|
102
|
+
export function parse_plugin_key(key) {
|
|
103
103
|
const at_index = key.lastIndexOf('@');
|
|
104
104
|
return {
|
|
105
105
|
name: at_index > 0 ? key.substring(0, at_index) : key,
|
|
@@ -257,4 +257,160 @@ export async function clean_orphaned_versions() {
|
|
|
257
257
|
}
|
|
258
258
|
return { cleaned: cleaned_paths.length, paths: cleaned_paths };
|
|
259
259
|
}
|
|
260
|
+
// --- Cache linking ---
|
|
261
|
+
/**
|
|
262
|
+
* Symlink a local directory into the plugin cache.
|
|
263
|
+
* Backs up existing cache directory if present.
|
|
264
|
+
*/
|
|
265
|
+
export async function link_local_plugin(local_path, key) {
|
|
266
|
+
const resolved_path = resolve(local_path);
|
|
267
|
+
const { name, marketplace } = parse_plugin_key(key);
|
|
268
|
+
// Validate local path exists
|
|
269
|
+
try {
|
|
270
|
+
const stat = await lstat(resolved_path);
|
|
271
|
+
if (!stat.isDirectory()) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
key,
|
|
275
|
+
symlinkPath: '',
|
|
276
|
+
targetPath: resolved_path,
|
|
277
|
+
error: `Path is not a directory: ${resolved_path}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
key,
|
|
285
|
+
symlinkPath: '',
|
|
286
|
+
targetPath: resolved_path,
|
|
287
|
+
error: `Path does not exist: ${resolved_path}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const cache_dir = get_plugin_cache_dir();
|
|
291
|
+
const plugin_dir = join(cache_dir, marketplace, name);
|
|
292
|
+
// Ensure parent directory exists
|
|
293
|
+
await ensure_directory_exists(join(cache_dir, marketplace));
|
|
294
|
+
// If plugin_dir exists and is not a symlink, back it up
|
|
295
|
+
try {
|
|
296
|
+
const stat = await lstat(plugin_dir);
|
|
297
|
+
if (stat.isSymbolicLink()) {
|
|
298
|
+
// Already a symlink — remove it
|
|
299
|
+
await rm(plugin_dir);
|
|
300
|
+
}
|
|
301
|
+
else if (stat.isDirectory()) {
|
|
302
|
+
// Back up existing directory
|
|
303
|
+
const backup_path = `${plugin_dir}.backup`;
|
|
304
|
+
await rename(plugin_dir, backup_path);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Doesn't exist — fine
|
|
309
|
+
}
|
|
310
|
+
// Create symlink
|
|
311
|
+
try {
|
|
312
|
+
await symlink(resolved_path, plugin_dir, 'dir');
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
key,
|
|
319
|
+
symlinkPath: plugin_dir,
|
|
320
|
+
targetPath: resolved_path,
|
|
321
|
+
error: `Failed to create symlink: ${msg}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
success: true,
|
|
326
|
+
key,
|
|
327
|
+
symlinkPath: plugin_dir,
|
|
328
|
+
targetPath: resolved_path,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Remove a symlink from the plugin cache and restore backup if present.
|
|
333
|
+
*/
|
|
334
|
+
export async function unlink_local_plugin(key) {
|
|
335
|
+
const { name, marketplace } = parse_plugin_key(key);
|
|
336
|
+
const cache_dir = get_plugin_cache_dir();
|
|
337
|
+
const plugin_dir = join(cache_dir, marketplace, name);
|
|
338
|
+
// Verify it's actually a symlink
|
|
339
|
+
try {
|
|
340
|
+
const stat = await lstat(plugin_dir);
|
|
341
|
+
if (!stat.isSymbolicLink()) {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
key,
|
|
345
|
+
restored: false,
|
|
346
|
+
error: `'${key}' is not a symlink — nothing to unlink`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
key,
|
|
354
|
+
restored: false,
|
|
355
|
+
error: `'${key}' not found in cache`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// Remove symlink
|
|
359
|
+
await rm(plugin_dir);
|
|
360
|
+
// Restore backup if present
|
|
361
|
+
const backup_path = `${plugin_dir}.backup`;
|
|
362
|
+
let restored = false;
|
|
363
|
+
try {
|
|
364
|
+
await lstat(backup_path);
|
|
365
|
+
await rename(backup_path, plugin_dir);
|
|
366
|
+
restored = true;
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// No backup to restore
|
|
370
|
+
}
|
|
371
|
+
return { success: true, key, restored };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* List all symlinked entries in the plugin cache.
|
|
375
|
+
*/
|
|
376
|
+
export async function list_linked_plugins() {
|
|
377
|
+
const cache_dir = get_plugin_cache_dir();
|
|
378
|
+
const links = [];
|
|
379
|
+
try {
|
|
380
|
+
const marketplaces = await readdir(cache_dir, { withFileTypes: true });
|
|
381
|
+
for (const mkt of marketplaces) {
|
|
382
|
+
if (!mkt.isDirectory() && !mkt.isSymbolicLink())
|
|
383
|
+
continue;
|
|
384
|
+
const mkt_path = join(cache_dir, mkt.name);
|
|
385
|
+
let entries;
|
|
386
|
+
try {
|
|
387
|
+
entries = await readdir(mkt_path, { withFileTypes: true });
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
for (const entry of entries) {
|
|
393
|
+
const entry_path = join(mkt_path, entry.name);
|
|
394
|
+
try {
|
|
395
|
+
const stat = await lstat(entry_path);
|
|
396
|
+
if (stat.isSymbolicLink()) {
|
|
397
|
+
const target = await readlink(entry_path);
|
|
398
|
+
links.push({
|
|
399
|
+
key: `${entry.name}@${mkt.name}`,
|
|
400
|
+
symlinkPath: entry_path,
|
|
401
|
+
targetPath: resolve(join(cache_dir, mkt.name), target),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// Skip on error
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Cache dir doesn't exist
|
|
413
|
+
}
|
|
414
|
+
return links;
|
|
415
|
+
}
|
|
260
416
|
//# sourceMappingURL=plugin-cache.js.map
|
package/dist/index.js
CHANGED
package/dist/utils/paths.js
CHANGED
|
@@ -31,6 +31,9 @@ export function get_claude_settings_path() {
|
|
|
31
31
|
export function get_mcpick_dir() {
|
|
32
32
|
return join(get_base_dir().baseDir, 'mcpick');
|
|
33
33
|
}
|
|
34
|
+
export function get_dev_overrides_path() {
|
|
35
|
+
return join(get_mcpick_dir(), 'dev-overrides.json');
|
|
36
|
+
}
|
|
34
37
|
export function get_server_registry_path() {
|
|
35
38
|
return join(get_mcpick_dir(), 'servers.json');
|
|
36
39
|
}
|